从汇编的角度了解C++原理——虚函数

本文用到的反汇编工具是objconv,使用方法可以看我另一篇文章https://blog.csdn.net/weixin_45001971/article/details/128660642

其它文章:
从汇编的角度了解C++原理——类的储存结构和函数调用
从汇编的角度了解C++原理——new和malloc的区别
从汇编的角度了解C++原理——虚函数

1、虚函数

1.1、虚函数储存结构

在这里插入图片描述
反汇编。

main:
        sub     rsp, 56                             
        lea     rcx, [rsp+20H]                    
        call    ??0A@@QEAA@XZ      				//调用构造函数                  
        mov     eax, 4294967295                  
        add     rsp, 56                                
        ret                                             
        
??0A@@QEAA@XZ:									//调用A类的构造函数
        mov     qword [rsp+8H], rcx                  
        mov     rax, qword [rsp+8H]                    
        lea     rcx, [rel ??_7A@@6B@]           //获取虚表??_7A@@6B@的地址
        mov     qword [rax], rcx                //把虚表地址放在对象的头部      
        mov     rax, qword [rsp+8H]                    
        mov     dword [rax+8H], 10              //在对象首地址偏移8个字节的位置定义d1变量       
        mov     rax, qword [rsp+8H]                     
        ret                                                           

??_7A@@6B@:                     				//A类虚表                       
        dq ?func2@A@@UEAAXXZ                    //虚函数func2      

以上例的汇编代码可以得出带虚函数的类的储存结构如下图所示。
在这里插入图片描述
带有虚函数的对象的头部会放置8个字节大小的虚表地址,有了虚表之后的对象会以8个字节为单位去对齐,如上例中的A类,如果没有虚函数,它的大小为4个字节,而加了虚函数之后,大小变为了16个字节。

1.2、子类重写虚函数

在代码中添加A的子类B,重写func2方法。
在这里插入图片描述

反汇编

main:
        sub     rsp, 56                             
        lea     rcx, [rsp+20H]                       
        call    ??0B@@QEAA@XZ          		//调用B类构造                
        mov     eax, 4294967295                        
        add     rsp, 56                                
        ret                                             
        
??0A@@QEAA@XZ:								//A类构造函数
        mov     qword [rsp+8H], rcx                  
        mov     rax, qword [rsp+8H]                     
        lea     rcx, [rel ??_7A@@6B@]     	//把A类虚表的地址放在头部              
        mov     qword [rax], rcx                      
        mov     rax, qword [rsp+8H]                   
        mov     dword [rax+8H], 10                  
        mov     rax, qword [rsp+8H]           
        ret                                             

??0B@@QEAA@XZ:								//B类构造函数
        mov     qword [rsp+8H], rcx                
        sub     rsp, 40                             
        mov     rcx, qword [rsp+30H]               
        call    ??0A@@QEAA@XZ    			//调用A类构造                   
        mov     rax, qword [rsp+30H]             
        lea     rcx, [rel ??_7B@@6B@]       //把B类虚表的地址放在头部           
        mov     qword [rax], rcx                    
        mov     rax, qword [rsp+30H]                   
        add     rsp, 40                                 
        ret                                           
       
??_7A@@6B@:                         		//A类虚表                        
        dq ?func2@A@@UEAAXXZ                //A::func2                             
        dq ?func3@A@@UEAAXXZ                //A::func3      
             
??_7B@@6B@:                                 //B类虚表                       
        dq ?func2@B@@UEAAXXZ                //B::func2,被替换为了B实现的func2          
        dq ?func3@A@@UEAAXXZ                //A::func3                         

从该例中我们可以看到,父类有虚函数时,不光它自己有一张虚表,它的子子孙孙都会各带有一个自己的虚表,子类重写虚函数时,会把子类实现的函数指针替换上虚表,把原先父类的函数指针覆盖掉。

1.3、在栈上调用虚函数

在main里添加方法的调用。
在这里插入图片描述
反汇编。

main:
        sub     rsp, 56                               
        lea     rcx, [rsp+20H]                         
        call    ??0B@@QEAA@XZ                        
        lea     rcx, [rsp+20H]                        
        call    ?func1@A@@QEAAXXZ   		//调用A::func1                 
        lea     rcx, [rsp+20H]                        
        call    ?func2@B@@UEAAXXZ   		//调用B::func2                       
        lea     rcx, [rsp+20H]                         
        call    ?func3@A@@UEAAXXZ   		//调用A::func3                         
        mov     eax, 4294967295                         
        add     rsp, 56                                
        ret   

在栈上调用方法时,因为类型是确定的,所以编译器在编译阶段就会找到对应的函数去调用,调用过程与普通方法一样。

1.4、在堆上调用虚函数(通过指针调用,多态)

修改例程如下。
在这里插入图片描述
反汇编

main:
        sub     rsp, 72                                
        mov     ecx, 16                                
        call    ??2@YAPEAX_K@Z                         
        mov     qword [rsp+28H], rax                   
        cmp     qword [rsp+28H], 0                    
        jz      ?_001                                 
        mov     rcx, qword [rsp+28H]		//定义指针b                   
        call    ??0B@@QEAA@XZ                       
        mov     qword [rsp+30H], rax        //rsp+30H指向对象          
        jmp     ?_002                       //跳到?_002            

?_001:  mov     qword [rsp+30H], 0   
                  
?_002:  mov     rax, qword [rsp+30H]                   
        mov     qword [rsp+38H], rax        //rsp+38H指向对象         
        mov     rax, qword [rsp+38H]        //rax指向对象           
        mov     qword [rsp+20H], rax        //rsp+20H指向对象       
        mov     rcx, qword [rsp+20H]        //rcx指向对象          
        call    ?func1@A@@QEAAXXZ           //调用A::func1       
        mov     rax, qword [rsp+20H]                 
        mov     rax, qword [rax]            //取虚表        
        mov     rcx, qword [rsp+20H]                   
        call    near [rax]                  //执行虚表第一个函数,即B::func2           
        mov     rax, qword [rsp+20H]                   
        mov     rax, qword [rax]                       
        mov     rcx, qword [rsp+20H]                
        call    near [rax+8H]             	//执行虚表第二个函数,即A::func3             
        mov     eax, 4294967295                         
        add     rsp, 72                               
        ret                                          
        
??_7B@@6B@:                                           
        dq ?func2@B@@UEAAXXZ                        
        dq ?func3@A@@UEAAXXZ                          

从该例可以看到,通过指针来调用函数时。
如果是普通函数,编译器会直接根据指针类型,找到对应的的方法,而不是根据对象本身的类型,如本例中B类也实现了func1方法,但通过A类指针调用时,写到汇编里的时A::func1。
如果是虚函数,编译器不会根据名字来查找函数,而是让汇编代码通过虚表中的偏移量来调用,如本例中,b指针执行了func2和func3,这两个函数都没有被直接调用,而是以“call near [rax + 偏移量]”的形式调用了,这也是C++中父类指针指向子类对象的多态的实现原理。

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
在 C++ 中,函数是通过函数表来实现的。每个对象都有一个指向函数表的指针,函数表是一个数组,存储了该对象的函数的地址。 当调用一个函数时,编译器会先查找该对象的函数表,然后根据函数的索引找到对应的函数地址,最终调用该函数。 以下是一个简单的示例,展示了函数汇编代码实现: ```c++ class Base { public: virtual void foo() { printf("Base::foo()\n"); } }; class Derived : public Base { public: virtual void foo() { printf("Derived::foo()\n"); } }; int main() { Base* ptr = new Derived(); ptr->foo(); delete ptr; return 0; } ``` 对应的汇编代码如下(采用 AT&T 语法): ```asm .file "main.cpp" .section .text .globl main .p2align 4,,15 .type main, @function main: .LFB0: .cfi_startproc subq $8, %rsp movl $8, %edi call operator new(unsigned long) movq %rax, %rdi leaq .LC0(%rip), %rsi movl $1, %edx movl $0, %eax call __printf_chk movq %rax, %rdi movq %rax, -8(%rbp) movq $vtable for Derived(%rip), %rax movq (%rax), %rax movq (%rax), %rax movq -8(%rbp), %rdx movq %rdx, %rsi movq %rax, (%rsp) call *%rax leaq -8(%rbp), %rax movq (%rax), %rax movq %rax, (%rsp) call operator delete(void*) xorl %eax, %eax addq $8, %rsp .cfi_endproc .LFE0: .size main, .-main .section .rodata .align 8 .LC0: .string "Base::foo()\n" .section .rodata.cst4 .align 4 vtable for Derived: .quad 0 .quad typeinfo for Derived .quad Derived::foo() .section .note.GNU-stack,"",@progbits ``` 可以看到,在调用函数时,程序首先通过函数表找到对应的函数地址,然后通过 `call` 指令调用该函数函数表的地址是通过 `vtable for Derived(%rip)` 获取的。调用完毕后,还需要调用 `operator delete` 释放内存。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值