本文通过C++反编译,帮助理解C++中的一些概念(指针引用、this指针、虚函数、析构函数、lambda表达式),
希望能在深入理解C++其它一些高级特性(多重继承、RTTI、异常处理)能起到抛砖引玉的作用吧
常用反汇编工具有:objdump、IDA Pro、godbolt
以下代码均使用x86-64 gcc 6.3编译。
指针和引用
引用类型的存储方式和指针是一样的,都是使用内存空间存放地址值。
只是引用类型是通过编译器实现寻址,而指针需要手动寻址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| void funRef(int &ref){ ref++; } int main(){ int var = 0x41; int *pnVar = &var; char *pcVar = (char*)&var; printf("%s",pcVar); funRef(var); return 0; }
|
用godbolt查看的效果如图,C++代码与对应的汇编代码用相同的颜色标注,非常方便查看。
switch
在分支数少的情况下可以用if–else if模拟,
但是分支比较大的情况下,需要比较的次数太多,
如果是有序线性的数值,可将每个case语句块的地址预先保存在数组中,
考察switch语句的参数,并依次查询case语句块地址的数组,
从而得到对应case语句块的首地址,
这样可以降低比较的次数,提升效率。
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
| int main(){ int nIdx=1; scanf("%d",&nIdx); int result = 0; switch(nIdx){ case 1: result = 1; break; case 2: result = 2; break; case 3: result = 3; break; case 5: result = 3; break; case 7: result = 3; break; } return result; }
|
如下图,编译器把switch跳转表放到了.L4所指向的区域,其中的元素.L2、.L3 … .L8指向case对应代码地址。
this指针
this指针中保存了所属对象的首地址。
在调用成员函数的过程中,编译器利用rdi寄存器保存了对象的首地址,
并以寄存器传参的方式传递到成员函数中。
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 Location{ public: Location(){ m_x = 1; m_y = 2; } short getY(){ return m_y; } private: int m_x; short m_y; }; int main(){ Location loc; short y = loc.getY(); return 0; }
|
对应的汇编如下:
虚函数和虚表
编译器会为每一个包含虚函数的类(或通过继承得到的子类)生成一个表,其中包含指向类中每一个虚函数的指针。
这样的表就叫做虚表(vtable)。
此外,每个包含虚函数的类都获得另外一个数据成员,用于在运行时指向适当的虚表。
这个成员通常叫做虚表指针(vtable pointer),并且是类中的第一个数据成员。
在运行时创建对象时,对象的虚表指针将设置为指向合适的虚表。
如果该对象调用一个虚函数,则通过在该对象的虚表中进行查询来选择正确的函数。
代码举例如下,详细代码在这里。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| class BaseClass { public: BaseClass(){x=1;y=2;}; virtual void vfunc1() = 0; virtual void vfunc2(){}; virtual void vfunc3(); virtual void vfunc4(){}; void hello(){printf("hello,y=%d",this->y);}; private: int x; int y; }; class SubClass : public BaseClass { public: SubClass(){z=3;}; virtual void vfunc1(){}; virtual void vfunc3(); virtual void vfunc5(){}; private: int z; };
|
虚表布局
下图是一个简化后的内存布局,它动态分配了一个SubClass类型的对象,编译器会确保该对象的第一个字段虚表指针指向正确的虚表。虚表指向编译器为每个类在只读段创建的一块区域,即虚表,类似于数组,其中的大部分元素指向在代码段中的成员函数地址。C++编译器会在编译阶段给这些函数名做name mangling,以实现c++中函数重载、namespace等标准。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| vtable for SubClass: .quad 0 .quad typeinfo for SubClass .quad SubClass::vfunc1() .quad BaseClass::vfunc2() .quad SubClass::vfunc3() .quad BaseClass::vfunc4() .quad SubClass::vfunc5() vtable for BaseClass: .quad 0 .quad typeinfo for BaseClass .quad __cxa_pure_virtual .quad BaseClass::vfunc2() .quad BaseClass::vfunc3() .quad BaseClass::vfunc4()
|
SubClass 中包含两个指向属于BaseClass的函数( BaseClass::vfunc2 和 BaseClass::vfunc4)的指针。
这是因为 SubClass 并没有重写这2个函数,而是直接继承自BaseClass 。
由于没有针对纯虚函数BaseClass::vfunc1的实现,因此,在 BaseClass的虚表中并没有存储 vfunc1 的地址。
这时,编译器会插入一个错误处理函数的地址,名为 purecall,万一被调用,它会令程序终止或者其他编译器想要发生的行为。
另外,一般的成员函数不在虚表里面,因为不涉及动态调用,如BaseClass中的hello()函数。
###创建对象
这里已在堆上动态创建对象为例。
调用new操作符,在堆上动态分配一块SubClass大小的内存,rax指向这块内存的开始。
SubClass需要的内存大小为24字节=8(虚表指针)+4*3(3个int类型的成员变量)+4(内存对齐)
对象首地址的值作为参数调用SubClass构造函数。
1
| BaseClass *a_ptr = new SubClass();
|
1 2 3 4 5 6 7
| main: mov edi, 24 call operator new(unsigned long) mov rbx, rax mov rdi, rbx call SubClass::SubClass()
|
SubClass的构造函数,在完成自身的任务之前会调用基类的构造函数,然后对this指针的内存的虚表指针修改为指向SubClass自身的虚表。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| SubClass::SubClass(): push rbp mov rbp, rsp sub rsp, 16 mov QWORD PTR [rbp-8], rdi mov rax, QWORD PTR [rbp-8] mov rdi, rax call BaseClass::BaseClass() mov edx, OFFSET FLAT:vtable for SubClass+16 mov rax, QWORD PTR [rbp-8] mov QWORD PTR [rax], rdx mov rax, QWORD PTR [rbp-8] mov DWORD PTR [rax+16], 3 nop leave ret
|
BaseClass的构造函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| BaseClass::BaseClass(): push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi mov edx, OFFSET FLAT:vtable for BaseClass+16 mov rax, QWORD PTR [rbp-8] mov QWORD PTR [rax], rdx mov rax, QWORD PTR [rbp-8] mov DWORD PTR [rax+8], 1 mov rax, QWORD PTR [rbp-8] mov DWORD PTR [rax+12], 2 nop pop rbp ret
|
调用成员函数
1、非虚函数
hello()是类BaseClass中的非虚成员函数,不需要通过虚表查找,编译器直接生成调用语句call BaseClass::hello()
,并且第一个参数默认为this指针。
1 2 3
| BaseClass *a_ptr = new SubClass(); a_ptr->hello();
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| main: mov rdi, rax call BaseClass::hello()
.LC0: .string "hello\357\274\214y=%d" BaseClass::hello(): push rbp mov rbp, rsp sub rsp, 16 mov QWORD PTR [rbp-8], rdi mov rax, QWORD PTR [rbp-8] mov eax, DWORD PTR [rax+12] mov esi, eax mov edi, OFFSET FLAT:.LC0 mov eax, 0 call printf nop leave ret
|
2、虚函数
a_ptr是BaseClass类型的指针,动态分配的是SubClass类型的内存。
call_vfunc函数的参数是基类BaseClass,再调用vfunc3函数时需要先根据虚表指针定位到虚表,再通过偏移,解引用找到vfunc3的代码段地址,完成调用。
1 2 3 4 5 6 7 8
| int main(){ BaseClass *a_ptr = new SubClass(); call_vfunc(a_ptr); } void call_vfunc(BaseClass *a) { a->vfunc3(); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| main: mov rax, QWORD PTR [rbp-24] mov rdi, rax call call_vfunc(BaseClass*) call_vfunc(BaseClass*): push rbp mov rbp, rsp sub rsp, 16 mov QWORD PTR [rbp-8], rdi mov rax, QWORD PTR [rbp-8] mov rax, QWORD PTR [rax] add rax, 16 mov rax, QWORD PTR [rax] mov rdx, QWORD PTR [rbp-8] mov rdi, rdx call rax nop leave ret
|
析构函数
这里以堆分配的对象析构为例,完整代码在这里。
堆分配的对象的析构函数在分配给对象的内存释放之前通过 delete 操作符调用。
其过程如下:
1、如果类拥有任何虚函数,则还原对象的虚表指针,使其指向相关类的虚表。如果一个子类在创建过程中覆盖了虚表指针,就需要这样做。
2、执行程序员为析构函数指定的代码。
3、如果类拥有本身就是对象的数据成员,则执行这些成员的析构函数。
4、如果对象拥有一个超类,则调用超类的析构函数
5、如果是释放堆的对象,则用一个代理析构函数执行1~4步骤,并在最后调用delete操作符释放堆上的对象。
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
| class BaseClass { public: BaseClass(){x=1;y=2;}; virtual ~BaseClass(){printf("~BaseClass()\n");}; virtual void vfunc1() = 0; private: int x; int y; }; class SubClass : public BaseClass { public: SubClass(){z=3;}; virtual ~SubClass(){printf("~SubClass()\n");}; virtual void vfunc1(){}; private: int z; }; int main() { BaseClass *a_ptr = new SubClass(); delete a_ptr; }
|
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
| vtable for SubClass: .quad 0 .quad typeinfo for SubClass .quad SubClass::'scalar deleting destructor' .quad SubClass::~SubClass() .quad SubClass::vfunc1() main: mov QWORD PTR [rbp-24], rbx cmp QWORD PTR [rbp-24], 0 je .L9 mov rax, QWORD PTR [rbp-24] mov rax, QWORD PTR [rax] add rax, 8 mov rax, QWORD PTR [rax] mov rdx, QWORD PTR [rbp-24] mov rdi, rdx call rax SubClass::'scalar deleting destructor': push rbp mov rbp, rsp sub rsp, 16 mov QWORD PTR [rbp-8], rdi mov rax, QWORD PTR [rbp-8] mov rdi, rax call SubClass::~SubClass() mov rax, QWORD PTR [rbp-8] mov esi, 24 mov rdi, rax call operator delete(void*, unsigned long) leave ret .LC1: .string "~SubClass()" SubClass::~SubClass(): push rbp mov rbp, rsp sub rsp, 16 mov QWORD PTR [rbp-8], rdi mov edx, OFFSET FLAT:vtable for SubClass+16 mov rax, QWORD PTR [rbp-8] mov QWORD PTR [rax], rdx mov edi, OFFSET FLAT:.LC1 call puts mov rax, QWORD PTR [rbp-8] mov rdi, rax call BaseClass::~BaseClass() nop leave ret
.LC0: .string "~BaseClass()" BaseClass::~BaseClass(): push rbp mov rbp, rsp sub rsp, 16 mov QWORD PTR [rbp-8], rdi mov edx, OFFSET FLAT:vtable for BaseClass+16 mov rax, QWORD PTR [rbp-8] mov QWORD PTR [rax], rdx mov edi, OFFSET FLAT:.LC0 call puts nop leave ret
|
通过分析C++析构函数的调用过程,我们就知道了为什么C++基类的析构函数要声明为virtual了。我们希望当调用C++基类BaseClass的析构函数时能够触发动态绑定,能够找到当前对象所属类的虚函数表中的析构函数。
如果不声明BaseClass的析构函数为virtual,那么在调用delete a_ptr
时,将只会释放BaseClass大小的内存,给SubClass中成员变量分配的内存将得不到释放,从而导致内存泄漏。
C++11中的Lambda表达式
lambda表达式表示一个可调用的代码单元。可以理解为一个未命名的内联函数。
lambda表达式具有如下形式:
1
| [capture list](parameter list) -> return type {function body}
|
下面定义了一个C++函数,其中有一个lambda表达式。v1之前的&符号指出v1是以引用方式捕获,当lambda返回v1时,它返回的是v1指向对象的值,所以j的值是0,而不是42.
1 2 3 4 5 6
| void fcn1(){ int v1 =42; auto f= [&v1] {return v1;}; v1 = 0; auto j = f(); }
|
对应的反汇编代码如下,可以看到编译器为fcn1中的lambda表达式在代码段中生成了一段指令,当调用这个lambda时就会执行到这段指令,跟普通的函数调用一致。
可以看出传递给fcn1()::{lambda()#1}
函数的参数rdi的值其实就是v1变量的地址,所以这个lambda是是采用引用方式捕获变量的。
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
| .Ltext0: fcn1()::{lambda()#1}::operator()() const: push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi mov rax, QWORD PTR [rbp-8] mov rax, QWORD PTR [rax] mov eax, DWORD PTR [rax] pop rbp ret fcn1(): push rbp mov rbp, rsp sub rsp, 16 mov DWORD PTR [rbp-8], 42 lea rax, [rbp-8] mov QWORD PTR [rbp-16], rax mov DWORD PTR [rbp-8], 0 lea rax, [rbp-16] mov rdi, rax call fcn1()::{lambda()#1}::operator()() const mov DWORD PTR [rbp-4], eax nop leave ret
|
参考
《IDA Pro权威指南》
《C++反汇编与逆向分析技术揭秘》
《C++ Primer(第5版)》