逆向分析技术
0x01 32位软件分析技术
1.启动函数
Win32应用程序都需要实现WinMain函数
Windows程序的执行并非从WinMain开始,而是从编译器生成的一段代码开始
所有C/C++程序启动函数作用为:
- 检索指向新进程的命令行指针
- 检索指向新进程的环境变量指针
- 全局变量初始化
- 内存栈初始化
- 调用应用程序入口点函数main或WinMain
- 入口点函数返回时,调用C运行库的exit函数,进行一些清理
- 处理,最后调用ExitProcess退出
用户编写的入口点函数地址为:0x00401000
2.函数
程序由不同的函数组成。一个函数包括函数名,入口参数,返回值,函数功能等部分。
2.1 函数的识别
在大多数情况,编译器使用call和ret指令来调用函数及返回调用位置。
call 指令会将当前下一条指令的地址压入栈中,随后jmp到指定地址。
ret指令会用当前栈顶的地址赋予给ip,也就是pop ip
2.2 函数的参数
函数参数传递有3种方式,分别是栈方式,寄存器方式及通过全局变量进行隐含参数传递的方式。
2.2.1利用栈传递参数
栈是一种“后进先出”的存储区,栈顶指针esp指向栈中第一个可用的数据项。在调用函数时,调用者将参数压入栈中,然后调用函数,调用结束后,由调用者或者函数本身修改栈,保持栈平衡。
为了实现函数调用而建立的协议成为调用约定:
约定类型 | _cdecl | pascal | stdcall | fastcall |
---|---|---|---|---|
参数传递顺序 | 从右到左 | 从左到右 | 从右到左 | 使用寄存器和栈 |
平衡栈者 | 调用者 | 子程序 | 子程序 | 子程序 |
允许使用VARARG | 是 | 否 | 否 |
非优化的编译器用一个专门的寄存器(通常是ebp)对参数进行寻址。c,c++,pascal等高级语言的函数执行过程基本一致。
在许多时候,编译器会按优化方式来编译程序,栈寻址稍有不同。这是,编译器为了节省ebp寄存器或尽可能减少代码以提高速度,会直接通过esp对参数进行寻址。esp的值可能在函数执行期间会发生变化,改变话出现在每次有数据进出栈时。要想确定对哪个变量进行了寻址,就要知道程序当前位置的esp的值,为此必须从函数的开始部分进行跟踪。
为了允许使用操作符和函数重载,C++编译器往往会按照某种规则改写每一个入口点的符号名,从而允许同一个名字有多个用法而不会破坏现有的基于C的编译器,这项技术被称为名称修饰。
C编译时函数名修饰的定义如下。
- stdcall调用约定在函数名前面加一个下划线前缀,在后面加一个“@”符号及其参数的字节数,格式为“_functioname@number”
- __cdel调用约定尽在输出函数名前面加一个下划线,格式为"_functionname"
- fastcall调用约定在输出函数名前面加一个“@”符号,在后面加一个“@”符号及其参数的字节数,格式为“@functionname@number”
他们均不会改变输出函数名中的字符大小写。pascal调用约定不同,pascal约定输出的函数不能有任何修饰且全部为大写。
C++编译时函数名修饰约定规则如下。
- stdcall调用约定以“?”标识函数名的开始,后跟函数名;在函数名后,以“@@YG”标识参数表的开始,后跟参数表;参数表的第一项为该函数的返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前;在参数表后面,以“@Z”标识整个名字的结束。其格式为“?functionname@@YG*****@Z”或“?functionname@@YG*XZ”
- __cdecl调用约定规则与上面的stdcall调用约定规则相同,只是参数表的开始标识由“@@YG”变成了“@@YA”
- fastcall调用约定规则与上面的stdcall调用约定规则相同,只是参数表的开始标识由“@@YG”变成了“@@YI”
2.3函数的返回值
2.3.1用return操作符返回值
在一般情况下,函数的返回值都放在eax寄存器中返回。
2.3.2通过参数按引用方式返回值
这里利用[esp+08]也就是参数a的内存地址接受了返回值。
3.数据结构
3.1局部变量
局部变量是函数内部定义的一个变量,其作用域和生命周期局限在所在函数内。从汇编的角度来看,局部变量分配空间时通常会使用栈和寄存器。
3.1.1利用栈存放局部变量
局部变量在栈中进行分配,函数执行后会释放这些栈,程序用“sub esp-8”语句为局部变量分配空间,用[ebp-xxx]寻址调用这些变量,而参数的调用相当于ebp的偏移量是整的,即[ebp+xxx]这样在你想的时候容易区分。编译器在优化模式的时候,通过esp寄存器直接对局部变量和参数进行寻址。当函数退出的时候,用add esp,8来平衡栈,以释放局部变量的内存。
如图函数内[ebp+8]是第一个参数,[ebp+C]是第二个参数,[ebp-4]是局部变量。
这里没有使用add esp,4指令来给局部变量分配内存,而是直接push ecx。
3.1.2利用寄存器存放局部变量
除了栈占用2给寄存器,编译器会利用剩下的6个通用寄存器尽可能有效地存放局部变量。如果寄存器不够编译就会将变量放到栈中。
3.2全局变量
全局变量作用于整个程序,放在全局变量的内存区。局部变量则存在于函数栈区中,函数调用结束后就会消失。全局变量通常位于.data块的一个固定地址处。当程序访问全局变量的时候,通常用一个硬编码地址直接对内存进行寻址。
全局变量可用用来传递返回值和参数。
这里用4080c0存放了7。此地址为.data区
与全局变量类似的是静态变量,它们都可以按直接寻址方式寻址。不同的是,静态变量的作用范围是有限的。
3.3数组
“基址+偏移量”,这种间接寻址一般出现在给一些数组或者结构赋值的情况下,其寻址形式一般是“基址+n”,基址可以是常量也可以是寄存器,为定值。根据n值的不同可以对结构中的相应单元赋值。
放在栈中的数值,这些栈在编译时分配。数组在声明时可以直接计算偏移地址。
4.4虚函数
虚函数是在程序运行时定义的函数。虚函数的地址不能再编译时确定,只能在调用即将进行时确定。所有对虚函数的引用通常都放在一个专用数组--虚函数表(Vitrutal Table,VTBL)中。数组的每个元素存放的就是类中虚函数的地址。调用虚函数时,程序取出虚函数表指针(Vitrual Table Pointer,VPTR)得到虚函数表的地址,再根据这个地址到虚函数表中去除该函数的地址,最后调用该函数。
这里我觉得书上的注释写的有点混乱,esi的值应该一直都是this指针的值,书上这个eax=VTBL=**Add()我也没看太明白。
4.控制语句
4.1IF-THEN-ELSE语句
cmp不会修改操作数,两个操作数相减会影响处理的几个标志,例如零标志,进位标志,符号标志和溢出标志。jz等指令就是条件跳转指令。
实际上,在许多情况下编译器都是用test或者or之类较短的逻辑指令来替换cmp指令,形式通常为test eax,eax。如果eax的值为0,则其逻辑与运算结果为0,设置ZF=1。否则ZF=0。
4.2SWITCH-CASE语句
源代码
#include <stdio.h>
int main(void)
{
int a;
scanf("%d",&a);
switch(a)
{
case 1 :printf("a=1");
break;
case 2 :printf("a=2");
break;
case 10:printf("a=10");
break;
default :printf("a=default");
break;
}
return 0;
}
未优化版本按顺序cmp并进行跳转。
编译器在优化时用“dec eax”指令代替cmp指令,使指令更短,执行速度更快。而且,在优化后,编译器会合理排列swtich后面的各个case节点,以最优化方式找到需要的节点。
如果个case是一个算术级数(等差数列),那么比编译器会用一个跳转表来实现。
jmp dword ptr [4*eax+004010B0]根据eax进行索引。
4.3转移机器码的计算
根据转移距离的不同,转移指令有如下类型。
- 短转移:无条件转移和条件转移的机器码均为2字节,转移范围是(-128~127)
- 长转移:无条件转移的机器码为五字节,条件转移的机器码为6字节。这是因为条件转移需要2字节表示其转移类型(je,jg,jns),其他四字节表示偏移量,无条件只需要一个字节表示转移类型(jmp)。
- 子程序调用指令(call):call指令调用有两类,一类调用是我们平时经常接触的,类似长转移;另一类调用的参数涉及寄存器,栈等值等,比较复杂。例如call dowrod [eax+2]。
4.3.1短转移指令机器码计算
无条件短转移的机器码时EB xx,其中EB00h~EB7Fh时向后转移,EB80h~EBFFh是向前转移。
转移指令的机器码形式是:
位移量=目的地址-起始地址-转移指令长度
转移指令机器码=转移类别机器码+位移量
4.3.2长转移指令机器码计算
与上面公式基本相同,偏移量四字节
4.4条件设置指令(SETcc)
条件设置指令的形式是SETcc r/m8,其中r/m8表示8位寄存器或单字节内存单元。
条件设置指令根据处理器定义的16位条件测试一些标志位,把结果记录到目标操作数中。当条件满足时,目标操作数置1,否则置0。
#include <stdio.h>
int main(void)
{
int c,a,b=2,c1=9,c2=4;
scanf("%d",&a);
c = (a < b)? c1:c2;
return c;
}
4.5纯算法实现逻辑
#include <windows.h>
int main(void)
{
if(FindWindow(NULL,"计算器"))
return 1;
else
return 5;
}
4.5循环语句
int main(void)
{
int sum=0,i=0;
for(i=0;i<=100;i++)
sum =sum +i;
return 0;
}
未优化
优化
5.数学算术符
5.1整数的加法和减法
一般情况下,整数的加法和减法分别被编译成add和sub指令。在编译优化时,很多人喜欢用lea指令来代替add和sub指令。lea指令允许用户在1个时钟内完成对c=a+b+78h的计算,其中a,b与c都是在有寄存器的情况下才有效的。会被编译成lea c,[a+b+78]指令。
#include <stdio.h>
int main(void)
{
int a,b;
//scanf("%d",&a);
//scanf("%d",&b);
printf("%d",a+b+0x78);
return 0;
}
5.2整数的乘法
乘法运算符一般被编译成mul,imul指令,这些指令运算较慢。编译器为了提高效率,倾向于用其他指令来完成统一的计算。如果一个数是2的幂,用左移指令shl来实现乘法运算。另外,加法对于提高3,5,6,7,9等数的乘法运算效率非常有用,示例如下。如eax*5可以写成lea eax,[eax+4*eax]。lea指令可以实现寄存器乘以2,4或8的运算。
#include <stdio.h>
int main(void)
{
int a;
printf("%d %d %d", a*11+4,a*9,a*2);
return 0;
}
5.3整数的除法
除法运算符一般被编译成div,idiv指令。除法指令的代价是相当高的,大概需要比乘法指令消耗10倍的cpu时钟。
如果被除数是一个未知数,那么编译器会使用div指令,程序执行的效率会下降。
除数/被除数如果有一个是常量则会复杂很多,编译器会用一些技巧来更有效地实现除法运算。如果除数是2的幂,那么可用处理速度较快的移位指令shr a,n来替换,移位指令只需要花费一个时钟,a为被除数,n为基数。若进行符号数计算,则使用sar指令。
#include <stdio.h>
int main(void)
{
int a;
scanf("%d",&a);
printf("%d ", a/11);
return 0;
}
未优化
优化
这里为什么说是右移32+1,是因为目前认为结果只算了edx,而edx是高32位,所以认为这是结果就是右移32,而sar又右移了一位所以是,32+1。
6.文本字符串
6.1字符串的存储格式
6.1.1C字符串
以\0结尾
6.1.2DOS字符串
以$结尾
6.1.3PASCAL字符串
开头一字节表示长度,字符串长度不能超过255字节。
6.1.4Delphi字符串
与上面类似,不过用了两个字节存储长度,长度不能超过65535字节。
6.2字符寻址指令
mov eax,[401000h] ;直接寻址,将地址401000h处的数据放入eax中
mov eax,[ecx] ;寄存器间接寻址,将ecx存放的地址处的值放入eax中
lea eax,[401000h] ;将401000h写入eax寄存器
lea指令经常被用来计算常量的和,等价于add指令。lea eax,[eax+8]等价于add eax,8。lea指令的效率远高于add指令。
计算字符串长度
repnz scasb这一条指令是重复将al与edi指向的附加段中数据注意比较,如果等于0的时候,跳出,这时候由于ecx每重复一次-1,所以取反的话就得出的是字符串算上\0的长度,所以在-1就是字符串真是长度。
7.指令修改技巧
很多指令都针对eax进行了优化,所以要尽量使用eax寄存器。例如xchg eax,ecx只需要一字节,而是用其他寄存器需要两字节。
0x02 64位软件逆向技术
1.寄存器
z64系统通用寄存器的名称,第一个字母从E改为R,大小扩展到64位,数量增加了8个(R8-R15),扩充了8个128位XMM寄存器(在64位程序中,XMM寄存器经常被用来优化代码)。64位寄存器与x86下的32位寄存器兼容,例如RAX(64位),EAX(低32位),AX(低16位),AL(低8位)和AH(8~15位)。x64新扩展的寄存器高低位访问,使用WORD,BYTE,DWORD后缀,例如R8(64位),R8D(低32位),R8W(低16位),R8B(低8位)
2.函数
2.1栈平衡
栈中存储的数据主要包括局部变量,函数参数,函数返回地址等。每当调用一个函数时,就会根据函数的需要申请相应的占空间。当函数调用完成时,就需要释放刚才申请的占空间,保证栈顶与函数调用前的位置一定,这个释放占空间的过程成为栈平衡。
在x64环境下,某些汇编指令对栈顶的对齐值有要求,因此,VS编译器在申请栈空间时,会尽量保证栈顶地址的对齐值为16。
2.2启动函数
定位64位程序的入口函数(main和WinMain)。
我这里点击wmainCRTStartup,随后点击__tmainCRTStartup翻页寻找到call main。在/MD选项下编译会显示main符号,运行库为/MT选项时不会显示main符号。这时可用从入口代码中找到第一个call cs:exit我这里的IDA显示的是 call cs:__imp_exit。上面第一个call一般就是main函数。
2.3调用约定
x86应用程序的函数调用有stdcall,__cdecl,fastall等方式,但x64应用程序只有1种寄存器快速调用约定。前4个参数使用寄存器传递,如果参数超过4个,多余的参数就放在栈里,入栈顺序从右到左,由函数调用方平衡占空间。前四个参数存放的寄存器是固定的,分别是第一个参数RCX,第二个参数RDX,第三个参数R8,第四个参数R9,其余的参数从右到左依次入栈。任何大于8字节或者不是1字节,2字节,4字节,8字节的参数由地址来传递,所有浮点数的传递都是使用XMM寄存器完成的,它们在XMM0,XMM1,XMM2和XMM3种传递。
函数的前4个参数虽然使用寄存器来传递,但是栈仍然为这4个参数预留了空间(32字节),为方便描述,这里称之为预留占空间,在x64环境里,前4个参数使用寄存器传递,当函数功能比较复杂的时候,寄存器可能不够实用。为了避免这个问题,可以使用预留栈空间,方法时函数嗲用着多申请32字节的占空间,当函数寄存器不够用时,可以把寄存器的值保存到刚才申请的栈空间种。预留栈空间由函数调用者提前申请。由函数调用者负责平衡栈空间。
2.4参数传递
2.4.1两个参数的传递
#include "stdafx.h"
int Add(int nNum1, int nNum2) {
return nNum1 + nNum2;
}
int _tmain(int argc, _TCHAR* argv[]) {
printf("%d\r\n", Add(1, 2));
return 0;
}
第一第二个参数分别传递给了ecx,edx。
以上是debug版的汇编代码,当程序被编译成release版时,函数参数的传递并无本质区别。当开启内联函数扩展编译优化选项时,函数可能会进行内联扩展优化,编译器会在编译时将可计算结果的变量转换成常量。
编译器直接计算结果得出了3。
2.4.24个以上参数的传递
int Add(int nNum1, int nNum2, int nNum3, int nNum4, int nNum5, int nNum6) {
return nNum1 + nNum2 + nNum3 + nNum4 + nNum5 + nNum6;
}
int _tmain(int argc, _TCHAR* argv[]) {
printf("%d\r\n", Add(1, 2, 3, 4, 5, 6));
return 0;
}
纠结了半天栈底,结果看了半天没有栈底相关操作。。add函数由main函数(调用者)平衡栈。
2.4.3参数为结构体
这里有点奇怪,四个预留栈帧占20h,结构体8h,不知道为什么要留出40h的空间。算上其对齐值感觉也应该是30h,后续有思路再回来补上吧。
struct tagPoint {
int x1;
int y1;
};
void fun(tagPoint pt) {
printf("x=%d y=%d\r\n", pt.x1, pt.y1);
}
int _tmain(int argc, _TCHAR* argv[]) {
tagPoint pt = { 1, 2 };
fun(pt);
return 0;
}
如果参数为结构体且结构体小于8字节,在传递结构体参数时,应直接把整个结构体的内容放在寄存器中。在函数里,通过访问寄存器的高32位和低32位来分别访问结构体的成员。
结构体大于8字节的时候
#include "stdafx.h"
struct tagPoint {
int x1;
int y1;
int x2;
int y2;
};
void fun(tagPoint pt) {
printf("x1=%d y1=%d x2=%d y2=%d\r\n", pt.x1, pt.y1, pt.x2, pt.y2);
}
int _tmain(int argc, _TCHAR* argv[]) {
tagPoint pt = { 1, 2, 3, 4 };
fun(pt);
return 0;
}
fun函数
如果参数是结构体大于8字节,在传递参数时,会先把结构内容复制到栈空间中,再把结构体地址当作参数传递。函数内部通过结构体地址加偏移量访问。
2.4.4thiscall
class CAdd {
public:
int Add(int nNum1, int nNum2) {
return nNum1 + nNum2;
}
};
int _tmain(int argc, _TCHAR* argv[]) {
CAdd Object;
printf("%d\r\n", Object.Add(1, 2));
return 0;
}
没什么特别的,隐式传参this指针。
2.4.5函数返回值
在64位环境下,RAX寄存器来保存返回值,浮点数类型使用MMX0寄存器保存返回值。当返回值大于8字节时,将栈空间的地址作为参数间接访问,进而达到目的。
3.数据结构
3.1局部变量
局部变量进入函数的时候分配,函数返回的时候释放。
int _tmain(int argc, _TCHAR* argv[]) {
int nNum1 = argc;
int nNum2 = 2;
printf("%d\r\n", nNum1 + nNum2);
return 0;
}
rsp + 0h ~ rsp + 20h一共32字节为预留参数栈空间。
rsp + 20h ~ rsp + 28h 为目前的两个变量空间
不过有点奇怪就是为什么一共给了30h的预留栈空间和据变量空间。可能是为了内存对齐吧。
之类的低地址是预留栈空间,高地址是局部变量。(32位情况下,安全机制会对一些参数进行重新排列如字符串会在高地址靠近返回地址放置栈溢出攻击)
3.2全局变量
#include "stdafx.h"
int g_nNum1;
int g_nNum2;
int _tmain(int argc, _TCHAR* argv[]) {
printf("%d\r\n", g_nNum1 + g_nNum2);
return 0;
}
全局变量的地址先定义的在低地址,后定义的在高地址,一般通过固定的地址访问。
3.3数组
数组寻址公式
数组元素地址 = 数组首地址+sizof(一维数组类型)*下表+sizeof(数组类型)*下标二
#include "stdafx.h"
int g_ary[4] = { 4, 5, 6, 7 };
int _tmain(int argc, _TCHAR* argv[]) {
int ary[4] = { 1, 2, 3, 4 };
printf("%d %d\r\n", ary[2], ary[argc]);
printf("%d %d\r\n", g_ary[3], g_ary[argc]);
return 0;
}
当访问数组下标为常量的时候可能进行优化计算出偏移量。下标未知的时候会用一维数组寻址公式进行寻找。
#include "stdafx.h"
int g_ary[2][3] = { 7, 8, 9, 10, 11, 12 };
int _tmain(int argc, _TCHAR* argv[]) {
int ary[2][3] = { 1, 2, 3, 4, 5, 6 };
printf("%d %d\r\n", ary[1][2], ary[argc][1]);
printf("%d %d\r\n", g_ary[0][1], g_ary[argc][2]);
return 0;
}
4.控制语句
4.1IF语句
特征识别:如果有一个jxx指令用于向下跳转,且跳转的if_end中没有jmp指令,将jxx指令取反后就是原先的条件,如if xx >,汇编代码会翻译成jle 即<=就跳转。
图形识别:逆向分析工具中,虚线表示条件条件,实现表示无条跳转
4.2IF......ELSE语句
4.3IF.....ELSEIF.....ELSE语句
都比较简单,无非就是取反条件由原本c代码的满足即执行变成了,满足取反条件即跳转,即不满足原先c代码即执行。所以跳转目的到中间的代码则为原先满足条件的代码区域。
4.4SWITCH...CASE语句
int _tmain(int argc, _TCHAR* argv[]) {
switch (argc) {
case 1: printf("argc == 1"); break;
case 2: printf("argc == 2"); break;
case 3: printf("argc == 3"); break;
case 6: printf("argc == 6"); break;
case 7: printf("argc == 7"); break;
case 8: printf("argc == 8"); break;
}
return 0;
}
ds:(xxxx)[rcx+rax*4] 前者是跳转表起始地址,[rcx+rax*4]是计算选择跳转表中的那一项
跳转表如上,分别是地址,会被寻址后赋予eax,然后jmp eax。
switch分支数小于6的时候会用if...else实现。switch分支数大于6的时候会优化。
当case>=6,且case间隔较小的时候,编译器会采用case表的方式实现swtich语句。
当case项较多的时候,编译器直接用if语句来实现switch,为了减少if语句的判定次数,采用了另一种优化方案,判定树。将每个case作为一个节点,从这些节点中找到一个中间值作为根节点,形成一棵平衡二叉树。如图。
4.5转移指令机器码计算
4.5.1call/jmp direct
位移量=目的地址-起始地址-跳转指令长度
转移指令机器码=转移指令类别机器码+位移量
4.5.2 call/jmp memory direct
ff15 3c414200 call dword ptr ds:[42413c]
这是32位系统,42413c为绝对地址。
64位系统,指令地址由原先的4字节变成了八字节,如果和原先一样,指令长度会增加,所以在x64系统中,指令后仍是4字节,但是改地址是相对地址。
5.循环语句
5.1do...while循环
int _tmain(int argc, _TCHAR* argv[]) {
int nCount = 0;
do
{
printf("%d\r\n", nCount);
nCount++;
} while (nCount < argc);
return 0;
}
特征识别:首先有一个jxx指令用于向上跳转,且跳转到目的语句中没有jxx跳转指令。不取反jxx指令即可还原do...while代码。
图形识别:向上的虚线箭头,中间为do...while代码。
5.2while循环
while循环会比do...while循环多一次if判断,所以while循环性能不如do循环。在release版本中,编译器会把while循环优化成等价的do循环。
5.3for循环
循环语句感觉都挺好判断的,这一块的内容主要在于实践,多逆向就行了。
6.数学运算符
6.1加法和减法
lea优化加减法。
6.2常量折叠
一些常量表达式编译期间直接计算出结果,在机器指令中直接使用结果。
6.3整数的乘法
imul有符号,mul无符号。
lea指令实现*2,4,8的优化
显然*4和*9被优化了。
6.4整数的除法
6.4.1
当除数为2^n的时候,编译器一般进行位移优化。如果x>=0。
x/(2^n)=x>>n;如果x<0,则x/(2^n)=(x+(2^n-1))>>n。
release版本程序,cdq用来扩充符号位后续来用and判断是否加3or7。
当除数为-2^n的时候,编译器一般进行位移优化。如果x>=0。
x/-(2^n)=-(x>>n);如果x<0,则x/-(2^n)=-((x+(2^n-1))>>n)。
多了个neg取反的过程,还有上面为什么是sub,我觉得其实你and 1也行,不过edx已经是-1了,直接sub更快。
6.4.2
剩下的有大量的数学知识,我打算后续碰上了再来补充。
6.4.3整数的取模
取模运算可以通过除法指令计算实现。但因为除法指令的执行周期较长,所以通常的优化方法是将其转换成等价的位运算或者除法运算,再有除法运算进行优化。
7.虚函数
7.1虚表
这里只总结一些,具体的分析写实验里去了。
虚表特征如下:
- 如果一个类有虚函数,就有一个指向虚表的指针
- 不同的类虚表不同,相同的类共享一个虚表
- 虚表指针放在对象首地址
- 虚表地址在全局数据区
- 虚表每一个元素指向类成员函数
- 虚表不一定以0结尾
- 虚表成员函数顺序,按照类声明的顺序排列
- 虚表在构造函数中会初始化
- 虚表在析构函数中会被赋值
构造函数调用顺序
- 调用虚基类构造函数(多个按继承顺序调用)
- 调用普通基类构造函数(多个按继承顺序调用)
- 调用对象成员的构造函数(多个按定义顺序调用)
- 调用派生构造函数
析构函数嗲用顺序
- 调用派生析构函数
- 调用对象成员析构函数(多个按定义顺序调用)
- 调用普通基类析构函数(多个按继承顺序调用)
- 调用虚基类析构函数(多个按继承顺序调用)
派生类的虚表填充过程:
- 复制基类的虚表
- 如果派生类虚表有覆盖基类的虚函数,使用派生类的的虚函数地址覆盖对应表项
- 如果派生类有新增虚函数,放在虚表后面
文章评论