C++的优化汇编代码
太
push ebx push esi
简
push edi
单
push ecx lea edi, [ebp+var_44]
当
mov ecx, 11h
作
mov eax, 0CCCCCCCCh
内
rep stosd pop ecx
联
mov [ebp+var_4], ecx
4. C++代码 release 版本的优化效果
#include "stdafx.h" #include<string.h> const int MAX_NAME_LEN = 20; class Function
{ public: SetAge(int iAge) { m_age = iAge; } SetName(const char* szName) { strcpy(m_szName,szName); } private: int m_age; char m_szName[MAX_NAME_LEN];
Release 版本 _main proc near push 18h call ??2@YAPAXI@Z push eax call sub_401000 add esp, 8 xor eax, eax retn _main endp
SetAge Function__SetAge proc near
其次,是辅助寄存器 ebx,edi,esi 的状态保存。作为通用寄存器,他们经常被用在一些常见的操作中,特别是在字符串、数组 等的操作中,edi、esi 通常作为存储目的、源数据的地址指针来使用。因此这里先保存这三个寄存器的值。虽然在本例中,并没 有用到 ebx 和 esi,但是还是按照惯例保存了。
C+ห้องสมุดไป่ตู้的优化汇编代码
对于一个资深程序员来说,了解我们的程序的最底层的运行机制是很重要的,特别对于 C/C++程序员来说,这点显得尤为 突出。
在很多情况下,知道其底层运行机制对我们理解更深层次的东西是非常有帮助的。比如说,如果你对这些底层的运行记住
比较熟悉,那么可能在 COM 编程中,你会更容易理解他的虚表(vtbl)技术,或者 windows 编程中经常涉及的 TRUNK 机制。 其实在不同的 C/C++编译器中,由同样的 C/C++代码编译成的汇编(机器)代码是不同的。主要讨论 Microsoft Visual C++.Net
return 0; }
函数 main
main proc near
Debug 版本
var_48= byte ptr -48h var_8= dword ptr -8 var_4= dword ptr -4
push ebp mov ebp, esp sub esp, 48h push ebx push esi push edi lea edi, [ebp+var_48] mov ecx, 12h mov eax, 0CCCCCCCCh rep stosd push 18h call operator_new add esp, 4 mov [ebp+var_8], eax mov eax, [ebp+var_8] mov [ebp+var_4], eax mov ecx, [ebp+var_4] push ecx call j_InitFun add esp, 4 xor eax, eax pop edi pop esi pop ebx add esp, 48h cmp ebp, esp call __chkesp mov esp, ebp pop ebp retn main endp
Push 22h Mov ecx,dword prt [pFun] Call Function::SetAge 第五步,退出函数是的恢复工作,受限是恢复前面提到的 3 个常用的辅助寄存器:ebx、edi、esi。三条指令完成这步操作: Pop edi Pop esi Pop ebx 最后是释放局部变量空间,恢复现场。就是让程序在跳出子函数后,不会觉得有什么被改变了。因为本文的例子是调试版 本,所以有检测是否正确实行的代码: Add esp 0C0h Cmp ebp,esp Call @ILT+3240(_RTC_CheckEsp);非调试版本不会有这样的指令 Mov esp,ebp Pop ebp 在进入函数体时,程序就将当前的堆栈指针 esp 传入 ebp 寄存器,在程序的执行过程中不改变 ebp 中的值,在退出函数体时, 再降 ebp 中的值恢复到 esp 中,通过这样的方式来实现恢复程序现场。 3. 局部变量空间分配及栈操作 在前面谈到局部变量空间分配的问题,在本例中,InitFun 函数没没有定义任何局部变量,但是也分配了 0C0h 字节的空间。 其实在所有的 Visual C++函数中,编译器都要分配 0C0h 字节的空间。如果定义了局部变量,则在 0C0h 字节的空间的基础上再 加。 堆栈操作在汇编语言中是占到了很大的比重的,C++语言从某种程度上说也是基于堆栈的语言,因为它其中的好多操作都是 基于堆栈的。特别是从面向对象的角度来看,一般在我们的整个程序中,全局变量所占的比例是很小的,其它绝大多数的变量 都是局部变量。这些局部变量的分配和释放都是通过堆栈操作来完成的。 因为在 C++语言中,程序栈是向下生长的,即在堆栈空间内,变量是从高地址向低地址方向依次分配的。所以,我们在前 面看到的局部变量内存分配是通过 sub 指令来完成的,而不是 add 指令,因此,像下面的指令: Sub esp,0C0h 为函数分配 0C0h 字节的空间,在退出函数体是,也可以通过这样的指令来释放局部变量空间: Add esp 0C0h 但是,在函数体内部,可能由于某些原因,push/pop 等堆栈指针操作可能不成对,或者其它指令改变了 esp 的值,会使得这 条指令不能恢复进入函数体时的 esp 的值。 这种情况多发生在不同 DLL 版本的访问方式上。如,Borland C++编译器编译的 DLL 提供的 int Add(int,int),你在 Visual C++ 程序调用该函数,则很可能出现这种问题,因为两者在寄存器使用约定上,栈操作方式上都不尽相同。或者不同的调用约定如: __cdecl、__stdcall、__pascal、__fastcall 之间转换不明确很有可能引发这种问题。所以,如果程序当中不应该使用 Add esp 0C0h 的方式释放内存来恢复现场,而是恢复保存在基址指针 ebp 寄存器中的值来实现。 但是这也对我们提出了一个要求,不可随意改变这些寄存器的值,而我们在向 C++代码中嵌入汇编代码是很有可能在不经 意间写出这样的代码: _asm add ebp,4 它自行修改了基址指针的值,在该函数执行结束时肯定引发访问异常。 最后我们给出例子程序的堆栈操作步骤,如下图:
push ebp Mov ebp,esp Sub esp,0C0h 我们知道,ebp 寄存器在 Visual C++中是被默认用来做基址指针的。因此,在刚进入函数执行阶段,都要对 ebp 进行相应的 操作。
第一步,先保存当前 ebp 中的值,然后将他用在本函数中。 第二步,获取当前堆栈指针,获得的堆栈指针将作为局部变量的基址指针使用。最要引起我们注意的是第三条语句,在 C++ 中,程序局部变量是在堆栈中分配的,可是并没有在每个函数中发现诸如 AllocMem 等申请内存的函数或指令。实际上,函数 中的局部变量空间的分配就是由这条指令完成的。在本例中,程序分配了 0C0h 字节的空间供该子函数使用。至于为什么要分配 多少字节,我们在后面再讨论。
编译器生成的机器代码。Visual C++不同版本的编译器生成的代码没有什么大的区别,这些在讨论后面的实现中会有详细的论述。 1. C++代码与汇编码
下面我要给出一个全局函数代码,为了简单起见,代码比较简单。
Void InitFun(Function* pFun) {
pFun->SetAge(34); PFun->SetName("brucewang"); } Function 是我们定义的一个类,这个函数的功能是接受一个 Function 类型对象的指针以对该对象进行初始化。SetAge 和 SetName 是 Function 中定义的两个函数,分别设置 Function 中定义的 age 和 name 属性。下面给出 Visual C++编译之后的汇编代 码:
var_44= byte ptr -44h
var_4= dword ptr -4
arg_0= dword ptr 8
太
简
push ebp mov ebp, esp
单
sub esp, 44h
当
push ebx push esi
作
push edi
内
push ecx
联
lea edi, [ebp+var_44] mov ecx, 11h
;前期工作:设置基址指针,为局部变量分配内存 push ebp Mov ebp,esp Sub esp,0C0h ;保存三个常用辅助寄存器原始信息 Push ebx Push esi Push edi ;以 0CCCCCCCCh 值初始化局部变量内存空间 Lea edi,[ebp-0C0h] Mov ecx,30h Rep stos dword ptr [edi] ;函数主体,执行函数逻辑 Push 22h Mov ecx,dword prt [pFun] Call Function::SetAge Push offset string "brucewang" Mov ecx,dword ptr[pFun] Call Function::SetName Pop edi Pop esi Pop ebx Add esp 0C0h Cmp ebp,esp Call @ILT+3240(_RTC_CheckEsp) Mov esp,ebp Pop ebp 2. 解析 C++Debug 版本汇编代码 首先,我们进入函数体,就要执行三条初始化指令: