C++的优化汇编代码
合集下载
相关主题
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
return 0; }
wenku.baidu.com
函数 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
}; void InitFun(Function* pFun) {
pFun->SetAge(34); pFun->SetName("brucewang"); }
int main(int argc, char* argv[]) {
Function* pFun = new Function; InitFun(pFun);
函
mov eax, 0CCCCCCCCh
数
rep stosd pop ecx
被
mov [ebp+var_4], ecx
优
mov eax, [ebp+var_4]
化
mov ecx, [ebp+arg_0] mov [eax], ecx
掉
pop edi
了
pop esi
pop ebx
mov esp, ebp
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
C++的优化汇编代码
对于一个资深程序员来说,了解我们的程序的最底层的运行机制是很重要的,特别对于 C/C++程序员来说,这点显得尤为 突出。
在很多情况下,知道其底层运行机制对我们理解更深层次的东西是非常有帮助的。比如说,如果你对这些底层的运行记住
比较熟悉,那么可能在 COM 编程中,你会更容易理解他的虚表(vtbl)技术,或者 windows 编程中经常涉及的 TRUNK 机制。 其实在不同的 C/C++编译器中,由同样的 C/C++代码编译成的汇编(机器)代码是不同的。主要讨论 Microsoft Visual C++.Net
第四步,就是函数调用了。早前面给出的 C++源代码中,我们有两个调用类成员函数的语句: pFun->SetAge(34); PFun->SetName("brucewang"); 在谈到对象的方法的调用上,我们有必要讨论它的具体存储方式,在 C++中,对象的成员数据和方法是分开存储的。这样, 对同一个类的多个对象,可以共享相同的代码。
这就为我们引入了一个新的问题:当我们调用某个方法是,类方法代码如何知道应该对那个对象数据进行操作呢?在 C++ 代码级别,这个问题是不存在的,因为 C++调用对象方法时是加上对象限定符的。但是在汇编码级别是怎样处理的呢?在 Visual
C++中,对象数据指针是用 ecx 寄存器来传递的,也就是说在方法中对对象数据的访问,是使用 ecx 寄存器中的数值来作为对象 基址指针对对象数据进行存取等操作的。实际上,这个 ecx 就是你在实现类成员方法是在其中使用的 this 指针。所以你看到的 汇编代码是这样的:
pop ebp
retn 4
Function__SetAge endp
SetName Function__SetName proc near
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
作
mov eax, 0CCCCCCCCh
内
rep stosd pop ecx
联
mov [ebp+var_4], ecx
编译器生成的机器代码。Visual C++不同版本的编译器生成的代码没有什么大的区别,这些在讨论后面的实现中会有详细的论述。 1. C++代码与汇编码
下面我要给出一个全局函数代码,为了简单起见,代码比较简单。
Void InitFun(Function* pFun) {
pFun->SetAge(34); PFun->SetName("brucewang"); } Function 是我们定义的一个类,这个函数的功能是接受一个 Function 类型对象的指针以对该对象进行初始化。SetAge 和 SetName 是 Function 中定义的两个函数,分别设置 Function 中定义的 age 和 name 属性。下面给出 Visual C++编译之后的汇编代 码:
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
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 字节的空间供该子函数使用。至于为什么要分配 多少字节,我们在后面再讨论。
其次,是辅助寄存器 ebx,edi,esi 的状态保存。作为通用寄存器,他们经常被用在一些常见的操作中,特别是在字符串、数组 等的操作中,edi、esi 通常作为存储目的、源数据的地址指针来使用。因此这里先保存这三个寄存器的值。虽然在本例中,并没 有用到 ebx 和 esi,但是还是按照惯例保存了。
;前期工作:设置基址指针,为局部变量分配内存 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 版本汇编代码 首先,我们进入函数体,就要执行三条初始化指令: