`

C++内嵌汇编(一):反汇编分析C++代码

 
阅读更多

注:本文是“百度文库”中下载的文章^^谢谢作者

 

Sam: 使用binutils-2.23.1这个软件中的小工具objdump -d *.o 可以对一个目标文件进行反汇编:)

了解反汇编的一些小知识对于我们在开发软件时进行编程与调试大有好处,下面以 VS2008 环境下的 VC++ 简单介绍一下反汇编的一些小东西!

1 、新建简单的 VC 控制台应用程序

A 、打开 Microsoft Visual Studio 2008 ,选择主菜单 “File”

B 、选择子菜单 “New” 下面的 “Project” ,打开 “New Project” 对话框。

C 、左边选择 Visual C++ 下的 win32 ,右边选择 Win32 Console Application ,然后输入一个工程名,点击 “OK” 即可,在出现的向导中,一切默认,点击 Finish 即可。

D 、在出现的编辑区域内会出现以你设定的工程名命名的 CPP 文件。内容如下:

#include "stdafx.h"
int _tmain(int argc, _TCHAR* argv[])
{
     return 0;
} 

 

2 VS 查看汇编代码

A VC 处于调试状态才能看到汇编指令窗口 。因此,可以在 return 0 上设置一个断点:把光标移到 return 0 那一行上,然后按下 F9 键设置一个断点。

B 、按下 F5 键进入调试状态,当程序停在 return 0  这一行上时,打开菜单 “Debug” 下的 “Windows” 子菜单,选择 “Disassembly” 。这样,出现一个反汇编的窗口,显示下面的信息:

--- d:\my documents\visual studio 2008\projects\casmtest\casmtest\casmtest_main.cpp


// CAsmTest.cpp : 定义控制台应用程序的入口点。

#include "stdafx.h"

int _tmain(int argc, _TCHAR* argv[])
{
00411370  push        ebp 
00411371  mov         ebp,esp                   // 此后 ,ebp 寄存器中实际保存的是原来 esp 的内容
00411373  sub         esp,0C0h
00411379  push        ebx 
0041137A  push        esi 
0041137B  push        edi 
0041137C  lea         edi,[ebp-0C0h]         // 作用: 将 (ebp-0C0h) 这个数值放入 edi
00411382  mov         ecx,30h
00411387  mov         eax,0CCCCCCCCh
0041138C  rep stos    dword ptr es:[edi]
 return 0;
0041138E  xor         eax,eax            // 嵌入汇编时,如果函数没有指定返回值,则 eax 为默认返回值
}
00411390  pop         edi 
00411391  pop         esi 
00411392  pop         ebx 
00411393  mov        esp,ebp
00411395  pop         ebp 
00411396  ret 

 

      上面就是系统生成的 main 函数原型,确切的说是 _tmain() 的反汇编的相关信息,相信学过汇编语言的肯定就能够了解它所做的操作了。

3 、简单了解一下常见的汇编指令

为了照顾到没学过汇编程序的同志们,这里简单介绍一下常见的几种汇编指令。

A add :加法指令,第一个是目标操作数,第二个是源操作数,格式为:目标操作数 = 目标操作数 + 源操作数。

B sub :减法指令,格式同 add

C call :调用函数,一般函数的参数放在寄存器中。

D ret :跳转会调用函数的地方。对应于 call ,返回到对应的 call 调用的下一条指令,若有返回值,则放入 eax 中。

E push :把一个 32 位的操作数压入堆栈中,这个操作在 32 位机中会使得 esp 被减 4 (字节), esp 通常是指向栈顶的(这里要指出的是:学过单片机的同学请注意单片机种的堆栈与 Windows 下的堆栈是不同的,请参考相应资料),这里顶部是地址小的区域,那么,压入堆栈的数据越多, esp 也就越来越小。

F pop :与 push 相反, esp 每次加 4 (字节),一个数据出栈。 pop 的参数一般是一个寄存器,栈顶的数据被弹出到这个寄存器中。

一般不会把 sub add 这样的算术指令,以及 call ret 这样的跳转指令归入堆栈相关指令中。但是实际上在函数参数传递过程中, sub add 最常用来操作堆栈; call ret 对堆栈也有影响。

G mov :数据传送。第一个参数是目的操作数,第二个参数是源操作数,就是把源操作数拷贝到目的一份。

H xor :异或指令,这本身是一个逻辑运算指令,但在汇编指令中通常会见到它被用来实现清零功能。用 xor eax,eax 这种操作来实现 mov eax,0 ,可以使速度更快,占用字节数更少。

I lea load effect address. 取得第二个参数的有效地址(也就是去偏移地址)后放入到前面的寄存器(第一个参数)中。

然而 lea 也同样可以实现 mov 的操作,例如:

                                  lea edi,[ebx-0ch]

方括号表示存储单元,也就是提取方括号中的数据所指向的内容,然而 lea 提取内容的地址,这样就实现了把( ebx-0ch )放入到了 edi 中,但是 mov 指令是不支持第二个操作数是一个寄存器减去一个数值的。

J stos :串行存储指令,它实现把 eax 中的数据放入到 edi 所指的地址中,同时 edi 后移 4 个字节,这里的 stos 实际上对应的是 stosd ,其他的还有 stosb,stosw 分别对应 1 2 个字节。

注意:

(1) stosb:  [esi] 中一个字节送入 AL

(2) stosw: [esi] 中一个字送入 AX

(3) stosd:  [esi] 中一个双字送入 EAX

K jmp :无条件跳转指令,对应于大量的条件跳转指令。

L jg :条件跳转,大于时成立,进行跳转,通常条件跳转之前会有一条比较指令(用于设置标志位)。

M jl :小于时跳转。

N jge :大于等于时跳转。

O cmp :比较大小指令,结果用来设置标志位。

P rep:

重复执行后面的指令

rep stos dword ptr [edi]

是将 edi 指向的区域初始化为 0CCCCCCCCh

应该是 12h*4 个字节,可以理解为一个函数,传来的某个参数为指针,然后将这个指针指向的区域初始化

void fun(long *p)

{

int i;

// 12h=18

for(i=0;i<18;i++)

{

p[i]=0CCCCCCCCh;

}

}

相当于这个函数的功能

 

4 、函数参数传递方式

函数调用规则 指的是调用者和被调用函数间传递参数及返回参数的方法,在 Windows 上,常用的有 Pascal 方式、 WINAPI 方式( _stdcall )、 C 方式( _cdecl )。

A _cdecl C 调用规则:

a )参数从右到左进入堆栈;

b )在函数返回后,调用者要负责清除堆栈,这种调用方式通常会生成较大的可执行程序。

B _stdcall 又称为 WINAPI ,调用规则如下:

a )参数从右到左进入堆栈;

b )被调用的函数在返回前自行清理堆栈,这种方式生成的代码比 cdecl 小。

C Pascal 调用规则(主要用于 Win16 函数库中,现在基本不用):

a )参数从左到右进入堆栈;

b )被调用的函数在返回前自行清理堆栈。

c )不支持可变参数的函数调用。

5 VC 中访问无效变量出错原因

我们看上面主函数反汇编后的其中一段代码如下:

0041137C  lea         edi,[ebp-0C0h]
00411382  mov         ecx,30h
00411387  mov         eax,0CCCCCCCCh
0041138C  rep stos    dword ptr es:[edi]

从代码的表面上看,它是实现把从 ebp-0C0h 开始的 30h 个字的空间写入 0CCCCCCCCh 。其中 eax 为四位的数据,这样可以计算:

                      0C0h = 30h * 4

也就是把从 ebp-0C0h ebp 之间的空间初始化为 0CC CC CC CC h 。大家在学习反汇编的过程中会发现,其实编译器会根据情况把相应长度的这样一段作为局部变量的空间,而这里把局部变量区域全都初始化成 0CCCCCCCCh 也是有其用意的,做 VC 编程的工作者,特别是初学者可能不会对 0CCCCCCCCh 这个常量陌生。 0cch 实际上是 int 3 指令的机器码,这是一个断点中断指令 (在反编译出的信息中大家会看到 int 3 ),因为局部变量不可被执行,或者如果在没有初始化的时候进行了访问,则就会出现访问失败错误。这个在 VC 编译 Debug 版本中才能看到提示这个错误,在 Release 版本中,会以另外一种错误形式体现。下面,我们修改主程序看下 new delete 的反汇编的效果(注释直接加到反汇编的代码中了)。

VC 生成工程,写入源代码如下:

1 )情况 1

// ASM_Test.cpp : Defines the entry point for the console application.                    (   源代码 1 )
//
#include "stdafx.h"
#include "stdlib.h"

int _tmain(int argc, _TCHAR* argv[])
{
    int *pTest = new int(3);                // 定义一个整型指针,并初始化为 3
    printf( "*pTest = %d\r\n", *pTest );    // 调用库函数 printf 输出数据
    delete []pTest;                            // 删除这个指针

    return 0;
}

 

这里仅仅看下在 new delete 进行空间管理时进行反汇编时可能出现的一些情况,我们把上面源代码称为源代码( 1 ),我们按照前面讲解的查看 VS 下反汇编的方法可以看到对应于上面代码的反汇编代码如下:

--- f:\mysource\asm_test\asm_test\asm_test.cpp ---------------------------------                      ( 反汇编代码 1 )
// ASM_Test.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include "stdlib.h"

int _tmain(int argc, _TCHAR* argv[])
{

; ( 1 )函数预处理部分
004113C0  push        ebp 
004113C1  mov         ebp,esp ; 保存堆栈的栈顶位置
004113C3  sub         esp,0E8h ; 要置为 0CCCCCCCCh 保留变量空间长度
004113C9  push        ebx       ; 保存寄存器 ebx 、 esi 、 edi
004113CA  push        esi 
004113CB  push        edi 
004113CC  lea         edi,[ebp-0E8h]    ; 提出要置为 0CCCCCCCCh 的空间起始地址
004113D2  mov         ecx,3Ah      ; 要置为 0CCCCCCCCh 的个数,每个占 4 个字节
004113D7  mov         eax,0CCCCCCCCh  ; 于是 3Ah * 4 = 0E8h
004113DC  rep       stos    dword ptr es:[edi]  ; 进行置为 0CCCCCCCCh 操作

;( 2 )定义一个 int 型指针,分配空间后,并初始化为 3 ,
    int *pTest = new int(3);                 // 定义一个整型指针,并初始化为 3
004113DE  push        4    ; 要分配的空间长度,会根据定义的数据类型而不同
004113E0  call        operator new (411186h)   ; 分配空间,并把分配空间的起始地址放入 eax 中
004113E5  add         esp,4    ; 由于 new 与 delete 函数本身没有对栈进行弹出操作,所以,要编写者自己处理
004113E8  mov         dword ptr [ebp-0E0h],eax  ; 比较分配的空间是否为 0 ,如果为 0
004113EE  cmp         dword ptr [ebp-0E0h],0
004113F5  je          wmain+51h (411411h)
004113F7  mov         eax,dword ptr [ebp-0E0h]      ; 对于分配的地址分配空间进行赋值为: 3
004113FD  mov         dword ptr [eax],3
00411403  mov         ecx,dword ptr [ebp-0E0h]
00411409  mov         dword ptr [ebp-0E8h],ecx   ; 似乎用 [ebp - 0E0h] 和 [ebp - 0E8h] 作为了中间存储单元
0041140F  jmp         wmain+5Bh (41141Bh)
00411411  mov         dword ptr [ebp-0E8h],0     ; 上面分配空间失败时的操作


0041141B  mov         edx,dword ptr [ebp-0E8h]
00411421  mov         dword ptr [pTest],edx           ; 数据最后送入 pTest 变量中

; 调用 printf 函数进行数据输出
    printf( "*pTest = %d\r\n", *pTest );     // 调用库函数 printf 输出数据
00411424  mov         esi,esp   ; 用于调用 printf 后的 Esp 检测,不明白编译器为什么这样做
00411426  mov         eax,dword ptr [pTest]   ; 提取要打印的数据,先是地址,下面一条是提取具体数据
00411429  mov         ecx,dword ptr [eax]
0041142B  push        ecx         ; 两个参数入栈
0041142C  push        offset string "*pTest = %d\r\n" (41573Ch)
00411431  call        dword ptr [__imp__printf (4182C4h)]      ; 调用函数
00411437  add         esp,8         ; 由于库函数无出栈管理操作,同 new 与 delete ,所以要加 8 ,进行堆栈处理
0041143A  cmp         esi,esp        ; 对堆栈的栈顶进行测试
0041143C  call        @ILT+325(__RTC_CheckEsp) (41114Ah) 

;进行指针变量的清理工作
    delete []pTest;                             // 删除这个指针
00411441  mov         eax,dword ptr [pTest]   ;[pTest] 中放入的是分配的地址,下面几条指令转悠一圈
00411444  mov         dword ptr [ebp-0D4h],eax   ; 就是要把要清理的地址送入堆栈,然后调用 delete 函数
0041144A  mov         ecx,dword ptr [ebp-0D4h]
00411450  push        ecx 
00411451  call        operator delete (411091h)
00411456  add         esp,4     ; 对堆栈进行处理,同 new 与 printf 函数

;函数结束后,进行最终的清理工作
    return 0;
00411459  xor         eax,eax   ; 做相应的清理工作,堆栈中保存的变量送回原寄存器
}
0041145B  pop         edi 
0041145C  pop         esi 
0041145D  pop         ebx 
0041145E  add         esp,0E8h       ; 进行堆栈的栈顶判断
00411464  cmp         ebp,esp
00411466  call        @ILT+325(__RTC_CheckEsp) (41114Ah)
0041146B  mov         esp,ebp
0041146D  pop         ebp 
0041146E  ret  

--- No source file -------------------------------------------------------------; 后面不再是源代码 

 

在列出反汇编程序时把反汇编代码的上下的分解注释也列了出来,亲手去查看的朋友可能会发现在这段代码的之外的其他部分会有大量的 int 3 汇编中的中断指令,这个是与上面的所说的 0CCCCCCCCh 具有一致性,这些区域是无效区域,但代码访问这些区域时就会出现非法访问提示。当然,你应该可以想到,那个提示是可以被屏蔽掉的,你可以把这部分区域填充上数据或者修改 int 3 调用的中断程序。

从以上反汇编程序,我们可以发现几点:

A 、一些内部的库函数是不会对堆栈进行出栈管理的,所以若要对反汇编程序进行操作时,一点要注意这一点

B 、编译器会自动的加上一些对栈顶的检查工作,这个是我们在做 VC 调试时经常遇到的一个问题,就是堆栈错误

当然 以上只是对 debug 版本下的程序进行反汇编 ,如果为 release 版本,代码就会进行大量的优化,在理解时会有一定的难度 ,有兴趣朋友可以试着反汇编一下,推荐大家有 IDA 返回工具,感觉挺好用的。

 

**********************************************************************************

********************************************************************************** 

 

5   、补充示例

#include<stdio.h>

int fun(int a, int b) {
   a = 0x4455;
   b = 0x6677;
   return a + b;
}

int main(void){
	fun(0x8899,0x1100);
	return 0;
}

反汇编后可以看到汇编代码如下:

   第一部分为main函数框架的汇编代码:

int main(void){
002813F0  push        ebp  
002813F1  mov         ebp,esp 
002813F3  sub         esp,0C0h 
002813F9  push        ebx  
002813FA  push        esi  
002813FB  push        edi  
002813FC  lea         edi,[ebp-0C0h] 
00281402  mov         ecx,30h 
00281407  mov         eax,0CCCCCCCCh 
0028140C  rep stos    dword ptr es:[edi] 

	fun(0x8899,0x1100);
0028140E  push        1100h
00281413  push        8899h 
00281418  call        fun (2811CCh)   ;002811CCh地址处为指令: jmp fun(2813A0h),即fun函数的入口
0028141D  add         esp,8 
	return 0;
00281420  xor         eax,eax 
}
00281422  pop         edi  
00281423  pop         esi  
00281424  pop         ebx  
00281425  add         esp,0C0h 
0028142B  cmp         ebp,esp 
0028142D  call        @ILT+310(__RTC_CheckEsp) (28113Bh) 
00281432  mov         esp,ebp 
00281434  pop         ebp  
00281435  ret 

 

   第二部分为fun函数的汇编代码:

int fun(int a, int b) {
002813A0  push        ebp  
002813A1  mov         ebp,esp    ;注意:其实pop和push操作,只用到了esp指针
002813A3  sub         esp,0C0h 
002813A9  push        ebx  
002813AA  push        esi  
002813AB  push        edi  
002813AC  lea         edi,[ebp-0C0h] 
002813B2  mov         ecx,30h 
002813B7  mov         eax,0CCCCCCCCh 
002813BC  rep stos    dword ptr es:[edi]       ;(重复) eax --放入--> es:[edi]
   a = 0x4455;
002813BE  mov         dword ptr [a],4455h     ;将0x00004455放入地址[a]中,即地址0x17493处
   b = 0x6677;
002813C5  mov         dword ptr [b],6677h     ;将0x00006677放入地址[b]中,即地址0x26231处
   return a + b;
002813CC  mov         eax,dword ptr [a] 
002813CF  add         eax,dword ptr [b] 
}

 002813D2  pop         edi  
002813D3  pop         esi  
002813D4  pop         ebx  
002813D5  mov         esp,ebp           
002813D7  pop         ebp  
002813D8  ret 

 

FAQ:

1、在main()函数编译后的汇编代码中,call fun前将参数压入堆栈;可以再fun()函数编译后的汇编代码中怎么没见pop出这些参数呢?在fun()编译后的指令中通过什么来获得输入参数呢?

     回答:

     A. 将esp内容传给ebp,通过ebp来获取输入参数

     call fun指令背着我做了一件事情——将call fun的下一条语句地址压栈。fun()函数编译后的最后一条指令是ret,ret指令pop了push给他的地址,然后返回到这个地址==>“调 用函数前的call”和“调用函数后的ret”一个push、一个pop,肯定不会让堆栈不平衡,老外叫做no stack unwinding。因此,如果在fun()函数汇编后的代码中上来就pop eax取出参数,就等于根ret抢返回地址了,这样fun()使用完就回不到主函数的断点了╮(╯▽╰)╭

     那么怎么在被调用函数中获取参数呢?Easy! 既然参数已经在堆栈中,我们只要指定“esp+某个偏移”就可以访问了。

     然而,只要被调用函数中出现push或者pop指令,就会用到esp指针,esp就会变化,不方便定位传入的参数位置。其实,在调用过程中被调用函数的传入参数位置是固定的。于是,我们用ebp来获取他!

     地球人都知道,ebp指向栈底,故在fun()进入后要先将ebp内容压栈,fun()返回前将栈中“原来 ebp的值”pop到ebp中:)于是,才有了下面两句指令:

002813A0  push        ebp  
002813A1  mov         ebp,esp

   B. ebp如何获取输入参数

   mov ebp, esp; 后堆栈应该变成这个样子:


/-------------------\  Higher Address
 | 参数2:  0x1100h | 
 +-----------------+
 | 参数1:  0x8899h |
 +-----------------+
 |   函数返回地址  |
 |    0x00401087   |
 +-----------------+
 |       ebp       |
\-------------------/   Lower Address <== stack pointer
& ebp all point to here, now

   ∵一个int是32位,可以分析出,第一个参数的地址是ebp + 08h,第二个参数就是ebp + 0ch。主要到,此时ebp中的值就是esp的值!

 

2、一个简单的问题休息一下:

0028140E  push        1100h
00281413  push        8899h 
00281418  call        fun (2811CCh)   ;002811CCh地址处为指令: jmp fun(2813A0h),即fun函数的入口
0028141D  add         esp,8

这个代码中最后一行add esp, 8指令作用是什么?

    回答:平衡堆栈,相当于弹出(just throw out)传入被调用函数的参数。

 

关键点:

     注意:其实pop和push操作,只用到了esp指针! ebp指定了栈底,但并没有什么实际作用——在16位汇编和32位汇编中均如此!

 

3、calling convention 调用传统

这个历史遗留问题。如果认真思考过,你一定想函数的参数为什么偏用堆栈转递呢,寄存器不也可以传递吗?而且很快阿。参数的传递顺序不
一定要是由后到前的,从前到后传递也不会出现任何问题啊?再有为什么一定要等到函数返回了再处理堆栈平衡的问题呢,能否在函数返回前就让堆栈平衡呢?
所有上述提议都是绝对可行的,而他们之间不同的组合就造就了函数不同的调用方法。也就是你常看到或听到的stdcall,pascal,

fastcall,WINAPI,cdecl等等。这些不同的处理函数调用方式就叫做calling convention。
默认情况下C语言使用的是cdecl方式,也就是上面提到的。参数由右到左进栈,调用函数者处理堆栈平衡。如果你在我们刚才的程序中fun函数前加入

__stdcall,再来用上面的方法分析一下。
8:        fun(0x8899,0x1100);
00401058   push        1100h  ; <== 参数仍然是由右到左传递的
0040105D   push        8899h  
00401062   call        fun (00401000)
;<== 这里没有了 add esp, 08h

1:    int __stdcall fun(int a, int b) {
00401000   push        ebp
00401001   mov         ebp,esp
00401003   sub         esp,40h
00401006   push        ebx
00401007   push        esi
00401008   push        edi
00401009   lea         edi,[ebp-40h]
0040100C   mov         ecx,10h
00401011   mov         eax,0CCCCCCCCh
00401016   rep stos    dword ptr [edi]
2:       a = 0x4455;
00401018   mov         dword ptr [ebp+8],4455h
3:       b = 0x6677;
0040101F   mov         dword ptr [ebp+0Ch],6677h
4:       return a + b;
00401026   mov         eax,dword ptr [ebp+8]
00401029   add         eax,dword ptr [ebp+0Ch]
5:    }
0040102C   pop         edi
0040102D   pop         esi
0040102E   pop         ebx
0040102F   mov         esp,ebp
00401031   pop         ebp
00401032   ret         8; <== ret 取出返回地址后,
                       ; 给esp加上 8。看!堆栈平衡在函数内完成了。
                       ; ret指令这个语法设计就是专门用来实现函数
                       ; 内完成堆栈平衡的
于是得出结论,stdcall是由右到左传递参数,被调用函数恢复堆栈的calling convention. 其他几种calling convention的修饰关键词分别是__pascal,__fastcall,WINAPI(这个要包含windows.h才可以用)。现在,你可以用上面说的方法自己分析一下他们各自的特点了。

 

 

 

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics