如何进行反汇编
在调试的环境下,我们可以很方便地通过反汇编窗口查看程序生成的反汇编信息。如下图所示。
图1:打开调试窗口-选择反汇编
记得中断程序的运行,不然看不到反汇编的指令。
图2:中断程序
#include<; #include<windows.h> const long Lenth=5060000/5; int main(){ while(true){ for(long i=0;i<Lenth;i++){ ; } Sleep(10); } }查看一个简单的程序及其生成的汇编指令
反汇编窗口
图3:反汇编窗口
反汇编预备知识
函数调用大家都不陌生,调用者向被调用者传递一些参数,然后执行被调用者的代码,最后被调用者向调用者返回结果,还有大家比较熟悉的一句话,就是函数调用是在栈上发生的,那么在计算机内部到底是如何实现的呢?
对于程序,编译器会对其分配一段内存,在逻辑上可以分为代码段,数据段,堆,栈
代码段:保存程序文本,指令指针EIP就是指向代码段,可读可执行不可写
数据段:保存初始化的全局变量和静态变量,可读可写不可执行
BSS:未初始化的全局变量和静态变量
堆(Heap):动态分配内存,向地址增大的方向增长,可读可写可执行
栈(Stack):存放局部变量,函数参数,当前状态,函数调用信息等,向地址减小的方向增长,非常非常重要,可读可写可执行
图4:编译器内存分配
寄存器
EAX:累加(Accumulator)寄存器,常用于函数返回值
EBX:基址(Base)寄存器,以它为基址访问内存
ECX:计数器(Counter)寄存器,常用作字符串和循环操作中的计数器
EDX:数据(Data)寄存器,常用于乘除法和I/O指针
ESI:源变址寄存器
DSI:目的变址寄存器
ESP:堆栈(Stack)指针寄存器,指向堆栈顶部
EBP:基址指针寄存器,指向当前堆栈底部
EIP:指令寄存器,指向下一条指令的地址
常用汇编指令介绍
这里简单介绍一下常见的几种汇编指令。
add:加法指令,第一个是目标操作数,第二个是源操作数,格式为:目标操作数 = 目标操作数 + 源操作数;
sub:减法指令,格式同 add;
call:调用函数,一般函数的参数放在寄存器中;
ret:跳转会调用函数的地方。对应于call,返回到对应的call调用的下一条指令,若有返回值,则放入eax中;
push:把一个32位的操作数压入堆栈中,这个操作在32位机中会使得esp被减4(字节),esp通常是指向栈顶的(这里要指出的是:学过单片机的同学请注意单片机种的堆栈与Windows下的堆栈是不同的,请参考相应资料),这里顶部是地址小的区域,那么,压入堆栈的数据越多,esp也就越来越小;
pop:与push相反,esp每次加4(字节),一个数据出栈。pop的参数一般是一个寄存器,栈顶的数据被弹出到这个寄存器中;
一般不会把sub、add这样的算术指令,以及call、ret这样的跳转指令归入堆栈相关指令中。但是实际上在函数参数传递过程中,sub和add最常用来操作堆栈;call和ret对堆栈也有影响。
mov:数据传送。第一个参数是目的操作数,第二个参数是源操作数,就是把源操作数拷贝到目的一份。
xor:异或指令,这本身是一个逻辑运算指令,但在汇编指令中通常会见到它被用来实现清零功能。
用 xor eax,eax这种操作来实现 mov eax,0,可以使速度更快,占用字节数更少。
lea:取得第二个参数地址后放入到前面的寄存器(第一个参数)中。
然而lea也同样可以实现mov的操作,例如:
lea edi,[ebx-0ch]
方括号表示存储单元,也就是提取方括号中的数据所指向的内容,然而lea提取内容的地址,这样就实现了把(ebx-0ch)放入到了edi中,但是mov指令是不支持第二个操作数是一个寄存器减去一个数值的。
stos:串行存储指令,它实现把eax中的数据放入到edi所指的地址中,同时edi后移4个字节,这里的stos实际上对应的是stosd,其他的还有stosb,stosw分别对应1,2个字节。
jmp:无条件跳转指令,对应于大量的条件跳转指令。
jg:条件跳转,大于时成立,进行跳转,通常条件跳转之前会有一条比较指令(用于设置标志位)。
jl:小于时跳转。
jge:大于等于时跳转。
cmp:比较大小指令,结果用来设置标志位。
rep 根据ECX寄存器的值进行重复循环操作
注:
mov ax,[bx]
[ ]表示是间接寻址,bx和[bx]的区别是,前者操作数就是bx中存放的数,后者操作数是以bx中存放的数为地址的单元中的数。比如bx中存放的数是40F6H,40F6H、40F7H两个单元中存放的数是22H、23H,则
mov ax,[bx];2223H传送到ax中
mov ax,bx;40F6H传送到ax中
ILT是INCREMENTAL LINK TABLE的缩写,这个@ILT其实就是一个静态函数跳转的表,它记录了一些函数的入口然后跳过去,每个跳转jmp占一个字节,然后就是一个四字节的内存地址,加起为五个字节
比如代码中有多处地方调用boxer函数,别处的调用也通过这个ILT表的入口来间接调用,而不是直接call 该函数的偏移,这样在编译程序时,如果boxer函数更新了,地址变了,只需要修改跳表中的地址就可以,有利于提高链接生成程序的效率。这个是用在程序的调试阶段,当编译release程序时,就不再用这种方法。
我试着将HEX数据改成00 00,对应的汇编指令变成了add byte ptr [eax],al ,反过来,如果将一个地方的汇编指令改成add byte ptr [eax],al ,对应的HEX数据就成了00 00,也就是说,他们是一一对应的,编译器认为,00 00 这样两个字节宽度的二进制数对应的汇编指令就是add byte ptr [eax],al ;
dword 双字 就是四个字节
ptr pointer缩写 即指针
函数参数传递方式
函数调用规则指的是调用者和被调用函数间传递参数及返回参数的方法,在Windows上,常用的有Pascal方式、WINAPI方式(_stdcall)、C方式(_cdecl)。
- _cdecl C调用规则:
- 参数从右到左进入堆栈;
- 在函数返回后,调用者要负责清除堆栈,这种调用方式通常会生成较大的可执行程序。
- _stdcall又称为WINAPI,调用规则如下:
- 参数从右到左进入堆栈;
- 被调用的函数在返回前自行清理堆栈,这种方式生成的代码比cdecl小。
- Pascal调用规则(主要用于Win16函数库中,现在基本不用):
- 参数从左到右进入堆栈;
- 被调用的函数在返回前自行清理堆栈;
- 不支持可变参数的函数调用。