汇编下的C++虚函数

#include <cstdio>

class B
{
private:
    int x;
public:
    B(int i) :x(i) {}
    virtual void printinfo() const
    {
        printnum();
        printf("Base\n");
    }
    virtual void printnum() const
    {
        printf("%d\n", x);
    }
};

class D : public B
{
private:
    int y;
public:
    D(int i, int j) :B(i), y(j){}
    virtual void printinfo() const
    {
        printnum();
        printf("Derived\n");
    }
};

void PutInfo(const B &b)
{
    b.printinfo();
}

int main()
{
    B b(1);
    D d(2, 3);
    printf("\tB\tD\nsize:\t%d\t%d\n", sizeof b, sizeof d);
    printf("addr:\t%x\t%x\n", &b, &d);
    putchar('\n');
    PutInfo(b);
    PutInfo(d);

    return 0;
}

使用VS2013生成,Release模式,无优化,运行结果如下:


18f918和18f90c这两个是变量b,d的地址,每次运行都可能不同。

由于B类有虚函数,因为会有一个虚表指针(4字节),加上一个int成员变量x(4字节),因此大小为8字节。

D类是B类派生类,在B类基础上,增加了一个int成员变量y,所以大小为12字节。

 

一些基本的约定:

Windows栈从高地址向低地址增长,栈底地址高于栈顶地址,局部变量按声明顺序从高地址向低地址存放。

__cdecl函数调用方式:一般来说,函数参数从右向左依次入栈,然后是返回地址入栈,然后是ebp入栈,再将ebp设为栈顶指针esp,用于访问数据,esp减小,开辟栈空间以保存局部变量。

具体汇编代码如下:

; int __cdecl main(int argc, const char **argv, const char **envp)
_main proc near

d= D ptr -14h
b= B ptr -8
argc= dword ptr  8
argv= dword ptr  0Ch
envp= dword ptr  10h

push    ebp
mov     ebp, esp
sub     esp, 14h
push    1               ; i
lea     ecx, [ebp+b]    ; this
call    ??0B@@QAE@H@Z   ; B::B(int)
push    3               ; j
push    2               ; i
lea     ecx, [ebp+d]    ; this
call    ??0D@@QAE@HH@Z  ; D::D(int,int)
push    0Ch
push    8
push    offset aBDSizeDD ; "\tB\tD\nsize:\t%d\t%d\n"
call    ds:__imp__printf
add     esp, 0Ch
lea     eax, [ebp+d]
push    eax
lea     ecx, [ebp+b]
push    ecx
push    offset aAddrXX  ; "addr:\t%x\t%x\n"
call    ds:__imp__printf
add     esp, 0Ch
push    0Ah             ; Ch
call    ds:__imp__putchar
add     esp, 4
lea     edx, [ebp+b]
push    edx             ; b
call    ?PutInfo@@YAXABVB@@@Z ; PutInfo(B const &)
add     esp, 4
lea     eax, [ebp+d]
push    eax             ; b
call    ?PutInfo@@YAXABVB@@@Z ; PutInfo(B const &)
add     esp, 4
xor     eax, eax
mov     esp, ebp
pop     ebp
retn
_main endp

sub    esp, 14h

为局部变量分配空间,b和d一共20字节,因此esp减去14h。

 

push   1               ; i

lea    ecx, [ebp+b]    ; this

call   ??0B@@QAE@H@Z   ; B::B(int)

1入栈,同时ecx = &b。

lea reg, mem的作用是将mem的地址传递给reg寄存器,类似于reg = &mem,[reg]是取地址为reg的数据,类似于*reg,所以

lea ecx, [ebp+b]

将ebp-8赋给ecx,ebp-8就是b的地址,接下来调用B的构造函数B::B(int)。

; void __thiscall B::B(B *this, int i)
??0B@@QAE@H@Z proc near

this= dword ptr -4
i= dword ptr  8

push    ebp
mov     ebp, esp
push    ecx
mov     [ebp+this], ecx
mov     eax, [ebp+this]
mov     dword ptr [eax], offset ??_7B@@6B@ ; const B::`vftable'
mov     ecx, [ebp+this]
mov     edx, [ebp+i]
mov     [ecx+4], edx
mov     eax, [ebp+this]
mov     esp, ebp
pop     ebp
retn    4
??0B@@QAE@H@Z endp

从代码可以看到,成员函数中其实是有一个隐藏的参数this,用于保存对象地址。

mov [ebp+this], ecx

表明this指针通过ecx寄存器传递,在B::B(int)函数中,形成的stack是这样的:


mov    eax, [ebp+this]

mov    dword ptr [eax], offset ??_7B@@6B@ ; const B::`vftable'

这两句将b的地址&b复制到eax,并将B类的虚表地址复制到&b处。

mov    ecx, [ebp+this]

mov    edx, [ebp+i]

mov    [ecx+4], edx

这里再将ecx设为&b,edx设为ebp+8中的数据,即参数i的值1,再将edx复制到&b+4处,即成员变量b.x被初始化为1。

 

push   3               ; j

push   2               ; i

lea    ecx, [ebp+d]    ; this

call   ??0D@@QAE@HH@Z  ; D::D(int,int)

将D类构造函数的参数压入栈中,ecx = &d,调用D::D(int, int)。

; void __thiscall D::D(D *this, int i, int j)
??0D@@QAE@HH@Z proc near

this= dword ptr -4
i= dword ptr  8
j= dword ptr  0Ch

push    ebp
mov     ebp, esp
push    ecx
mov     [ebp+this], ecx
mov     eax, [ebp+i]
push    eax             ; i
mov     ecx, [ebp+this] ; this
call    ??0B@@QAE@H@Z   ; B::B(int)
mov     ecx, [ebp+this]
mov     dword ptr [ecx], offset ??_7D@@6B@ ; const D::`vftable'
mov     edx, [ebp+this]
mov     eax, [ebp+j]
mov     [edx+8], eax
mov     eax, [ebp+this]
mov     esp, ebp
pop     ebp
retn    8
??0D@@QAE@HH@Z endp

this = &d,调用基类构造函数B::B(int),初始化d.x,然后D类虚表地址复制到&d,再初始化d.y。

 

看一下虚表中的数据:

.rdata:00402164                   ; const B::`vftable'
.rdata:00402164 30 10 40 00       ??_7B@@6B@ dd offset ?printinfo@B@@UBEXXZ
.rdata:00402164                                                 ; DATA XREF: B::B(int)+Ao
.rdata:00402164                                                 ; B::printinfo(void)
.rdata:00402168 60 10 40 00       dd offset ?printnum@B@@UBEXXZ ; B::printnum(void)

按照成员函数声明的顺序,B类虚表的第一项是B::printinfo(void)地址,第二项是B::printnum(void)地址。

D类虚表类似:

.rdata:00402158                   ; const D::`vftable'
.rdata:00402158 B0 10 40 00       ??_7D@@6B@ dd offset ?printinfo@D@@UBEXXZ
.rdata:00402158                                                 ; DATA XREF: D::D(int,int)+16o
.rdata:00402158                                                 ; D::printinfo(void)
.rdata:0040215C 60 10 40 00       dd offset ?printnum@B@@UBEXXZ ; B::printnum(void)

注意到由于派生类D的printinfo覆盖了基类的printinfo,但并未对B::printnum(void)进行覆盖,所以虚表第一项是D::printinfo(void)地址,第二项仍然是B::printnum(void)的地址。

 

再看

PutInfo(b);

PutInfo(d);

对应的汇编:

lea     edx, [ebp+b]
push    edx             ; b
call    ?PutInfo@@YAXABVB@@@Z ; PutInfo(B const &)
add     esp, 4
lea     eax, [ebp+d]
push    eax             ; b
call    ?PutInfo@@YAXABVB@@@Z ; PutInfo(B const &)
add     esp, 4

lea     edx, [ebp+b]
push    edx             ; b
edx = &b,然后edx入栈,调用PutInfo(B const &)。

PutInfo(const B &)代码:
; void __cdecl PutInfo(B *b)
?PutInfo@@YAXABVB@@@Z proc near

b= dword ptr  8

push    ebp
mov     ebp, esp
mov     eax, [ebp+b]
mov     edx, [eax]
mov     ecx, [ebp+b]
mov     eax, [edx]
call    eax
pop     ebp
retn
?PutInfo@@YAXABVB@@@Z endp


左图为main形成的stack,右图为PutInfo(b)形成的stack,

在PutInfo(b)中,

eax = &b

edx = offset B::vftable

ecx = &b

eax = B::vftable = offset B::printinfo(void)。

然后call eax调用B::printinfo(void)。

类似地,在PutInfo(d)中,由于虚表第一项是offset D::printinfo(void),因此call eax会调用D::printinfo(void)。

于是,对象在运行时根据虚表中保存的函数地址,自动调用所属类的虚函数。


如果仅仅将参数传递方式改为pass by value,即

void PutInfo(const B b)

结果如下:



; int __cdecl main(int argc, const char **argv, const char **envp)
_main proc near

d= D ptr -14h
b= B ptr -8
argc= dword ptr  8
argv= dword ptr  0Ch
envp= dword ptr  10h

push    ebp
mov     ebp, esp
sub     esp, 14h
push    1               ; i
lea     ecx, [ebp+b]    ; this
call    ??0B@@QAE@H@Z   ; B::B(int)
push    3               ; j
push    2               ; i
lea     ecx, [ebp+d]    ; this
call    ??0D@@QAE@HH@Z  ; D::D(int,int)
push    0Ch
push    8
push    offset aBDSizeDD ; "\tB\tD\nsize:\t%d\t%d\n"
call    ds:__imp__printf
add     esp, 0Ch
lea     eax, [ebp+d]
push    eax
lea     ecx, [ebp+b]
push    ecx             ; b
push    offset aAddrXX  ; "addr:\t%x\t%x\n"
call    ds:__imp__printf
add     esp, 0Ch
push    0Ah             ; Ch
call    ds:__imp__putchar
add     esp, 4
sub     esp, 8
mov     ecx, esp        ; this
lea     edx, [ebp+b]
push    edx             ; __that
call    ??0B@@QAE@ABV0@@Z ; B::B(B const &)
call    ?PutInfo@@YAXVB@@@Z ; PutInfo(B)
add     esp, 8
sub     esp, 8
mov     ecx, esp        ; this
lea     eax, [ebp+d]
push    eax             ; __that
call    ??0B@@QAE@ABV0@@Z ; B::B(B const &)
call    ?PutInfo@@YAXVB@@@Z ; PutInfo(B)
add     esp, 8
xor     eax, eax
mov     esp, ebp
pop     ebp
retn
_main endp

sub     esp, 8

mov    ecx, esp        ; this

lea    edx, [ebp+b]

push   edx             ; __that

call   ??0B@@QAE@ABV0@@Z ; B::B(B const &)

call   ?PutInfo@@YAXVB@@@Z ; PutInfo(B)

add    esp, 8

可以看到,程序又开辟了8字节的空间,这用于PutInfo函数中的临时变量(形参b)的存放,之后又回收了这8字节的空间。

源代码并没有声明copy构造函数,但是在PutInfo中,参数传递必须利用实参构造形参,因此编译器自动生成了一个copy构造函数B::B(const &)。

考察B::B(const &),__that是已经构造的对象,this是将要构造的对象,在调用copy函数之前,ecx = esp这是栈顶,也是形参b的地址,或者说this指针,edx是实参b的地址。

; void __thiscall B::B(B *this, B *__that)
??0B@@QAE@ABV0@@Z proc near

this= dword ptr -4
__that= dword ptr  8

push    ebp
mov     ebp, esp
push    ecx
mov     [ebp+this], ecx
mov     eax, [ebp+this]
mov     dword ptr [eax], offset ??_7B@@6B@ ; const B::`vftable'
mov     ecx, [ebp+this]
mov     edx, [ebp+__that]
mov     eax, [edx+4]
mov     [ecx+4], eax
mov     eax, [ebp+this]
mov     esp, ebp
pop     ebp
retn    4
??0B@@QAE@ABV0@@Z endp

构造函数中,初始化B类虚表地址。

mov    edx, [ebp+__that]

mov    eax, [edx+4]

mov    [ecx+4], eax

然后复制that->x到this->x,完成对象的copy。

 

pass by value版本的PutInfo:

; void __cdecl PutInfo(B b)
?PutInfo@@YAXVB@@@Z proc near

b= B ptr  8

push    ebp
mov     ebp, esp
lea     ecx, [ebp+b]    ; this
call    ?printinfo@B@@UBEXXZ ; B::printinfo(void)
pop     ebp
retn
?PutInfo@@YAXVB@@@Z endp

直接调用了B::printinfo(void),与实参的实际类型无关。




  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值