(Owed by: 春夜喜雨 http://blog.csdn.net/chunyexiyu)
1. 引言
C 调用中构造函数、分析函数和虚拟函数时,如何处理汇编码与我们通常的理解有什么不同? 通常我们知道,当函数调用时,类的实例会被引入this指针是真的吗?如何传入? 虚函数呢?虚函数表在哪里?虚函数指针是如何初始化的?
在下面的分析中使用VS分析,使用x通过配置简化汇编生成64位编译结果。 编译选项: 优化:已禁用(//Od) SDL检查:是否(//sdl-) 安全检查:禁止安全检查(//GS-)
2. 测试代码:
struct Demo {
Demo(){
} virtual ~Demo() {
} virtual const char* GetPtr() {
return str.c_str(); } std::string str; }; int main() {
Demo* pD = new Demo; const char* p = pD->GetPtr(); delete pD; return 0; }
根据Demo代码及实现特点: 可以知道Demo类占用的空间大小中有一个虚函数表: Demo大小为 sizeof(std::string) sizeof(void*) = 32 8 = 40 = 0x28h; 在虚函数表中放置两个函数指针,一个指向GetPtr,指向析构函数; 同时,std::string作为一个类别,结构函数将在Demo::Demo在调用过程中,分析函数调用Demo::~Demo中调;另外Demo::Demo成员变量虚函数表指针也需要初始化。
根据main代码及实现特点: 可以知道,会先从heap上new会调用一个例子Demo::Demo构造函数; 然后在虚函数表中调用虚函数Demo::GetPtr; 然后delete时,调用Demo::~Demo分析,并释放从heap申请空间;
3. Demo类
3.1 Demo-构造函数
struct Demo {
Demo(){
00007FF70A9D1380 mov qword ptr [rsp 8],rcx 00007FF70A9D1385 sub rsp,28h 00007FF70A9D1389 mov rax,qword ptr [this] 00007FF70A9D138E lea rcx,[Demo::`vftable' (07FF70A9D3F10h)]
00007FF70A9D1395 mov qword ptr [rax],rcx
00007FF70A9D1398 mov rax,qword ptr [this]
00007FF70A9D139D add rax,8
00007FF70A9D13A1 mov rcx,rax
00007FF70A9D13A4 call std::basic_string<char,std::char_traits<char>,std::allocator<char> >::basic_string<char,std::char_traits<char>,std::allocator<char> > (07FF70A9D1560h)
}
00007FF70A9D13A9 mov rax,qword ptr [this]
00007FF70A9D13AE add rsp,28h
00007FF70A9D13B2 ret
可以看到再Demo构造函数中:我们已知Demo中有两个变量,虚函数表指针 与 std::string类型变量。 首先把rcx传入参数,也即this指针(堆/栈地址)存入到address[rsp+8]的位置; 并把栈顶指针调减40字节=28h,以低地址为上,高地址为下的话,也即栈顶上移40字节
mov qword ptr[rsp+8], rcx sub rsp, 28h
接着把this指针放到rax上,并在rcx放入虚函数表地址,并rcx上虚函数地址放到this指针的前8个字节上
mov rax, qword ptr [this] lea rcx, [Demo::
vftable
(07FF70A9D3F10h)] mov qword ptr [rax], rcx
虚函数表07FF70A9D3F10h地址存储的值为:
0x00007FF70A9D3F10 10 14 9d 0a f7 7f 00 00 // 指向一段代码处理,这段代码处理中会调用Demo::~Demo,并在析构后调用delete pD来清除内存。 0x00007FF70A9D3F18 f0 13 9d 0a f7 7f 00 00 // 指向GetPtr函数00007FF70A9D13F0
然后,把this指针+8字节放入到rax,之后从rax放入到rcx作为输入,调用std::basic_string的构造函数
mov rax, qword ptr [this] add rax, 8 mov rcx, rax call std::basic_string<…>::basic_string<…>
最后,把this指针放入到rax,作为返回值 把一开始rsp的退栈再加回去,rsp指针调增40字节=28h,以低地址为上,高地址为下的话,也即栈顶下移40字节
mov rax, qword ptr [this] add rsp, 28h ret
从这个分析中可以看出:
- 首先rcx总是作为函数的调用输入项参数,由调用者设值;
- rax作为返回值设置的项,对于结构体存的是其地址;
- 构造函数把this自身地址作为返回项;
- 像std::string成员变量,构造函数中会自动调用这些非基本类型的构造函数,完成这些类型的成员变量的构造。
3.2. Demo-析构函数
virtual ~Demo() {
00007FF70A9D13C0 mov qword ptr [rsp+8],rcx
00007FF70A9D13C5 sub rsp,28h
00007FF70A9D13C9 mov rax,qword ptr [this]
00007FF70A9D13CE lea rcx,[Demo::`vftable' (07FF70A9D3F10h)]
00007FF70A9D13D5 mov qword ptr [rax],rcx
}
00007FF70A9D13D8 mov rax,qword ptr [this]
00007FF70A9D13DD add rax,8
00007FF70A9D13E1 mov rcx,rax
00007FF70A9D13E4 call std::basic_string<char,std::char_traits<char>,std::allocator<char> >::~basic_string<char,std::char_traits<char>,std::allocator<char> > (07FF70A9D1530h)
00007FF70A9D13E9 add rsp,28h
00007FF70A9D13ED ret
首先也是把rcx传入参数,也即this指针(堆/栈地址)存入到address[rsp+8]的位置; 并把栈顶指针调减40字节=28h
mov qword ptr [rsp+8], rcx sub rsp, 28h
把this指针放到rax上,并在rcx放入虚函数表地址,并rcx上虚函数地址放到this指针的前8个字节上
mov rax, qword ptr [this] lea rcx, [Demo::
vftable
(07FF70A9D3F10h)] mov qword ptr [rax], rcx
把this指针+8字节放入到rax,之后从rax放入到rcx作为输入,调用std::basic_string的析构函数
mov rax, qword ptr [this] add rax, 8 mov rcx, rax call std::basic_string<…>::~basic_string<…>
最后,把一开始rsp的退栈再加回去,rsp指针调增40字节=28h;
add rsp, 28h ret
从这块分析中看到:
- 析构函数最后没有专门对返回寄存器rax上设值;上面方法里,返回值rax存储的是调用basic_string析构函数设值的值,也可能没有设值,毕竟是析构函数;
- 析构函数中,重新把虚函数表指针又设到自己的前8个字节里了(这个可能是为了能继续往父类上层进行调用码?因为如果从子类来的话,还真没有子类的祖父的析构函数指针。)
- 析构函数中,会自动调用非基本类型成员变量的析构函数。
3.3. Demo-普通虚函数GetPtr
virtual const char* GetPtr()
{
00007FF70A9D13F0 mov qword ptr [rsp+8],rcx
00007FF70A9D13F5 sub rsp,28h
return str.c_str();
00007FF70A9D13F9 mov rax,qword ptr [this]
00007FF70A9D13FE add rax,8
00007FF70A9D1402 mov rcx,rax
00007FF70A9D1405 call std::basic_string<char,std::char_traits<char>,std::allocator<char> >::c_str (07FF70A9D1500h)
}
00007FF70A9D140A add rsp,28h
00007FF70A9D140E ret
GetPtr是一个普通的虚函数,函数内部实现还比较简单 一开始也是把rcx传入参数,也即this指针(堆/栈地址)存入到address[rsp+8]的位置,并调整栈顶指针;
mov qword ptr [rsp+8], rcx sub rsp, 28h
接下来根据实现,调用成员变量的方法,成员变量str的位置在this+8字节位置,找到后存到rcx作为调用输入
mov rax, qword ptr[this] add rax, 8 mov rcx, rax call std::basic_string<…>::c_str
最后退栈,返回;此时可以看到rax没有主动设值,这个值直接使用c_str()调用时设上来的rax值;
add rsp, 28h ret
4. 主函数:main
305: int main()
306: {
00007FF70A9D1450 sub rsp,78h
00007FF70A9D1454 mov qword ptr [rsp+58h],0FFFFFFFFFFFFFFFEh
307: Demo* pD = new Demo;
00007FF70A9D145D mov ecx,28h
00007FF70A9D1462 call operator new (07FF70A9D190Ch)
00007FF70A9D1467 mov qword ptr [rsp+30h],rax
00007FF70A9D146C cmp qword ptr [rsp+30h],0
00007FF70A9D1472 je main+35h (07FF70A9D1485h)
00007FF70A9D1474 mov rcx,qword ptr [rsp+30h]
00007FF70A9D1479 call Demo::Demo (07FF70A9D1380h)
00007FF70A9D147E mov qword ptr [rsp+38h],rax
00007FF70A9D1483 jmp main+3Eh (07FF70A9D148Eh)
00007FF70A9D1485 mov qword ptr [rsp+38h],0
00007FF70A9D148E mov rax,qword ptr [rsp+38h]
00007FF70A9D1493 mov qword ptr [rsp+40h],rax
00007FF70A9D1498 mov rax,qword ptr [rsp+40h]
00007FF70A9D149D mov qword ptr [pD],rax
308: const char* p = pD->GetPtr();
00007FF70A9D14A2 mov rax,qword ptr [pD]
00007FF70A9D14A7 mov rax,qword ptr [rax]
00007FF70A9D14AA mov rcx,qword ptr [pD]
00007FF70A9D14AF call qword ptr [rax+8]
00007FF70A9D14B2 mov qword ptr [p],rax
309: delete pD;
00007FF70A9D14B7 mov rax,qword ptr [pD]
00007FF70A9D14BC mov qword ptr [rsp+48h],rax
00007FF70A9D14C1 mov rax,qword ptr [rsp+48h]
00007FF70A9D14C6 mov qword ptr [rsp+28h],rax
00007FF70A9D14CB cmp qword ptr [rsp+28h],0
00007FF70A9D14D1 je main+9Eh (07FF70A9D14EEh)
00007FF70A9D14D3 mov rax,qword ptr [rsp+28h]
00007FF70A9D14D8 mov rax,qword ptr [rax]
00007FF70A9D14DB mov edx,1
00007FF70A9D14E0 mov rcx,qword ptr [rsp+28h]
00007FF70A9D14E5 call qword ptr [rax]
00007FF70A9D14E7 mov qword ptr [rsp+50h],rax
00007FF70A9D14EC jmp main+0A7h (07FF70A9D14F7h)
00007FF70A9D14EE mov qword ptr [rsp+50h],0
310: return 0;
00007FF70A9D14F7 xor eax,eax
311: }
00007FF70A9D14F9 add rsp,78h
00007FF70A9D14FD ret
结合上面的构造函数/析构函数的分析,来看一看这块的实现 main函数一开始也是先调栈顶寄存器位置,以低地址为上,高地址为下的话,栈顶寄存器地址上移78h字节=120字节;
sub rsp, 78h mov qword ptr[rsp+58h], 0FFFFFFFFFFFFFFFEh
调用new Demo操作,Demo成员变量占用空间32+8=40字节=28h; 把28h放入到ecx,作为new操作的输入; 把new操作的返回rax放入到[rsp+36h]地址上,并和0进行比较,确定是否申请到内存;申请不到时直接跳转;
mov ecx, 28h call operator new mov qword ptr [rsp+30h], rax cmp qword ptr[rsp+30h], 0 je main+35h(07FF70A9D148Eh) –>跳转执行代码 07FF70A9D148E mov qword ptr[rsp+38h], 0;然后向下执行 mov rax, qword ptr[rsp+38h]…
把new到的空间指针地址放入到rcx,然后调用Demo的构造函数; 接着把Demo构造函数返回rax放入到地址[rsp+38h];然后又在[rsp+40h]地址上存了一圈,放回到rax,把地址最终放入到pD地址上。
mov rcx, qword ptr [rsp+30h] call Demo::Demo(07FF70A9D1380h) mov qword ptr [rsp+38h], rax //mov rax, qword ptr[rsp+38h] //mov qword ptr [rsp+40h], rax //mov rax, qword ptr[rsp+40h] mov qword ptr[pD], rax
调用Demo::GetPtr这一虚函数: 从上面Demo构造函数知道,虚函数表指针成员变量存储在Demo的前8个字节中,通常析构函数指针在虚函数表的第一个位置,GetPtr指针则在第二个位置; pD是指针,前8个字节虚函数表指针是指针的指针,所以借用rax做指针地址重定向;
mov rax, qword ptr[pD] // 把pD指针地址放入rax mov rax, qword ptr[rax] //把rax指向值作为地址,重定向后,新值放入rax,相当于把虚
函数表指针放入到rax 把pD放入到rcx中,相当于把pD的this作为函数输入,然后调用rax虚函数表指针指向位置+偏移8字节位置的函数; 并把执行结果放入到结果变量[p]位置
mov rcx, qword ptr[pD] call qword ptr[rax+8] mov qword ptr[p], rax
调用delete pD操作: 此时能够预料到的事情是,要调用虚析构函数啦,应该和上面差不多,也需要重订向出虚函数指针进行调用。 先是检查了下指针是否为0|Null,为Null时就不往下执行了
mov rax, qword ptr[pD] //mov qword ptr[rsp+48h], rax //mov rax, qword ptr[rsp+48h] mov qword ptr[rsp+28h], rax cmp qword ptr[rsp+28h], 0 je main+9Eh
指针重定向,把rax指向到虚函数指针
mov rax, qword ptr[rsp+28h] mov rax, qword ptr[rax]
把pD指针放入rcx作为数据,通过rax上虚函数表指针位置,来调用虚析构函数; 虚析构函数执行结果rax被存到了[rsp+50h]位置
mov edx, 1 mov rcx, qword ptr[rsp+28h] call qword ptr[rax] mov qword[rsp+50h], rax
最后,对eax进行xor操作,对eax清0,作为返回值;然后退栈返回;
xor eax, eax add rsp, 78h ret
5. Demo-vector deleting destructor
Demo-vector deleting destructor 虚函数表析构函数指向位置的代码处理
Demo::`vector deleting destructor':
00007FF70A9D1410 mov dword ptr [rsp+10h],edx
00007FF70A9D1414 mov qword ptr [rsp+8],rcx
00007FF70A9D1419 sub rsp,28h
00007FF70A9D141D mov rcx,qword ptr [this]
00007FF70A9D1422 call Demo::~Demo (07FF70A9D13C0h)
00007FF70A9D1427 mov eax,dword ptr [rsp+38h]
00007FF70A9D142B and eax,1
00007FF70A9D142E test eax,eax
00007FF70A9D1430 je Demo::`scalar deleting destructor'+31h (07FF70A9D1441h)
00007FF70A9D1432 mov edx,28h
00007FF70A9D1437 mov rcx,qword ptr [this]
00007FF70A9D143C call operator delete (07FF70A9D1CCCh)
00007FF70A9D1441 mov rax,qword ptr [this]
00007FF70A9D1446 add rsp,28h
00007FF70A9D144A ret
在main函数中,看到调用析构函数指向时,输入参数两个,一个是常量1,一个是pD地址(也即pD的this地址)
mov edx, 1 mov rcx, qword ptr[rsp+28h]
下来分析这段代码的处理: 先进行输入参数保存,然后上移栈顶指针
mov dword ptr [rsp+10h], edx mov qword ptr [rsp+8h], rcx sub rsp, 28h
接着设值this地址到rcx作为输入,调用析构函数
mov rcx, qword ptr[this] call Demo::~Demo
把输入edx上的值给eax,并和1做逻辑与; 通过test eax指令,判断eax为0时则不走后续的delete操作,这个发生在输入的edx为0的情况; 也就是输入edx为0时不用走delete,只走析构; edx为1时(或最后一位为1时),走完析构走delete内存删除;
mov eax, dword ptr[rsp+38h] and eax, 1 test eax, eax je Demo::
vector deleting destructor
+31h(07FF70A9D1441h) // 跳转到mov rax, qword ptr[this] --> add rsp 28h --> ret
调用内存删除: 参数上,Demo的大小40字节=28h存入到edx,this指针存入到rcx;然后调用delete清除this指向的该40字节内存;
mov edx, 28h mov rcx, qword ptr [this] call operator delete
最后,把this作为结果返回放入rax寄存器,回退栈
mov rax, qword ptr [this] add rsp, 28h rt
附加验证:Demo栈上变量
当Demo是一个栈上变量,且并不是new出来的时候,是否还会涉及虚析构函数呢? 从下面的实例中可以得知: 在这种情况下,编译器直接锁定调用的虚函数,并不用去查虚函数表;包括虚析构函数,包括定义的虚函数。
代码使用:
int main()
{
Demo de;
const char* p = de.GetPtr();
return 0;
}
汇编码为:
305: int main()
306: {
00007FF6F40B1450 sub rsp,68h
00007FF6F40B1454 mov qword ptr [rsp+28h],0FFFFFFFFFFFFFFFEh
307: Demo de;
00007FF6F40B145D lea rcx,[de]
00007FF6F40B1462 call Demo::Demo (07FF6F40B1380h)
00007FF6F40B1467 nop
308: const char* p = de.GetPtr();
00007FF6F40B1468 lea rcx,[de]
00007FF6F40B146D call Demo::GetPtr (07FF6F40B13F0h)
00007FF6F40B1472 mov qword ptr [p],rax
309: return 0;
00007FF6F40B1477 mov dword ptr [rsp+20h],0
00007FF6F40B147F lea rcx,[de]
00007FF6F40B1484 call Demo::~Demo (07FF6F40B13C0h)
00007FF6F40B1489 mov eax,dword ptr [rsp+20h]
310: }
00007FF6F40B148D add rsp,68h
00007FF6F40B1491 ret
分析除函数入栈、退栈外的代码: 把变量地址[de]放入rcx作为参数,调用构造函数
lea rcx, [de] call Demo::Demo nop
把变量地址[d]放入rcx作为参数,直接调用Demo::GetPtr该虚函数;并把结果返回rax赋值该[p]
lea rcx, [de] call Demoe::GetPtr mov qword ptr[p], rax
把变量地址[de]放入rcx作为参数,直接调用Demo::~Demo虚析构处理;并给eax放入0,用于main返回值;
mov dword ptr[rsp+20h], 0 lea rcx, [de] call Demo::~Demo mov eax, dword ptr[rsp+20]
附加验证: placement new出的Demo变量
我们已知placement new是有调用new操作的,所以预测调用时会涉及到虚函数表查找函数,实时也确实如此。 另外placement new出的变量,不能使用delete删除内存,因为内存本就从别人那共享的,由别人来管理内存清理。 当然placement new出的变量,析构函数还是需要调用的,但因为是placement new出来的,需要显式的调用析构函数; 关于placement new的使用可以参考这个文章:https://www.cnblogs.com/fnlingnzb-learner/p/9279039.html
int main()
{
char a[120];
Demo* pD = new (a) Demo();;
const char* p = pD->GetPtr();
pD->Demo::~Demo();
return 0;
}
汇编码:
305: int main()
306: {
00007FF690E81460 sub rsp,0C8h
307: char a[120];
308: Demo* pD = new (a) Demo();;
00007FF690E81467 lea rdx,[a]
00007FF690E8146C mov ecx,28h
00007FF690E81471 call operator new (07FF690E81220h)
00007FF690E81476 mov qword ptr [rsp+28h],rax
00007FF690E8147B mov rcx,qword ptr [rsp+28h]
00007FF690E81480 call Demo::Demo (07FF690E81390h)
00007FF690E81485 mov qword ptr [pD],rax
309: const char* p = pD->GetPtr();
00007FF690E8148A mov rax,qword ptr [pD]
00007FF690E8148F mov rax,qword ptr [rax]
00007FF690E81492 mov rcx,qword ptr [pD]
00007FF690E81497 call qword ptr [rax+8]
00007FF690E8149A mov qword ptr [p],rax
310: pD->~Demo();
00007FF690E8149F mov rax,qword ptr [pD]
00007FF690E814A4 mov rax,qword ptr [rax]
00007FF690E814A7 xor edx,edx
00007FF690E814A9 mov rcx,qword ptr [pD]
00007FF690E814AE call qword ptr [rax]
311: return 0;
00007FF690E814B0 xor eax,eax
312: }
00007FF690E814B2 add rsp,0C8h
00007FF690E814B9 ret
从汇编代码中可以看到,placement new与new的差异是: 一是在rdx上多传入一个变量,传递使用的内存地址; 二是调用的new函数的重载函数位置不同,也即调用了一个有内存地址输入的函数;
lea rdx, [a] call operator new (07FF690E81220h)
(Owed by: 春夜喜雨 http://blog.csdn.net/chunyexiyu)