zoukankan      html  css  js  c++  java
  • 从汇编看c++多重继承中this指针的变化

    先来看一下下面的c++源码:

    #include <iostream>
    using namespace std;
    
    class X {
    public:
        virtual void print1() {
            cout << "X::print1 = " << (long)this << endl;
        }
        virtual void print2() {
            cout << "X::print2 = " << (long)this << endl;
        }
    };
    
    class Y {
    public:
        virtual void print3() {
            cout << "Y::print3 = " << (long)this << endl;
        }
        virtual void print4() {
            cout << "Y::print4 = " << (long)this << endl;
        }
    };
    
    class Z : public X, public Y {
    public:
        virtual void print2() {
            cout << "Z::print2 = " << (long)this << endl;
        }
        virtual void print4() {
            cout << "Z::print4 = " << (long)this << endl;
        }
    };
    
    int main() {
        Z z;
        Z* zp = &z;
        X* xp = zp;
        Y* yp = zp;
        cout << "zp = " << (long)zp << endl;
        cout << "xp = " << (long)xp << endl;
        cout << "yp = " << (long)yp << endl;
        /*******************以派生类指针调用基类虚函数*******************/
        zp->print1();
        zp->print3();
        /*******************以派生里指针调用派生类虚函数**************/
        zp->print2();
        zp->print4();
        /*******************以基类指针调用基类虚函数***************/
        xp->print1();
        yp->print3();
        /***********************以基类指针调用派生类虚函数***********/
        xp->print2();
        yp->print4();
    }

    类Z多重继承与类X和类Y,类X和类Y各有两个虚函数,分别输出this指针的值。其中,类Z覆写了类X和类Y中的两个虚函数。

    下面是这段代码的输出结果:

    从上面的输出结果可以看到,以派生类指针zp调用基类虚函数print1和print3,输出的this指针值分别为父类X对象和父类Y对象的首地址;而已基类指针xp和zp调用派生类虚函数print2和print4则都输出的是派生类Z对象的首地址,那么,这当中this指针是如何调整的呢?先来看一下main函数里面的汇编码(只看调用函数的部分,其他部分省略):

    ; 42   :     /*******************以派生类指针调用基类虚函数*******************/
    ; 43   :     zp->print1();
    
        mov    ecx, DWORD PTR _zp$[ebp];将对象z的首地址给寄存器ecx
        mov    edx, DWORD PTR [ecx];将对象z的首地址处内存内容(即vftable首地址)给寄存器edx
        mov    ecx, DWORD PTR _zp$[ebp];将对象z的首地址(也是父类X对象首地址)给寄存器ecx,作为隐含参数传递给虚函数print1
        mov    eax, DWORD PTR [edx];将vftable首地址处内存内容(即print1的地址)给寄存器eax
        call    eax;调用虚函数print1
    
    ; 44   :     zp->print3();
    
        mov    ecx, DWORD PTR _zp$[ebp];将对象z的首地址给就寄存器ecx
        add    ecx, 4;将寄存器ecx里面的额内容加4,得到父类Y对象首地址,存于ecx中,做为隐含参数传递给虚函数print3
        mov    edx, DWORD PTR _zp$[ebp];将对象z的首地址给寄存器edx
        mov    eax, DWORD PTR [edx+4];将偏移对象z首地址4byte处内存内容(即vftable首地址)给寄存器eax
        mov    edx, DWORD PTR [eax];将vftable首地址处内存内容(即print3的地址)给寄存器edx
        call    edx;调用虚函数print3
    
    ; 45   :     /*******************以派生里指针调用派生类虚函数**************/
    ; 46   :     zp->print2();
    
        mov    eax, DWORD PTR _zp$[ebp];将对象z的首地址给寄存器eax
        mov    edx, DWORD PTR [eax];将对象z首地址处的内存内容(即vftable首地址)给寄存器edx
        mov    ecx, DWORD PTR _zp$[ebp];将对象z的首地址(也是父类X对象的首地址)给寄存器ecx,作为隐含参数传递给虚函数print2
        mov    eax, DWORD PTR [edx+4];将偏移vftable首地址4byte处内存内容(即print2的地址)给寄存器eax
        call    eax;调用虚函数print2
    
    ; 47   :     zp->print4();
    
        mov    ecx, DWORD PTR _zp$[ebp];将对象z的首地址给寄存器ecx
        add    ecx, 4;将ecx里面的值加上4,得到父类Y对象的首地址,存放到ecx,作为隐含参数,传递给虚函数print4
        mov    edx, DWORD PTR _zp$[ebp];将对象z的首地址给寄存器edx
        mov    eax, DWORD PTR [edx+4];将偏移对象z首地址4byte处内存内容(即父类Y对象首地址处内存内容)给寄存器eax,eax存放vftable首地址
        mov    edx, DWORD PTR [eax+4];将偏移vftable首地址4byte处内存内容(即print4的地址)给寄存器eax
        call    edx;调用虚函数print4
    
    ; 48   :     /*******************以基类指针调用基类虚函数***************/
    ; 49   :     xp->print1();
    
        mov    eax, DWORD PTR _xp$[ebp];将父类X对象首地址给寄存器eax
        mov    edx, DWORD PTR [eax];将父类X对象首地址处内存内容(即vftable首地址)给寄存器edx
        mov    ecx, DWORD PTR _xp$[ebp];将父类X对象首地址给寄存器ecx,作为隐含参数传递给虚函数print1
        mov    eax, DWORD PTR [edx];将vftable首地址处内容(即print1的地址)给寄存器eax
        call    eax;调用虚函数print1
    
    ; 50   :     yp->print3();
    
        mov    ecx, DWORD PTR _yp$[ebp];将父类Y对象的首地址给寄存器ecx
        mov    edx, DWORD PTR [ecx];将父类Y对象首地址处内存内容(即vftable首地址)给寄存器edx
        mov    ecx, DWORD PTR _yp$[ebp];将父类Y对象首地址给寄存器ecx,作为隐含参数传递给虚函数print3
        mov    eax, DWORD PTR [edx];将vftable首地址处内存内容(即print3的地址)给寄存器eax
        call    eax;调用虚函数print3
    
    ; 51   :     /***********************以基类指针调用派生类虚函数***********/
    ; 52   :     xp->print2();
    
        mov    ecx, DWORD PTR _xp$[ebp];将父类X对象的首地址给寄存器ecx
        mov    edx, DWORD PTR [ecx];将父类X对象首地址处内存内容(即vftable首地址)给寄存器edx
        mov    ecx, DWORD PTR _xp$[ebp];将父类X对象的首地址给寄存器ecx,作为隐含参数传递给虚函数print2
        mov    eax, DWORD PTR [edx+4];将偏移vftable首地址4byte处的内存内容(即print2的首地址)给寄存器eax
        call    eax;调用虚函数print2
    
    ; 53   :     yp->print4();
    
        mov    ecx, DWORD PTR _yp$[ebp];将父类Y对象的首地址给寄存器ecx
        mov    edx, DWORD PTR [ecx];将父类Y对象首地址处内存内容(即vftable首地址)给寄存器edx
        mov    ecx, DWORD PTR _yp$[ebp];将父类Y对象首地址给寄存器ecx,作为隐含参数传递给虚函数print4
        mov    eax, DWORD PTR [edx+4];将偏移vftable首地址4byte处内存内容(即print4的首地址)给寄存器eax
        call    eax;调用虚函数print4

    从汇编代码可以看出,不管使用哪种指针调用哪类虚函数,传递给虚函数的this指针都是引入这个虚函数的类对象首地址。比如,以基类指针zp调用基类Y的虚函数print4,传递给print4的this指针就是父类Y对象的首地址;以基类指针yp调用派生类Z的虚函数print4,传递给print4的this指针也是父类Y对象的首地址。那么,既然传递给虚函数print4的this指针是父类Y对象的首地址,虚函数是如何保证正确输出的呢?下面来看一下print4的汇编码,了解其内部过程:

    ?print4@Z@@UAEXXZ PROC                    ; Z::print4, COMDAT
    ; _this$ = ecx
    
    ; 29   :     virtual void print4() {
    
        push    ebp
        mov    ebp, esp
        push    ecx;压栈ecx寄存器是为保留this指针预留空间
        mov    DWORD PTR _this$[ebp], ecx;寄存器ecx里面存放父类Y对象首地址,存于刚才预留的空间
    
    ; 30   :         cout << "Z::print4 = " << (long)this << endl;
    
        push    OFFSET ?endl@std@@YAAAV?$basic_ostream@DU?$char_traits@D@std@@@1@AAV21@@Z ; std::endl
        mov    eax, DWORD PTR _this$[ebp];将父类Y对象的首地址给寄存器eax
        sub    eax, 4;eax里面的值减4,这是eax里面存放的是对象z的首地址,存于寄存器eax
        push    eax;压栈eax寄存器,为输出传递参数
        push    OFFSET ??_C@_0N@BGKDGLPK@Z?3?3print4?5?$DN?5?$AA@
        push    OFFSET ?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A ; std::cout
        call    ??$?6U?$char_traits@D@std@@@std@@YAAAV?$basic_ostream@DU?$char_traits@D@std@@@0@AAV10@PBD@Z ; std::operator<<<std::char_traits<char> >
        add    esp, 8
        mov    ecx, eax
        call    ??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QAEAAV01@J@Z ; std::basic_ostream<char,std::char_traits<char> >::operator<<
        mov    ecx, eax
        call    ??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QAEAAV01@P6AAAV01@AAV01@@Z@Z ; std::basic_ostream<char,std::char_traits<char> >::operator<<
    
    ; 31   :     }
    
        mov    esp, ebp
        pop    ebp
        ret    0
    ?print4@Z@@UAEXXZ ENDP                    ; Z::print4


    主要看注释的部分,这里,虽然传递给print4的this指针(由寄存器ecx传过来)是父类Y对象的首地址,但是,在进行输出的时候,编译器为我们进行了this指着的调整,即上面汇编代码中sub eax 4的部分,将this指针重新调整到指向对象z的首地址处,因此能够正确输出。那么,编译器又是如何知道要调整的大小的呢,比如,编译器怎么知道要调整4byte呢?通过在命令行查看对象z的内存布局,打印出如下语句:

    可以看到,this指着要调整的值,被记录在相应的虚表之中(如何用命令行查看一个对象的内存布局,请参看

    c+中如何查看一个类的内存布局

    下面是print1函数的汇编码:

    ?print1@X@@UAEXXZ PROC                    ; X::print1, COMDAT
    ; _this$ = ecx
    
    ; 6    :     virtual void print1() {
    
        push    ebp
        mov    ebp, esp
        push    ecx;压栈寄存器ecx的目的是为了保存this指针预留空间
        mov    DWORD PTR _this$[ebp], ecx;ecx里面保存有父类X对象的首地址(也是对象z的首地址),存于刚才预留的空间
    
    ; 7    :         cout << "X::print1 = " << (long)this << endl;
    
        push    OFFSET ?endl@std@@YAAAV?$basic_ostream@DU?$char_traits@D@std@@@1@AAV21@@Z ; std::endl
        mov    eax, DWORD PTR _this$[ebp];将父类对象首地址(也是对象z的首地址)传给寄存器eax
        push    eax;将寄存器eax的值压栈,为输出传递参数
        push    OFFSET ??_C@_0N@LOBPFBMB@X?3?3print1?5?$DN?5?$AA@
        push    OFFSET ?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A ; std::cout
        call    ??$?6U?$char_traits@D@std@@@std@@YAAAV?$basic_ostream@DU?$char_traits@D@std@@@0@AAV10@PBD@Z ; std::operator<<<std::char_traits<char> >
        add    esp, 8
        mov    ecx, eax
        call    ??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QAEAAV01@J@Z ; std::basic_ostream<char,std::char_traits<char> >::operator<<
        mov    ecx, eax
        call    ??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QAEAAV01@P6AAAV01@AAV01@@Z@Z ; std::basic_ostream<char,std::char_traits<char> >::operator<<
    
    ; 8    :     }
    
        mov    esp, ebp
        pop    ebp
        ret    0
    ?print1@X@@UAEXXZ ENDP                    ; X::print1

    下面是print2函数的汇编码:

    ; 26   :     virtual void print2() {
    
        push    ebp
        mov    ebp, esp
        push    ecx;压栈寄存器ecx的目的是为了保存this指针预留空间
        mov    DWORD PTR _this$[ebp], ecx;寄存器ecx里面存放父类X对象的首地址(也是z对象首地址),保存到刚才预留的空间
    
    ; 27   :         cout << "Z::print2 = " << (long)this << endl;
    
        push    OFFSET ?endl@std@@YAAAV?$basic_ostream@DU?$char_traits@D@std@@@1@AAV21@@Z ; std::endl
        mov    eax, DWORD PTR _this$[ebp];将父类X对象首地址(也是z对象首地址)给寄存器eax
        push    eax;压栈寄存器eax,为输出传递参数
        push    OFFSET ??_C@_0N@JJODJOFK@Z?3?3print2?5?$DN?5?$AA@
        push    OFFSET ?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A ; std::cout
        call    ??$?6U?$char_traits@D@std@@@std@@YAAAV?$basic_ostream@DU?$char_traits@D@std@@@0@AAV10@PBD@Z ; std::operator<<<std::char_traits<char> >
        add    esp, 8
        mov    ecx, eax
        call    ??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QAEAAV01@J@Z ; std::basic_ostream<char,std::char_traits<char> >::operator<<
        mov    ecx, eax
        call    ??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QAEAAV01@P6AAAV01@AAV01@@Z@Z ; std::basic_ostream<char,std::char_traits<char> >::operator<<
    
    ; 28   :     }
    
        mov    esp, ebp
        pop    ebp
        ret    0
    ?print2@Z@@UAEXXZ ENDP                    ; Z::print2

    下面是print3函数的汇编码:

    ?print3@Y@@UAEXXZ PROC                    ; Y::print3, COMDAT
    ; _this$ = ecx
    
    ; 16   :     virtual void print3() {
    
        push    ebp
        mov    ebp, esp
        push    ecx;压栈ecx的目的是为包存this指针预留空间
        mov    DWORD PTR _this$[ebp], ecx;寄存器ecx里面保存父类Y对象的首地址,存于刚才预留的空间里面
    
    ; 17   :         cout << "Y::print3 = " << (long)this << endl;
    
        push    OFFSET ?endl@std@@YAAAV?$basic_ostream@DU?$char_traits@D@std@@@1@AAV21@@Z ; std::endl
        mov    eax, DWORD PTR _this$[ebp];将父类Y对象的首地址给寄存器eax
        push    eax;压栈eax寄存器,为输出传递参数
        push    OFFSET ??_C@_0N@BJEJNLCE@Y?3?3print3?5?$DN?5?$AA@
        push    OFFSET ?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A ; std::cout
        call    ??$?6U?$char_traits@D@std@@@std@@YAAAV?$basic_ostream@DU?$char_traits@D@std@@@0@AAV10@PBD@Z ; std::operator<<<std::char_traits<char> >
        add    esp, 8
        mov    ecx, eax
        call    ??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QAEAAV01@J@Z ; std::basic_ostream<char,std::char_traits<char> >::operator<<
        mov    ecx, eax
        call    ??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QAEAAV01@P6AAAV01@AAV01@@Z@Z ; std::basic_ostream<char,std::char_traits<char> >::operator<<
    
    ; 18   :     }
    
        mov    esp, ebp
        pop    ebp
        ret    0
    ?print3@Y@@UAEXXZ ENDP                    ; Y::print3

    可以看到,print1和print3函数里面没有this指针的调整,因为它们本身就是父类X和父类Y的虚函数,传过来的this指针是正确的,所以无需调整,而print2和print4被类Z重写了,成了类Z的虚函数,而传递过来的还是父类X和父类Y的首地址,因此需要调整。但是在print2函数里面看不到调整的汇编代码,这是因为,父类X对象的首地址和对象z的首地址一样,所以也可以不调整。

  • 相关阅读:
    码农提高工作效率 (转)
    Python快速教程 尾声
    C#基础——谈谈.NET异步编程的演变史
    [C#]動態叫用Web Service
    零极限 核心中的核心和详解
    项目经理应该把30%的时间用在编程上
    高效能程序员的七个习惯
    我们如何进行代码审查
    工作经常使用的SQL整理,实战篇(二)
    C# Socket网络编程精华篇 (转)
  • 原文地址:https://www.cnblogs.com/chaoguo1234/p/3175399.html
Copyright © 2011-2022 走看看