复习常用的汇编指令
注:该部分参考《天书夜读:从汇编语言到windows内核编程》谭文,邵坚磊著的第一章部分。
1.1用VC 6.0查看汇编代码
在VC 6.0下创建工程,并建立1.cpp源文件。如下:
设置断点,打开菜单“Debug” 下的“Windows”子菜单,选择“Disassembly"。出现如下的反汇编代码:
下面讲解常用的汇编指令。
1.2常用汇编指令
1.2.1堆栈相关指令
push:把一个32位的操作数压入堆栈中。这个操作导致esp被减4。esp被形象地称为栈顶。我们认为顶部是
地址小的区域。那么,压入的堆栈的数据越多,这个堆栈也就越堆越高,esp也就越来越小。在32位平台上,esp每次减少4(字节)。
pop:相反,esp被加4,一个数据出栈。pop的参数一般是一个寄存器,栈顶的数据被弹出到这个寄存器中。
sub:减法。第一个参数是被减数所在的寄存器;第二个参数是减数。(对应的还有add指令。)
add:加法。
ret:返回。相当于跳转回调用函数的地方。(对应的call指令来调用函数,返回到call之后的下一条指令。)
call:调用函数。
call和jmp的不同之处:call指令会把它的下一条指令的地址,然后跳转到它调用的函数的开头处;而单纯的jmp是不会这样做的。同时,ret会自动地弹出返回地址。
注:call的本质相当于push+jmp。ret的本质相当于pop+jmp。
C语言函数的变量保存在栈里。
1.2.2数据传送指令
mov:数据移动。第一个参数是目的,第二个参数是来源。在C语言中相当于赋值号。
xor:异或。这虽然是逻辑运算的指令,但是有趣的是,xor eax,eax这样的操作常用来代替mov eax,0。好处是速度更快,占用字节数更少。
lea:取得地址(第二个参数)后放入到前面的寄存器(第一个参数)中。
注:见到xor eax,eax,应该马上明白这是清零操作。
但实际上,有时候lea用来做和mov同样的事情,比如赋值。看下面一条指令:
lea edi,[ebp-0cch]
方括弧表示存储器,也就是ebp-0cch这个地址所指的存储器的内容。但是lea要求取[ebp-0cch]的地址,那么地址也就是ebp-0cch,这个地址将被放入edi中。换句话说,这等同于:
mov edi,ebp-0cch
但是以上mov指令是错误的,因为mov不支持后一个操作数写成寄存器减去数字,但是lea支持,所以可以用lea来代替它。
为了讲解stos,下面解说前面提到的代码:
stos是串存储指令,它的功能是将eax中的数据放入edi所指的地址中,同时edi会增加4(字节数)。rep使指令重复执行ecx中填写的次数。方括弧表示存储器,这个地址实际上就是edi的内容所指向的地址。这里stos其实对应的是stosd,其它还有stosb、stosw,分别对应于处理4、1、2个字节,这里对堆栈中30h*4(0c0h)个字节初始化为0cch(也就是int 3指令的机器码),这样发生意外时执行堆栈里面的内容会引发调试中断。
1.2.3跳转与比较指令
jmp:无条件跳转。
jg:大于的时候跳转。
jl:小于的时候跳转。
jge:大于等于的时候跳转。
cmp:比较。是jg、jl、jge之类的条件指令的执行条件。
1.3C函数的参数传递过程
函数和堆栈的关系密切,这是因为:我们通过堆栈把参数从外部传入到内部。此外,我们在堆栈中划分区域来容纳函数的内部变量。
调用 push 和 pop 指令的时候,寄存器 esp 用于指向栈顶的位置——栈顶总是栈中地址最小的位置。push 执行的结果,esp 总是减少。pop 则增加。对于C程序默认的调用方式,堆栈总是调用方把参数反序(从右到左)地压入堆栈中,被调用方把堆栈复原。这些参数对齐到机器字长,16位、32位、64位CPU分别对齐到2、4、8个字节。这种调用是C编译器默认的C方式。
C 语言所写的程序中,堆栈用于传递函数参数。这时称为调用栈。 写一个简单的函数如下:
这是标准的 C 函数调用方式.其过程是:
1) 调用方把参数反序的压入堆栈中。
2) 调用函数。
3) 调用者把堆栈清理复原。
这就是C编译器默认的_cdecl方式,而Windows API一般采用的_stdcall则是被调用者恢复堆栈(可变参数函数调用除外)。
至于返回值都是写入eax中,然后返回的。
注:在Windows中,不管哪种调用方式都是返回值放在eax中,然后返回。外部从eax中得到返回值。
_cdecl方式下被调用函数需要做以下一些事情:
1) 保存 ebp。 ebp 总是被我们用来保存这个函数执行之前的 esp 的值。执行完毕之后,我们用 ebp 恢复 esp。同时,调用我们的上层函数也用 ebp 做同样的事情。所以先把 ebp 压入堆栈。返回之前弹出,避免 ebp 被我们改动。
2) 保存 esp 到 ebp 中。 上面两步的代码如下:
3) 在堆栈中腾出一个区域用来保存局部变量。这就是常说的所谓局部变量在栈空间中。方法为把 esp 减少一个数值。这样等于压入了一堆变量。日后要恢复时,只要把 esp 恢复成 ebp 中保存的数据就可以了。
4) 保存 ebx,esi,edi 到堆栈中。函数调用完后恢复。这是一个编程规范。 对应的代码如下:
5) 把局部变量区域初始化成 0cccccccch。0cccccccch 实际是 INT 3指令的机器码。这是一个中断指令
。因为局部变不可能被执行,如果执行了必然程序有错,这时发生中断来提示开发者。这是 VC 编译
DEBUG 版本的特有操作。
相关代码如下:
6) 然后做函数里应该做的事情。参数的获取是 ebp+8 字节为第一个参数,ebp+12 为第二个参数(注意倒序压入),依次加。ebp+4 字节处是 要返回的地址。
7) 恢复 ebx,esi,edi ,esp,ebp,最后返回。 代码如下:
为了简单起见,我的函数没有返回值。如果要返回值,函数应该在返回之前,把返回值放入 eax 中。外
部通过 eax 得到返回值。 所以用 VC2003 编译 Debug 版本,反汇编代码如下:
而这个函数的调用方式是:
这样一来,函数调用的过程就很清楚了。
总结:
EBP在地址高端,ESP在地址低端。函数参数在EBP的正偏移处,函数内部局部变量在EBP的负偏移处。调用函数时,参数是倒序压栈(从右到左)。
函数被调用过程(以myfunction为例):
1)保存调用者的EBP,将ESP赋给EBP;
2)ESP减小一个范围,用来存放函数内部的局部变量;
3)保存EBX, ESI, EDI;
4)堆栈区内的局部变量区初始化为0cch(对应int 3中断),访问不存在的局部变量会引发该中断;
5)EBP+X访问函数参数,EBP-X分配函数局部变量;
6)返回值放在EAX中;
7)恢复EDI, ESI, EBX, EBP;
8)调用者:恢复堆栈,清楚堆栈内部被调用函数的函数参数。