现在的位置: 首页 > 综合 > 正文

汇编与C++

2013年06月14日 ⁄ 综合 ⁄ 共 4771字 ⁄ 字号 评论关闭
 

不同的C/C++编译器中,由同样的C++代码编译成的(机器)汇编代码是不同的。本文主要讨论Microsoft Visual C++ .Net编译器生成的机器代码。
 
代码与汇编码C++
下面给出一个全局函数代码。
void InitFun(Function * pFun)
{
       pFun->SetAge(22);
       pFun->SetName("Tcliuqiang");
}
Function是我们定义的一个类,这个函数的功能是接受一个Function类型对象的指针以对该对象进行初始化。SetAge和SetName是Function中定义的两个函数,分别设置Function中定义的age和name属性。
下面给出Visual C++编译之后的汇编代码:
;//{{ 前期工作:设置基址指针,为局部变量分配内存
@01: push        ebp 
 @02: mov         ebp,esp
 @03: sub         esp,0C0h
;//{{ 保存三个常用辅助寄存器原始信息以用于本函数
 @04: push        ebx 
 @05: push        esi 
 @06: push        edi 
;//{{ (以0CCCCCCCCH值)初始化已分配的局部变量内存空间
 @07: lea         edi,[ebp-0C0h]
 @08: mov         ecx,30h
 @09: mov         eax,0CCCCCCCCh
 @0A: rep stos    dword ptr [edi]
;//{{ :函数主体:执行函数逻辑
 @0B: push        16h 
 @0C: mov         ecx,dword ptr [pFun] ; //{{ [ebp+8] 从栈中取得所参数 }}
 @0D: call        Function::SetAge (419F9Bh)
 @0E: push        offset string "Tcliuqiang" (44E0C8h)
 @0F: mov         ecx,dword ptr [pFun] ; //{{ [ebp+8] 从栈中取得所参数 }}
 @10: call        Function::SetName (419C8Ah)
;//{{ 恢复三个常用辅助寄存器原始信息
 @11: pop         edi 
 @12: pop         esi 
 @13: pop         ebx 
;//{{ 检测程序是否发生过异常,只在Visual C++调试版本中才会有这样的指令
 @14: add         esp,0C0h
 @15: cmp         ebp,esp
 @16: call        @ILT+3240(__RTC_CheckEsp) (419CADh)
;//{{ 释放局部变量空间,恢复上级程序执行现场
 @17: mov         esp,ebp
 @18: pop         ebp 
 @19: ret
我们给出的代码市比较简单的。但是从上面可以看到编译后的机器代码却有一大段。
 
解析C++汇编代码
     首先,进入函数体,就要执行三条初试化指令:
    @01: push        ebp 
    @02: mov         ebp,esp
    @03: sub         esp,0C0h
 
 
   其次,是辅助寄存器ebx,edi,esi的状态保存。作为通用寄存器,它们经常被用在一些常见的操作中。特别是在字符串、数组等的操作中,edi、esi通常作为存储目的、源数据的地址指针来使用。因此这里先保存这三个寄存器的值。虽然在本例中,并没有用到ebx和esi,但两者还是被保存了。
 
第三,变量的初始化。我们可以看到,程序使用了四条指令来初始化局部变量空间:
     @07: lea         edi,[ebp-0C0h]
     @08: mov         ecx,30h
     @09: mov         eax,0CCCCCCCCh
     @0A: rep stos    dword ptr [edi]
程序获得局部变量空间的起始地址(低地址)并将它送入edi寄存器,设置ecx寄存器为变量空间长度30H(DWORD型48双字长度,也即192字节),eax为0CCCCCCCCh值,然后通过循环指令rep stos将eax中的值存入以edi为起始地址的192字节的地址空间内。这里可以发现一个问题就是在Visual C++中,局部变量一律以0CCCCCCCCh来填充。所以在程序调试时经常出现的数据“0CCCCCCCC”。
 
  第四,就是函数调用了。在前面给出的C++源代码中,我们有两个调用类成员函数的语句:
pFun->SetAge(22);
     pFun->SetName("Tcliuqiang");
C++中,类对象的成员数据和类方法是分立存储的。同一类的多个对象,可以共享相同的代码。
 
当调用某个方法时,类方法代码如何知道应该对那个对象数据进行操作?在C++代码级别,这个问题是不存在的。因为在调用对象方法时是要加上对象限定符的,如上面的pFun->SetAge或objFun.SetAge等。但是在汇编(机器)码级别是怎样处理的呢?在Visual C++中,对象数据指针地址是用ecx寄存器来传递的。也就是说,在类方法中,对对象数据的访问,是使用ecx寄存器中的数值来作为对象基址指针对对象数据进行存取等操作的。实际上,这个ecx就是在实现类成员方法时在其中使用的this指针。所以,我们看到的汇编实现是这样的:
@0B: push        16h 
     @0C: mov         ecx,dword ptr [pFun]
     @0D: call        Function::SetAge (419F9Bh)
首先将常数16h(22)推入堆栈,将pFun所指对象的地址值传入ecx寄存器,然后调用类成员方法Function::SetAge。
     @0E: push        offset string "Tcliuqiang" (44E0C8h)
     @0F: mov         ecx,dword ptr [pFun]
     @10: call        Function::SetName (419C8Ah)
后面的实现也是如此。
 
第五,退出函数体时的恢复工作。首先时恢复前面提到的三个常用的辅助寄存器:ebx,edi,esi。三条指令完成这步操作:
     @11: pop         edi 
     @12: pop         esi 
     @13: pop         ebx 
 
最后是释放局部变量空间,恢复现场。就是让程序在跳出子函数后,不会觉得有什么被改变了。因为本例是调试版本,所以还有检测是否正确执行的代码(@14,@15,@16):
;//{{ 检测ebp,esp是否相同,如不同则说明在运行时出现异常
       @14: add         esp,0C0h
       @15: cmp         ebp,esp
      @16: call        @ILT+3240(__RTC_CheckEsp) (419CADh)
    ;//}} 非调试版本不会有这样的指令
     @17: mov         esp,ebp
     @18: pop         ebp
     @19: ret
在进入函数体时,程序就将当前的堆栈指针传入ebp寄存器;在程序执行的过程当中,不改变ebp中的值。在退出函数体时,再将ebp中的值恢复到esp寄存器,通过这样的方式来实现恢复程序现场。
 
局部变量空间分配及栈操作
上例中,InitFun函数内没有定义任何局部变量,但是也分配了0C0H字节的空间。其实在所有的Visual C++函数中,编译器都要分配0C0H的保留空间。如果定义了局部变量,则在0C0H的基础上再加。如:
int Add(int s1,int s2){
       int s3=s1+s2;
       return s3;
}
该函数内定义了一个INT型的变量,编译器为它分配了额外的0CH字节内存空间。 Visual C++为每个变量分配多于它本身需要的内存空间。
 
 堆栈操作在C++语言中是占到了很大的比重的,C++语言从某种程度上来说也是基于堆栈的语言,因为它其中的好多操作都是基于堆栈的。特别是从面向对象的角度来看,一般在整个程序中,全局变量所占的比例是很小的,其它绝大多数的变量(包括如INT等基本数据类型、自定义类型)都是局部变量。这些局部变量的分配和释放都是通过堆栈操作来完成的。
  因为在C++语言当中,程序栈是向下生长的,即在堆栈空间内,变量是从高地址向低地址方向依次分配的。所以,前面的看到的局部变量内存分配是通过sub指令完成的,而不是add指令。因此,像下面的指令,
       @03: sub         esp,0C0h
为函数分配0C0H字节的局部变量空间。在退出函数体时,也可以通过这样的指令来释放局部变量空间:
       @14: add         esp,0C0h
但是,在函数体内,可能会由于某些原因,push/pop等堆栈操作指令可能不成对,或者其他指令改变了esp值,会使得这条指令不能恢复进入函数体时esp的值。这种情况多发生在不同DLL版本的访问方式上。如,这里的int Add(int,int)是由Borland C++编译器所编译的DLL提供的,当在Visual C++程序调用该函数,则很可能出现这种问题,因为两者在寄存器使用约定上,栈操作方式上都不尽相同。或者不同的调用约定如__cdecl,__stdcall,__pascal,__fastcall之间转换不明确很有可能引发这种问题。如,假设某函数属性定义为__cdecl,而调用时按照__stdcall或__pascal方式就很可能产生问题。所以,程序当中不使用@14语句那样释放内存恢复现场,而是恢复保存在基址指针ebp寄存器中的值来实现,如下所示:
       @17: mov         esp,ebp
但这也向我们提出了要求,不可随便改变这些寄存器的值,而我们在向C++代码中嵌入汇编代码时很有可能在不经意间写出这样的代码。尝试向你的代码添加这样一条指令:
       _asm add ebp,4
它自行修改了基址指针的值,在该函数执行结束时肯定引发访问异常(Access Violation)。
最后,我们给出例子程序的堆栈操作步骤,如下
 
    pFun
    ebp
 
 
[ebp + 0Ch]
 
[ebp + 0C0h]
 
 
 
高地址空间(FFFFH) 
 
 
参数入栈
 
保存ebp值
 
分配
0C0h
字节
的局
部变
量空
间,即
Sub esp,0C0h
 
Esp现在所指处
 
 
 
低地址空间(0000H)                                                                                
该图给出了在进入函数体时,程序对程序栈所作的操作。

 

【上篇】
【下篇】

抱歉!评论已关闭.