汇编语言基础之五- 一个源代码与反汇编代码对照的例子
如下的C++代码,对应的汇编代码会是什么样子呢?
#include <stdio.h> int Addemup(int,int); void main(void) { int x = 5; int y = 10; int z = 0; z = Addemup(x,y); printf("z= %i\n",z); } int Addemup(int a, int b) { int c = 0; c=a+b; return(c); }
1: #include <stdio.h> 2: 3: int Addemup(int,int); 4: 5: void main(void) 6: { addemup!main: 00401000 55 push ebp ; save base pointer 00401001 8bec mov ebp,esp ; set stack pointer 00401003 83ec0c sub esp,0xc ; make room for locals //在这一段中,我们先保存了栈底指针(ebp),以便稍后恢复它。 //之后让栈基指向栈顶,把原来的栈顶作为新栈底了,形象点的说,就是栈向上移动了原来栈长度的一段距离。 //在这里,我们可以看出,若要访问函数参数,那么采用的相对于ebp的位移就是正的, //而函数的局部变量的位移就是负的。 7: int x = 5; 8: int y = 10; 9: int z = 0; 00401006 c745fc05000000 mov dword ptr [ebp-0x4],0x5 ; local x = 5 0040100d c745f80a000000 mov dword ptr [ebp-0x8],0xa ; local y = 10 00401014 c745f400000000 mov dword ptr [ebp-0xc],0x0 ; local z = 0 //ptr指令是修改属性运算符,用来明确指出变量、标号或地址表达式的类型属性(只在所在的指令内有效)。 //类型放在PTR 之前,可以是BYTE、WORD、DWORD、NEAR、FAR。 //这里的操作是将x5这个操作数扩展成dword存入地址为[ebp-0x4]的内存中 10: 11: z = Addemup(x,y); 0040101b 8b45f8 mov eax,[ebp-0x8] ; load eax with y 0040101e 50 push eax ; push y on stack 0040101f 8b4dfc mov ecx,[ebp-0x4] ; load ecx with x 00401022 51 push ecx ; push x on stack 00401023 e81b000000 call addemup!Addemup ; call Addemup 00401028 83c408 add esp,0x8 ; fixup stack for args 0040102b 8945f4 mov [ebp-0xc],eax ; z returned in eax //这里是Addemup函数的调用部分。先将x,y压入栈中,作为调用的参数。 //压入了两个dword型参数,每个dword占4个字节,所以栈顶指针在压栈之后向上移动了8个字节. //调用结束之后,指示栈顶的esp被加回了8个字节,也就是恢复了栈的状态。 //这里注意,一般函数调用的返回值都是放在eax之中的, //所以将eax的值拷贝到[ebp-0xc]中完成的就是给z赋予函数返回结果的动作。 //可以看出,这里在函数调用结束后,去掉参数的栈顶的恢复工作是由调用者完成的。 12: 13: printf("z= %i\n",z); 0040102e 8b55f4 mov edx,[ebp-0xc] ; load edx with z 00401031 52 push edx ; push z on the stack 00401032 6830704000 push 0x407030 ; push ptr to “z=%i\n” 00401037 e822000000 call addemup!printf ; call printf 0040103c 83c408 add esp,0x8 ; fix stack for args //这里是一个对printf函数的调用,过程与上面的函数调用一样。 //1.先将参数压栈; 2.保存当前栈底位置; 3.调用函数; 4.恢复栈顶位置 14: } 0040103f 8be5 mov esp,ebp ; restore stack ptr 00401041 5d pop ebp ; restore base ptr 00401042 c3 ret ; return //这里还是main函数。函数的调用者负责调整栈顶指针,被调用的函数体要将栈顶,栈底都恢复到被调用之前的状态。 15: 16: int Addemup(int a, int b) 17: { addemup!Addemup: 00401043 55 push ebp ; save base pointer 00401044 8bec mov ebp,esp ; set stack pointer 00401046 51 push ecx ; make room for local //1. 保存栈底,2. 设置栈底,3. 修改栈顶,为本地变量分配空间 18: int c = 0; 19: 20: c = a + b; 00401047 c745fc00000000 mov dword ptr [ebp-0x4],0x0 ; local c = 0 0040104e 8b4508 mov eax,[ebp+0x8] ; set eax to local a 00401051 03450c add eax,[ebp+0xc] ; eax = eax + local b 00401054 8945fc mov [ebp-0x4],eax ; local c = a + b //eax是累加器 21: return(c); 00401057 8b45fc mov eax,[ebp-0x4] ; set eax to result c //eax被用来返回值 22: } 0040105a 8be5 mov esp,ebp ; restore stack ptr 0040105c 5d pop ebp ; restore base ptr 0040105d c3 ret ; return to caller //恢复栈顶,恢复栈底
总结一下函数调用的过程中,栈的运作吧。该例子中使用的调用约定是CDECL,后面的文章会讲到。
调用者负责的部分:
阶段 | 动作 | 细节 |
调用之前(状态一) | 无 | 现在栈拥有一个调用函数之前的状态。假设ebp=a; esp=b; |
准备工作 | 将所有参数压入栈 | 随着压栈的动作,栈顶指针向低地址端移动。esp=b-x。(状态二) |
调用函数 | 无 | 见下表 |
收尾工作 | 修改栈顶指针 | 恢复到状态一。esp=b, ebp=a |
被调用者负责的部分:
阶段 | 动作 | 细节 |
初进函数(状态二) | 无 | 现在栈拥有一个状态。假设ebp=a; esp=b-x; |
初始化 | 1. 保存栈底,2. 设置栈底,3. 修改栈顶,为本地变量分配空间 | ebp=b-x; esp=b-x-y;值a在栈中。(状态三) |
执行函数体 | 无 | |
收尾工作 | 1.恢复栈顶,2.恢复栈底 | 恢复到状态二。esp=b-x; ebp = a;(状态二) |
表格注:
x是参数所占的空间
y是局部变量所占的空间
有点麻烦,要多想几次才能记得下来。不过相信汇编代码多看一些,见得多了自然也就烂熟于心了。
练习:
if(i == 0) goto Label1;
C代码:
对应的汇编代码:
mov eax, [ebp – 4] ;这里假设i是第一个局部变量,如果是第一个参数,那应该是[ebp + 8]
cmp eax, 0x0
jz Label1
问题:
为什么代码中第七行下面的一句push ecx的目的是make room for local? 如何做到的?
答:注释错误。PUSH ECX的目的是为了保存ECX的值。