C++虚函数调用过程深度理解(二)

前言

在上文C++虚函数调用过程深度理解中,笔者探讨验证了关于虚函数调用过程的一些理解。

但回顾后却发现仍然略过了一些细节,这部分细节是关于虚函数调用时this指针如何转换得到目标虚函数地址。上文有所涉及,但没有深入。接下来,笔者将继续验证这部分细节。

  • 依然使用前文示例代码中的菱形继承类模型来验证。

验证

对main函数中的语句略微修改,如下:

int main(int argc, char* argv[]) {
    DiamondDerived diamond;

    DerivedB *tmp = ⋄

    Base* base = static_cast<Base*>(tmp);

    base->VirtualFunc("called_from_main");

    return 0;
}

该部分反汇编,笔者注释后如下:

  • 先看DiamondDerived diamond;对应的汇编代码
DiamondDerived diamond;

;; 将edx寄存器设置1,入参
;; ecx edx 一般为函数入参寄存器
00007FF76566AE8C  mov         edx,1  

;; 取diamond的内存地址赋值给rcx寄存器
00007FF76566AE91  lea         rcx,[diamond]  

;; 调用构造函数
00007FF76566AE95  call        DiamondDerived::DiamondDerived (07FF765661249h)  

;; 占位,内存对齐。这里是指令对齐,指令加载在内存中。
00007FF76566AE9A  nop  

DerivedB *tmp = &diamond;对应汇编:

;; 取diamond的内存地址赋值给rax寄存器
00007FF76566AE9B  lea         rax,[diamond]  

;; 检查rax寄存器的值是否等于0
00007FF76566AE9F  test        rax,rax  

;;上一句判断中若相等,则跳转到目标内存07FF76566AEB5h,可以在下面语句中找到。
00007FF76566AEA2  je          __$EncStackInitStart+5Eh (07FF76566AEB5h)  

;; 取diamond的内存地址到rax寄存器
00007FF76566AEA4  lea         rax,[diamond]  

;; rax寄存器加载的地址加8,即diamond内存地址向高偏移8字节
;; 这是编译器的设置,将diamond的内存地址偏移8字节,这里应该是diamond对象中DerivedB的虚函数表指针。
00007FF76566AEA8  add         rax,8  

;; 将rax加载的地址赋值给栈上临时变量
00007FF76566AEAC  mov         qword ptr [rbp+198h],rax  

;; 跳转至内存07FF76566AEC0h 即转到下面这句 00007FF76566AEC0  rax,qword ptr [rbp+198h]  
00007FF76566AEB3  jmp         __$EncStackInitStart+69h (07FF76566AEC0h)  

;; 将栈上临时变量设置0
00007FF76566AEB5  mov         qword ptr [rbp+198h],0  

;;读取栈上临时变量的值赋值给rax,即diamond内存偏移8字节的内存地址赋值给rax
00007FF76566AEC0  mov         rax,qword ptr [rbp+198h]  

;; 将rax加载的地址值写入tmp对应内存地址,大小为8字节。
00007FF76566AEC7  mov         qword ptr [tmp],rax  


Base* base = static_cast<Base*>(tmp);对应汇编:

;; 比较tmp内存地址处的8字节数据是否等于0
00007FF76566AECB  cmp         qword ptr [tmp],0  

;; 若不等于0,则跳转至内存07FF76566AEDFh,即这句 00007FF76566AEDF  mov         rax,qword ptr [tmp]  
00007FF76566AED0  jne         __$EncStackInitStart+88h (07FF76566AEDFh)  

;; 将栈上临时变量的值设为0
00007FF76566AED2  mov         qword ptr [rbp+198h],0  

;;跳转至内存 07FF76566AEFBh
00007FF76566AEDD  jmp         __$EncStackInitStart+0A4h (07FF76566AEFBh)  

;; 读tmp内存的8字节数据到rax。
;;
;; 经过上一步DerivedB *tmp = &diamond; 的解析后,
;;   这里tmp存储的值 = (diamond内存地址 + 8) = 一个内存地址(其8字节内容目测是DerivedB虚函数表的内存地址)
;;
;; rax = tmp = (diamond内存地址 + 8)
00007FF76566AEDF  mov         rax,qword ptr [tmp]  

;; 从rax加载的地址读取8字节数据到rax寄存器中,这是解指针。
;; rax = 读取地址(diamond内存地址 + 8)处的8字节数据 = DerivedB虚函数表的内存地址
00007FF76566AEE3  mov         rax,qword ptr [rax]  

;; 从rax加载的地址偏移4后读取4字节数据赋值给rax,也是读地址,解指针
00007FF76566AEE6  movsxd      rax,dword ptr [rax+4]  

;; 将tmp对应的内存处读取8字节数据给rcx
;; rcx = tmp
00007FF76566AEEA  mov         rcx,qword ptr [tmp]  

;; rcx = rcx + rax
;; tmp为DerivedB虚函数表指针,rcx = tmp,rax为虚函数表指针偏移4字节后的4字节数据,验证确定它是某个偏移值

;; 笔者多次验证后,这里根据对象布局计算偏移,最后计算出的地址为存储虚基表指针的地址。
;; 虚基表指针vbtbl,虚表指针vptr。
00007FF76566AEEE  add         rcx,rax  

;; rax = rcx
00007FF76566AEF1  mov         rax,rcx  

;; rax的值写入8字节数据到栈上临时变量对应的内存处
00007FF76566AEF4  mov         qword ptr [rbp+198h],rax  

;; 将栈上临时变量的值赋值给rax寄存器。前面有语句跳转至这里。
00007FF76566AEFB  mov         rax,qword ptr [rbp+198h]  

;;将rax的值写入8字节数据到base对应内存处
00007FF76566AF02  mov         qword ptr [base],rax  

base->VirtualFunc("called_from_main");对应汇编:

;; 取字符串常量的内存地址给rdx,即rdx = 07FF76566FCD0h
00007FF76566AF06  lea         rdx,[string "called_from_main" (07FF76566FCD0h)]  

;; 加载栈上临时变量的内存地址给rcx,作入参
00007FF76566AF0D  lea         rcx,[rbp+148h]  

;;调用构造
00007FF76566AF14  call        std::basic_string<char,std::char_traits<char>,std::allocator<char> >::basic_string<char,std::char_traits<char>,std::allocator<char> > (07FF7656612C6h)  

;; 占位操作,内存对齐;
00007FF76566AF19  nop  

;; 从base对应内存处读取8字节数据到rax中,即base指针的值给rax
;; rax = base
00007FF76566AF1A  mov         rax,qword ptr [base]  

;; 从rax上对应的内存地址处读取8字节数据到rax
;; rax = *rax = *base 得到前面base指针指向内存地址的8字节数据
00007FF76566AF1E  mov         rax,qword ptr [rax]  

;; 取栈上临时变量内存地址赋值给rdx,做下列调用入参
00007FF76566AF21  lea         rdx,[rbp+148h]  

;; 读取base对应内存地址处8字节数据给rcx
;; rcx = base,当做下列调用的this指针
00007FF76566AF28  mov         rcx,qword ptr [base]  

;; 调用rax存储的内存地址偏移8字节位置的函数
00007FF76566AF2C  call        qword ptr [rax+8]  

00007FF76566AF2F  nop  
00007FF76566AF30  lea         rcx,[rbp+148h]  
00007FF76566AF37  call        std::basic_string<char,std::char_traits<char>,std::allocator<char> >::~basic_string<char,std::char_traits<char>,std::allocator<char> > (07FF765661136h) 

跳转细节

  • 指令00007FF76566AF2C call qword ptr [rax+8] 执行后跳转至:
;; 跳转到这句
00007FF76566173A  jmp         DiamondDerived::VirtualFunc (07FF765661E84h)

然后再跳转至:

00007FF765661E84  movsxd      rax,dword ptr [rcx-4]  
00007FF765661E88  sub         rcx,rax  
00007FF765661E8B  jmp         DiamondDerived::VirtualFunc (07FF7656615D2h)  

上面经过编译器的一些计算,再次跳转至:

00007FF7656615D2  jmp         DiamondDerived::VirtualFunc (07FF765664430h)

最后终于到了目标的虚函数位置:

;; rdx为最开始取的栈上临时变量地址
00007FF765664430  mov         qword ptr [rsp+10h],rdx  

;; rcx此时变换后应该为某个偏移值
00007FF765664435  mov         qword ptr [rsp+8],rcx  

;; 入栈
00007FF76566443A  push        rbp  
00007FF76566443B  push        rdi  
;; rsp指针向低地址移动,就是数据入栈后修改栈顶指针
00007FF76566443C  sub         rsp,0F8h  

;; 重新设置栈底指针
00007FF765664443  lea         rbp,[rsp+20h]  
00007FF765664448  lea         rcx,[__75408C75_main@cpp (07FF76567A032h)]  
00007FF76566444F  call        __CheckForDebuggerJustMyCode (07FF765661645h)  
        std::cout << "DiamondDerived::VirtualFunc " << this << " " << info << std::endl;
00007FF765664454  mov         rax,qword ptr [this]  

这上面的汇编中的各内存地址关系比较混乱,以下是笔者绘制的一张图可以加快理解:

  • rbp是栈底指针寄存器,rsp是栈顶指针寄存器。

在这里插入图片描述

  • 上图中的地址值及内存存储的值均为举例值,并不代表真实值,只表述各内存地址之间的关系。
  • 图片中上面一段内存上的0816244048十进制数字,在内存地址1624之间的内存存储着十六进制数值0xA00xA0为某个地址。
  • 下面一段内存显示内存0xA0的关系,0xA0偏移4字节后得到0xA4地址,在0xA4开始的4字节内存存储着十六进制数字18h

结尾

  • 行文至此,关于虚函数调用过程中指针的转换过程的分享就结束了。
  • 如果觉得还不错,你的点赞关注加收藏便是笔者继续更新的最大动力哦^_^
  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值