vs2008学习汇编
了解反汇编的一些小知识对于我们在开发软件时进行编程与调试大有好处,下面以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
00411373 sub esp,0C0h
00411379 push ebx
0041137A push esi
0041137B push edi
0041137C lea edi,[ebp-0C0h]
00411382 mov ecx,30h
00411387 mov eax,0CCCCCCCCh
0041138C rep stos dword ptr es:[edi]
return 0;
0041138E xor 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:取得第二个参数地址后放入到前面的寄存器(第一个参数)中。
然而lea也同样可以实现mov的操作,例如:
lea edi,[ebx-0ch]
方括号表示存储单元,也就是提取方括号中的数据所指向的内容,然而lea提取内容的地址,这样就实现了把(ebx-0ch)放入到了edi中,但是mov指令是不支持第二个操作数是一个寄存器减去一个数值的。
J、stos:串行存储指令,它实现把eax中的数据放入到edi所指的地址中,同时edi后移4个字节,这里的stos实际上对应的是stosd,其他的还有stosb,stosw分别对应1,2个字节。
K、jmp:无条件跳转指令,对应于大量的条件跳转指令。
L、jg:条件跳转,大于时成立,进行跳转,通常条件跳转之前会有一条比较指令(用于设置标志位)。
M、jl:小于时跳转。
N、jge:大于等于时跳转。
O、cmp:比较大小指令,结果用来设置标志位。
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之间的空间初始化为0CCCCCCCCh。大家在学习反汇编的过程中会发现,其实编译器会根据情况把相应长度的这样一段作为局部变量的空间,而这里把局部变量区域全都初始化成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具有一致性,这些区域是无效区域,但代码访问这些区域时就会出现非法访问提示。当然,你应该可以想到,那个提示是可以被屏蔽掉的,你可以把这部分区域填充上数据或者修改 iint 3 调用的中断程序。
从以上反汇编程序,我们可以发现几点:
A、一些内部的库函数是不会对堆栈进行出栈管理的,所以若要对反汇编程序进行操作时,一点要注意这一点
B、编译器会自动的加上一些对栈顶的检查工作,这个是我们在做VC调试时经常遇到的一个问题,就是堆栈错误
当然以上只是对debug版本下的程序进行反汇编,如果为release 版本,代码就会进行大量的优化,在理解时会有一定的难度,有兴趣朋友可以试着反汇编一下,推荐大家有IDA返回工具,感觉挺好用的。