前言
在上文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的内存地址赋值给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 = ⋄ 的解析后,
;; 这里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
是栈顶指针寄存器。
- 上图中的地址值及内存存储的值均为举例值,并不代表真实值,只表述各内存地址之间的关系。
- 图片中上面一段内存上的
0
,8
,16
,24
,40
,48
为十进制
数字,在内存地址16
和24
之间的内存存储着十六进制
数值0xA0
,0xA0
为某个地址。 - 下面一段内存显示内存
0xA0
的关系,0xA0
偏移4
字节后得到0xA4
地址,在0xA4
开始的4
字节内存存储着十六进制
数字18h
。
结尾
- 行文至此,关于
虚函数调用过程中指针的转换过程
的分享就结束了。 - 如果觉得还不错,你的
点赞关注加收藏
便是笔者继续更新
的最大动力哦^_^
。