0%

【Reverse#0x01】C/C++汇编笔记下

对C++的类相关机制、面向对象特性(继承、多态)在汇编语言中的实现方式的总结。

C++类基础

类的基本数据

不论是私有变量还是公有变量,在类实例化以后,在内存中都是一模一样的。就和局部变量一样,通过相对于栈帧指针的偏移来获取。
私有/公有只是编译器在编译时的检查。

类的成员函数

成员函数和我们平时写的普通函数的区别就在于成员函数会多一个参数this(不论有没有实际用到,都会加上去),此外函数命名方式也不同(会加上一个classname::)。
在i386下,程序会将ecx作为传递this的方式;在amd64下,程序会将this作为第一个参数传递(也就是塞到rdi中)。

构造函数 & 复制构造函数 & 析构函数

构造函数在创建类实例时被调用,返回值是类实例的地址。
复制构造函数就是以待复制的类为第二个参数(第一个参数固定为this)的构造函数。注意,在对类对象进行赋值的时候,会自动调用该函数进行复制。

对于析构函数:

  1. 若是局部变量,那么析构函数会在作用域结束的时候被自动调用。(比如在函数进入尾声前)
  2. 若是全局变量,其析构函数不会在main函数中被调用,其构造函数也不会。
    具体来说,会在__do_gloable_ctors()中调用其构造函数,在__do_global_dtors()按照构造的逆序调用其析构函数。(时间上分别在main之前和main之后)

C++面向对象

函数重载

函数重载属于静态绑定,也就是由编译器来完成这项工作。
编译器将会把有重载的多个函数生成为完全不同的函数,但是同名。

运算符重载

运算符重载也属于静态绑定,由编译器完成。
编译器会把重载运算符的函数生成出来,在使用该运算符时就相当于调用了该函数。

模板

对于模板的类型识别也是由编译器完成的。
编译器会识别程序用到了哪些类型的模板,然后针对每一个使用到的类型都生成一套对应的函数

比如有一个模板类Base,创建了两个Base类对象,分别是<int>和<char>类型的。
于是对于Base类中的一个函数func(T),在生成结果中,编译器会生成两个函数,一个叫做Base<int>::func(int),一个叫做 Base<char>::func(char)

虚函数

C++中的虚函数和成员函数的调用方式不同。成员函数将会直接调用对应的函数地址;而对于虚函数,编译器会创建一个结构叫做虚函数表(vtable),存在内存当中,记录该类的虚函数地址,每个类都会有一个(注意是类,不是类对象)。
在有虚函数的类的对象中,其内存的最低8个字节用来存储对应的虚函数表的地址,之后才是各个成员变量。

虚函数的相关机制基本可以总结为以下三点:

  1. 当用指针调用虚函数的时候,程序会从虚函数表中根据偏移找到相应的函数进行调用。
  2. 子类调用构造函数的时候,先调用父类的构造函数,父类的构造函数会先用自己的虚函数表覆盖在最开始的类变量地址中,接着子类在将自己的虚函数表覆盖在开始的类变量地址中,然后在对变量成员进行初始化。
  3. 如果子类中有对父类的虚函数进行重载,那么子类的虚函数表中存储的这个虚函数就是子类重载后的虚函数。

对于多重继承(子类继承父类继承祖父类):

  1. 多重继承的构造函数执行流程是:祖父类构造函数(Base1)->父类构造函数(Base2)->子类构造函数(Sub)。在每个构造函数开始之前都会把自己的虚函数表赋值给虚表指针。
  2. 类变量地址中的数据是按照:虚表指针->按照祖父类(Base1)的类变量->父类类变量(Base2)->子类类变量(Sub)依次排布

对于多继承(子类继承了两个父类):

  1. 构造函数执行流程是:父类Base1构造函数(初始化在偏移为0的位置)->父类Base2构造函数(紧跟在Base1的内存空间之后,初始化Base2)->子类Sub构造函数(在Base2的内存空间之后存放Sub独有的成员变量)。
  2. 子类对象在内存中会这样存储:虚函数表Part1——父类Base1成员变量——虚函数表Part2——父类Base2成员变量——子类成员变量

因此,在多继承的情况下,子类Sub的虚函数会被分为两个部分,第一部分是继承Base1的虚函数表,第二部分是继承Base2的虚函数表。这两个部分在内存区域中是连续存储的。