浅谈C++ 虚函数分析
虚函数调用属于运行时多态,在类的继承关系中,通过父类指针来调用不同子类对象的同名方法,而产生不同的效果。
C++中的多态是通过晚绑定(对象构造时)来实现的。
用法
在函数之前声明关键字virtual表示这是一个虚函数,在函数后增加一个=0表示这是一个纯虚函数,纯虚函数的类不能创建具体实例。
该示例作后文分析使用,一个包含纯虚函数的父类,一个重写了父类方法的子类,一个无继承的类。
structBase{ Base():val(7777){} virtualintfuck(inta)=0; intval; }; structDer:publicBase{ Der()=default; intfuck(inta)override{returnval+4396;} }; structA{ A()=default; voidfunny(inta){} }; intmain(){ Derder; Base*pbase=&der; pbase->fuck(sizeof(Der));//调用Der::fuck(inta); Aa; a.funny(sizeof(A));//A::funny(inta); return3; }
实现
原来就了解虚函数是通过虚表的偏移来获取实际调用函数地址来实现的,但是在何时确定这个偏移和具体的偏移细节也没有说明,今儿个来探探究竟。
拿上面的代码进行反汇编获提取部分函数,main,Base::Base(),Base::fuck(),Der::Der(),Der::fuck,A::funny()如下:
_ZN4BaseC2Ev: .LFB1: .cfi_startproc pushq%rbp .cfi_def_cfa_offset16 .cfi_offset6,-16 movq%rsp,%rbp .cfi_def_cfa_register6 movq%rdi,-8(%rbp)//还是main函数的栈帧-32(%rpb)的地址 leaq16+_ZTV4Base(%rip),%rdx//关键点来了,取虚表偏移16的地址也就是__cxa_pure_virtual,这里是没有意义的 movq-8(%rbp),%rax movq%rdx,(%rax)//将__cxa_pure_virtual的地址存放在地址rax的内存中(这个例子中也就是main函数的栈帧-32(%rpb)的地方), movq-8(%rbp),%rax//然后往后偏移8个字节,也就是跳过虚表指针,对成员变量val初始化。 movl$7777,8(%rax) nop//注:上面是用这个示例中实际的地址带入的,实际上对于一个有的类的处理是一个通用逻辑的,构造函数传入的第一个参数rdi是this指针,由于有虚表存在的影响,这里会修改this指针所在地址的内容,也就是虚表的偏移地址(非起始地址) popq%rbp .cfi_def_cfa7,8 ret .cfi_endproc .LFE1: .size_ZN4BaseC2Ev,.-_ZN4BaseC2Ev .weak_ZN4BaseC1Ev .set_ZN4BaseC1Ev,_ZN4BaseC2Ev .section.text._ZN3Der4fuckEi,"axG",@progbits,_ZN3Der4fuckEi,comdat .align2 .weak_ZN3Der4fuckEi .type_ZN3Der4fuckEi,@function _ZN3Der4fuckEi: .LFB3: .cfi_startproc pushq%rbp .cfi_def_cfa_offset16 .cfi_offset6,-16 movq%rsp,%rbp .cfi_def_cfa_register6 movq%rdi,-8(%rbp) movl%esi,-12(%rbp) movq-8(%rbp),%rax movl8(%rax),%eax//成员变量val,val是从rdi中偏移8字节取的值 addl$4396,%eax//val+4396 popq%rbp .cfi_def_cfa7,8 ret .cfi_endproc .LFE3: .size_ZN3Der4fuckEi,.-_ZN3Der4fuckEi .section.text._ZN1A5funnyEi,"axG",@progbits,_ZN1A5funnyEi,comdat .align2 .weak_ZN1A5funnyEi .type_ZN1A5funnyEi,@function _ZN1A5funnyEi: .LFB4: .cfi_startproc pushq%rbp .cfi_def_cfa_offset16 .cfi_offset6,-16 movq%rsp,%rbp .cfi_def_cfa_register6 movq%rdi,-8(%rbp) movl%esi,-12(%rbp) nop popq%rbp .cfi_def_cfa7,8 ret .cfi_endproc .LFE4: .size_ZN1A5funnyEi,.-_ZN1A5funnyEi .section.text._ZN3DerC2Ev,"axG",@progbits,_ZN3DerC5Ev,comdat .align2 .weak_ZN3DerC2Ev .type_ZN3DerC2Ev,@function _ZN3DerC2Ev: .LFB7: .cfi_startproc pushq%rbp .cfi_def_cfa_offset16 .cfi_offset6,-16 movq%rsp,%rbp .cfi_def_cfa_register6 subq$16,%rsp movq%rdi,-8(%rbp)//rdi是取的main栈帧-32(%rbp)的地址 movq-8(%rbp),%rax movq%rax,%rdi call_ZN4BaseC2Ev//Base的构造函数,并且又把传进来的参数作为实参传进去了,这里跟踪进去 leaq16+_ZTV3Der(%rip),%rdx//取虚表偏移16字节_ZN3Der4fuckEi的地址 movq-8(%rbp),%rax movq%rdx,(%rax)//rax在之前的Base构造函数中是被修改了的,这里将继续修改内容,前一次的修改失效。 nop leave .cfi_def_cfa7,8 ret .cfi_endproc .LFE7: .size_ZN3DerC2Ev,.-_ZN3DerC2Ev .weak_ZN3DerC1Ev .set_ZN3DerC1Ev,_ZN3DerC2Ev .text .globlmain .typemain,@function main: .LFB5: .cfi_startproc pushq%rbp .cfi_def_cfa_offset16 .cfi_offset6,-16 movq%rsp,%rbp .cfi_def_cfa_register6 subq$48,%rsp leaq-32(%rbp),%rax//取-32(%rbp)的地址,对应Base*pbase; movq%rax,%rdi call_ZN3DerC1Ev//调用了构造函数,并且以-32(%rbp)的地址作为参数,这里跟踪进去 leaq-32(%rbp),%rax//-32(%rbp)被修改,该内存中的内容为Der虚表的偏移地址 movq%rax,-8(%rbp) movq-8(%rbp),%rax movq(%rax),%rax//rax=M[rax],取出虚表偏移中的地址 movq(%rax),%rdx//rdx=M[rax],取出虚表偏移的内容(也就是函数地址),算上上面这是做了两次解引用 movq-8(%rbp),%rax movl$16,%esi//sizeof(Der)=16,包含一个虚表指针和intval; movq%rax,%rdi//虚表偏移中的地址 call*%rdx//调用函数 leaq-33(%rbp),%rax movl$1,%esi movq%rax,%rdi call_ZN1A5funnyEi//普通成员函数,实现简单 movl$3,%eax leave .cfi_def_cfa7,8 ret .cfi_endproc .LFE5: .sizemain,.-main .weak_ZTV3Der .section.data.rel.ro.local._ZTV3Der,"awG",@progbits,_ZTV3Der,comdat .align8 .type_ZTV3Der,@object .size_ZTV3Der,24 _ZTV3Der: .quad0 .quad_ZTI3Der .quad_ZN3Der4fuckEi//Der::fuck(inta); .weak_ZTV4Base .section.data.rel.ro._ZTV4Base,"awG",@progbits,_ZTV4Base,comdat .align8 .type_ZTV4Base,@object .size_ZTV4Base,24 _ZTV4Base: .quad0 .quad_ZTI4Base .quad__cxa_pure_virtual//纯虚函数,无对应符号表 .weak_ZTI3Der .section.data.rel.ro._ZTI3Der,"awG",@progbits,_ZTI3Der,comdat .align8 .type_ZTI3Der,@object .size_ZTI3Der,24
现在是一个纯虚函数,类中也没有虚析构函数,通过反汇编来看一些这个实现。
_ZTV3Der和_ZTV4Base是两个虚表,大小为24,8字节对齐,分别对应Der子类和Base父类。虚表中偏移16字节(偏移大小可能和实现相关)为虚函数地址,每次构造函数的被调用的时候,会将该偏移地址存储到父类指针所在内存中,所以在上代码中看到,在Base和Der类的构函数中都出现了设置偏移地址的操作,但是子类构造函数会覆盖父类的修改。这样一来,实际的函数运行地址依赖构造函数,子类对象被构造就调用子类的方法,父类构造就调用父类的方法(非纯虚函数),实现了运行时多态。
增加一个虚函数后,后面的虚函数地址就添加到虚表之中,如下
virtualvoidBase::shit(){} voidDer::shit()override{} _ZTV3Der: .quad0 .quad_ZTI3Der .quad_ZN3Der4fuckEi .quad_ZN3Der4shitEv .weak_ZTV4Base .section.data.rel.ro._ZTV4Base,"awG",@progbits,_ZTV4Base,comdat .align8 .type_ZTV4Base,@object .size_ZTV4Base,32 _ZTV4Base: .quad0 .quad_ZTI4Base .quad__cxa_pure_virtual .quad_ZN4Base4shitEv .weak_ZTI3Der .section.data.rel.ro._ZTI3Der,"awG",@progbits,_ZTI3Der,comdat .align8 .type_ZTI3Der,@object .size_ZTI3Der,24
再调用另外一个虚函数就简单很多了,直接地址进行偏移(这里shit在fuck之后,所以+8)
movq-8(%rbp),%rax movq(%rax),%rax addq$8,%rax movq(%rax),%rdx movq-8(%rbp),%rax movq%rax,%rdi call*%rdx
简单画了一下虚函数运行的内存结构图
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。