资讯详情

类构造函数与虚函数调用-汇编代码分析

(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

从这个分析中可以看出:

  1. 首先rcx总是作为函数的调用输入项参数,由调用者设值;
  2. rax作为返回值设置的项,对于结构体存的是其地址;
  3. 构造函数把this自身地址作为返回项;
  4. 像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

从这块分析中看到:

  1. 析构函数最后没有专门对返回寄存器rax上设值;上面方法里,返回值rax存储的是调用basic_string析构函数设值的值,也可能没有设值,毕竟是析构函数;
  2. 析构函数中,重新把虚函数表指针又设到自己的前8个字节里了(这个可能是为了能继续往父类上层进行调用码?因为如果从子类来的话,还真没有子类的祖父的析构函数指针。)
  3. 析构函数中,会自动调用非基本类型成员变量的析构函数。

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)

标签: d142对射式光电传感器

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台