C语言是面向过程的计数机语言,其本质就是函数的调用,从汇编的角度深层次剖析函数之间的调用关系,将对C语言的理解更上一层楼。
先来看一段代码
#include<stdio.h>
int myfun(int x, int y)
{
int z = x + y;
return z;
}
int main()
{
int a = 0xAAAAAAAA;
int b = 0xBBBBBBBB;
int c = myfun(a, b);
printf("i am back \n");//查看调用函数是否返回
printf("ret=%d\n", c);
return 0;
}
这个实例实现了myfun()
函数的调用,接下来分析myfun
是如何被调用的。
首先转到反汇编(我使用的工具是VS2013)
int main()
9: {
010613C0 push ebp
010613C1 mov ebp,esp
010613C3 sub esp,0E4h
010613C9 push ebx
010613CA push esi
010613CB push edi
010613CC lea edi,[ebp+FFFFFF1Ch]
010613D2 mov ecx,39h
010613D7 mov eax,0CCCCCCCCh
010613DC rep stos dword ptr es:[edi]
10: int a = 0xAAAAAAAA;
010613DE mov dword ptr [ebp-8],0AAAAAAAAh
11: int b = 0xBBBBBBBB;
010613E5 mov dword ptr [ebp-14h],0BBBBBBBBh
12: int c = myfun(a, b);
010613EC mov eax,dword ptr [ebp-14h]
010613EF push eax
010613F0 mov ecx,dword ptr [ebp-8]
010613F3 push ecx
010613F4 call 01061096
即看到的是下面三行汇编,其作用是为main
开辟栈桢,这里姑且将它命名为main
,ebp
是栈底,esp
是栈顶,自栈顶底向栈顶生长,push
是压栈指令,作用是将指令压入栈,压入都是从栈顶压入。
010613C0 push ebp
010613C1 mov ebp,esp
010613C3 sub esp,0E4h
main中int a
,int b
的初始化,[ebp-8]
,ebp
是栈底,-8自下初始化a,同样,[ebp-14h]
在ebp
向下14h初始化。
10: int a = 0xAAAAAAAA;
010613DE mov dword ptr [ebp-8],0AAAAAAAAh
11: int b = 0xBBBBBBBB;
010613E5 mov dword ptr [ebp-14h],0BBBBBBBBh
实参到形参,从上一段汇编可以看到,[ebp-14h]-->b
,[ebp-8]-->a
,而先入栈的是b,最后才是a。即调用函数中临时变量的建立是从右往左的,这里从原理上直接体现了出来
12: int c = myfun(a, b);
010613EC mov eax,dword ptr [ebp-14h]
010613EF push eax
010613F0 mov ecx,dword ptr [ebp-8]
010613F3 push ecx
函数临时变量已经建立好,call
语句将计算机要执行的命令从当前的main
跳转至myfun
,其本质是将目标函数的地址覆盖到EIP
中(EIP:程序指针,用来存放正在执行的指令的下一条指令的地址)
010613F4 call 01061096
010613F9 add esp,8
call “0106096”
(学过微机原理我们知道call不能直接调用子程序,所以call了jmp间接跳转)这个地址就是用来覆盖EIP
的地址,清楚的看到,程序跳转到了“0106096
”这个地址,而且图右边的EIP也将指令变为“0106096” 值得注意的是这条指令的地址010613F9 add esp,8
,010613F9
在下方的内存中查看栈顶esp
的写入情况,可以看到010613F9
已经被push
进去了,至于为什么压入这条指令其实显而易见,call
将EIP
中当前正在执行指令的下一条指令地址覆盖为010613F9
,而函数的调用最终会结束并回到原函数继续执行,所以必须把指令地址保存起来。
进去到函数之后
3: int myfun(int x, int y)
4: {
01061470 push ebp
01061471 mov ebp,esp
01061473 sub esp,0CCh
01061479 push ebx
0106147A push esi
0106147B push edi
0106147C lea edi,[ebp+FFFFFF34h]
01061482 mov ecx,33h
01061487 mov eax,0CCCCCCCCh
0106148C rep stos dword ptr es:[edi]
5: int z = x + y;
0106148E mov eax,dword ptr [ebp+8]
01061491 add eax,dword ptr [ebp+0Ch]
01061494 mov dword ptr [ebp-8],eax
6: return z;
01061497 mov eax,dword ptr [ebp-8]
7: }
0106149A pop edi
0106149B pop esi
0106149C pop ebx
0106149D mov esp,ebp
0106149F pop ebp
010614A0 ret
和主函数一样,首先开辟栈桢
01061470 push ebp
01061471 mov ebp,esp
01061473 sub esp,0CCh
当前栈底是接着esp
之前push010613F9
开辟的,所以ebp到010613F9
为ebp+4(地址int)
,而010613F9
下是a(a是int再加4)
,所以ebp+8
是a,b在a下,即ebp+och(12)
,add
是对取回的ab求和
0106148E mov eax,dword ptr [ebp+8]
01061491 add eax,dword ptr [ebp+0Ch]
ebp-8
自然是进入到栈桢空间,将eax
内的a+b存入
mov dword ptr [ebp-8],eax
又将a+b的结果存入eax
用于返回值
mov eax,dword ptr [ebp-8]
函数的调用大致完成,撤销栈桢
0106149D mov esp,ebp
这里pop
弹出函数一开始push
的ebp
地址,也就是主函数时的栈底
0106149F pop ebp
程序重新回到主函数,之前被push
的010613F9
重新回到EIP中,到此,函数的调用完成。