-
-
【读书笔记】C++反汇编与逆向分析技术揭秘2
-
2022-11-27 23:51
3386
-
1.1.编译环境
1.2.OllyDbg
常用快捷键
- F2:设置断点
- F3:加载一个可执行程序
- F4:程序执行到光标处
- F5:缩小,还原当前窗口
- F7:单步步入
- F8:单步步过
- F9:运行程序
- Ctrl+F2:重新运行程序到起始处
- Ctrl+F9:执行到函数返回处,用于跳出函数实现
- Alt+F9:执行到用户代码处,用于快速跳出系统函数
- Ctrl+G:快速定位跳转地址
1.3.IDA
常用快捷键
- 空格键:反汇编窗口切换文本跟图形
- a:解析成字符串的首地址
- b:十六进制与二进制转换
- c:解释位一条指令
- d:解释为数据,每按一次转换数据长度
- g:快速查找到对应地址
- h:十六进制与十进制转换
- k:将数据解释为栈变量
- m:解释为枚举成员
- n:重新命名
- t:把偏移改为结构体
- u:取消定义函数、代码、数据的定义
- x:查看交叉引用
- y:更改变量的类型
- 分号:添加注释
- shift+F9:添加结构体
- Alt+T:搜索文本
- ins:插入结构体
- Alt+Q:修改数据类型为结构体类型
2.1.无符号整数
以unsigned int为例
- 取值范围:0~4294967295(0x00000000~0xFFFFFFFF)
- 小尾方式存放:低数据位存放在内存的低端,高数据位存放在内存的高端
- 不存在正负之分,都是正数
2.2.有符号正数
以int为例
- 最高位是符号位,0表示正数,1表示负数
- 取值范围:-2147483648~2147483648
- 正数区间(0x00000000~0x7FFFFFFF),负数区间(0x80000000~0xFFFFFFFF)
- 负数在内存中都是以补码形式存放的,可以表达位:对这个数值取反+1
2.3.浮点数类型
SSE指令集
3.1.找main函数入口方法
VS2019 Release版本
- 有3个参数,main函数是启动函数中唯一具有3个参数的函数
- 找到入口代码第一次调用exit函数处,离exit最近的且有3个参数的函数通常就是main函数
找main函数入口
4.1.加法
release版
1 2 3 4 5 6 7 8 9 10 11 | int main( int argc, char * argv[]) {
int n1 = argc;
int n2 = argc; / / 复写传播:n2等价于引用argc, n2则被删除
n1 = n1 + 1 ; / / 下一句n1重新赋值了,所以这句被删除了
n1 = 1 + 2 ; / / 常量折叠:n1 = 3
n1 = n1 + n2; / / 常量传播和复写传播: n1 = 3 + argc
printf( "n1 = %d\n" , n1);
return 0 ;
}
|
ida
4.2.减法
release
1 2 3 4 5 6 7 8 9 10 11 | int main( int argc, char * argv[]) {
int n1 = argc;
int n2 = 0 ;
scanf_s( "%d" , &n2);
n1 = n1 - 100 ;
n1 = n1 + 5 - n2; / / n1 = n1 - 95 - n2
printf( "n1 = %d \r\n" , n1);
return 0 ;
}
|
ida
4.3.乘法
release
1 2 3 4 5 6 7 8 9 10 11 12 | int main( int argc, char * argv[]) {
int n1 = argc;
int n2 = argc;
printf( "n1 * 15 = %d\n" , n1 * 15 ); / / 变量乘常量 ( 常量值为非 2 的幂 )
printf( "n1 * 16 = %d\n" , n1 * 16 ); / / 变量乘常量 ( 常量值为 2 的幂 )
printf( "2 * 2 = %d\n" , 2 * 2 ); / / 两常量相乘
printf( "n2 * 4 + 5 = %d\n" , n2 * 4 + 5 ); / / 混合运算
printf( "n1 * n2 = %d\n" , n1 * n2); / / 两变量相乘
return 0 ;
}
|
ida
4.4.除法
常用指令
- cdq:把eax的最高位填充到edx,如果eax ≥ 0,edx = 0,如果eax < 0,edx = 0xFFFFFFFF
- sar:算术右移
- shr:逻辑右移
- neg:将操作数取反+1
- div:无符号数除法
- idiv:有符号除法
- mul:无符号数乘法
- imul:有符号数乘法
4.4.1.除数为无符号2的幂
release
1 2 3 4 5 6 7 | int main(unsigned argc, char * argv[]) {
printf( "a / 16 = %u" , argc / 16 );
return 0 ;
}
|
ida
4.4.2.除数为无符号非2的幂
release
1 2 3 4 5 6 | int main( int argc, char * argv[]) {
printf( "argc / 3 = %u" , (unsigned)argc / 3 ); / / 变量除以常量,常量为无符号非 2 的幂
return 0 ;
}
|
ida
4.4.3.另一种除数为无符号非2的幂
release
1 2 3 4 5 6 7 | int main(unsigned argc, char * argv[]) {
printf( "a / 7 = %u" , argc / 7 );
return 0 ;
}
|
ida反汇编
4.4.4.除数为有符号2的幂
release
1 2 3 4 5 6 7 | int main( int argc, char * argv[]) {
printf( "a / 8 = %d" , argc / 8 );
return 0 ;
}
|
ida
4.4.5.除数为有符号非2的幂
release
1 2 3 4 5 6 7 | int main( int argc, char * argv[]) {
printf( "a / 9 = %d" , argc / 9 ); / / / / 变量除以常量,常量为非 2 的幂
return 0 ;
}
|
ida
4.4.6.第二种除数为有符号非2的幂
release版
1 2 3 4 5 | int main( int argc, char * argv[]) {
printf( "argc / 7 = %d" , argc / 7 ); / / 变量除以常量,常量为非 2 的幂
return 0 ;
}
|
ida
4.4.7.除数为有符号负2的幂
release版
1 2 3 4 5 6 7 | int main( int argc, char * argv[]) {
printf( "a / -4 = %d" , argc / - 4 );
return 0 ;
}
|
ida反汇编
4.4.8.除数为有符号负非2的幂
release
1 2 3 4 5 6 7 | int main( int argc, char * argv[]) {
printf( "a / -5 = %d" , argc / - 5 );
return 0 ;
}
|
ida
4.4.9.另一种除数为有符号负非2的幂
release版
1 2 3 4 5 6 | int main( int argc, char * argv[]) {
printf( "argc / -7 = %d" , argc / - 7 ); / / 变量除以常量,常量为负非 2 的幂
return 0 ;
}
|
ida
4.5.取模
release版
1 2 3 4 5 6 | int main( int argc, char * argv[]) {
printf( "%d" , argc % 8 ); / / 变量模常量,常量为 2 的幂
printf( "%d" , argc % 9 ); / / 变量模常量,常量为非 2 的幂
return 0 ;
}
|
ida反汇编
4.6.条件跳转指令表
4.7.条件表达式
第一种,相差为1
release
1 2 3 4 5 6 7 8 9 | int main( int argc, char * argv[])
{
printf( "%d\r\n" ,argc = = 5 ? 5 : 6 );
return 0 ;
}
|
ida
第二种,相差大于1
release
1 2 3 4 5 6 7 8 9 | int main( int argc, char * argv[])
{
printf( "%d\r\n" ,argc = = 5 ? 4 : 10 );
return 0 ;
}
|
ida
第三种变量表达式
release
1 2 3 4 5 6 7 8 | int main( int argc, char * argv[]) {
int n1, n2;
scanf_s( "%d %d" , &n1, &n2);
printf( "%d\n" , argc ? n1 : n2);
return 0 ;
}
|
ida
第四种表达式无优化使用分支
release
1 2 3 4 5 6 7 8 | int main( int argc, char * argv[]) {
int n1, n2;
scanf_s( "%d %d" , &n1, &n2);
printf( "%d\n" , argc ? n1 : n2 + 3 );
return 0 ;
}
|
ida
5.1.if
release
1 2 3 4 5 6 7 8 | int main( int argc, char * argv[]) {
if (argc = = 0 ) {
printf( "argc == 0" );
}
return 0 ;
}
|
ida
总结
5.2.if else
release
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | int main( int argc, char * argv[]) {
if (argc > 0 ) {
printf( "argc > 0" );
}
else if (argc = = 0 ) {
printf( "argc == 0" );
}
else {
printf( "argc <= 0" );
}
return 0 ;
}
|
ida
5.3.switch
5.3.1.分支少于4个
release
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | int main( int argc, char * argv[]) {
int n = 1 ;
scanf_s( "%d" , &n);
switch (n) {
case 1 :
printf( "n == 1" );
break ;
case 3 :
printf( "n == 3" );
break ;
case 100 :
printf( "n == 100" );
break ;
}
return 0 ;
}
|
ida
5.3.2.分支大于4个且值连续
会对case语句块制作地址表,以减少比较跳转次数
release
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | int main( int argc, char * argv[]) {
int n = 1 ;
scanf_s( "%d" , &n);
switch (n) {
case 1 :
printf( "n == 1" );
break ;
case 2 :
printf( "n == 2" );
break ;
case 3 :
printf( "n == 3" );
break ;
case 5 :
printf( "n == 5" );
break ;
case 6 :
printf( "n == 6" );
break ;
case 7 :
printf( "n == 7" );
break ;
}
return 0 ;
}
|
ida
5.3.3.分支大于4个,值不连续,且最大case值和case值的差小于256
有两张表
- case语句块地址表:每一项保存一个case语句块的首地址,有几个case就有几项,default也在里面
- case语句块索引表:保存地址表的编号,索引表的大小等于最大case值和最小case值的差
release版
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | int main( int argc, char * argv[]) {
int n = 1 ;
scanf_s( "%d" , &n);
switch (n) {
case 1 :
printf( "n == 1" );
break ;
case 2 :
printf( "n == 2" );
break ;
case 3 :
printf( "n == 3" );
break ;
case 5 :
printf( "n == 5" );
break ;
case 6 :
printf( "n == 6" );
break ;
case 255 :
printf( "n == 255" );
break ;
}
return 0 ;
}
|
ida
5.3.4.分支大于4个,值不连续,且最大case值和case值的差大于256
将每个case值作为一个节点,找到这些节点的中间值作为跟节点,形成一颗平衡二叉树,以每个节点作为判定值,大于和小于关系分别对应左子树和右子树。
release版
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | int main( int argc, char * argv[]) {
int n = 0 ;
scanf_s( "%d" , &n);
switch (n) {
case 2 :
printf( "n == 2\n" );
break ;
case 3 :
printf( "n == 3\n" );
break ;
case 8 :
printf( "n == 8\n" );
break ;
case 10 :
printf( "n == 10\n" );
break ;
case 35 :
printf( "n == 35\n" );
break ;
case 37 :
printf( "n == 37\n" );
break ;
case 666 :
printf( "n == 666\n" );
break ;
default:
printf( "default\n" );
break ;
}
return 0 ;
}
|
ida
5.4.do while
release
1 2 3 4 5 6 7 8 9 10 11 | int main( int argc, char * argv[]) {
int sum = 0 ;
int i = 0 ;
do {
sum + = i;
i + + ;
} while (i < = argc);
return sum ;
}
|
ida
5.5.while
release版
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | int main( int argc, char * argv[])
{
int sum = 0 ;
int i = 0 ;
while (i < = 100 )
{
sum = sum + i;
i + + ;
}
printf( "%d\r\n" , sum );
return 0 ;
}
|
ida
5.6.for
main.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | int main( int argc, char * argv[])
{
int sum = 0 ;
/ / 内部会优化,把步长改为 4 ,减少循环次数
for ( int n = 1 ; n < = 100 ; n + + )
{
sum = sum + n;
}
printf( "%d\r\n" , sum );
return 0 ;
}
|
ida
6.1.各种调用方式的考察
调用约定
- _cdecl:默认的调用约定,外平栈,按从右至左的顺序压参数入栈
- _stdcall:内平栈,按从右至左的顺序压参数入栈
- _fastcall::前两个参数用ecx和edx传参,其余参数通过栈传参方式,按从右至左的顺序压参数入栈
6.2.函数的参数
release
1 2 3 4 5 6 7 8 9 10 11 12 | void addNumber( int n1) {
n1 + = 1 ;
printf( "%d\n" , n1);
}
int main( int argc, char * argv[]) {
int n = 0 ;
scanf_s( "%d" , &n); / / 防止变量被常量扩散优化
addNumber(n);
return 0 ;
}
|
ida
变量的作用域
- 全局变量:属于进程作用域,整个进程都能够访问到
- 静态变量:属于文件作用域,在当前源码文件内可以访问到
- 局部变量:属于函数作用域,在函数内可以访问到
7.1.全局变量和局部变量的区别
全局变量和局部变量的区别
- 全局变量:可以在程序中的任何文职使用
- 局部变量:局限于函数作用域内,若超出作用域,则由栈平衡操作释放局方局部变量的空间
- 局部变量:通过申请栈空间存放,利用栈指针ebp或esp间接访问,其地址是一个未知可变值
- 全局变量:与常量类似,通过立即数访问
7.2.局部静态变量的工作方式
局部静态变量
- 存放在静态存储区
作用域:所定义的函数
生命周期:持续到程序结束
- 只初始化一次
release版
1 2 3 4 5 6 7 8 9 10 11 12 | void showStatic( int n) {
static int g_static = n; / / 定义局部静态变量,赋值为参数
printf( "%d\n" , g_static); / / 显示静态变量
}
int main( int argc, char * argv[]) {
for ( int i = 0 ; i < 5 ; i + + ) {
showStatic(i); / / 循环调用显示局部静态变量的函数,每次传入不同值
}
return 0 ;
}
|
ida
7.3.堆变量
堆变量
- 使用malloc和new申请堆空间,返回的数据是申请的堆空间地址
- 使用free和delete释放堆空间
release
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | int main( int argc, char * argv[]) {
char * buffer1 = (char * )malloc( 10 ); / / 申请堆空间
char * buffer2 = new char[ 10 ]; / / 申请堆空间
if (buffer2 ! = NULL) {
delete[] buffer2; / / 释放堆空间
buffer2 = NULL;
}
if (buffer1 ! = NULL) {
free(buffer1); / / 释放堆空间
buffer1 = NULL;
}
return 0 ;
}
|
ida
8.1.数组在函数内
在函数内定义数组
- 去其它声明,该数组即为局部变量,拥有局部变量的所有特性
- 数组名称表示该数组的首地址
- 占用的内存空间大小为:sizeof(数据类型)x数组中元素个数
- 数组的各元素应为同一数据类型,以此可以区分局部变量与数组
字符数组初始化为字符串
release
1 2 3 4 5 6 7 8 9 10 | int main( int argc, char * argv[]) {
char s[] = "Hello World!" ;
printf( "%s" ,s);
return 0 ;
}
|
ida
8.2.数组作为参数
1.strlen()
release
1 2 3 4 5 6 7 | int main( int argc, char * argv[]) {
return strlen(argv[ 0 ]);
}
|
ida
2.strcpy()
在字符串初始化时,利用xmm寄存器初始化数组的值,一次可以初始化16字节,效率更高
release版
1 2 3 4 5 6 7 8 9 10 | int main( int argc, char * argv[]) {
char buffer [ 20 ] = { 0 }; / / 字符数组定义
strcpy( buffer , argv[ 0 ]); / / 字符串复制
printf( buffer );
return 0 ;
}
|
ida
8.3.存放指针类型数组的数组
release
1 2 3 4 5 6 7 8 9 10 | int main( int argc, char * argv[]) {
const char * ary[ 3 ] = { "Hello " , "World " , "!\n" }; / / 字符串指针数组定义
for ( int i = 0 ; i < 3 ; i + + ) {
printf(ary[i]); / / 显示输出字符串数组中的各项
}
return 0 ;
}
|
ida
8.4.函数指针
release
1 2 3 4 5 6 7 8 9 10 11 12 | int _stdcall show( int n) { / / 函数定义
printf( "show : %d\n" , n);
return n;
}
int main( int argc, char * argv[]) {
int (_stdcall * pfn)( int ) = show; / / 函数指针定义并初始化
int ret = pfn( 5 ); / / 使用函数指针调用函数并获取返回值
printf( "ret = %d\n" , ret);
return 0 ;
}
|
ida
9.1.对象的内存布局
1.空类
2.内存对齐
- 结构体中的数据成员类型最大值为M,指定对齐值为N,则实际对齐值为q=min(M,N)
3.静态数据成员
- 类中的数据成员被修饰为静态时,它与局部静态变量类似,存放的位置和全局变量一致
9.2.this指针
对象调用成员的方法以及取出数据成员的过程
利用寄存器ecx保存对象的首地址
以寄存器传参的方式将其传递到成员函数中
debug版
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | class Person {
public:
void setAge( int age) { / / 公有成员函数
this - >age = age;
}
public:
int age; / / 公有数据成员
};
int main( int argc, char * argv[]) {
Person person;
person.setAge( 5 ); / / 调用成员函数
printf( "Person : %d\n" , person.age); / / 获取数据成员
return 0 ;
}
|
ida
9.3.对象作为函数参数
debug
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | class Person {
public:
int age;
int height;
};
void show(Person person) { / / 参数为类Person的对象
printf( "age = %d , height = %d\n" , person.age,person.height);
}
int main( int argc, char * argv[]) {
Person person;
person.age = 1 ;
person.height = 2 ;
show(person);
return 0 ;
}
|
ida
含有数组数据成员的对象传参
debug
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | class Person {
public:
int age;
int height;
char name[ 32 ]; / / 定义数组类型的数据成员
};
void show(Person person) {
printf( "age = %d , height = %d name:%s\n" , person.age,
person.height, person.name);
}
int main( int argc, char * argv[]) {
Person person;
person.age = 1 ;
person.height = 2 ;
strcpy(person.name, "tom" ); / / 赋值数据成员数组
show(person);
return 0 ;
}
|
ida
9.4.对象作为返回值
debug
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | class Person {
public:
int count;
int buffer [ 10 ]; / / 定义两个数据成员,该类的大小为 44 字节
};
Person getPerson() {
Person person;
person.count = 10 ;
for ( int i = 0 ; i < 10 ; i + + ) {
person. buffer [i] = i + 1 ;
}
return person;
}
int main( int argc, char * argv[]) {
Person person;
person = getPerson();
printf( "%d %d %d" , person.count, person. buffer [ 0 ],person. buffer [ 9 ]);
return 0 ;
}
|
ida
getperson函数
根据生命周期将对象进行分类,分析各类对象构造函数和析构函数的调用时机
- 局部对象
- 堆对象
- 参数对象
- 返回对象
- 全局对象
- 静态对象
10.1.构造函数的出现时机
10.1.1.局部对象
debug
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | class Person {
public:
Person() { / / 无参构造函数
age = 20 ;
}
int age;
};
int main( int argc, char * argv[]) {
Person person; / / 类对象定义
return 0 ;
}
|
ida
构造函数
总结:局部对象构造函数的必要条件
- 该成员函数是这个对象在作用域内调用的第一个成员函数,根据this指针可以区分每个对象
- 这个成员函数通过thiscall方式调用
- 这个函数返回this指针
10.1.2堆对象
debug
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | class Person {
public:
Person() {
age = 20 ;
}
int age;
};
int main( int argc, char * argv[]) {
Person * p = new Person;
/ / 为了突出本节讨论的问题,这里没有检查new运算的返回值
p - >age = 21 ;
printf( "%d\n" , p - >age);
return 0 ;
}
|
ida
总结:
- 使用new申请堆空间之后,需要调用构造函数来完成对象数据成员的初始化
- 如果堆空间申请失败,则不调用构造函数
- 如果new执行成功,返回值是对象的首地址
- 识别堆对象的构造函数:重点分析new的双分支结构,在判定new成功的分支迅速定位并得到构造函数
10.1.3.参数对象
当对象作为函数参数时,会调用赋值构造函数
debug
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | class Person {
public:
Person() {
name = NULL; / / 无参构造函数,初始化指针
}
Person(const Person& obj) {
/ / 注:如果在复制构造函数中直接复制指针值,那么对象内的两个成员指针会指向同一个资源,这属于浅拷贝
/ / this - >name = obj.name;
/ / 为实参对象中的指针所指向的堆空间制作一份副本,这就是深拷贝了
int len = strlen(obj.name);
this - >name = new char[ len + sizeof(char)]; / / 为便于讲解,这里没有检查指针
strcpy(this - >name, obj.name);
}
void setName(const char * name) {
int len = strlen(name);
if (this - >name ! = NULL) {
delete[] this - >name;
}
this - >name = new char[ len + sizeof(char)]; / / 为便于讲解,这里没有检查指针
strcpy(this - >name, name);
}
public:
char * name;
};
void show(Person person) { / / 参数是对象类型,会触发复制构造函数
printf( "name:%s\n" , person.name);
}
int main( int argc, char * argv[]) {
Person person;
person.setName( "Hello" );
show(person);
return 0 ;
}
|
ida
调用赋值构造函数
10.4.返回对象
返回对象与参数对象类似,都会使用赋值构造函数。但是,两者使用时机不同
- 当对象为参数时,在进入函数前使用赋值构造函数
- 返回对象则在函数返回时使用赋值构造函数
debug
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | class Person {
public:
Person() {
name = NULL; / / 无参构造函数,初始化指针
}
Person(const Person& obj) {
/ / 注:如果在复制构造函数中直接复制指针值,那么对象内的两个成员指针会指向同一个资源,这属于浅拷贝
/ / this - >name = obj.name;
/ / 为实参对象中的指针所指向的堆空间制作一份副本,这就是深拷贝了
int len = strlen(obj.name);
this - >name = new char[ len + sizeof(char)]; / / 为便于讲解,这里没有检查指针
strcpy(this - >name, obj.name);
}
void setName(const char * name) {
int len = strlen(name);
if (this - >name ! = NULL) {
delete[] this - >name;
}
this - >name = new char[ len + sizeof(char)]; / / 为便于讲解,这里没有检查指针
strcpy(this - >name, name);
}
public:
char * name;
};
Person getObject() {
Person person;
person.setName( "Hello" );
return person; / / 返回类型为对象
}
int main( int argc, char * argv[]) {
Person person = getObject();
return 0 ;
}
|
ida
调用getObject函数
10.2.析构对象的出现时机
10.2.1.局部对象
重点考察作用域的结束处,当对象所在作用域结束后,将销毁作用域所有变量的栈空间,此时便是析构函数出现的时机。析构函数同样属于成员函数,因此在调用的过程中也需要传递this指针。
debug
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | class Person {
public:
Person() {
age = 1 ;
}
~Person() {
printf( "~Person()\n" );
}
private:
int age;
};
int main( int argc, char * argv[]) {
Person person;
return 0 ; / / 退出函数后调用析构函数
}
|
ida
10.2.2.堆对象
用detele释放对象所在的空间,delete的使用便是找到堆对象调用析构的关键点
debug
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | class Person {
public:
Person() {
age = 20 ;
}
~Person() {
printf( "~Person()\n" );
}
int age;
};
int main( int argc, char * argv[]) {
Person * person = new Person();
person - >age = 21 ; / / 为了便于讲解,这里没检查指针
printf( "%d\n" , person - >age);
delete person;
return 0 ;
}
|
ida
析构代理函数
对于具有虚函数的类而言,构造函数和析构函数的识别过程更加简单。而且,在类中定义虚函数后,如果没有提供
构造函数,编译器会生成默认的构造函数。
对象的多态需要通过虚表和虚指针完成,虚表指针被定义在对象首地址处,因此虚函数必须作为成员函数使用。
11.1.虚函数的机制
当类中定义有虚函数,编译器会将给类中所有虚函数的首地址保存在一张地址表,这张表被称为虚函数地址表,简称虚表。同时还会在类中添加一个隐藏数据成员,称为虚表指针,该指针保存虚表的首地址,用于记录和查找虚函数。
11.1.1.默认构造函数初始化虚表指针的过程
- 没有编写构造函数时,编译器默认提供构造函数,以完成虚表指针的初始化
- 虚表中虚函数的地址排列顺序:先声明的虚函数的地址会被排列在虚表靠前的位置
- 第一个被声明的虚函数的地址在虚表的首地址处
debug
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | class Person {
public:
virtual int getAge() { / / 虚函数定义
return age;
}
virtual void setAge( int age) { / / 虚函数定义
this - >age = age;
}
private:
int age;
};
int main( int argc, char * argv[]) {
Person person;
/ / int size = sizeof(Person);
/ / 定义了虚函数后,因为还含有隐藏数据成员虚表指针,所以Person大小为 8
/ / printf( "%d" ,size); 8
return 0 ;
}
|
ida
构造函数
11.1.2.调用自身类的虚函数
直接通过对象调用自身的成员虚函数,编译器使用了直接调用函数方式,没有访问虚表指针,而是间接获取虚函数地址。
debug
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | class Person {
public:
virtual int getAge() { / / 虚函数定义
return age;
}
virtual void setAge( int age) { / / 虚函数定义
this - >age = age;
}
private:
int age;
};
int main( int argc, char * argv[]) {
Person person;
person.setAge( 20 );
printf( "%d\n" , person.getAge());
return 0 ;
}
|
ida
setAge函数
11.1.3.析构函数分析
执行析构函数时,实际上是在还原虚表指针,让其指向自身的虚表首地址,防止在析构函数中调用虚函数时取到非自身虚表,从而导致函数调用错误。
debug
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | class Person {
public:
~Person() {
printf( "~Person()\n" );
}
public:
virtual int getAge() { / / 虚函数定义
return age;
}
virtual void setAge( int age) { / / 虚函数定义
this - >age = age;
}
private:
int age;
};
int main( int argc, char * argv[]) {
Person person;
person.setAge( 20 );
printf( "%d\n" , person.getAge());
return 0 ;
}
|
ida析构函数分析
11.2.虚函数的识别
判断是否为虚函数
- 类中隐式定义了一个数据成员
- 该数据成员在首地址处,占一个指针大小
- 构造函数会将此数据成员初始化为某个数组的首地址
- 这个地址属于数据区,是相当固定的地址
- 在这个数组中,每个元素都是函数地址
- 这些函数被调用时,第一个参数是this指针
- 在这些函数内部,很有可能堆this指针使用间接的访问方式
12.1.识别类与类之间的关系
debug
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | class Base { / / 基类定义
public:
Base() {
printf( "Base\n" );
}
~Base() {
printf( "~Base\n" );
}
void setNumber( int n) {
base = n;
}
int getNumber() {
return base;
}
public:
int base;
};
class Derive : public Base { / / 派生类定义
public:
void showNumber( int n) {
setNumber(n);
derive = n + 1 ;
printf( "%d\n" , getNumber());
printf( "%d\n" , derive);
}
public:
int derive;
};
int main( int argc, char * argv[]) {
Derive derive;
derive.showNumber(argc);
return 0 ;
}
|
ida
子类Derive构造函数
子类Derive析构函数
人类说话方法的多态模拟类结构
debug
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | class Person { / / 基类——“人”类
public:
Person() {}
virtual ~Person() {}
virtual void showSpeak() {} / / 这里用纯虚函数更好,相关的知识点后面会讲到
};
class Chinese : public Person { / / 中国人:继承自人类
public:
Chinese() {}
virtual ~Chinese() {}
virtual void showSpeak() { / / 覆盖基类虚函数
printf( "Speak Chinese\r\n" );
}
};
class American : public Person { / / 美国人:继承自人类
public:
American() {}
virtual ~American() {}
virtual void showSpeak() { / / 覆盖基类虚函数
printf( "Speak American\r\n" );
}
};
class German : public Person { / / 德国人:继承自人类
public:
German() {}
virtual ~German() {}
virtual void showSpeak() { / / 覆盖基类虚函数
printf( "Speak German\r\n" );
}
};
void speak(Person * person) { / / 根据虚表信息获取虚函数首地址并调用
person - >showSpeak();
}
int main( int argc, char * argv[]) {
Chinese chinese;
American american;
German german;
speak(&chinese);
speak(&american);
speak(&german);
return 0 ;
}
|
speak函数分析,虚函数的调用过程是间接寻址方式
12.2.多重继承
debug
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | class Sofa {
public:
Sofa() {
color = 2 ;
}
virtual ~Sofa() { / / 沙发类虚析构函数
printf( "virtual ~Sofa()\n" );
}
virtual int getColor() { / / 获取沙发颜色
return color;
}
virtual int sitDown() { / / 沙发可以坐下休息
return printf( "Sit down and rest your legs\r\n" );
}
protected:
int color; / / 沙发类成员变量
};
/ / 定义床类
class Bed {
public:
Bed() {
length = 4 ;
width = 5 ;
}
virtual ~Bed() { / / 床类虚析构函数
printf( "virtual ~Bed()\n" );
}
virtual int getArea() { / / 获取床面积
return length * width;
}
virtual int sleep() { / / 床可以用来睡觉
return printf( "go to sleep\r\n" );
}
protected:
int length; / / 床类成员变量
int width;
};
/ / 子类沙发床定义,派生自Sofa类和Bed类
class SofaBed : public Sofa, public Bed {
public:
SofaBed() {
height = 6 ;
}
virtual ~SofaBed() { / / 沙发床类的虚析构函数
printf( "virtual ~SofaBed()\n" );
}
virtual int sitDown() { / / 沙发可以坐下休息
return printf( "Sit down on the sofa bed\r\n" );
}
virtual int sleep() { / / 床可以用来睡觉
return printf( "go to sleep on the sofa bed\r\n" );
}
virtual int getHeight() {
return height;
}
protected:
int height;
};
int main( int argc, char * argv[]) {
SofaBed sofabed;
return 0 ;
}
|
ida
构造函数
析构函数
单继承类和多重继承类特征总结
单继承
- 在类对象占用的内存空间中,只保存一份虚表指针。
- 因为只有一个虚表指针,所以只有一个虚表。
- 虚表中各项保存了类中各虚函数的首地址。
- 构造时先构造父类,再构造自身,并且只调用一次父类构造函数。
- 析构时先析构自身,再析构父类,并且只调用一次父类析构函数
多重继承
- 在类对象占用内存空间中,根据继承父类(有虚函数)个数保存对应的虚表指针。
- 根据保存的虚表指针的个数,产生相应个数的虚表。
- 转换父类指针时,需要调整到对象的首地址。
- 构造时需要调用多个父类构造函数。
- 构造时先构造继承列表中的第一个父类,然后依次调用到最后一个继承的父类构造函数。
- 析构时先析构自身,然后以构造函数相反的顺序调用所有父类的析构函数。
- 当对象作为成员时,整个类对象的内存结构和多重继承相似。当类中无虚函数时,整个类对象内存结构和多重继承完全一样,可酌情还原。当父类或成员对象存在虚函数时,通过观察虚表指针的位置和构造、析构函数中填写虚表指针的数目、顺序及目标地址,还原继承或成员关系。
12.3.抽象类
debug
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | class AbstractBase {
public:
AbstractBase() {
printf( "AbstractBase()" );
}
virtual void show() = 0 ; / / 定义纯虚函数
};
class VirtualChild : public AbstractBase { / / 定义继承抽象类的子类
public:
virtual void show() { / / 实现纯虚函数
printf( "抽象类分析\n" );
}
};
int main( int argc, char * argv[]) {
VirtualChild obj;
obj.show();
return 0 ;
}
|
ida
子类构造函数
抽象类构造函数
抽象类的虚表信息
在抽象类的虚表信息中,因为纯虚函数没有实现代码,所以没有首地址,编译器为了防止误调用虚函数,将虚表
中保存的纯虚函数的首地址项替换成函数__purecall
,用于结束程序。在分析过程中,一旦在虚表中发现函数地址为__purecall
函数时,就可以高度怀疑此虚表对应的类是一个抽象类。
[2022冬季班]《安卓高级研修班(网课)》月薪两万班招生中~
最后于 4天前
被zhang_derek编辑
,原因:
文章来源: https://bbs.pediy.com/thread-275320.htm
如有侵权请联系:admin#unsafe.sh