类的基本布局

为了说明以下内容,让我们考虑这个简单的例子:

class A
{
int a1;
public:
virtual int A_virt1();
virtual int A_virt2();
static void A_static1();
void A_simple1();
}; class B
{
int b1;
int b2;
public:
virtual int B_virt1();
virtual int B_virt2();
}; class C: public A, public B
{
int c1;
public:
virtual int A_virt2();
virtual int B_virt2();
};

在大多数情况下,MSVC按以下顺序排列类:

  1. 指向虚拟函数表(vtable或vftable)的指针,仅当类具有虚拟方法且基类中没有合适的表可重用时添加
  2. 基类
  3. 类成员

虚拟函数表由虚拟方法的地址按其第一次出现的顺序组成。重载函数的地址替换基类中函数的地址。因此,我们三个类的布局如下所示:

class A size(8):
+---
0 | {vfptr}
4 | a1
+--- A's vftable:
0 | &A::A_virt1
4 | &A::A_virt2 class B size(12):
+---
0 | {vfptr}
4 | b1
8 | b2
+--- B's vftable:
0 | &B::B_virt1
4 | &B::B_virt2 class C size(24):
+---
| +--- (base class A)
0 | | {vfptr}
4 | | a1
| +---
| +--- (base class B)
8 | | {vfptr}
12 | | b1
16 | | b2
| +---
20 | c1
+--- C's vftable for A:
0 | &A::A_virt1
4 | &C::A_virt2 C's vftable for B:
0 | &B::B_virt1
4 | &C::B_virt2

上面的图表是由VC8编译器使用未记录的开关生成的。若要查看编译器生成的类布局,请使用-d1reportSingleClassLayout查看单个类的布局-d1reportAllClassLayout查看所有类(包括内部CRT类)的布局,这些布局将转储到stdout。如您所见,C有两个vftable,因为它继承了两个类,这两个类都已经有了虚拟函数。A的地址替换C的vftable中A::A的地址,C::B的地址替换另一个表中B::B的地址。

调用约定和类方法

默认情况下,MSVC中的所有类方法都使用thiscall约定。类实例地址(this指针)在ecx寄存器中作为隐藏参数传递。在方法体中,编译器通常会立即将其放入其他寄存器(如esi或edi)和/或堆栈变量中。类成员的所有进一步寻址都是通过该寄存器和/或变量完成的。但是,在实现COM类时,使用了“stdcall”约定。下面是各种类方法类型的概述。

  1. Static Methods

    静态方法不需要类实例,因此它们的工作方式与公共函数相同。不,这个指针被传递给他们。因此,不可能可靠地区分静态方法和简单函数。例子:

    A::A_static1();
    call A::A_static1
  2. 简单方法
    简单方法需要一个类实例,因此这个指针作为一个隐藏的第一个参数传递给它们,通常使用thiscall约定,即在ecx寄存器中。如果基对象不在派生类的开头,则需要调整此指针,使其在调用函数之前指向基子对象的实际开头。例子:
    ;pC->A_simple1();
    ;esi = pC
    push
    mov ecx, esi
    call A::A_simple1 ;pC->B_simple1(,);
    ;esi = pC
    lea edi, [esi+] ;adjust this
    push
    push
    mov ecx, edi
    call B::B_simple1

    如您所见,在调用B的方法之前,会将this指针调整为指向B子对象。

  3. 虚方法
    要调用虚拟方法,编译器首先需要从vftable中获取函数地址,然后以与简单方法相同的方式调用该地址的函数(即将此指针作为隐式参数传递)。例子:
       ;pC->A_virt2()
    ;esi = pC
    mov eax, [esi] ;fetch virtual table pointer
    mov ecx, esi
    call [eax+] ;call second virtual method ;pC->B_virt1()
    ;edi = pC
    lea edi, [esi+] ;adjust this pointer
    mov eax, [edi] ;fetch virtual table pointer
    mov ecx, edi
    call [eax] ;call first virtual method
  4. 构造函数和析构函数
    构造函数和析构函数的工作方式类似于一个简单的方法:它们得到一个隐式的this指针作为第一个参数(例如,在thiscall约定的情况下是ecx)。构造函数返回eax中的this指针,即使它在形式上没有返回值。

RTTI实现

RTTI(运行时类型标识)是一种特殊的编译器生成信息,用于支持C++类操作符,如dynamic_cast<> 和 typeid(),也适用于C++异常。由于RTTI的性质,它只需要(并生成)多态类,即具有虚拟函数的类。

MSVC编译器在vftable前面放置一个指向名为“Complete Object Locator”的结构的指针。之所以调用这个结构,是因为它允许编译器从一个特定的vftable指针中找到完整对象的位置(因为一个类可以有几个指针)。COL如下所示:

struct RTTICompleteObjectLocator
{
DWORD signature; //always zero ?
DWORD offset; //offset of this vtable in the complete class
DWORD cdOffset; //constructor displacement offset
struct TypeDescriptor* pTypeDescriptor; //TypeDescriptor of the complete class
struct RTTIClassHierarchyDescriptor* pClassDescriptor; //describes inheritance hierarchy
};
分类描述描述描述分类的Inheritance Hierarchy。这是所有Cols for a class共享的。
struct RTTIClassHierarchyDescriptor
{
DWORD signature; //always zero?
DWORD attributes; //bit 0 set = multiple inheritance, bit 1 set = virtual inheritance
DWORD numBaseClasses; //number of classes in pBaseClassArray
struct RTTIBaseClassArray* pBaseClassArray;
};

基类数组描述所有基类以及允许编译器在执行“ _dynamic_cast_ operator”转换运算符期间将派生类转换为其中任何一个的信息。每个条目(基类描述符)具有以下结构:

struct RTTIBaseClassDescriptor
{
struct TypeDescriptor* pTypeDescriptor; //type descriptor of the class
DWORD numContainedBases; //number of nested classes following in the Base Class Array
struct PMD where; //pointer-to-member displacement info
DWORD attributes; //flags, usually 0
}; struct PMD
{
int mdisp; //member displacement
int pdisp; //vbtable displacement
int vdisp; //displacement inside vbtable
};

PMD结构描述基类是如何放置在完整类中的。在简单继承的情况下,它位于距对象开头的固定偏移处,该值是“_mdisp_ ”字段。如果它是虚拟基,则需要从vbtable获取额外的偏移量。用于调整从派生类到基类的指针的伪代码如下所示:

 //char* pThis; struct PMD pmd;
pThis+=pmd.mdisp;
if (pmd.pdisp!=-)
{
char *vbtable = pThis+pmd.pdisp;
pThis += *(int*)(vbtable+pmd.vdisp);
}

例如,我们三个类的RTTI层次结构如下所示:

更多信息

RTTI

如果有,RTTI是一个有价值的信息来源,可以用来逆转。从RTTI可以恢复类名、继承层次结构,在某些情况下还可以恢复类布局的某些部分。我的RTTI扫描仪脚本显示了大部分信息。(见附录一)

静态和全局初始值器

全局和静态对象需要在主程序启动之前初始化。MSVC通过生成初始化函数并将它们的地址放在表中来实现这一点,该表在CRT启动期间由cinit函数处理。表通常位于.data节的开头。典型的初始值设定项如下所示:

    _init_gA1:
mov ecx, offset _gA1
call A::A()
push offset _term_gA1
call _atexit
pop ecx
retn
_term_gA1:
mov ecx, offset _gA1
call A::~A()
retn

因此,从这个表中我们可以发现:

  • 全局/静态对象地址
  • 构造器
  • 析构器

另请参见MSVC _#pragma_ directive _init_seg_ [5]

展开函数

如果在函数中创建了任何自动对象,则VC++编译器自动生成异常处理结构,以确保在发生异常时删除这些对象。

unwind_1tobase:  ; state 1 -> -1
lea ecx, [ebp+a1]
jmp A::~A()

通过在函数体中找到相反的状态更改,或者只找到对同一堆栈变量的第一次访问,我们还可以找到构造函数

    lea     ecx, [ebp+a1]
call A::A()
mov [ebp+__$EHRec$.state],

对于使用new()运算符构造的对象,展开函数确保在构造函数失败时删除已分配的内存:

unwind_0tobase: ; state 0 -> -1
mov eax, [ebp+pA1]
push eax
call operator delete(void *)
pop ecx
retn

在函数体中:

;A* pA1 = new A();
push
call operator new(uint)
add esp,
mov [ebp+pA1], eax
test eax, eax
mov [ebp+__$EHRec$.state], ; state 0: memory allocated but object is not yet constructed
jz short @@new_failed
mov ecx, eax
call A::A()
mov esi, eax
jmp short @@constructed_ok
@@new_failed:
xor esi, esi
@@constructed_ok:
mov [esp+14h+__$EHRec$.state], -
;state -1: either object was constructed successfully or memory allocation failed
;in both cases further memory management is done by the programmer

另一种类型的展开函数用于构造函数和析构函数。它确保在发生异常时销毁类成员。在这种情况下,函数使用保存在堆栈变量中的this指针:

unwind_2to1:
mov ecx, [ebp+_this] ; state 2 -> 1
add ecx, 4Ch
jmp B1::~B1

在这里,functlet在偏移量4Ch处销毁B1类型的类成员。因此,从展开functlet可以发现:

  • 表示C++对象的堆栈变量,或指向用“ _operator new_”分配的对象的指针。
  • 析构器
  • 构造器
  • 如果是新的物体,它们的大小

构造函数/析构函数递归

这个规则很简单:构造函数调用其他构造函数(基类和成员变量的构造函数),析构函数调用其他析构函数。典型的构造函数执行以下操作:
  • 调用基类的构造函数。
  • 调用复杂类成员的构造函数。
  • 如果类具有虚拟函数,则初始化vfptr
  • 执行程序员编写的构造函数体。

典型的析构函数几乎以相反的顺序工作:

  • 如果类具有虚拟函数,则初始化vfptr
  • 执行程序员编写的析构函数体。
  • 调用复杂类成员的析构函数
  • 调用基类的析构函数

MSVC生成的析构函数的另一个显著特点是,它们的状态变量通常以最大值初始化,然后与每个被析构函数化的子对象一起递减,这使得它们的识别更容易。请注意,简单的构造函数/析构函数通常由MSVC内联。这就是为什么您经常可以看到vftable指针在同一个函数中用不同的指针重复加载。

对象数组的构造/析构

MSVC编译器使用helper函数来构造和销毁对象数组。请考虑以下代码:

    A* pA = new A[n];
delete [] pA;

它被转换为以下伪代码:

array = new char(sizeof(A)*n+sizeof(int))
if (array)
{
*(int*)array=n; //store array size in the beginning
'eh vector constructor iterator'(array+sizeof(int),sizeof(A),count,&A::A,&A::~A);
}
pA = array; 'eh vector destructor iterator'(pA,sizeof(A),count,&A::~A);

如果有vftable,则在删除数组时将调用“vector deleting destructor'析构函数”:

   ;pA->'vector deleting destructor'();
mov ecx, pA
push ; flags: 0x2=deleting an array, 0x1=free the memory
call A::'vector deleting destructor'

如果A的析构函数是虚拟的,则它实际上被调用:

mov ecx, pA
push
mov eax, [ecx] ;fetch vtable pointer
call [eax] ;call deleting destructor

因此,通过向量构造函数/析构函数迭代器调用,我们可以确定:

  • 对象数组的地址
  • 构造器
  • 毁灭者
  • 类size

删除析构函数

当类具有虚拟析构函数时,编译器生成一个帮助函数-删除析构函数。其目的是确保在销毁类时调用正确的delete运算符。删除析构函数的伪代码如下所示:

virtual void * A::'scalar deleting destructor'(uint flags)
{
this->~A();
if (flags&) A::operator delete(this);
};

这个函数的地址放在vftable中,而不是析构函数的地址中。这样,如果另一个类重写虚拟析构函数,将调用该类的运算符delete。尽管在实际代码中,delete运算符很少被重写,所以通常会看到对default delete()的调用。有时编译器还可以生成向量删除析构函数。它的代码如下:

virtual void * A::'vector deleting destructor'(uint flags)
{
if (flags&) //destructing a vector
{
array = ((int*)this)-; //array size is stored just before the this pointer
count = array[];
'eh vector destructor iterator'(this,sizeof(A),count,A::~A);
if (flags&) A::operator delete(array);
}
else {
this->~A();
if (flags&) A::operator delete(this);
}
};

Visual C++ 里的 Classes, Methods and RTTI的更多相关文章

  1. vs里 .sln和.suo 文件 Visual Studio里*.sln和*.suo文件的作用

    Visual Studio里*.sln和*.suo文件的作用      VS项目采用两种文件类型(.sln   和   .suo)来存储特定于解决方案的设置.这些文件总称为解决方案文件,为解决方案资源 ...

  2. Visual C++ 里的异常处理

    微软Visual C++是Win32最广泛使用的编译器,因此Win32反向器对其内部工作非常熟悉.能够识别编译器生成的粘合代码有助于快速集中于程序员编写的实际代码.它还有助于恢复程序的高级结构.我将集 ...

  3. 在Visual Studio里配置及查看IL(转载)

    原文地址:http://www.myext.cn/other/a_25162.html 在之前的版本VS2010中,在Tools下有IL Disassembler(IL中间语言查看器),但是我想直接集 ...

  4. 在Visual Studio里配置及查看IL

    原文地址:http://www.myext.cn/other/a_25162.html 在之前的版本VS2010中,在Tools下有IL Disassembler(IL中间语言查看器),但是我想直接集 ...

  5. 在 Visual Studio 里一秒打开 ILSpy,并反编译当前项目

    下载 ILSpy(如果已有 ILSpy,忽略此步骤) 1.打开官方git 仓库 - https://github.com/icsharpcode/ILSpy 2.点击右侧的 Releases 最新版, ...

  6. 【译】Visual Studio 15 预览版更新说明

    序:恰逢Build2016大会召开,微软发布了VS2015的update2更新包和VS2016预览版.本人正在提升英文水平中,于是在这里对VS2016预览版的官方文档进行了部分翻译.因为VS有些功能使 ...

  7. RTTI (Run-Time Type Identification,通过运行时类型识别) 转

    参考一: RTTI(Run-Time Type Identification,通过运行时类型识别)程序能够使用基类的指针或引用来检查这些指针或引用所指的对象的实际派生类型.   RTTI提供了以下两个 ...

  8. RTTI(Runtime Type Information )

    RTTI 是“Runtime Type Information”的缩写,意思是:运行时类型信息.它提供了运行时确定对象类型的方法.本文将简略介绍 RTTI 的一些背景知识.描述 RTTI 的概念,并通 ...

  9. 免费的Visual Studio的插件

    在做了深入(的)研究之后(通过在google网站搜索),,我编译了15个免费Visual Studio 2005插件表..其中一些插件将提高您(的)代码(的)质量,,另外一些能使您编译(的)更快,,但 ...

随机推荐

  1. C语言函数返回指针方法

    1.将函数内部定义的变量用static修饰 由于static修饰的变量,分配在静态内存区(类似于全局变量区),函数返回时,并不会释放内存,因此可以将要返回的变量加static修饰. int *test ...

  2. Go 关键字Select

    select select 是Go语言中常用的一个关键字,Linux再也早也引入了这个函数,用来实现非阻塞的一种方式,一个select语句用来选择哪个case中的发送或接收操作可以被立即执行.它类似于 ...

  3. Go基础编程实践(六)—— 文件

    检查文件是否存在 在此程序同目录下创建log.txt文件,以检测. package main import ( "os" "fmt" ) func main() ...

  4. nohup 日志按天输出

    输出日志在当前目录: nohup java -jar ace-auth.jar >> nohup`date +%Y-%m-%d`.out 2>&1 & 指定日志目录输 ...

  5. C#类型成员:构造函数

    一.构造函数 构造函数是类的特殊方法,它永远不会返回值(即使是void),并且方法名和类名相同,同样支持重载.在使用new关键字创建对象时构造函数被间接调用,为对象初始化字段和属性的值. 无参构造函数 ...

  6. springboot IDEA新建Maven项目的Plugins出现红线的解决方法

    将pom.xml文件copy到桌面,删除项目中的pom.xml.发现项目maven中没有任何东西后,然后将桌面的pom.xml粘贴到项目目录下,刷新maven就ok了

  7. 解决centos7下 selenium报错--unknown error: DevToolsActivePort file doesn't exist

    解决centos7下 selenium报错--unknown error: DevToolsActivePort file doesn't exist 早上在linux下用selenium启动Chro ...

  8. 手写MQ框架(一)-准备启程

    一.背景 很久以前写了DAO框架和MVC框架,前段时间又重写了DAO框架-GDAO(手写DAO框架(一)-从“1”开始,源码:https://github.com/shuimutong/gdao.gi ...

  9. Ext下载文件

    项目中前台用的是Ext JS,要从数据库中查询数据并导出为Excel表格 对此研究了下,代码如下: 前台代码: /** * 进行下载文件(form方式) */ _downloadDraft:funct ...

  10. vector-空间增长

    使用 vector 的时候,一般是从一个空 vector 开始,根据需要逐步填充数据. 这里的关键惭怍是 push_back(),它将一个新元素添加到 vector 中,该元素成为 vector 的最 ...