从汇编看c++成员函数指针(二)

    添加时间:2013-7-7 点击量:

    下面先看一段c++源码:



    #include <cstdio>
    
    using namespace std;

    class X {
    public:
    virtual int get1() {
    return 1;
    }
    virtual int get2() {
    return 2;
    }
    };

    class Y {
    public:
    virtual int get3() {
    return 3;
    }
    virtual int get4() {
    return 4;
    }
    };

    class Z : public X, public Y {
    public:
    int get2() {
    return 5;
    }
    int get4() {
    return 6;
    }
    };

    int main() {
    Z z;
    Z
    zp = &z;
    X
    xp = zp;
    Y
    yp = zp;
    int(X::xgp1)() = &X::get1;
    int(X::xgp2)() = &X::get2;
    int(Y::ygp3)() = &Y::get3;
    int(Y::ygp4)() = &Y::get4;
    int(Z::zgp1)() = &Z::get1;
    int(Z::zgp2)() = &Z::get2;
    int(Z::zgp3)() = &Z::get3;
    int(Z::zgp4)() = &Z::get4;
    /输出各个成员函数指针的值/
    printf(
    &X::get1 = %lu\n, &X::get1);
    printf(
    &X::get2 = %lu\n, &X::get2);
    printf(
    &Y::get3 = %lu\n, &Y::get3);
    printf(
    &Y::get4 = %lu\n, &Y::get4);
    printf(
    &Z::get1 = %lu\n, &Z::get1);
    printf(
    &Z::get2 = %lu\n, &Z::get2);
    printf(
    &Z::get3 = %lu\n, &Z::get3);
    printf(
    &Z::get4 = %lu\n, &Z::get4);
    printf(
    \n);
    printf(
    xgp1 = %lu\n, xgp1);
    printf(
    xgp2 = %lu\n, xgp2);
    printf(
    ygp3 = %lu\n, ygp3);
    printf(
    ygp4 = %lu\n, ygp4);
    printf(
    zgp1 = %lu\n, zgp1);
    printf(
    zgp2 = %lu\n, zgp2);
    printf(
    zgp3 = %lu\n, zgp3);
    printf(
    zgp4 = %lu\n, zgp4);

    /用成员函数指针类X中的虚函数/
    (zp
    ->xgp1)();
    (zp
    ->zgp1)();
    (xp
    ->xgp1)();
    (zp
    ->xgp2)();
    (zp
    ->zgp2)();
    (xp
    ->xgp2)();

    /用成员函数指针调用Y中的虚函数/
    (zp
    ->ygp3)();
    (zp
    ->zgp3)();
    (yp
    ->ygp3)();
    (zp
    ->ygp4)();
    (zp
    ->zgp4)();
    (yp
    ->ygp4)();

    }


    类Z多重持续与类X和类Y,类X和类Y分别有两个虚函数,而类Z重写了他们傍边的各一个。main函数主如果输出各个成员函数指针的值以及用成员函数指针调用响应的函数


    下面是输出成果:



    经由过程从汇编看c++中成员函数指针(一)我们知道,指向虚函数的成员指针保存的是响应vcall函数的地址,从成果中我们可以看到,这8个成员函数指针只保存了两个vcall函数的地址。下面是vcall函数的汇编码:



    _TEXT    SEGMENT
    
    ??_9X@@¥BA@AE PROC
    ; X::`vcall{0}, COMDAT
    mov eax, DWORD PTR [ecx];存放器ecx里面存有对象首地址,这里将对象首地址处内容(即vftable首地址)给存放器eax
    jmp DWORD PTR [eax];跳转到vftable首地址内存(里面存放虚函数的地址)所存地址处履行
    ??_9X@@¥BA@AE ENDP ; X::`vcall{0}
    ;
    Function compile flags: /Odtp
    _TEXT ENDS
    ; COMDAT ??_9X@@¥B3AE
    _TEXT SEGMENT
    ??_9X@@¥B3AE PROC
    ; X::`vcall{4}, COMDAT
    mov eax, DWORD PTR [ecx];存器ecx里面存有对象首地址,这里将对象首地址处内容(即vftable首地址)给存放器eax
    jmp DWORD PTR [eax+4];跳转到偏移vftable首地址4byte处内存(里面存放虚函数首地址)所存地址处履行
    ??_9X@@¥B3AE ENDP ; X::`vcall{4}
    _TEXT ENDS


    从汇编码可以看到,不管是vcall{0}还是vcall{4},它们所做的工作都是类似的。按照存于存放器ecx里面对象的首地址,找到响应的虚表,然后跳转到响应的虚函数去履行。而这里所有的虚函数要么相对于虚表的首地址偏移量为0 要么是4, 是以,只要这两个vcall函数就够用了,
    ,只要包管传进来的对象首地址正确,可以或许找到正确的虚表即可。


    接下来让我们来看一下定义各个成员函数指针的汇编码:



    ; 39   :     int(X::xgp1)() = &X::get1;
    

    mov DWORD PTR _xgp1¥[ebp], OFFSET ??_9X@@¥BA@AE ; X::`vcall{0} 将X::vcall{0}的地址给xgp1

    ; 40 : int(X::xgp2)() = &X::get2;

    mov DWORD PTR _xgp2¥[ebp], OFFSET ??_9X@@¥B3AE ; X::`vcall{4} 将X::vcall{4}的地址给xgp2

    ; 41 : int(Y::ygp3)() = &Y::get3;

    mov DWORD PTR _ygp3¥[ebp], OFFSET ??_9X@@¥BA@AE ; X::`vcall{0} 将X::vcall{0}的地址给ypg3

    ; 42 : int(Y::ygp4)() = &Y::get4;

    mov DWORD PTR _ygp4¥[ebp], OFFSET ??_9X@@¥B3AE ; X::`vcall{4} 将X::vcall{4}的地址给ygp4

    ; 43 : int(Z::zgp1)() = &Z::get1;

    mov DWORD PTR ¥T4081[ebp], OFFSET ??_9X@@¥BA@AE ; X::`vcall{0} 将X::vcall{0}的地址写给姑且对象¥T4081首地址处内存
    mov DWORD PTR ¥T4081[ebp+4], 0;将0写给偏移姑且对象¥T4081说地址4byte处内存 这里的0是相对于对象z首地址的偏移量
    mov ecx, DWORD PTR ¥T4081[ebp];将姑且对象¥T4081首地址处的值给存放器ecx
    mov DWORD PTR _zgp1¥[ebp], ecx;将ecx的值给对象zpg1对象首地址处内存
    mov edx, DWORD PTR ¥T4081[ebp+4];将偏移姑且对象¥T4081首地址4byte处内存内容给存放器edx
    mov DWORD PTR _zgp1¥[ebp+4], edx;将edx的内容给偏移对象zgp1首地址4byet处内存
    ;上方这一段完成了从姑且对象¥T4081到对象zpg1的拷贝
    ;可以看到zpg1是一个对象,它的首地址存储的是响应vcall函数的地址
    ;紧挨着的内存存储的是其所指虚函数所属类相对于对象z首地址的偏移量
    ;接下来的zgp2 zgp3 zgp4和zgp1的过程类似 存储的都是响应vcall函数的地址和
    ;其所指虚函数所属类相对于对象z首地址偏移量

    ; 44 : int(Z::zgp2)() = &Z::get2;

    mov DWORD PTR ¥T4082[ebp], OFFSET ??_9X@@¥B3AE ; X::`vcall{4} ;存vcall地址
    mov DWORD PTR ¥T4082[ebp+4], 0;偏移量为0
    mov eax, DWORD PTR ¥T4082[ebp]
    mov DWORD PTR _zgp2¥[ebp], eax
    mov ecx, DWORD PTR ¥T4082[ebp+4]
    mov DWORD PTR _zgp2¥[ebp+4], ecx

    ; 45 : int(Z::zgp3)() = &Z::get3;

    mov DWORD PTR ¥T4083[ebp], OFFSET ??_9X@@¥BA@AE ; X::`vcall{0};存vcall地址
    mov DWORD PTR ¥T4083[ebp+4], 4;偏移量为4
    mov edx, DWORD PTR ¥T4083[ebp]
    mov DWORD PTR _zgp3¥[ebp], edx
    mov eax, DWORD PTR ¥T4083[ebp+4]
    mov DWORD PTR _zgp3¥[ebp+4], eax

    ; 46 : int(Z::zgp4)() = &Z::get4;

    mov DWORD PTR ¥T4084[ebp], OFFSET ??_9X@@¥B3AE ; X::`vcall{4} 存vcall地址
    mov DWORD PTR ¥T4084[ebp+4], 4;偏移量为4
    mov ecx, DWORD PTR ¥T4084[ebp]
    mov DWORD PTR _zgp4¥[ebp], ecx
    mov edx, DWORD PTR ¥T4084[ebp+4]
    mov DWORD PTR _zgp4¥[ebp+4], edx


    从汇编码可以看到,xgp1 xgp2  ygp3 ygp4和从汇编看c++中成员函数指针(一)所讲的一样,仅仅存储的是响应vcall的地址;而zgp1 zgp2 zgp3 zgp4倒是一个对象,它们存储了两类信息,在其首地址处存储的是响应vcall函数的地址,而接下来的内存中存储的是成员函数指针所指成员函数地点类相对于对象z首地址的偏移量。是以,可以发明,若是是单一的类,成员函数指针仅仅存储的是vcall函数的地址(对于单一持续也一样,因为基类相对于派生类偏移量都邑为0,可以不保存),然则多重持续还要多存一个偏移量,那么这个多存的偏移量有什么感化呢?下面来看用成员函数指针调用虚函数的汇编代码:



    ; 65   :     
    
    ;
    66 : /用成员函数指针类X中的虚函数/
    ;
    67 : (zp->xgp1)();

    mov ecx, DWORD PTR _zp¥[ebp];将对象z的首地址(也是父类X首地址)给存放器ecx,作为隐含参数传递给响应的vcall函数
    call DWORD PTR _xgp1¥[ebp];调用vcall函数

    ; 68 : (zp->zgp1)();

    mov ecx, DWORD PTR _zp¥[ebp];将对象z的首地址(也是父类X首地址)给存放器ecx
    add ecx, DWORD PTR _zgp1¥[ebp+4];将偏移对象zgp1首地址4byte处内存内容取出(即父类X对象相对于对象z首地址偏移量)
    ;加到ecx存放器里面的值上 获得指向父类X对象首地址(也是对象z的首地址)
    ;作为隐含参数传给响应的vcall函数
    call DWORD PTR _zgp1¥[ebp];调用vcall函数 zgp1首地址存有vcall函数地址

    ; 69 : (xp->xgp1)();

    mov ecx, DWORD PTR _xp¥[ebp];将父类X对象首地址(也是对象z的首地址)给存放器ecx,作为隐含参数传递给vcall函数
    call DWORD PTR _xgp1¥[ebp];调用vcall函数

    ;接下来的调用和上方的一样
    ;
    70 : (zp->xgp2)();

    mov ecx, DWORD PTR _zp¥[ebp]
    call DWORD PTR _xgp2¥[ebp]

    ; 71 : (zp->zgp2)();

    mov ecx, DWORD PTR _zp¥[ebp]
    add ecx, DWORD PTR _zgp2¥[ebp+4]
    call DWORD PTR _zgp2¥[ebp]

    ; 72 : (xp->xgp2)();

    mov ecx, DWORD PTR _xp¥[ebp]
    call DWORD PTR _xgp2¥[ebp]

    ; 73 :
    ;
    74 : /用成员函数指针调用Y中的虚函数/
    ;
    75 : (zp->ygp3)();

    cmp DWORD PTR _zp¥[ebp], 0;减查zp指针是否和0相等,便是否为空 防止因为zp为空指针而使得调剂后的指针指向错误内存
    je SHORT ¥LN5@main;若是zp为空指针,跳到标号¥LN5@main处履行 不然 次序履行 这里是次序履行
    mov ecx, DWORD PTR _zp¥[ebp];将对象z的首地址给存放器ecx
    add ecx, 4;对象z的首地址加4 获得是父类Y对象的首地址,存到存放器ecx
    mov DWORD PTR tv192[ebp], ecx;将ecx的值给姑且变量tv192
    jmp SHORT ¥LN6@main;跳转到标号¥LN6@main处履行
    LN5@main:
    mov DWORD PTR tv192[ebp], 0;若zp指针为空,姑且变来那个tv192赋0
    LN6@main:
    mov ecx, DWORD PTR tv192[ebp];将tv192的值给存放器ecx(这里是父类Y对象的首地址),作为vcall函数的隐含参数
    call DWORD PTR _ygp3¥[ebp];调用响应的vcall函数

    ; 76 : (zp->zgp3)();

    mov ecx, DWORD PTR _zp¥[ebp];将对象z的首地址给存放器ecx
    add ecx, DWORD PTR _zgp3¥[ebp+4];将zgp3存储的偏移量加在ecx的值上 可以获得父类Y对象的首地址 作为隐含参数传递给vcall函数
    call DWORD PTR _zgp3¥[ebp];调用响应的vcall函数

    ; 77 : (yp->ygp3)();

    mov ecx, DWORD PTR _yp¥[ebp];将父类Y对象的首地址给存放器ecx 作为隐含参数传递给vcall函数
    call DWORD PTR _ygp3¥[ebp];调用vcall函数

    ;下面的调用和上方的类似

    ; 78 : (zp->ygp4)();

    cmp DWORD PTR _zp¥[ebp], 0
    je SHORT ¥LN7@main
    mov edx, DWORD PTR _zp¥[ebp]
    add edx, 4
    mov DWORD PTR tv202[ebp], edx
    jmp SHORT ¥LN8@main
    LN7@main:
    mov DWORD PTR tv202[ebp], 0
    LN8@main:
    mov ecx, DWORD PTR tv202[ebp]
    call DWORD PTR _ygp4¥[ebp]

    ; 79 : (zp->zgp4)();

    mov ecx, DWORD PTR _zp¥[ebp]
    add ecx, DWORD PTR _zgp4¥[ebp+4]
    call DWORD PTR _zgp4¥[ebp]

    ; 80 : (yp->ygp4)();

    mov ecx, DWORD PTR _yp¥[ebp]
    call DWORD PTR _ygp4¥[ebp]


    经由过程汇编代码可以看出,多重持续下的成员函数指针多存储的偏移量信息,是为了在调用响应的vcall函数之前,将this指针调剂到响应的地位,以便传给vcall函数后,以此找到正确的虚表。比如上方汇编代码中的第79行:



    ; 79   :     (zp->zgp4)();
    

    mov ecx, DWORD PTR _zp¥[ebp];将对象z的首地址给存放器ecx
    add ecx, DWORD PTR _zgp4¥[ebp+4];将zgp4存储的偏移量加载对象z的首地址上 从定义zgp4的汇编代码可知
    ;zgp4存储的偏移量为4 正好是父类X对象的大小,是以此时ecx里面存储的是
    ;父类Y对象的首地址
    call DWORD PTR _zgp4¥[ebp];调用vcall函数


    从上方的汇编代码还可以发明,指向成员函数的指针也支撑多态,因为他们都邑调用vcall函数,查询虚表。是以 (zp->zgp4)和(yp->ygp4)以及(zp->ygp4)都邑调用类Z覆写之后的函数。并且可以用zp来接见ygp4,实际上从汇编码可以看出,是编译器做了转换,将zp指针调剂为指向父类Y对象首地址的指针(相当于向上转换),于是就和yp->yp4一样了。然则反过来不可,就是不克不及yp->zgp4,因为如许做的话就比如要先将yp向下转换为Z类型,而这时不容许的。

    文艺不是炫耀,不是花哨空洞的文字堆砌,不是一张又一张的逆光照片,不是将旅行的意义转化为名牌包和明信片的物质展示;很多时候它甚至完全不美——它嘶吼、扭曲,它会痛苦地抽搐,它常常无言地沉默。——艾小柯《文艺是一种信仰》
    分享到: