首页
社区
课程
招聘
【读书笔记】C++反汇编与逆向分析技术揭秘2
2022-11-27 23:51 19934

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

2022-11-27 23:51
19934

目录

一、准备工作

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
#include <stdio.h>
 
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
#include <stdio.h>
 
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
#include <stdio.h>
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
#include <stdio.h>
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
#include <stdio.h>
 
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
#include <stdio.h>
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
#include <stdio.h>
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
#include <stdio.h>
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
#include <stdio.h>
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
#include <stdio.h>
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
#include <stdio.h>
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
#include <stdio.h>
 
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
#include <stdio.h>
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
#include <stdio.h>
 
 
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
#include <stdio.h>
 
 
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
#include <stdio.h>
 
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
#include <stdio.h>
 
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
#include <stdio.h>
 
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
#include <stdio.h>
 
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
#include <stdio.h>
 
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
#include <stdio.h>
 
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
#include <stdio.h>
 
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
#include <stdio.h>
 
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
#include <stdio.h>
 
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
#include <stdio.h>
 
 
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
#include <stdio.h>
 
 
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
#include <stdio.h>
 
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
#include <stdio.h>
 
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
#include <stdio.h>
#include <stdlib.h>
 
 
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
#include <stdio.h>
#include <stdlib.h>
 
 
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
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
 
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
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
 
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
#include <stdio.h>
#include <string.h>
 
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
#include <stdio.h>
 
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
#include <stdio.h>
 
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
#include <stdio.h>
 
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
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
 
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
#include <stdio.h>
#include <string.h>
 
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
#include <stdio.h>
 
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
#include <stdio.h>
 
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
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
 
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
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
 
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
#include <stdio.h>
 
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
#include <stdio.h>
 
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
#include <stdio.h>
 
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
#include <stdio.h>
 
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
#include <stdio.h>
 
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
#include <stdio.h>
 
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
#include <stdio.h>
 
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
#include <stdio.h>
 
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
#include <stdio.h>
 
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函数时,就可以高度怀疑此虚表对应的类是一个抽象类。


[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

最后于 2023-3-29 01:38 被zhang_derek编辑 ,原因: 上次pdf附件
上传的附件:
收藏
点赞28
打赏
分享
打赏 + 1.00雪花
打赏次数 1 雪花 + 1.00
 
赞赏  K4NG   +1.00 2023/03/14
最新回复 (13)
雪    币: 5983
活跃值: (3705)
能力值: ( LV7,RANK:110 )
在线值:
发帖
回帖
粉丝
pxhb 2 2022-11-28 09:37
2
0
感谢分享,很好的基础教程
雪    币: 2327
活跃值: (2561)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
院士 2022-11-28 21:12
3
0
学习了,感谢分享。
雪    币: 229
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
heye123 2022-12-1 12:39
4
0
感谢分享
雪    币: 189
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
WolfKingDong 2022-12-5 08:44
5
0
感谢分享
雪    币: 25387
活跃值: (1404)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
挽梦雪舞 2022-12-5 10:03
6
0
感谢分享
雪    币: 61
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
wx_man_611 2022-12-5 10:07
7
0
2019 有点高端
雪    币: 40
活跃值: (1406)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
快乐的小跳蛙 2022-12-5 14:58
8
0
厉害厉害
雪    币: 69
活跃值: (140)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Esc 2022-12-6 19:06
9
0
感谢分享
雪    币: 1382
活跃值: (3100)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
小希希 2022-12-6 22:37
10
0
写得好,练习markdown也是不错
雪    币: 188
活跃值: (336)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
yukihotaru 2022-12-13 19:00
11
0
除法部分实在太反人类了。
雪    币: 4371
活跃值: (7354)
能力值: ( LV6,RANK:80 )
在线值:
发帖
回帖
粉丝
zhang_derek 1 2023-3-29 01:38
12
0

上传pdf附件

最后于 2023-3-30 14:17 被zhang_derek编辑 ,原因:
雪    币: 1918
活跃值: (313)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
wangez8 2023-3-29 08:53
13
0
感谢分享
雪    币: 21
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
毛仙 2023-4-3 09:49
14
0
感谢分享
游客
登录 | 注册 方可回帖
返回