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 文件。内容如下:
2 、 VS 查看汇编代码
A 、 VC 处于调试状态才能看到汇编指令窗口 。因此,可以在 return 0 上设置一个断点:把光标移到 return 0 那一行上,然后按下 F9 键设置一个断点。
B 、按下 F5 键进入调试状态,当程序停在 return 0 这一行上时,打开菜单 “Debug” 下的 “Windows” 子菜单,选择 “Disassembly” 。这样,出现一个反汇编的窗口,显示下面的信息:
- <span>--- d:\my documents\visual studio 2008\projects\casmtest\casmtest\casmtest_main.cpp</span>
- // 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中:)于是,才有了下面两句指令:
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才可以用)。现在,你可以用上面说的方法自己分析一下他们各自的特点了。