C++-逆向分析-类的成员函数地址与对象地址动态绑定-this指针-成员函数和成员虚函数反汇编解析

1.类的成员函数

类实例化为对象,数据成员是根据对象的首地址来偏移的,而对象的成员函数在内存中是公用的,但在C++或者C写代码时,却可以根据对象加操作符.或者->来进行访问,这在底层到底是如何实现的呢?
不同类的相同函数名的函数在内存中有不同的实现。C++中的函数在编译时会根据命名空间、类、参数签名等信息进行重新命名,形成新的函数名。

2.this指针

this指针具有如下特点:

名称属性:标识符this表示。
类型属性:classname* const
值属性:表示当前调用该函数对象的首地址。
作用域:this指针是编译器默认传给类中非静态函数的隐含形参,其作用域在非静态成员函数的函数体内。
链接属性:在类作用域中,不同类的非静态成员函数中,this指针变量的链接属性是内部的,但其所指对象是外部的,即this变量是不同的实体,但指向对象是同一个。
存储类型:this指针是由编译器生成,当类的非静态成员函数的参数个数一定时,this指针存储在ECX寄存器中;若该函数参数个数未定(可变参数函数),则存放在栈中。
this指针并不是对象的一部分,this指针所占的内存大小是不会反映在sizeof操作符上的。this指针的类型取决于使用this指针的成员函数类型以及对象类型。

类的成员函数默认第一个参数为T* const register this。

3.类成员函数指针

C++编译器在代码编译阶段会对类对象调用的成员函数进行静态绑定(虚函数进行动态绑定),类成员函数的地址在代码编译时就确定,类成员函数地址可以使用成员函数指针进行保存。
成员函数指针定义语法如下:

ReturnType (ClassName::* pointerName) (ArgumentLList);

ReturnType:成员函数返回类型
ClassName: 成员函数所属类的名称
Argument_List: 成员函数参数列表
pointerName:指针名称

class Test
{
public:
    void print()
    {
        cout << "Test::print" << endl;
    }
};

成员函数指针语法极其严格:

  • 不能使用括号:例如&(Test::print)不对。
  • 必须有限定符:例如&print不对,即使在类ClassName作用域内也不行。
  • 必须使用取地址符号:直接写Test::print不行,必须写:&Test::print。

Test类的成员函数print的函数指针声明及初始化如下:

void (Test::* pFunc)() = &Test::print;

通常,为了简化代码,使用typedef关键字。

typedef void (Test::*pFunc)();
pFunc p = &Test::print;

可以通过函数指针调用成员函数,示例代码如下:

#include <iostream>
using namespace std;
 
class Test
{
public:
    void print()
    {
        cout << "Test::print" << endl;
    }
};
 
int main(int argc, char *argv[])
{
    void (Test::* pFunc)() = &Test::print;
    Test test;
    //通过对象调用成员函数
    (test.*pFunc)();//Test::print
    Test* pTest = &test;
    //通过指针调用成员函数
    (pTest->*pFunc)();//Test::print
    //pFunc();//error
    //error: must use '.*' or '->*' to call pointer-to-member
    //function in 'pFunc (...)', e.g. '(... ->* pFunc) (...)'
    return 0;
}

上述代码中,.*pFunc将pFunc绑定到对象test,->*pFunc绑定pFunc到pTest指针所指向的对象。在 (obj.*fptr) 和 (p->*fptr) 两边的括号是语法所强制要求的。成员函数名不是常规函数指针(保存的是某个确切地址),非虚成员函数指针保存的是成员函数在内存中的地址,虚成员函数指针保存的是虚表调用选项,详情查看汇编代码分析。

3.1.常规成员函数转换

#include <iostream>

class Foo {
public:
    int f(char *c = 0) {
        std::cout << "Foo::f()" << std::endl;
        return 1;
    }
};

class Bar {
public:
    void b(int i = 0) {
        std::cout << "Bar::b()" << std::endl;
    }
};

class FooDerived : public Foo {
public:
    int f(char *c = 0) {
        std::cout << "FooDerived::f()" << std::endl;
        return 1;
    }
};

int main(int argc, char *argv[]) {
    typedef int (Foo::*FPTR) (char*);
    typedef void (Bar::*BPTR) (int);
    typedef int (FooDerived::*FDPTR) (char*);

    FPTR fptr = &Foo::f;
    BPTR bptr = &Bar::b;
    FDPTR fdptr = &FooDerived::f;

    // bptr = static_cast<void(Bar::*)(int)>(fptr); // 错误
    fptr = static_cast<int(Foo::*)(char*)>(fdptr); // 正确,向上转型

    Bar obj;
    ( obj.*(BPTR) fptr )(1); // 调用 Foo::f()
}

Bar obj;
( obj.*(BPTR) fptr)(1);

尽管我们想要调用的是 Bar::b() ,但是 Foo::f() 却被调用了,因为fptr是静态绑定(翻译君注:这里的静态绑定,即指在编译阶段,fptr的值已经确定了,所以即使进行强制转换,依然调用的是Foo类的f()函数)。(请围观成员函数调用和 this指针)

3.2.虚函数情形

相同代码,只是将函数转为虚函数。

#include <iostream>

class Foo {
public:
    virtual int f(char *c = 0) {
        std::cout << "Foo::f()" << std::endl;
        return 1;
    }
};

class Bar {
public:
    virtual void b(int i = 0) {
        std::cout << "Bar::b()" << std::endl;
    }
};

class FooDerived : public Foo {
public:
    int f(char *c = 0) {
        std::cout << "FooDerived::f()" << std::endl;
        return 1;
    }
};

int main(int argc, char *argv[]) {
    typedef int (Foo::*FPTR) (char*);
    typedef void (Bar::*BPTR) (int);

    FPTR fptr = &Foo::f;
    BPTR bptr = &Bar::b;

    FooDerived objDer;
    (objDer.*fptr)(0); // 调用 FooDerived::f(),而不是 Foo::f()

    Bar obj;
    ( obj.*(BPTR) fptr )(1);// 调用 Bar::b(),而不是 Foo::f()
}

如我们所看到的,当成员函数是虚函数的时候,成员函数能够具有多态性并且现在调用的是 FooDerived::f() ,而且 Bar::b() 也能被正确调用了。因为 “一个指向虚成员的指针能在不同地址空间之间传递,只要二者使用的对象布局一样” (C++ Bjarne Stroustrup 的 《C++程序设计语言》 )。当成员函数是虚函数的时候,编译器会生成虚函数表,来保存虚函数的地址。只要成员函数在类中的相对地址一样,即使是在不同类之间,成员函数指针也能相互转换。这是和非虚函数之间的最大不同,因此,运行时的行为也是不同的。

3.3.成员函数指针数组及其应用

成员函数指针的一个重要应用就是根据输入来生成响应事件,下面的 Printer 类和指针数组 pfm 展示了这一点:

#include <stdio.h>
#include <string>
#include <iostream>

class Printer { // 一台虚拟的打印机
public:
    void Copy(char *buff, const char *source) { // 复制文件
        strcpy(buff, source);
    }

    void Append(char *buff, const char *source) { // 追加文件
        strcat(buff, source);
    }
};

enum OPTIONS { COPY, APPEND }; // 菜单中两个可供选择的命令

typedef void(Printer::*PTR) (char*, const char*); // 成员函数指针

void working(OPTIONS option, Printer *machine,
             char *buff, const char *infostr) {
    PTR pmf[2] = { &Printer::Copy, &Printer::Append }; // 指针数组

    switch (option) {
    case COPY:
        (machine->*pmf[COPY])(buff, infostr);
        break;
    case APPEND:
        (machine->*pmf[APPEND])(buff, infostr);
        break;
    }
}

int main() {
    OPTIONS option;
    Printer machine;
    char buff[40];

    working(COPY, &machine, buff, "Strings ");
    working(APPEND, &machine, buff, "are concatenated!");

    std::cout << buff << std::endl;
}

3.4.成员函数调用和 this 指针

Foo *const this = p;
void Foo::f(Foo *const this) {
    std::cout << "Foo::f()" << std::endl;
}

所以,不管p的值是神马,函数 Foo::f 都可以被调用,就像一个全局函数一样!p被作为 this 指针并当作参数传递给了函数。而在我们的例子中 this 指针并没有被解引用,所以,编译器放了我们一马(翻译君表示,这其实跟编译器没有关系,即使我们在成员函数中使用this指针,编译照样能通过,只不过在运行时会crash)。假如我们想知道成员变量 _i 的值呢?那么编译器就需要解引用 this 指针,这只有一个结果,那就是我们的好兄弟——未定义行为(undefined behavior)。对于一个虚函数调用,我们需要虚函数表来查找正确的函数,然后, this 指针被传递给这个函数。

4.成员函数和成员虚函数汇编分析

在VS2013下,新建控制台程序,然后输入如下代码:

#include <cstdio>
using namespace std;

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

int main() {
	X x;
	X* xp = &x;
	int(X::*gp1)() = &X::get1;
	int(X::*gp2)() = &X::get2;
	int(X::*gp3)() = &X::get3;
	
	/*********************输出各个成员函数指针的值*****************/
	printf("gp1 = %lu\n", gp1);
	printf("gp2 = %lu\n", gp2);
	printf("gp3 = %lu\n", gp3);

	/********************用成员函数指针调用虚函数*******************/
	(x.*gp1)();
	(xp->*gp1)();
	(x.*gp2)();
	(xp->*gp2)();
	(x.*gp3)();
	(xp->*gp3)();
}

在main入口处下断点,然后查看菜单:调试>窗口>反汇编。关闭符号,有利于理解。为了减少代码长度,将部分无关汇编代码省略。代码如下所示:

__setdefaultprecision:
000D1005  jmp         000D3730  
__setargv:
000D100A  jmp         000D3040  
X::get3:
000D100F  jmp         000D1510  
X::`vcall'{4}':
000D1014  jmp         000D1483  
......
X::`vcall'{0}':
000D1131  jmp         000D1488  
......
X::get1:
000D1159  jmp         000D1490  
......
_main:
000D1168  jmp         000D1550  
X::get2:
000D116D  jmp         000D14D0  
......

X::X:
000D1440  push        ebp  
000D1441  mov         ebp,esp  
000D1443  sub         esp,0CCh  
000D1449  push        ebx  
000D144A  push        esi  
000D144B  push        edi  
000D144C  push        ecx  
000D144D  lea         edi,[ebp+FFFFFF34h]  
000D1453  mov         ecx,33h  
000D1458  mov         eax,0CCCCCCCCh  
000D145D  rep stos    dword ptr es:[edi]  
000D145F  pop         ecx  
000D1460  mov         dword ptr [ebp-8],ecx  
000D1463  mov         eax,dword ptr [ebp-8]  
000D1466  mov         dword ptr [eax],0D685Ch  
000D146C  mov         eax,dword ptr [ebp-8]  
000D146F  pop         edi  
000D1470  pop         esi  
000D1471  pop         ebx  
000D1472  mov         esp,ebp  
000D1474  pop         ebp  
000D1475  ret  
......
X::`vcall'{4}':
000D1483  mov         eax,dword ptr [ecx]  
000D1485  jmp         dword ptr [eax+4]  
X::`vcall'{0}':
000D1488  mov         eax,dword ptr [ecx]  
000D148A  jmp         dword ptr [eax]  
000D148C  int         3  
000D148D  int         3  
000D148E  int         3  
000D148F  int         3  
--- e:\work\currentproject\微博\consoleapplication1\consoleapplication1\consoleapplication1.cpp 
     1: // ConsoleApplication1.cpp : 定义控制台应用程序的入口点。
     2: //
     3: 
     4: #include "stdafx.h"
     5: #include <cstdio>
     6: using namespace std;
     7: 
     8: class X {
     9: public:
    10: 	int get1() {
000D1490  push        ebp  
000D1491  mov         ebp,esp  
000D1493  sub         esp,0CCh  
000D1499  push        ebx  
000D149A  push        esi  
000D149B  push        edi  
000D149C  push        ecx  
000D149D  lea         edi,[ebp+FFFFFF34h]  
000D14A3  mov         ecx,33h  
000D14A8  mov         eax,0CCCCCCCCh  
000D14AD  rep stos    dword ptr es:[edi]  
000D14AF  pop         ecx  
000D14B0  mov         dword ptr [ebp-8],ecx  
    11: 		return 1;
000D14B3  mov         eax,1  
    12: 	}
000D14B8  pop         edi  
000D14B9  pop         esi  
000D14BA  pop         ebx  
000D14BB  mov         esp,ebp  
000D14BD  pop         ebp  
000D14BE  ret  

--- e:\work\currentproject\微博\consoleapplication1\consoleapplication1\consoleapplication1.cpp 
    13: 	virtual int get2() {
000D14D0  push        ebp  
000D14D1  mov         ebp,esp  
000D14D3  sub         esp,0CCh  
000D14D9  push        ebx  
000D14DA  push        esi  
000D14DB  push        edi  
000D14DC  push        ecx  
000D14DD  lea         edi,[ebp+FFFFFF34h]  
000D14E3  mov         ecx,33h  
000D14E8  mov         eax,0CCCCCCCCh  
000D14ED  rep stos    dword ptr es:[edi]  
000D14EF  pop         ecx  
000D14F0  mov         dword ptr [ebp-8],ecx  
    14: 		return 2;
000D14F3  mov         eax,2  
    15: 	}
000D14F8  pop         edi  
000D14F9  pop         esi  
000D14FA  pop         ebx  
000D14FB  mov         esp,ebp  
000D14FD  pop         ebp  
000D14FE  ret  

--- e:\work\currentproject\微博\consoleapplication1\consoleapplication1\consoleapplication1.cpp 
    16: 	virtual int get3() {
000D1510  push        ebp  
000D1511  mov         ebp,esp  
000D1513  sub         esp,0CCh  
000D1519  push        ebx  
000D151A  push        esi  
000D151B  push        edi  
000D151C  push        ecx  
000D151D  lea         edi,[ebp+FFFFFF34h]  
000D1523  mov         ecx,33h  
000D1528  mov         eax,0CCCCCCCCh  
000D152D  rep stos    dword ptr es:[edi]  
000D152F  pop         ecx  
000D1530  mov         dword ptr [ebp-8],ecx  
    17: 		return 3;
000D1533  mov         eax,3  
    18: 	}
000D1538  pop         edi  
000D1539  pop         esi  
000D153A  pop         ebx  
000D153B  mov         esp,ebp  
000D153D  pop         ebp  
000D153E  ret  
--- 无源文件 -----------------------------------------------------------------------
000D153F  int         3  
000D1540  int         3  
000D1541  int         3  
000D1542  int         3  
000D1543  int         3  
000D1544  int         3  
000D1545  int         3  
000D1546  int         3  
000D1547  int         3  
000D1548  int         3  
000D1549  int         3  
000D154A  int         3  
000D154B  int         3  
000D154C  int         3  
000D154D  int         3  
000D154E  int         3  
000D154F  int         3  
--- e:\work\currentproject\微博\consoleapplication1\consoleapplication1\consoleapplication1.cpp 
    19: };
    20: 
    21: int main() {
000D1550  push        ebp  
000D1551  mov         ebp,esp  
000D1553  sub         esp,100h  
000D1559  push        ebx  
000D155A  push        esi  
000D155B  push        edi  
000D155C  lea         edi,[ebp+FFFFFF00h]  
000D1562  mov         ecx,40h  
000D1567  mov         eax,0CCCCCCCCh  
000D156C  rep stos    dword ptr es:[edi]  
000D156E  mov         eax,dword ptr ds:[000D9000h]  
000D1573  xor         eax,ebp  
000D1575  mov         dword ptr [ebp-4],eax  
    22: 	X x;
000D1578  lea         ecx,[ebp-0Ch]  
000D157B  call        000D1145  
    23: 	X* xp = &x;
000D1580  lea         eax,[ebp-0Ch]  
000D1583  mov         dword ptr [ebp-18h],eax  
    24: 	int(X::*gp1)() = &X::get1;
000D1586  mov         dword ptr [ebp-24h],0D1159h  
    25: 	int(X::*gp2)() = &X::get2;
000D158D  mov         dword ptr [ebp-30h],0D1131h  
    26: 	int(X::*gp3)() = &X::get3;
000D1594  mov         dword ptr [ebp-3Ch],0D1014h  
    27: 	/*********************输出各个成员函数指针的值*****************/
    28: 	printf("gp1 = %lu\n", gp1);
000D159B  mov         esi,esp  
000D159D  mov         eax,dword ptr [ebp-24h]  
000D15A0  push        eax  
000D15A1  push        0D6868h  
000D15A6  call        dword ptr ds:[000DA11Ch]  
000D15AC  add         esp,8  
000D15AF  cmp         esi,esp  
000D15B1  call        000D1163  
    29: 	printf("gp2 = %lu\n", gp2);
000D15B6  mov         esi,esp  
000D15B8  mov         eax,dword ptr [ebp-30h]  
000D15BB  push        eax  
000D15BC  push        0D6878h  
000D15C1  call        dword ptr ds:[000DA11Ch]  
000D15C7  add         esp,8  
000D15CA  cmp         esi,esp  
000D15CC  call        000D1163  
    30: 	printf("gp3 = %lu\n", gp3);
000D15D1  mov         esi,esp  
000D15D3  mov         eax,dword ptr [ebp-3Ch]  
000D15D6  push        eax  
000D15D7  push        0D6888h  
000D15DC  call        dword ptr ds:[000DA11Ch]  
000D15E2  add         esp,8  
000D15E5  cmp         esi,esp  
000D15E7  call        000D1163  
    31: 
    32: 	/********************用成员函数指针调用虚函数*******************/
    33: 	(x.*gp1)();
000D15EC  mov         esi,esp  
000D15EE  lea         ecx,[ebp-0Ch]  
000D15F1  call        dword ptr [ebp-24h]  
000D15F4  cmp         esi,esp  
000D15F6  call        000D1163  
    34: 	(xp->*gp1)();
000D15FB  mov         esi,esp  
000D15FD  mov         ecx,dword ptr [ebp-18h]  
000D1600  call        dword ptr [ebp-24h]  
000D1603  cmp         esi,esp  
000D1605  call        000D1163  
    35: 	(x.*gp2)();
000D160A  mov         esi,esp  
000D160C  lea         ecx,[ebp-0Ch]  
000D160F  call        dword ptr [ebp-30h]  
000D1612  cmp         esi,esp  
000D1614  call        000D1163  
    36: 	(xp->*gp2)();
000D1619  mov         esi,esp  
000D161B  mov         ecx,dword ptr [ebp-18h]  
000D161E  call        dword ptr [ebp-30h]  
000D1621  cmp         esi,esp  
000D1623  call        000D1163  
    37: 	(x.*gp3)();
000D1628  mov         esi,esp  
000D162A  lea         ecx,[ebp-0Ch]  
000D162D  call        dword ptr [ebp-3Ch]  
000D1630  cmp         esi,esp  
000D1632  call        000D1163  
    38: 	(xp->*gp3)();
000D1637  mov         esi,esp  
000D1639  mov         ecx,dword ptr [ebp-18h]  
000D163C  call        dword ptr [ebp-3Ch]  
000D163F  cmp         esi,esp  
000D1641  call        000D1163  
    39: }
000D1646  xor         eax,eax  
000D1648  push        edx  
000D1649  mov         ecx,ebp  
000D164B  push        eax  
000D164C  lea         edx,ds:[000D1678h]  
000D1652  call        000D109B  
000D1657  pop         eax  
000D1658  pop         edx  
000D1659  pop         edi  
000D165A  pop         esi  
000D165B  pop         ebx  
000D165C  mov         ecx,dword ptr [ebp-4]  
000D165F  xor         ecx,ebp  
000D1661  call        000D1028  
000D1666  add         esp,100h  
000D166C  cmp         ebp,esp  
000D166E  call        000D1163  
000D1673  mov         esp,ebp  
000D1675  pop         ebp  
000D1676  ret  
000D1677  nop  
000D1678  add         dword ptr [eax],eax  
000D167A  add         byte ptr [eax],al  
000D167C  adc         byte ptr [esi],0Dh  
000D167F  add         ah,dh  
000D1681  ?? ?????? 
000D1682  ?? ?????? 
000D1683  inc         dword ptr [eax+eax]  
000D1686  add         byte ptr [eax],al  
000D1688  mov         word ptr [esi],ss  
000D168A  or          eax,0CC007800h  

类X有3个成员函数,其中get1是普通的成员函数,而get2和get3都分别是虚成员函数。gp1存储的确实是成员函数get1的地址,而gp2和gp3存储的确不是虚函数get2和get3的地址,而是X::vcall{0}和X::vcall{4}的地址。成汇编代码中可以看到普通成员函数在编译后,函数地址是确定的,并不会随着再改变,而

    mov    eax, DWORD PTR [ecx];寄存是ecx里面保存的是对象x的首地址,;因此,这里是将对象x首地址处内存内容(即vftable首地址)给寄存器eax
    jmp    DWORD PTR [eax];跳转到vftable首地址处内存(里面存的是虚函数get2的地址)所存储的地址处执行;这里就是跳转去执行虚函数get2

通过汇编码发现,普通成员函数时通过地址直接调用,而虚成员函数时通先调用vcall函数,然后由vcall函数查询虚表调用相应的虚函数,由此可以看出,一个类里面的每一给虚函数都有一个vcall函数与之对应,通过vcall函数来调用相应的虚函数。

5.作者答疑

− − − − − − − − 插件开发 − − − − − − − − p h 8 o n e = 1 8 9 2 8 8 9 9 7 2 8 W e i 8 X i n = 1 8 9 2 8 8 9 9 7 2 8 Q 8 Q = 3 1 2 1 1 7 2 7 1 --------插件开发--------\\ ph8one= \begin{matrix} 1 & 8 &9 & 2 &8 & 8 &9 & 9 &7 & 2 &8 \end{matrix}\\ Wei8Xin= \begin{matrix} 1 & 8 &9 & 2 &8 & 8 &9 & 9 &7 & 2 &8 \end{matrix}\\ Q8Q= \begin{matrix} 3 & 1 &2 & 1 &1 & 7 &2 & 7 &1 \end{matrix} 插件开发ph8one=18928899728Wei8Xin=18928899728Q8Q=312117271

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值