【读书笔记】C++反汇编与逆向分析技术揭秘2
2022-11-27 23:51:0 Author: bbs.pediy.com(查看原文) 阅读量:18 收藏

【读书笔记】C++反汇编与逆向分析技术揭秘2

2022-11-27 23:51 3386

目录

1.1.编译环境

  • VS2019
  • Release/Debug x86

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指令集

  • 八个寄存器:XMM0-XMM8,每个寄存器占16字节(128bit)

    图片描述

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.空类

  • 空类的长度位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