Chinaunix首页 | 论坛 | 博客
  • 博客访问: 84883
  • 博文数量: 18
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 321
  • 用 户 组: 普通用户
  • 注册时间: 2013-07-30 21:09
文章分类

全部博文(18)

文章存档

2015年(3)

2014年(9)

2013年(6)

我的朋友

分类: C/C++

2014-01-20 23:00:35

class CPerson {
public:
    CPerson() {}   
    virtual ~CPerson() {}   
    virtual void ShowSpeak() {   
    }   
};
class CChinese : public CPerson {
public:
    CChinese() {}   
    virtual ~CChinese() {}   
    virtual void ShowSpeak() {   
        printf("Speak Chinese\r\n");
    }   
};
class CAmerican : public CPerson {
public:
    CAmerican() {}
    virtual ~CAmerican() {
    }
    virtual void ShowSpeak() {
        printf("Speak American\r\n");
    }
};
class CGerman : public CPerson {
public:
    CGerman() {}
    virtual ~CGerman() {}
    virtual void ShowSpeak() {
        printf("Speak German\r\n");
    }
};
void Speak(CPerson *pPerson) {
    pPerson->ShowSpeak();
}

int main() {
    CChinese* pChinese = new CChinese();
    delete pChinese;
}
对main函数反汇编,得到的结果如下:
0x00000000004006fa :    push   %rbp
0x00000000004006fb :    mov    %rsp,%rbp
0x00000000004006fe :    push   %rbx
0x00000000004006ff :    sub    $0x18,%rsp
//传入需要开辟的内存空间大小,为8字节,通过%edi寄存器传递
0x0000000000400703 :    mov    $0x8,%edi
0x0000000000400708 :   callq  0x4005f0 <_Znwm@plt>
//开辟的内存空间首地址通过%rax返回,再次赋值给%rdi,调用构造函数
0x000000000040070d :   mov    %rax,%rbx
0x0000000000400710 :   mov    %rbx,%rdi
0x0000000000400713 :   callq  0x400764
//将首地址保存为局部变量,判断开辟的内存空间是否为空
0x0000000000400718 :   mov    %rbx,-0x10(%rbp)
0x000000000040071c :   cmpq   $0x0,-0x10(%rbp)
0x0000000000400721 :   je     0x400737
//这里开始准备调用析构函数,this指针赋给%rax
0x0000000000400723 :   mov    -0x10(%rbp),%rax
//将this指针所指内存区域的首8个字节,赋值给%rax,这时%rax内的值,是一张虚标的首地址
0x0000000000400727 :   mov    (%rax),%rax
//将虚表首地址的值加8,实际上是跳到了虚表的第二项,因为虚表的每一项是8个字节
0x000000000040072a :   add    $0x8,%rax
//将第二项的内容,即析构函数的地址赋值给%rax,并将this首地址赋值给%rdi
0x000000000040072e :   mov    (%rax),%rax
0x0000000000400731 :   mov    -0x10(%rbp),%rdi
//调用这个析构函数
0x0000000000400735 :   callq  *%rax
// 平衡堆栈,返回
0x0000000000400737 :   mov    $0x0,%eax
0x000000000040073c :   add    $0x18,%rsp
0x0000000000400740 :   pop    %rbx
0x0000000000400741 :   leaveq 
0x0000000000400742 :   retq   

这里就产生了一个疑问,从CChinese的继承关系来看,需表内应该只有两项,就是析构函数和ShowSpeak。析构函数应该处于虚表的第一个位置,这里为什么要跳到第二个表项呢?那么第一个表项到底是什么?它总共有几个表现呢?下面,我们用gdb走一遍,将其虚表里面的信息都整理出来。

//执行到断点后,采用info reg命令查看到rip的地址,然后用x/10i $rip来查看反汇编代码
//取得存放this指针的地址
(gdb) p $rbp-0x10
$2 = (void *) 0x7fffbb5b6640
//this的值为0x48d3010
(gdb) x/1xg 0x7fffbb5b6640 
0x7fffbb5b6640: 0x00000000048d3010
//虚表的地址为0x4009b0
(gdb) x/10xg 0x00000000048d3010
0x48d3010:      0x00000000004009b0      0x0000000000000000
//虚表第一项值为0x40085a,第二项的值为0x4007cc,第三项的值为0x400788
(gdb) x/10g 0x00000000004009b0
0x4009b0 <_ZTV8CChinese+16>:    0x000000000040085a      0x00000000004007cc
0x4009c0 <_ZTV8CChinese+32>:    0x0000000000400788      0x0000000000000000

(gdb) x/20i 0x000000000040085a 
0x40085a <~CChinese>:   push   %rbp
0x40085b <~CChinese+1>: mov    %rsp,%rbp
0x40085e <~CChinese+4>: sub    $0x10,%rsp
0x400862 <~CChinese+8>: mov    %rdi,-0x8(%rbp)
//调用CChinese的析构函数,由于每个析构函数内,都需要将属于这个类的虚表地址重新赋值一遍,
//因此这里会赋予0x4009b0,刚好是CChinese的虚表地址
0x400866 <~CChinese+12>:        mov    $0x4009b0,%edx
0x40086b <~CChinese+17>:        mov    -0x8(%rbp),%rax
0x40086f <~CChinese+21>:        mov    %rdx,(%rax)
//调用CPerson的析构函数
0x400872 <~CChinese+24>:        mov    -0x8(%rbp),%rdi
0x400876 <~CChinese+28>:        callq  0x4007a0 <~CPerson>
0x40087b <~CChinese+33>:        mov    $0x0,%eax
//由于eax为0,因此后面的test和je肯定会跳转到0x40088d,即越过对_ZdlPv@plt的调用
0x400880 <~CChinese+38>:        test   %al,%al
0x400882 <~CChinese+40>:        je     0x40088d <~CChinese+51>
0x400884 <~CChinese+42>:        mov    -0x8(%rbp),%rdi
0x400888 <~CChinese+46>:        callq  0x4005c0 <_ZdlPv@plt>
0x40088d <~CChinese+51>:        leaveq 
0x40088e <~CChinese+52>:        retq   

//这个函数和上面相比,唯一的区别是调用了_ZdPv@plt
(gdb) x/20i 0x00000000004007cc
0x4007cc <~CChinese>:   push   %rbp
0x4007cd <~CChinese+1>: mov    %rsp,%rbp
0x4007d0 <~CChinese+4>: sub    $0x10,%rsp
0x4007d4 <~CChinese+8>: mov    %rdi,-0x8(%rbp)
0x4007d8 <~CChinese+12>:        mov    $0x4009b0,%edx
0x4007dd <~CChinese+17>:        mov    -0x8(%rbp),%rax
0x4007e1 <~CChinese+21>:        mov    %rdx,(%rax)
0x4007e4 <~CChinese+24>:        mov    -0x8(%rbp),%rdi
0x4007e8 <~CChinese+28>:        callq  0x4007a0 <~CPerson>
//会调用_ZdlPv@plt
0x4007ed <~CChinese+33>:        mov    $0x1,%eax
0x4007f2 <~CChinese+38>:        test   %al,%al
0x4007f4 <~CChinese+40>:        je     0x4007ff <~CChinese+51>
0x4007f6 <~CChinese+42>:        mov    -0x8(%rbp),%rdi
0x4007fa <~CChinese+46>:        callq  0x4005c0 <_ZdlPv@plt>
0x4007ff <~CChinese+51>:        leaveq 
0x400800 <~CChinese+52>:        retq   

现在一切都明白了,_ZdPv@plt是用于释放内存的实现,由于CChinese的内存是被new出来的,因此需要采用带_ZdPv@plt调用的析构函数版本来实现析构;而该版本的析构在虚表里面处于第二项,因此在外部调用析构时,需要add 0x08
虚表的第三项就是ShowSpeak函数的地址
(gdb) x/20i 0x0000000000400788
0x400788 <_ZN8CChinese9ShowSpeakEv>:    push   %rbp
0x400789 <_ZN8CChinese9ShowSpeakEv+1>:  mov    %rsp,%rbp
0x40078c <_ZN8CChinese9ShowSpeakEv+4>:  sub    $0x10,%rsp
0x400790 <_ZN8CChinese9ShowSpeakEv+8>:  mov    %rdi,-0x8(%rbp)
0x400794 <_ZN8CChinese9ShowSpeakEv+12>: mov    $0x400990,%edi
0x400799 <_ZN8CChinese9ShowSpeakEv+17>: callq  0x4005b0
0x40079e <_ZN8CChinese9ShowSpeakEv+22>: leaveq 
0x40079f <_ZN8CChinese9ShowSpeakEv+23>: retq   

趁热打铁,我们再来看看CPerson的虚函数,长什么摸样
(gdb) x/20i 0x4007a0 
0x4007a0 <~CPerson>:    push   %rbp
0x4007a1 <~CPerson+1>:  mov    %rsp,%rbp
0x4007a4 <~CPerson+4>:  sub    $0x10,%rsp
0x4007a8 <~CPerson+8>:  mov    %rdi,-0x8(%rbp)
//这里面又用CPerson的虚表地址覆盖了虚表指针
0x4007ac <~CPerson+12>: mov    $0x400a30,%edx
0x4007b1 <~CPerson+17>: mov    -0x8(%rbp),%rax
0x4007b5 <~CPerson+21>: mov    %rdx,(%rax)
//不用释放内存块
0x4007b8 <~CPerson+24>: mov    $0x0,%eax
0x4007bd <~CPerson+29>: test   %al,%al
0x4007bf <~CPerson+31>: je     0x4007ca <~CPerson+42>
0x4007c1 <~CPerson+33>: mov    -0x8(%rbp),%rdi
0x4007c5 <~CPerson+37>: callq  0x4005c0 <_ZdlPv@plt>
0x4007ca <~CPerson+42>: leaveq 
0x4007cb <~CPerson+43>: retq   

再来看看Speak函数,分析一下虚函数是如何被调用的
(gdb) disas Speak
Dump of assembler code for function _Z5SpeakP7CPerson:
0x00000000004006d8 <_Z5SpeakP7CPerson+0>:       push   %rbp
0x00000000004006d9 <_Z5SpeakP7CPerson+1>:       mov    %rsp,%rbp
0x00000000004006dc <_Z5SpeakP7CPerson+4>:       sub    $0x10,%rsp
0x00000000004006e0 <_Z5SpeakP7CPerson+8>:       mov    %rdi,-0x8(%rbp)
//rax里面放了this指针的值
0x00000000004006e4 <_Z5SpeakP7CPerson+12>:      mov    -0x8(%rbp),%rax
//this指针所指对象首部8个字节赋值给了rax,其实就是虚表指针
0x00000000004006e8 <_Z5SpeakP7CPerson+16>:      mov    (%rax),%rax
//虚表指针往后偏移16个字节,相当于是虚表的第三项,赋值给rax
0x00000000004006eb <_Z5SpeakP7CPerson+19>:      add    $0x10,%rax
0x00000000004006ef <_Z5SpeakP7CPerson+23>:      mov    (%rax),%rax
//调用第三个虚表项所对应的函数
0x00000000004006f2 <_Z5SpeakP7CPerson+26>:      mov    -0x8(%rbp),%rdi
0x00000000004006f6 <_Z5SpeakP7CPerson+30>:      callq  *%rax
0x00000000004006f8 <_Z5SpeakP7CPerson+32>:      leaveq 
0x00000000004006f9 <_Z5SpeakP7CPerson+33>:      retq  







阅读(1404) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~