zoukankan      html  css  js  c++  java
  • 从汇编看c++的虚拟继承以及其内存布局(一)

    先看第一种最简单的情形,所有类中没有任何虚函数的菱形继承。

    下面是c++源码:

    class Top {//虚基类
    public:
        int i;
        Top(int ii) {
            i = ii;
        }
    };
    
    class Left : public virtual Top {
    public:
        int j;
        Left(int jj, int ii) : Top(ii) {
            j = jj;
        }
    };
    
    class Right : public virtual Top {
    public:
        int k;
        Right(int kk, int ii) : Top(ii) {
            k = kk;
        }
    };
    
    class Bottom : public Left, public Right {
    public:
        int l;
        Bottom(int ll, int jj, int kk, int ii) : Top(ii), Left(jj, ii), Right(kk, ii) {
            l = ll;
        }
    };
    
    int main() {
        Bottom b(1, 2, 3, 4);
        Bottom* bp = &b;
        //访问自身成员变量
        b.l = 1;
        bp->l = 2;
        //访问父类Left的成员变量
        Left* lp = bp;
        b.j = 1;
        bp->j = 2;
        lp->j = 3;
        //访问父类Right的成员变量
        Right* rp = bp;
        b.k = 1;
        bp->k = 2;
        rp->k = 3;
        //访问虚基类Top的成员变量
        Top* tp = bp;
        b.i = 1;
        bp->i = 2;
        tp->i = 3;
        
    };

    让我们来看看,汇编代码里面是怎样的情形,先看main函数里面的汇编码:

    ; 33   : int main() {
    
        push    ebp
        mov    ebp, esp
        sub    esp, 48                    ; 为对象程序所需变量预留空间,其中对象b只栈24byte
    
    ; 34   :     Bottom b(1, 2, 3, 4);
    
        push    1;压入标志1,作为判断是否调用虚基类构造函数的依据 1表示调用,0表示不调用
        push    4;压栈4,为对象b的构造函数传递参数
        push    3;压栈3,为对象b的构造函数传递参数
        push    2;压栈2,为对象b的构造函数传递参数
        push    1;压栈1,为对象b的构造函数传递参数
        lea    ecx, DWORD PTR _b$[ebp];获取对象b的首地址,传给寄存器ecx,作为隐含参数传递给对象b的构造函数
        call    ??0Bottom@@QAE@HHHH@Z            ; 调用对象b的构造函数
    
    ; 35   :     Bottom* bp = &b;
    
        lea    eax, DWORD PTR _b$[ebp];将对象b的首地址给寄存器eax
        mov    DWORD PTR _bp$[ebp], eax;将对象b的首地址给指针变量bp
    
    ; 36   :     
    ; 37   :     b.l = 1;
    
        mov    DWORD PTR _b$[ebp+16], 1;将1写入偏移对象首地址16字节处内存,即为对象b的成员变量l赋值1
    
    ; 38   :     bp->l = 2;
    
        mov    ecx, DWORD PTR _bp$[ebp];将对象b的首地址给寄存器ecx
        mov    DWORD PTR [ecx+16], 2;将2写入偏移对象首地址16字节处内存,即为对象b的成员变量l赋值2
                                 ;可以看到,无论是用对象本身,还是对象指针访问对象b的成员变量
                                 ;其成员变量的偏移量都在编译期固定了,为16字节
                                 ;且两种方式访问没有差别
    
    ; 39   :     
    ; 40   :     Left* lp = bp;
    
        mov    edx, DWORD PTR _bp$[ebp];将对象b的首地址给寄存器edx
        mov    DWORD PTR _lp$[ebp], edx;将对象b的首地址给对象指针lp,此时lp指向父对象Left的首地址
                                    ;从下面的内存布局图可以看到,父对象Left的首地址和Bottom一样
    
    ; 41   :     b.j = 1;
    
        mov    DWORD PTR _b$[ebp+4], 1;将1赋给偏移对象b首地址4byte处内存,即为
                                   ;继承来的成员变量j赋值1
    
    ; 42   :     bp->j = 2;
    
        mov    eax, DWORD PTR _bp$[ebp];将对象b的首地址给寄存器eax
        mov    DWORD PTR [eax+4], 2;将2写入偏移对象b首地址4byte处内存,即为继承来的成员变量j赋值2
                                ;可以看到,无论使用b对象本身,还是指针访问继承来的成员变量j,
                                ;其成员变量的偏移量都是编译器固定了,都为4byte
                                ;且两种方式访问无差别
    
    ; 43   :     lp->j = 3;
    
        mov    ecx, DWORD PTR _lp$[ebp];将父类Left对象的首地址给寄存器ecx
        mov    DWORD PTR [ecx+4], 3;将3赋给偏移Left对象首地址4byte处内存,即为父对象Left的成员变量j赋值3
                                ;可以看到,用这种方式访问父对象Left的成员变量,其偏移量也是编译器固定
                                ;为4byte
    
    ; 44   :     
    ; 45   :     Right* rp = bp;
    
        cmp    DWORD PTR _bp$[ebp], 0;比较指针的值是否为0,也就是判断bp是否为空指针
        je    SHORT $LN3@main;如果bp为空指针,则跳转到标号$LN3@main处执行,否则顺序执行,这里是顺序执行
        mov    edx, DWORD PTR _bp$[ebp];将对象b的首地址给寄存器edx
        add    edx, 8;寄存器edx里面的内容加8,现在edx里面保存的地址偏移了对象b的首地址8byte,即指向了对象Right的首地址
        mov    DWORD PTR tv90[ebp], edx;将寄存器edx内容存入临时变量tv90
        jmp    SHORT $LN4@main;跳转到标号$LN4@main处执行
    $LN3@main:
        mov    DWORD PTR tv90[ebp], 0;将临时变量tv90赋值为空指针,这是在上面判断bp指针为空的情况下执行,这里不执行这一句
    $LN4@main:
        mov    eax, DWORD PTR tv90[ebp];将临时变量tv90里面的值赋给寄存器eax,eax保存了对象Right的首地址
        mov    DWORD PTR _rp$[ebp], eax;将寄存器eax里面的值赋给指针rp
                                    ;到这里,完成了从指针bp到指针rp的转化,这里之所以有对bp指针为空的判断
                                    ;是因为,rp里面的地址值是由bp里面的地址值加8byte得来,如果不进行判断,一旦bp为空指针
                                    ;即bp不指向任何对象,那么rp将指向错误的内存,这种转换就有危险,编译器必须避免这种情况
    
    ; 46   :     b.k = 1;
    
        mov    DWORD PTR _b$[ebp+12], 1;将1写入偏移对象b首地址12byte处,即将1赋给继承来的成员变量k
    
    ; 47   :     bp->k = 2;
    
        mov    ecx, DWORD PTR _bp$[ebp];将对象b首地址给寄存器ecx
        mov    DWORD PTR [ecx+12], 2;将2写入偏移对象b首地址12byte处,即将2赋给继承来的成员变量k
                                 ;可以看到,这里其成员变量的偏移量也是编译器固定,为2byte
                                 ;且两种方式访问没有差别
    
    ; 48   :     rp->k = 3;
    
        mov    edx, DWORD PTR _rp$[ebp];将父对象Right首地址给寄存器edx
        mov    DWORD PTR [edx+4], 3;将3写给偏移父对象Right首地址4byte处,即将3赋给成员变量k
    
    ; 49   :     
    ; 50   :     Top* tp = bp;
    
        cmp    DWORD PTR _bp$[ebp], 0;比较bp指针的值是否为0,也就是判断bp是否为空,原因同上
        jne    SHORT $LN5@main;如果不为空,就跳转到标号处$LN5@main执行,否则顺序执行,这里跳转到标号处执行
        mov    DWORD PTR tv145[ebp], 0;如果bp为空指针,就将0赋给临时变量tv145,这里不执行这一句
        jmp    SHORT $LN6@main;跳转到标号处$LN6@main执行
    $LN5@main:
        mov    eax, DWORD PTR _bp$[ebp];将对象b的首地址给寄存器eax
        mov    ecx, DWORD PTR [eax];将对象b首地址里面的内容给寄存器ecx,对象b首地址处的值是vtable的地址,关于vtable将在下面解释
        mov    edx, DWORD PTR _bp$[ebp];将对象b的首地址给寄存器edx
        add    edx, DWORD PTR [ecx+4];ecx里面存有vtable的首地址,这里取偏移vtable首地址4byte处内存内容(即对象b,或者父对象Left首地址到虚基类首地址的偏移量), 然后加上对象b的首地址
                                  ;得到虚基类对象Top的首地址
        mov    DWORD PTR tv145[ebp], edx;寄存器edx里面保存虚基类对象Top的首地址,保存到临时变量tv145里面
    $LN6@main:
        mov    eax, DWORD PTR tv145[ebp];将临时变量tv145里面的值给寄存器eax
        mov    DWORD PTR _tp$[ebp], eax;寄存器eax里面含有虚基类对象Top首地址,给指针tp
                                    ;这里完成了从指针bp到tp的转换
    
    ; 51   :     b.i = 1;
    
        mov    ecx, DWORD PTR _b$[ebp];将对象b的首地址的内容给寄存器ecx,ecx里面是vtable的首地址
        mov    edx, DWORD PTR [ecx+4];取偏移vtable首地址4byte处的内容,即对象b首地址到虚基类Top首地址偏移量给寄存器edx
        mov    DWORD PTR _b$[ebp+edx], 1;将对象首地址加上edx里面的偏移量,得到虚基类Top首地址,将1写入这给地址所指内存,ji
                                     ;为继承自虚基类的成员变量i赋值
    
    ; 52   :     bp->i = 2;
    
        mov    eax, DWORD PTR _bp$[ebp];将对象b的首地址给寄存器eax
        mov    ecx, DWORD PTR [eax];将对象b首地址处内容给寄存器ecx,即将vtable的首地址给寄存器ecx
        mov    edx, DWORD PTR [ecx+4];将偏移vtable首地址4byte处内存内容给寄存器edx,即将对象b首地址到虚基类Top首地址偏移量给edx
        mov    eax, DWORD PTR _bp$[ebp];将对象b首地址给寄存器eax
        mov    DWORD PTR [eax+edx], 2;将对象b首地址加上刚才取出的偏移量,得到虚基类Top的首地址,将2写入改地址所处内存处,
                                  ;即为继承自虚基类的成员变量i赋值2
    
    ; 53   :     tp->i = 3;
    
        mov    ecx, DWORD PTR _tp$[ebp];将虚基类对象Top的首地址给寄存器ecx
        mov    DWORD PTR [ecx], 3;将3赋给虚基类对象Top首地址处内才能,即为成员变量i赋值
    
    ; 54   :     
    ; 55   : };
    
        xor    eax, eax
        mov    esp, ebp
        pop    ebp
        ret    0
    _main    ENDP

    接下来是Bottom构造函数的汇编码:

    ??0Bottom@@QAE@HHHH@Z PROC                ; Bottom::Bottom, COMDAT
    ; _this$ = ecx
    
    ; 28   :     Bottom(int ll, int jj, int kk, int ii) : Top(ii), Left(jj, ii), Right(kk, ii) {
    
        push    ebp
        mov    ebp, esp
        push    ecx;压栈ecx的目的是为保存对象b的首地址预留空间
        mov    DWORD PTR _this$[ebp], ecx;ecx里面保存这对象b的首地址,存放到刚才空间
        cmp    DWORD PTR _$initVBases$[ebp], 0;_$initVBases所代表的内存里面的内容存放的是调用Bottom构造器时压入的标志,其值为1
                                           ;这里与0进行比较
        je    SHORT $LN1@Bottom;如果上面比较结果相等,就跳到标号处$LN1@Bottom执行,否则顺序执行,这里是顺序执行
        mov    eax, DWORD PTR _this$[ebp];将对象b的首地址给寄存器eax
        mov    DWORD PTR [eax], OFFSET ??_8Bottom@@7BLeft@@@;将Bottom-Left的vtable首地址写入对象b首地址处内存
        mov    ecx, DWORD PTR _this$[ebp];将对象b的首地址给寄存器ecx
        mov    DWORD PTR [ecx+8], OFFSET ??_8Bottom@@7BRight@@@;将Bottom-Right的vtable首地址写入偏移对象b首地址8byte处
                                                            ;即写入对象Right的首地址处内存
        mov    edx, DWORD PTR _ii$[ebp];将参数ii的值给寄存器edx
        push    edx;压栈寄存器edx,作为参数传递给虚基类的构造函数Top
        mov    ecx, DWORD PTR _this$[ebp];将对象b的首地址给寄存器ecx
        add    ecx, 20                    ; 将对象b的首地址加上20,得到虚基类Top的首地址,存放到寄存器ecx,作为隐含参数传递给虚基类Top的构造函数
        call    ??0Top@@QAE@H@Z                ; 调用虚基类Top的构造函数
    $LN1@Bottom:;
        push    0;标志0,说明已经调用过虚基类Top的构造函数,在调用Right和Left的构造函数时,就不会再调用了。
        mov    eax, DWORD PTR _ii$[ebp];将参数ii的值给寄存器eax
        push    eax;压栈eax,给Left的构造函数传递参数
        mov    ecx, DWORD PTR _jj$[ebp];将参数jj的值给寄存器ecx
        push    ecx;压栈ecx,给Left的构造函数传递参数
        mov    ecx, DWORD PTR _this$[ebp];将对象b的首地址(也就是对象Left的首地址)给寄存器ecx,作为隐含参数传递给Left构造函数
        call    ??0Left@@QAE@HH@Z            ; 调用Left构造函数
        push    0;压栈标志0,说明已经调用过虚基类Top的构造函数,在调用Right和Left的构造函数时,就不会再调用了
        mov    edx, DWORD PTR _ii$[ebp];将参数ii的值给寄存器edx
        push    edx;压栈edx,给Right构造函数传递参数
        mov    eax, DWORD PTR _kk$[ebp];将参数kk的值给寄存器eax
        push    eax;压栈eax,给Right的构造函数传递参数
        mov    ecx, DWORD PTR _this$[ebp];将对象b的首地址给ecx
        add    ecx, 8;将对象b的首地址加上8,得到对象Right的首地址,存入寄存器ecx,作为隐含参数传递给Right构造函数
        call    ??0Right@@QAE@HH@Z            ; 调用Right构造函数
    
    ; 29   :         l = ll;
    
        mov    ecx, DWORD PTR _this$[ebp];将对象b的首地址给寄存器ecx
        mov    edx, DWORD PTR _ll$[ebp];将参数ll的值给寄存器edx
        mov    DWORD PTR [ecx+16], edx;将寄存器edx的内容写入偏移对象b首地址16byte处,即给对象b的成员变量l赋值
    
    ; 30   :     }
    
        mov    eax, DWORD PTR _this$[ebp]
        mov    esp, ebp
        pop    ebp
        ret    20                    ; 00000014H
    ??0Bottom@@QAE@HHHH@Z ENDP        

    下面是Left构造函数的汇编码:

    ??0Left@@QAE@HH@Z PROC                    ; Left::Left, COMDAT
    ; _this$ = ecx
    
    ; 12   :     Left(int jj, int ii) : Top(ii) {
    
        push    ebp
        mov    ebp, esp
        push    ecx;压栈ecx寄存器,是为保存对象Left的首地址预留空间
        mov    DWORD PTR _this$[ebp], ecx;寄存器ecx里面含有对象Left的首地址,存入刚才预留空间
        cmp    DWORD PTR _$initVBases$[ebp], 0;_$initVBases所代表的内存,里面含有调用Left构造函数传入的标志,其值为0
                                           ;这里是将它的值和0作比较
        je    SHORT $LN1@Left;如果上面比较相等,则跳转到标号$LN1@Left处执行,否则顺序执行,这里跳转到标号执行,因此不会调用
                           ;虚基类Top的构造函数,避免重复调用
                           ;标号之前的语句在构造对象b的时候都不会执行
        mov    eax, DWORD PTR _this$[ebp];将Left对象的首地址给eax寄存器
        mov    DWORD PTR [eax], OFFSET ??_8Left@@7B@;将??_8Left@@7B@所带表的内存地址(即Left的vtable首地址)写入对象Left的首地址处内存
                                                 ;由于这一句在构造对象b时不执行,设置无效
        mov    ecx, DWORD PTR _ii$[ebp];将参数ii的值给寄存器ecx
        push    ecx;将ecx压栈,给虚基类Top构造函数传递参数,但是这一句在构造对象b时不执行,因此传参无效
        mov    ecx, DWORD PTR _this$[ebp];将对象Left的首地址给ecx寄存器
        add    ecx, 8;将Left的首地址加上8,得到Top对象的首地址,作为隐含参数传递给Top的构造函数
        call    ??0Top@@QAE@H@Z                ; 调用Top的构造函数,但是在构造对象b时,这句不执行,因此调用无效
    $LN1@Left:
    
    ; 13   :         j = jj;
    
        mov    edx, DWORD PTR _this$[ebp];将对象Left的首地址给寄存器edx
        mov    eax, DWORD PTR _jj$[ebp];将参数jj给寄存器eax
        mov    DWORD PTR [edx+4], eax;将eax寄存器里面的内容写入偏移对象Left首地址4byte处内存,即给成员变量j赋值jj
    
    ; 14   :     }
    
        mov    eax, DWORD PTR _this$[ebp]
        mov    esp, ebp
        pop    ebp
        ret    12                    ; 0000000cH
    ??0Left@@QAE@HH@Z ENDP        

    下面是Right构造函数的汇编码:

    ??0Right@@QAE@HH@Z PROC                    ; Right::Right, COMDAT
    ; _this$ = ecx
    
    ; 20   :     Right(int kk, int ii) : Top(ii) {
    
        push    ebp
        mov    ebp, esp
        push    ecx;压栈ecx的目的是为了保存对象Right的首地址预留空间
        mov    DWORD PTR _this$[ebp], ecx;ecx寄存器保存有对象Right的首地址,存放到刚才预留空间
        cmp    DWORD PTR _$initVBases$[ebp], 0;_$initVBases所代表的内存存放调用Right构造函数时传入的标志,其值为0,说明
                                           ;这里将其值与0比较
        je    SHORT $LN1@Right;如果比较相等,就跳转到标号处执行$LN1@Right,不会调用虚基类的构造函数,否则,顺序执行,这里跳转到标号执行
                            ;所有标号之前的语句在构造对象b时都不会执行
        mov    eax, DWORD PTR _this$[ebp];将对象Right的首地址给寄存器eax
        mov    DWORD PTR [eax], OFFSET ??_8Right@@7B@;将??_8Right@@7B@的所带表的内存地址(即Right的vtable首地址)写入到对象Right的首地址处内存
        mov    ecx, DWORD PTR _ii$[ebp];将参数ii的值给寄存器ecx
        push    ecx;压栈ecx,为调用Top构造函数传递参数
        mov    ecx, DWORD PTR _this$[ebp];将对象Right首地址给寄存器ecx
        add    ecx, 8;将对象Right的首地址加8,得到对象Top首地址,作为隐含参数传递给Top的构造函数
        call    ??0Top@@QAE@H@Z                ; 调用Top构造函数
    $LN1@Right:
    
    ; 21   :         k = kk;
    
        mov    edx, DWORD PTR _this$[ebp];将Right首地址给寄存器edx
        mov    eax, DWORD PTR _kk$[ebp];将参数kk的值给寄存器eax
        mov    DWORD PTR [edx+4], eax;将eax里面的值写入偏移对象Right首地址4byte处,即为成员变量k赋值kk
    
    ; 22   :     }
    
        mov    eax, DWORD PTR _this$[ebp]
        mov    esp, ebp
        pop    ebp
        ret    12                    ; 0000000cH
    ??0Right@@QAE@HH@Z ENDP            

    下面是Top函数的汇编码:

    ??0Top@@QAE@H@Z PROC                    ; Top::Top, COMDAT
    ; _this$ = ecx
    
    ; 4    :     Top(int ii) {
    
        push    ebp
        mov    ebp, esp
        push    ecx;压栈的目的是为保留对象Top的首地址预留空间
        mov    DWORD PTR _this$[ebp], ecx;ecx寄存器里面含有对象Top的首地址,存到刚才预留的空间
    
    ; 5    :         i = ii;
    
        mov    eax, DWORD PTR _this$[ebp];将对象Top的首地址给寄存器eax
        mov    ecx, DWORD PTR _ii$[ebp];将参数ii的值给寄存器ecx
        mov    DWORD PTR [eax], ecx;将ecx的值写入对象Top首地址处,即给成员变量i赋值ii
    
    ; 6    :     }
    
        mov    eax, DWORD PTR _this$[ebp]
        mov    esp, ebp
        pop    ebp
        ret    4
    ??0Top@@QAE@H@Z ENDP    


    下面是类之间的继承关系图:

                           图1 菱形继承,所有类不含任何虚函数

    下面是每个类的内存布局

                      Left(12byte)

                                        Right(12byte)

                             Bottom(24byte)

    上面代码中,Left和Right类对象首地址处都含有一个vbtable(误写为了vtable)指针,指向一个vbtable,vbtable里面只有两项:第一项是vbtable指针所属类的虚表指针vptr(没有就从对象首地址开始算)相对于vbtale指针的偏移量;第二项是其父类虚表指针vptr(没有的话就是对象首地址)相对于vbtable的偏移量。

    从Bottom Left Right的构造函数可以看出来,在每次调用相应的构造函数之前,都会有编译器传入一个标志,以此来防止虚基类构造函数被多次调用。这就是为什么虚基类只有一份实例的原因。虚基类的构造函数总是由当前正构造的对象的构造函数调用,比如这里构造Bottom对象时,就由Bottom构造函数调用,Left和Right构造函数不会调用。

  • 相关阅读:
    exkmp
    欧拉通路、回路
    你有多久没有看过星星
    trie树模板(统计难题)
    SPFA(热浪)
    codevs1958 刺激
    洛谷1290 欧几里得的游戏
    洛谷1016 旅行家的预算
    Spfa算法模板
    Tyvj2017清北冬令营入学测试
  • 原文地址:https://www.cnblogs.com/chaoguo1234/p/3149212.html
Copyright © 2011-2022 走看看