今天我们来以一个简单的案例来解释c语言对应汇编语言的关系
#include "stdio.h"
long add(long a, long b)
{
long x = a, y = b;
return (x + y);
}
int main(int argc, char* argv[])
{
long a = 1, b = 2;
printf("%d\n", add(a, b));
return 0;
}
编译之后反汇编的代码如下 我们来逐一分析,需要注意我们这里是直接跳过了编译器附加的启动函数 来到了我们自己写的main函数
CPU Disasm
Address Hex dump Command Comments
004010A0 /$ 55 PUSH EBP ; INT StackFrame.main(argc,argv)
004010A1 |. 8BEC MOV EBP,ESP
004010A3 |. 83EC 08 SUB ESP,8
004010A6 |. C745 F8 01000 MOV DWORD PTR SS:[LOCAL.2],1
004010AD |. C745 FC 02000 MOV DWORD PTR SS:[LOCAL.1],2
004010B4 |. 8B45 FC MOV EAX,DWORD PTR SS:[LOCAL.1]
004010B7 |. 50 PUSH EAX ; /b => 2
004010B8 |. 8B4D F8 MOV ECX,DWORD PTR SS:[LOCAL.2] ; |
004010BB |. 51 PUSH ECX ; |a => 1
004010BC |. E8 BFFFFFFF CALL add ; \add
004010C1 |. 83C4 08 ADD ESP,8
004010C4 |. 50 PUSH EAX
004010C5 |. 68 48644100 PUSH OFFSET 00416448 ; /_Format = "%d
"
004010CA |. E8 71FFFFFF CALL printf ; \printf
004010CF |. 83C4 08 ADD ESP,8
004010D2 |. 33C0 XOR EAX,EAX
004010D4 |. 8BE5 MOV ESP,EBP
004010D6 |. 5D POP EBP
004010D7 \. C3 RETN
首先是前三句以及最后的三句 ,这六句在每次进入一个函数时都会出现
PUSH EBP
MOV EBP,ESP
SUB ESP,8
***********省略********
MOV ESP,EBP
POP EBP
RETN
push ebp pop ebp这一对是因为该函数中需要用到ebp寄存器,所以需要先把他原来的值压栈保护起来,等到该函数执行完毕时再返回
除了ebp之外,如果函数内还有用到其他寄存器比如eax ebx 也会出现类似的
开头先push eax 最后 pop eax 弹出eax
那么既然函数里需要用到ebp,那么ebp到底有什么用呢?
ebp其实是一个局部变量的基地址
我们可以看到
push ebp之后执行了一句mov ebp,esp
而esp是栈指针,所以此时ebp也指向了栈指针,而整个函数的局部变量,也是全部保存在栈中的,所以ebp其实是为了方便指向局部变量的一个寄存器
因为ebp的值在整个函数中都不会改变,所以此时引用栈中的局部变量就十分轻松。
接下来 sub esp,8是为了给局部变量留出空间
即ebp和esp之间存放局部变量
接下来
004010A6 |. C745 F8 01000 MOV DWORD PTR SS:[LOCAL.2],1
004010AD |. C745 FC 02000 MOV DWORD PTR SS:[LOCAL.1],2
就对应c语言中的 long a = 1, b = 2;
local.2 local.1即为ebp-8 ebp-4
然后紧跟的 下面四句
004010B4 |. 8B45 FC MOV EAX,DWORD PTR SS:[LOCAL.1]
004010B7 |. 50 PUSH EAX ; /b => 2
004010B8 |. 8B4D F8 MOV ECX,DWORD PTR SS:[LOCAL.2] ; |
004010BB |. 51 PUSH ECX ; |a => 1
这四句 即为传递参数给函数 首先先把两个局部变量分别放到eax ecx,然后再Push到栈中,为什么不能直接push到栈中而用寄存器中转呢,很简单,因为x86cpu只支持这样的指令
接下来就是调用add方法了 然后我们进入add方法内部看一看
004010BC |. E8 BFFFFFFF CALL add ; \add
我们可以注意到 之前所说的那6句又重复出现了 这种调用可以无限嵌套下去直到爆栈
CPU Disasm
Address Hex dump Command Comments
00401080 /$ 55 PUSH EBP ; INT StackFrame.add(a,b)
00401081 |. 8BEC MOV EBP,ESP
00401083 |. 83EC 08 SUB ESP,8
00401086 |. 8B45 08 MOV EAX,DWORD PTR SS:[ARG.1]
00401089 |. 8945 FC MOV DWORD PTR SS:[LOCAL.1],EAX
0040108C |. 8B4D 0C MOV ECX,DWORD PTR SS:[ARG.2]
0040108F |. 894D F8 MOV DWORD PTR SS:[LOCAL.2],ECX
00401092 |. 8B45 FC MOV EAX,DWORD PTR SS:[LOCAL.1]
00401095 |. 0345 F8 ADD EAX,DWORD PTR SS:[LOCAL.2]
00401098 |. 8BE5 MOV ESP,EBP
0040109A |. 5D POP EBP
0040109B \. C3 RETN
add方法没什么好解释的,和上面一样 下面四句对应c语言中的 long x = a, y = b; 即把形参分别赋值给局部变量 形参是之前已经压栈进来的 所以去栈对应的位置取就行
00401086 |. 8B45 08 MOV EAX,DWORD PTR SS:[ARG.1]
00401089 |. 8945 FC MOV DWORD PTR SS:[LOCAL.1],EAX
0040108C |. 8B4D 0C MOV ECX,DWORD PTR SS:[ARG.2]
0040108F |. 894D F8 MOV DWORD PTR SS:[LOCAL.2],ECX
下面两句完成了实际的加法功能,首先把局部变量放入寄存器中,然后再用寄存器做加法,为什么不能直接对两个内存单元做加法呢?和上面一样,不支持这种指令,
加法只能是先取内存单元到寄存器中,再对寄存器做加法
0401092 |. 8B45 FC MOV EAX,DWORD PTR SS:[LOCAL.1]
00401095 |. 0345 F8 ADD EAX,DWORD PTR SS:[LOCAL.2]
接下来三句之前解释过了,不过这里注意一个细节,就是加法完成后结果值保存在了eax中,这是一个默认的返回值寄存器,即调用方法后结果值默认保存在eax中
不过对于有多返回值的语言比如lua,就不太清楚会怎么样了
接下来返回到主函数中 然后紧跟着执行了一句
004010C1 |. 83C4 08 ADD ESP,8
这是什么意思呢?其实这是一种调用约定,因为我们之前调用函数时压入了两个参数,而函数调用完成后栈指针应该恢复到原来位置,而这种是一种调用者恢复栈指针的做法,也可以在函数里面恢复栈指针
接下来四句 还是同样 先进行压栈 然后call 返回之后进行清栈 printf函数内部我们就不深究了
004010C4 |. 50 PUSH EAX
004010C5 |. 68 48644100 PUSH OFFSET 00416448
004010CA |. E8 71FFFFFF CALL printf
004010CF |. 83C4 08 ADD ESP,8
最后一句值得注意的就是这个了,前面说过 eax是默认存放返回值的一个寄存器,而我们的main函数其实是由别的启动函数调用的 所以我们要返回值给他
return 0就表示程序正常结束
004010D2 |. 33C0 XOR EAX,EAX
然后就是还原esp 返回到启动函数了