Hexo
C++逆向学习
Posted on: 2024-04-26 Edited on: 2024-04-27 In:  Views: 

1.string分析

以下面的代码作为例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <string>
#include <stdlib.h>

using namespace std;
int main(int argc, char const *argv[])
{
string str;
for (int i = 0; i < 0x200; i++)
{
str += 'a';
std::cout << "size : " << str.size() << " capacity : " << str.capacity() << std::endl;
}
return 0;
}

结果如下

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
size : 1   capacity : 15
size : 2 capacity : 15
size : 3 capacity : 15
size : 4 capacity : 15
size : 5 capacity : 15
size : 6 capacity : 15
size : 7 capacity : 15
size : 8 capacity : 15
size : 9 capacity : 15
size : 10 capacity : 15
size : 11 capacity : 15
size : 12 capacity : 15
size : 13 capacity : 15
size : 14 capacity : 15
size : 15 capacity : 15
size : 16 capacity : 31
size : 17 capacity : 31
size : 18 capacity : 31
size : 19 capacity : 31
size : 20 capacity : 31
size : 21 capacity : 31
size : 22 capacity : 31
size : 23 capacity : 31
size : 24 capacity : 31
size : 25 capacity : 31
size : 26 capacity : 31
size : 27 capacity : 31
size : 28 capacity : 31
size : 29 capacity : 31
size : 30 capacity : 31
size : 31 capacity : 31
size : 32 capacity : 47
size : 33 capacity : 47
size : 34 capacity : 47
size : 35 capacity : 47
size : 36 capacity : 47
size : 37 capacity : 47
size : 38 capacity : 47
size : 39 capacity : 47
size : 40 capacity : 47
size : 41 capacity : 47
size : 42 capacity : 47
size : 43 capacity : 47
size : 44 capacity : 47
size : 45 capacity : 47
size : 46 capacity : 47
size : 47 capacity : 47
size : 48 capacity : 70
size : 49 capacity : 70
size : 50 capacity : 70
size : 51 capacity : 70
size : 52 capacity : 70
size : 53 capacity : 70
size : 54 capacity : 70
size : 55 capacity : 70
size : 56 capacity : 70
size : 57 capacity : 70
size : 58 capacity : 70
size : 59 capacity : 70
size : 60 capacity : 70
size : 61 capacity : 70
size : 62 capacity : 70
size : 63 capacity : 70
size : 64 capacity : 70
size : 65 capacity : 70
size : 66 capacity : 70
size : 67 capacity : 70
size : 68 capacity : 70
size : 69 capacity : 70
size : 70 capacity : 70
size : 71 capacity : 105
size : 72 capacity : 105
size : 73 capacity : 105
size : 74 capacity : 105
size : 75 capacity : 105
size : 76 capacity : 105
size : 77 capacity : 105
size : 78 capacity : 105
size : 79 capacity : 105
size : 80 capacity : 105
size : 81 capacity : 105
size : 82 capacity : 105
size : 83 capacity : 105
size : 84 capacity : 105
size : 85 capacity : 105
size : 86 capacity : 105
size : 87 capacity : 105
size : 88 capacity : 105
size : 89 capacity : 105
size : 90 capacity : 105
size : 91 capacity : 105
size : 92 capacity : 105
size : 93 capacity : 105
size : 94 capacity : 105
size : 95 capacity : 105
size : 96 capacity : 105
size : 97 capacity : 105
size : 98 capacity : 105
size : 99 capacity : 105
size : 100 capacity : 105
......

其实分析过Linux下的string机制的话,对string类是有一个大致的了解的,在str长度小于0x10的时候,string类是存储在栈上的,当长度超过0x10的时候,就会在堆上申请内存,将字符串存到堆上。

具体调试一下,初始化之后,str是存在栈上的

image-20240426120703205

image-20240426121111598

红框处的值只是栈中残留的数据,不是string类真实的数据,string类开头的八字节指向红框处,当有了字符串之后,如下图所示

image-20240426121341987

开头8字节指向字符串,第二个八字节为字符串的长度,字符串末尾以00结尾。

当字符串长度大于等于0x10之后,如下图所示

image-20240426121525346

开头八字节指向堆了,这个堆的大小为0x30

image-20240426155612576

观察输出:

1
2
size : 31   capacity : 31
size : 32 capacity : 47

当size为31时,加上末尾的00字节,此时的size为0x20,和capacity大小一致,结合glibc的堆块管理机制,可以推断第一次从堆中申请内存的大小为0x20,ptmalloc会返回0x30大小的堆块。

整个程序运行结束之后,堆的情况如下所示

image-20240426161205935

image-20240426161215280

image-20240426161223710

image-20240426161233355

image-20240426161242331

0x30->0x50->0x90->0x100->0x1f0->0x3d0,可以看出,堆内存有效数据的扩容大致两倍的扩容的。如malloc(0x20*2)=0x50,malloc(0x40*2)=0x90,malloc(0x80*2)=0x110,malloc(0xf0*2)=0x1f0,malloc(0x1e0*2)=0x3d0

不过在Windows下的扩容机制和Linux下稍有区别

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
size : 1   capacity : 15
size : 2 capacity : 15
size : 3 capacity : 15
size : 4 capacity : 15
size : 5 capacity : 15
size : 6 capacity : 15
size : 7 capacity : 15
size : 8 capacity : 15
size : 9 capacity : 15
size : 10 capacity : 15
size : 11 capacity : 15
size : 12 capacity : 15
size : 13 capacity : 15
size : 14 capacity : 15
size : 15 capacity : 15
size : 16 capacity : 31
size : 17 capacity : 31
size : 18 capacity : 31
size : 19 capacity : 31
size : 20 capacity : 31
size : 21 capacity : 31
size : 22 capacity : 31
size : 23 capacity : 31
size : 24 capacity : 31
size : 25 capacity : 31
size : 26 capacity : 31
size : 27 capacity : 31
size : 28 capacity : 31
size : 29 capacity : 31
size : 30 capacity : 31
size : 31 capacity : 31
size : 32 capacity : 47
size : 33 capacity : 47
size : 34 capacity : 47
size : 35 capacity : 47
size : 36 capacity : 47
size : 37 capacity : 47
size : 38 capacity : 47
size : 39 capacity : 47
size : 40 capacity : 47
size : 41 capacity : 47
size : 42 capacity : 47
size : 43 capacity : 47
size : 44 capacity : 47
size : 45 capacity : 47
size : 46 capacity : 47
size : 47 capacity : 47
size : 48 capacity : 70
size : 49 capacity : 70
size : 50 capacity : 70
size : 51 capacity : 70
size : 52 capacity : 70
size : 53 capacity : 70
size : 54 capacity : 70
size : 55 capacity : 70
size : 56 capacity : 70
size : 57 capacity : 70
size : 58 capacity : 70
size : 59 capacity : 70
size : 60 capacity : 70
size : 61 capacity : 70
size : 62 capacity : 70
size : 63 capacity : 70
size : 64 capacity : 70
size : 65 capacity : 70
size : 66 capacity : 70
size : 67 capacity : 70
size : 68 capacity : 70
size : 69 capacity : 70
size : 70 capacity : 70
size : 71 capacity : 105
size : 72 capacity : 105
size : 73 capacity : 105
size : 74 capacity : 105
size : 75 capacity : 105
size : 76 capacity : 105
size : 77 capacity : 105
size : 78 capacity : 105
size : 79 capacity : 105
size : 80 capacity : 105
size : 81 capacity : 105
size : 82 capacity : 105
size : 83 capacity : 105
size : 84 capacity : 105
size : 85 capacity : 105
size : 86 capacity : 105
size : 87 capacity : 105
size : 88 capacity : 105
size : 89 capacity : 105
size : 90 capacity : 105
size : 91 capacity : 105
size : 92 capacity : 105
size : 93 capacity : 105
size : 94 capacity : 105
size : 95 capacity : 105
size : 96 capacity : 105
size : 97 capacity : 105
size : 98 capacity : 105
size : 99 capacity : 105
size : 100 capacity : 105
......

Windows下string类按照1.5倍扩容

string的扩容机制源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
size_type _Grow_to(size_type _Count) const
{
size_type _Capacity = capacity();
if ( _Capacity < 32 ) {
_Capacity = _Capacity + 16;
}else {
_Capacity = max_size() - _Capacity / 2 < _Capacity
? 0 : _Capacity + _Capacity / 2; // try to grow by 50%
}
if (_Capacity < _Count)
_Capacity = _Count;

return (_Capacity);
}

基本而言就是每次增加上一次Capacity的1.5倍。

再使用如下程序进入IDA分析

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
#include <iostream>
#include <string>

using namespace std;

int main() {
// 构造函数
std::string str1; // 默认构造函数
std::string str2("Hello, World!"); // 从C字符串构造
std::string str3(str2); // 拷贝构造函数
std::string str4(std::move(str2)); // 移动构造函数

// 赋值运算符
str1 = str3; // 复制赋值
str2 = " Assigned String"; // 从C字符串赋值
str3 = std::move(str1); // 移动赋值

// 字符串长度
std::cout << "Length of str3: " << str3.length() << std::endl;

// 访问单个字符
std::cout << "First character of str3: " << str3[0] << std::endl;

// 字符串连接
str3 += " and more"; // 字符串连接

// 查找子串
size_t pos = str3.find("and");
if (pos != std::string::npos) {
std::cout << "Found 'and' at position: " << pos << std::endl;
}

// 字符串比较
std::string str5 = "Hello";
std::string str6 = "Hello";
if (str5.compare(str6) == 0) {
std::cout << "str5 and str6 are equal." << std::endl;
}

// 输入输出流操作
std::cout << "Enter a string: ";
std::getline(std::cin, str1);
std::cout << "You entered: " << str1 << std::endl;

// 资源管理
// std::string 的析构函数在这里自动调用

return 0;
}

IDA的反编译结果如下

image-20240426195602365

直接创建空的string类使用std::string::basic_string,根据字符串来创建类会先初始化一个std::allocator<char>::allocator()的类,然后再根据字符串创建string类,根据string类来创建string也是使用std::string::basic_string,根据move来创建string也很清楚,先调用std::move<std::string &>对string进行move,然后再使用std::string::basic_string来创建类。

image-20240426195811963

string类的赋值使用的是std::string::operator=(str1,str3),将str3赋值给str1,又或者将字符串赋值给str1.

image-20240426200042599

使用std::string::operator[]来取地址,第一个参数是string,第二个参数是要取的地址的索引。

image-20240426201051610

使用std::string::operator+=()来完成字符串的拼接。

image-20240426201401493

在返回之前调用析构函数,结束string类的生命周期。

2.vector分析

首先还是分析一下vector的扩容原则

例子如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<iostream>
#include<vector>
using namespace std;

int main(int argc, char** argv) {
std::vector<int> a;
int num[16];
for (int i = 0; i < 100; i++) {
a.push_back(i);
std::cout << "size : " << i + 1 << "\t" << "capacity : " << a.capacity() << std::endl;
}
system("pause");
return 0;
}

Linux下输出如下

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
size : 1        capacity : 1
size : 2 capacity : 2
size : 3 capacity : 4
size : 4 capacity : 4
size : 5 capacity : 8
size : 6 capacity : 8
size : 7 capacity : 8
size : 8 capacity : 8
size : 9 capacity : 16
size : 10 capacity : 16
size : 11 capacity : 16
size : 12 capacity : 16
size : 13 capacity : 16
size : 14 capacity : 16
size : 15 capacity : 16
size : 16 capacity : 16
size : 17 capacity : 32
size : 18 capacity : 32
size : 19 capacity : 32
size : 20 capacity : 32
size : 21 capacity : 32
size : 22 capacity : 32
size : 23 capacity : 32
size : 24 capacity : 32
size : 25 capacity : 32
size : 26 capacity : 32
size : 27 capacity : 32
size : 28 capacity : 32
size : 29 capacity : 32
size : 30 capacity : 32
size : 31 capacity : 32
size : 32 capacity : 32
size : 33 capacity : 64
size : 34 capacity : 64
size : 35 capacity : 64
size : 36 capacity : 64
size : 37 capacity : 64
size : 38 capacity : 64
size : 39 capacity : 64
size : 40 capacity : 64
size : 41 capacity : 64
size : 42 capacity : 64
size : 43 capacity : 64
size : 44 capacity : 64
size : 45 capacity : 64
size : 46 capacity : 64
size : 47 capacity : 64
size : 48 capacity : 64

很明显也是按照2倍大小扩容

在gdb中调试,查看堆的大小,也是符合2倍扩容的机制的。

image-20240426205329086

在Windows下和Linux下的又稍有不同

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
size : 1        capacity : 1
size : 2 capacity : 2
size : 3 capacity : 3
size : 4 capacity : 4
size : 5 capacity : 6
size : 6 capacity : 6
size : 7 capacity : 9
size : 8 capacity : 9
size : 9 capacity : 9
size : 10 capacity : 13
size : 11 capacity : 13
size : 12 capacity : 13
size : 13 capacity : 13
size : 14 capacity : 19
size : 15 capacity : 19
size : 16 capacity : 19
size : 17 capacity : 19
size : 18 capacity : 19
size : 19 capacity : 19
size : 20 capacity : 28
size : 21 capacity : 28
size : 22 capacity : 28
size : 23 capacity : 28
size : 24 capacity : 28
size : 25 capacity : 28
size : 26 capacity : 28
size : 27 capacity : 28
size : 28 capacity : 28
size : 29 capacity : 42
size : 30 capacity : 42
size : 31 capacity : 42
size : 32 capacity : 42
size : 33 capacity : 42
size : 34 capacity : 42
size : 35 capacity : 42
size : 36 capacity : 42
size : 37 capacity : 42
size : 38 capacity : 42
size : 39 capacity : 42
size : 40 capacity : 42
size : 41 capacity : 42
size : 42 capacity : 42
size : 43 capacity : 63
size : 44 capacity : 63
size : 45 capacity : 63
size : 46 capacity : 63
size : 47 capacity : 63
size : 48 capacity : 63
size : 49 capacity : 63
size : 50 capacity : 63
size : 51 capacity : 63
size : 52 capacity : 63
size : 53 capacity : 63
size : 54 capacity : 63

除了一开始的容量变化是

1
2
3
4
size : 1        capacity : 1
size : 2 capacity : 2
size : 3 capacity : 3
size : 4 capacity : 4

1,2,3,4不是1.5倍的增长率,后续的增长也是1.5倍的增长,源码为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
else if (max_size() - size() < _Count)
//可以申请的最大容量也不够用,抛出异常_THROW(length_error, "vector<T> too long");
_Xlen();
else if (_Capacity < size() + _Count){//空间不足,需要扩容
_Capacity = max_size() - _Capacity / 2 < _Capacity
? 0 : _Capacity + _Capacity / 2; // 尝试扩容1.5倍
if (_Capacity < size() + _Count)//扩容1.5倍后依然不够用,则容量等于当前数据个数加上新增数据个数
_Capacity = size() + _Count;
pointer _Newvec = this->_Alval.allocate(_Capacity);//申请新空间
pointer _Ptr = _Newvec;
_TRY_BEGIN
_Ptr = _Umove(_Myfirst, _VEC_ITER_BASE(_Where),
_Newvec); //move原先的数据
_Ptr = _Ucopy(_First, _Last, _Ptr); //copy新增的数据到新内存之后
_Umove(_VEC_ITER_BASE(_Where), _Mylast, _Ptr);
_CATCH_ALL
_Destroy(_Newvec, _Ptr);
this->_Alval.deallocate(_Newvec, _Capacity);//释放原来申请的内存
_RERAISE;
_CATCH_END

首先会尝试扩容1.5倍,如果扩容1.5倍之后依然无法满足size,就会将容量设置为当前size+新增数据的个数,然后再分配内存。

下面再用一个例子给出IDA分析视角

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
68
69
70
71
72
#include <iostream>
#include <vector>
#include <algorithm> // 用于 std::transform, std::accumulate, std::fill, std::find
#include <numeric> // 用于 std::accumulate

int main() {
// 构造函数
std::vector<int> vec1(10); // 构造一个大小为10的向量

// 使用 std::transform 填充向量
std::transform(vec1.begin(), vec1.end(), vec1.begin(), [](int i) { return i * i; });
// 现在 vec1 包含了 0^2, 1^2, 2^2, ..., 9^2

// 输出变换后的向量
for (int num : vec1) {
std::cout << num << " ";
}
std::cout << std::endl;

// 使用 std::accumulate 计算向量的总和
int sum = std::accumulate(vec1.begin(), vec1.end(), 0);
std::cout << "Sum of squares: " << sum << std::endl;

// 使用 std::accumulate 计算向量的乘积
int product = std::accumulate(vec1.begin(), vec1.end(), 1, std::multiplies<int>());
std::cout << "Product of squares: " << product << std::endl;

// 使用 std::transform 和 std::accumulate 结合计算向量的平均值
std::vector<int> vec2(10, 1); // 创建一个大小为10的向量,所有元素都是1
std::transform(vec2.begin(), vec2.end(), vec2.begin(), [](int i) { return i + 1; });
// 现在 vec2 包含了 2, 3, 4, ..., 11
double average = std::accumulate(vec2.begin(), vec2.end(), 0.0) / vec2.size();
std::cout << "Average of transformed vec2: " << average << std::endl;

// 使用 std::fill 将 vec1 的所有元素替换为 -1
std::fill(vec1.begin(), vec1.end(), -1);
std::cout << "After filling with -1: ";
for (int num : vec1) {
std::cout << num << " ";
}
std::cout << std::endl;

// 使用 std::find 查找 vec2 中的第一个元素 5
auto it = std::find(vec2.begin(), vec2.end(), 5);
if (it != vec2.end()) {
std::cout << "Found 5 at position: " << std::distance(vec2.begin(), it) << std::endl;
} else {
std::cout << "5 not found in vec2" << std::endl;
}

// 使用 emplace_back 添加一个元素到 vec1
vec1.emplace_back(42);
std::cout << "After emplace_back(42), vec1 back: " << vec1.back() << std::endl;

// 使用 resize 改变 vec2 的大小
vec2.resize(5);
std::cout << "After resize(5), vec2 size: " << vec2.size() << std::endl;

// 使用 reserve 为 vec1 预留空间
vec1.reserve(20);
std::cout << "After reserve(20), vec1 capacity: " << vec1.capacity() << std::endl;

// 使用 shrink_to_fit 释放不需要的内存
vec1.shrink_to_fit();
std::cout << "After shrink_to_fit(), vec1 capacity: " << vec1.capacity() << std::endl;

// 资源管理
// std::vector 的析构函数在这里自动调用

return 0;
}

IDA视角如下

image-20240427103916269

vector需要先初始化一个allocator,然后传入std::vector<int>::vector(vec1, 10LL, v57)用于分配内存空间。

image-20240427104613687

这一段对应着std::transform(vec1.begin(), vec1.end(), vec1.begin(), [](int i) { return i * i; });这行代码,具体实现如下

image-20240427105007728

再往下看

image-20240427105537782

这一段也挺好识别的,因为没有去符号表,c++的各种接口都很明显。

image-20240427105852206

后续部分也基本类似,如果不去掉符号表的话其实IDA的伪代码已经挺好看了。

3.类

以如下代码为例:

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
#include<stdio.h>
#include<stdlib.h>

class base {//基类
public:
int a;//成员变量
double b;
base() {//构造函数
this->a = 1;
this->b = 2.3;
printf("base constructor\n");
}
void func() {//成员函数
printf("%d %lf\n", a, b);
}
virtual void v_func1() {//虚函数
printf("base v_func1()\n");
}
virtual void v_func2() {//虚函数
printf("base v_func2()\n");
}
~base() {//析构函数
printf("base destructor\n");
}
};

class derived :public base {//派生类
public:
derived() {
printf("derived constructor\n");
}
virtual void v_func() {//虚函数
printf("derived v_func()");
}
~derived() {
printf("derived destructor\n");
}
};


int main(int argc, char** argv) {
base a;
a.func();
a.v_func1();

base* b = (base*)new derived();
b->func();
b->v_func2();
return 0;
}

其中有基类,派生类,类中有成员变量,成员函数,虚函数。

直接看到IDA视角

image-20240427112414536

image-20240427114159395

可以看到,base类的每一个函数都传入了this指针,this指针为类函数的第一个参数,通过rdi传参。

看到构造函数

image-20240427114400491

this的首字段并不是成员变量,而是一个地址,这个地址实际上就是虚函数表

image-20240427114739212

这个地址称为虚表指针,是由编译器在类中添加的隐藏成员。

在虚表地址上方还有一个typeinfo,typeinfo是编译器生成的特殊类型信息,包括对象继承关系、对象本身的描述等。

image-20240427132430075

在析构函数中,同样对this的首字段进行了赋值,因为编译器无法预知这个子类以后是否会被其他类继承,如果被继承,原来的子类就成了父类,在执行析构函数时会先执行当前对象的析构函数,然后向祖父类的方向按继承线路逐层调用各类析构函数,当前对象的析构函数开始执行时,其虚表也是当前对象的,所以执行到父类的析构函数时,虚表必须改写为父类的虚表。编译器产生的类实现代码,必须能够适应将来不可预知的对象关系,故在每个对象的析构函数内,要加入自己虚表的代码。

image-20240427133134294

v3是derived类实例的地址

image-20240427133258457

在构造函数中初始化了虚表指针

image-20240427133356169

*v3+8就是func2的地址。

再看一下类的继承,derived类的父类是base类,如上图,在derived类的构造函数中,会先执行base类的构造函数,再进行derived类的初始化,derived类的虚表函数中有三个函数指针,前两个都是base类的,第三个是自己的。我们将derived类自己的虚函数注释掉,再看看它的虚表会有什么变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
class derived :public base {//派生类
public:
derived() {
printf("derived constructor\n");
}
// virtual void v_func() {//虚函数
// printf("derived v_func()");
// }
~derived() {
printf("derived destructor\n");
}
};

image-20240427134139026

我们看到,即使derived没有自己的虚函数,它自己也有一套独立的虚表,内容和base类的一样。

再看到全局变量类,并且有虚函数的情况

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <iostream>

using namespace std;

class Global
{
public:
Global()
{ // 无参构造函数
printf("Global\n");
}
Global(int n)
{ // 有参构造函数
printf("Global(int n) %d\n", n);
}
Global(const char *s)
{ // 有参构造函数
printf("Global(char *s) %s\n", s);
}
virtual ~Global()
{ // 虚析构函数
printf("~Global()\n");
}
void show()
{
printf("Object Addr: 0x%p", this);
}
};
Global g_global1;
Global g_global2(10);
Global g_global3("hello C++");
int main(int argc, char *argv[])
{
g_global1.show();
g_global2.show();
g_global3.show();
return 0;
}

虚表应该在类的构造函数中初始化,但是在本例中,类为全局变量,不在main函数中声明,那么此时虚表应该在何时初始化呢?

在执行main函数之前会调用init_array中的函数,全局类的初始化函数就放在init_array中,称之为构造代理函数

image-20240427141158911

image-20240427141436812

image-20240427141445629

最终在__static_initialization_and_destruction_0中进行初始化,__cxa_atexit函数将在main函数结束时调用,用于析构全局类。

多重继承

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <iostream>

using namespace std;

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;
}

定义了一个sofa类和一个bed类,sofabed类继承自这两个类。

image-20240427143508629

sofabed类的构造函数会依次执行sofa和bed的构造函数

image-20240427145616527

这两个分别是sofa和bed的虚表指针,当sofa和bed构造完成之后就会执行sofabed的构造函数

image-20240427150002214

sofabed会将两个虚表指针修改为自身的虚表指针。可见,在多重继承中,子类虚表指针的个数取决于继承的父类的个数,有几个父类便会出现几个虚表指针。

再看有纯虚函数的类,也就是抽象类

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 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;
}

image-20240427155637587

抽象类也有虚表,只不过虚表函数什么都没干。

虚继承

cpp

B继承于A,C继承于A,D继承于B和C

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
#include <stdio.h>
// 定义家具类,虚基类,等同于类A
class Furniture
{
public:
Furniture()
{
printf("Furniture::Furniture()\n");
price = 0;
}
virtual ~Furniture()
{ // 家具类的虚析构函数
printf("Furniture::~Furniture()\n");
}
virtual int getPrice()
{ // 获取家具价格
printf("Furniture::getPrice()\n");
return price;
};

protected:
int price; // 家具类的成员变量
};
// 定义沙发类,继承自类Furniture,等同于类B
class Sofa : virtual public Furniture
{
public:
Sofa()
{
printf("Sofa::Sofa()\n");
price = 1;
color = 2;
}
virtual ~Sofa()
{ // 沙发类虚析构函数
printf("Sofa::~Sofa()\n");
}
virtual int getColor()
{ // 获取沙发颜色
printf("Sofa::getColor()\n");
return color;
}
virtual int sitDown()
{ // 沙发可以坐下休息
return printf("Sofa::sitDown()\n");
}

protected:
int color; // 沙发类成员变量
};
// 定义床类,继承自类Furniture,等同于类C
class Bed : virtual public Furniture
{
public:
Bed()
{
printf("Bed::Bed()\n");
price = 3;
length = 4;
width = 5;
}
virtual ~Bed()
{ // 床类的虚析构函数
printf("Bed::~Bed()\n");
}
virtual int getArea()
{ // 获取床面积
printf("Bed::getArea()\n");
return length * width;
}
virtual int sleep()
{ // 床可以用来睡觉
return printf("Bed::sleep()\n");
}

protected:
int length; // 床类成员变量
int width;
};
// 子类沙发床的定义,派生自类Sofa和类Bed,等同于类D
class SofaBed : public Sofa, public Bed
{
public:
SofaBed()
{
printf("SofaBed::SofaBed()\n");
height = 6;
}
virtual ~SofaBed()
{ // 沙发床类的虚析构函数
printf("SofaBed::~SofaBed()\n");
}
virtual int sitDown()
{ // 沙发可以坐下休息
return printf("SofaBed::sitDown()\n");
}
virtual int sleep()
{ // 床可以用来睡觉
return printf("SofaBed::sleep()\n");
}
virtual int getHeight()
{
printf("SofaBed::getHeight()\n");
return height;
}

protected:
int height; // 沙发类的成员变量
};
int main(int argc, char *argv[])
{
SofaBed sofabed;
Furniture *p1 = &sofabed; // 转换成虚基类指针
Sofa *p2 = &sofabed; // 转换成父类指针
Bed *p3 = &sofabed; // 转换成父类指针
printf("%p %p %p\n", p1, p2, p3);
return 0;
}

看到sofabed的构造函数

image-20240427160816242

furniture传入的参数是*this + 40,说明furniture这个基类位于sofabed类的最底部,sofa类位于最上方,bed类位于sofa类下方。

image-20240427162359350

开头0x10字节为sofa类,紧跟着的0x18字节为bed类,后面的6是sofabed类的height,之后的就是furniture类了。

虚基类继承只会初始化一次虚基类。

参考连接:

1.c++ 机制逆向分析 – wsxk’s blog – 小菜鸡

2.C++逆向学习(四) 类 - 先知社区 (aliyun.com)

--- 本文结束 The End ---