虚函数
引言
最近在找工作,投了一些c/c++开发工程师,几乎每一个面试官都会问我虚函数的问题,而我面试快一个月了,八股文还是没怎么背,一直在学习Linux驱动开发,这几天我狠下心来,花一天的时间好好整理了以下虚函数的知识点,做个总结,以至于后面再被面试官问到不会一脸懵逼。
虚函数的简介
c++中的三大核心机制是封装、继承、多态。多态分为静态多态和动态多态,静态多态是指函数重载和运算符重载,动态多态的体现则是虚函数。c++中实现虚函数功能的方式是做虚函数表(Virtual Table,VTBL)。在编译阶段,编译器会为每一个有虚函数的类产生一张虚表,每一个虚函数在表表中,虚表的每一项为虚函数的函数指针,这样,当类对象被定义时,对象中就有一个指向虚表的指针,这个指针就是vptr,当对象调用虚函数时,通过vptr找到虚表,再通过虚表找到虚函数的地址,这样就实现了动态绑定。
如何识别虚表
编写一个简单的例子,如下:
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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
class base
{
public:
base(std::string name)
{
this->member = 0;
this->member2 = 0;
std::cout << "base constructor " << name << std::endl;
}
~base()
{
std::cout << "base destructor " << std::endl;
}
virtual void func0(std::string str)
{
std::cout << "base func0 " << str << std::endl;
}
int base0_private_func(int a, int b)
{
std::cout << "base private_func " << a + b << std::endl;
return 0;
}
int member;
long long member2;
};
class base_fake
{
public:
base_fake(std::string name)
{
this->member = 1;
std::cout << "base_fake constructor " << name << std::endl;
}
~base_fake()
{
std::cout << "base_fake destructor " << std::endl;
}
virtual void func0_fake(std::string str)
{
std::cout << "base_fake func0_fake " << str << std::endl;
}
int member;
};
class letter : public base
{
public:
letter(std::string name) : base(name)
{
this->member = 11;
std::cout << "letter constructor " << name << std::endl;
}
~letter()
{
std::cout << "letter destructor " << std::endl;
}
virtual void func0(std::string str)
{
std::cout << "letter func0 " << str << std::endl;
}
virtual void func1(std::string str)
{
std::cout << "letter func1 " << str << std::endl;
}
int member = 0;
};
class number : public base
{
public:
number(std::string name) : base(name)
{
this->member = 22;
std::cout << "number constructor " << name << std::endl;
}
~number()
{
std::cout << "number destructor " << std::endl;
}
virtual void func0(std::string str)
{
std::cout << "number func0 " << str << std::endl;
}
virtual void func2(std::string str)
{
std::cout << "number func2 " << str << std::endl;
}
int member;
};
class misc : public letter, public number
{
public:
misc(std::string name) : letter(name), number(name)
{
this->member = 1122;
std::cout << "misc constructor " << name << std::endl;
}
~misc()
{
std::cout << "misc destructor " << std::endl;
}
virtual void func0(std::string str)
{
std::cout << "misc func0 " << str << std::endl;
}
virtual void func1(std::string str)
{
std::cout << "misc func1 " << str << std::endl;
}
virtual void func2(std::string str)
{
std::cout << "misc func2 " << str << std::endl;
}
void private_func(int a, int b)
{
std::cout << "misc private_func " << a + b << std::endl;
return;
}
int member;
};
class all_base : public base, public base_fake
{
public:
all_base(std::string name) : base(name), base_fake(name)
{
std::cout << "all_base constructor " << name << std::endl;
}
~all_base()
{
std::cout << "all_base destructor" << std::endl;
}
virtual void func0(std::string str)
{
std::cout << "all_base func0 " << str << std::endl;
}
virtual void func3(std::string str)
{
std::cout << "all_base func3 " << str << std::endl;
}
};
int main(void)
{
std::cout << "----------------" << std::endl;
all_base a("all_base");
a.func0("all_base");
a.func3("all_base");
std::cout << "----------------" << std::endl;
base b("base");
b.func0("all_base");
std::cout << "----------------" << std::endl;
misc m("misc");
m.func0("misc");
m.func1("misc");
m.func2("misc");
std::cout << "----------------" << std::endl;
return 0;
}output:
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----------------
base constructor all_base
base_fake constructor all_base
all_base constructor all_base
all_base func0 all_base
all_base func3 all_base
----------------
base constructor base
base func0 all_base
----------------
base constructor misc
letter constructor misc
base constructor misc
number constructor misc
misc constructor misc
misc func0 misc
misc func1 misc
misc func2 misc
----------------
misc destructor
number destructor
base destructor
letter destructor
base destructor
base destructor
all_base destructor
base_fake destructor
base destructor代码中类的继承关系如图1所示:
编译上述代码,用ghidra打开可执行文件,反编译分析该文件,分析程序流程
多重继承
按照程序执行流程,首先执行的是all_base类的构造函数,在all_base构造函数中调用了base和base_fake的构造函数,所以先分析base和base_fake的构造函数,如图2所示:
如图所示,在all_base类的构造函数中,先调用了base的构造函数,然后调用了base_fake的构造函数,最后初始化all_base类的VTBL和成员变量,这符合c++语法的构造顺序
先查看base的构造函数,如图3所示:
在图3中1处上一条汇编语句将this指针传入rax,然后初始化base类VTBL,2和3分别初始化了成员变量
再查看base_fake的构造函数,如图4所示:
在图4中1处汇编语句将this指针传入rax,然后初始化base_fake类VTBL,2初始化了成员变量
根据汇编语句和构造顺序可以得出这3个类的内存布局如下所示:
菱形继承
按照后续misc部分的执行流程,首先执行的是misc的构造函数,在misc构造函数中调用了letter和number的构造函数,在letter和number的构造函数中分别调用了base类构造函数
如图6所示,在misc类的构造函数中,先调用了letter的构造函数,然后调用了number的构造函数,最后初始化misc类的VTBL和成员变量,这符合c++语法的构造顺序
先查看letter的构造函数,如图7所示:
在图7中1处上一条汇编语句将this指针传入rax,然后2和3分别初始化了letter类VTBL和成员变量
再查看number的构造函数,如图8所示:
在图8中1处上一条汇编语句将this指针传入rax,然后2和3分别初始化了number类VTBL和成员变量
查看misc在执行letter和number的构造函数时后面的部分,如图9所示:
根据汇编语句和构造顺序可以得出这4个类的内存布局如下所示:
上述结构在gcc编译器中有一个比较让人诧异的地方,在misc中定义了int变量时,在后面不会有内存对齐的4字节,如果将int变量修改为long long变量,大小增加的为8字节,会增加内存对齐的4字节,如图11所示:
注意
根据上述分析,基类成员变量在内存中重复出现了两次,但按照c++虚继承语法规则,先构造基类并且只构造一次,查看资料了解编译器给构造函数多传入了一个参数,用来表示是否调用虚基类的构造,但是我在逆向的过程中并没有发现传入的参数
最后的菱形继承关系的内存布局和c++继承语法规则相冲突,个人一直无法理解,可能存在一些问题,文章仅供参考!
学习过程中还有很多不足,还望朋友们指正!