首先我们先提出几个问题:
-
形参开不开辟空,在哪开辟?
-
形参的入栈顺序?
-
返回值由谁带出?
-
函数调用完成后怎样知道返回到main函数之中?
-
怎样控制调用完成之后,继续执行下一句?
要解决这些问题,我们就要从汇编的角度切入。通过汇编代码能够使我们更加清晰地掌握函数的堆栈调用。 汇编分为两种形式inter x86 (从右向左看) 和 AT&T unix(从左向右看),我们学习的主要是inter x86下的汇编代码。
1、先来了解一下反汇编的一些指令
-
移值:mov
int a = 20;
inter x86 (从右向左看) mov dword ptr [a] , 14h;
AT&T unix(从左向右看)mov 14h , dword ptr [a];
都是指将十六进制的20 放在dword ptr [a]中;
-
移地址:led
led eax ,[ebp-4] 将ebp-4的地址传送到eax中;
-
压栈:push
push 10; 将10压入栈中
-
出栈:pop
pop eax ; ==> eax = pop();
-
累加指令:add
add eax, 4; ==> eax+=4;
-
累减指令:sub
sub eax, 4; ==> eax-=4;
2、寄存器
存储数据:eax、ebx、ecx、edx.
ebp:栈底指针寄存器
exp:栈顶指针寄存器
pc:下一行指令寄存器
看下面这个例子;
//main.c
int sum(int a, int b)
{
int temp = 0;
temp = a + b;
return temp;
}
int main()
{
int a = 10;
int b = 20;
int tmp = 0;
tmp = sum(a, b);
printf("tmp = %d\n", tmp);
return 0;
}
main.c的汇编代码如下,通过反汇编指令分析堆栈调用过程:
1: #include<stdio.h>
2:
3: int sum(int a, int b)
4: { //开辟0x44大小的函数栈桢空间并循环赋值为0xcccccccc
0040D760 push ebp
0040D761 mov ebp,esp
0040D763 sub esp,44h
0040D766 push ebx //三个保存sum函数现场的寄存器ebx、 esi、edi
0040D767 push esi
0040D768 push edi
0040D769 lea edi,[ebp-44h]
0040D76C mov ecx,11h
0040D771 mov eax,0CCCCCCCCh
0040D776 rep stos dword ptr [edi]
5: int temp = 0; //局部变量申请空间并初始化
0040D778 mov dword ptr [ebp-4],0
6: temp = a + b; //将运算结果保存在局部变量中
0040D77F mov eax,dword ptr [ebp+8]
0040D782 add eax,dword ptr [ebp+0Ch]
0040D785 mov dword ptr [ebp-4],eax
7: return temp; //返回局部变量
0040D788 mov eax,dword ptr [ebp-4] //通过寄存器eax返回
8: } //函数栈帧空间的回退
0040D78B pop edi
0040D78C pop esi
0040D78D pop ebx
0040D78E mov esp,ebp //释放sum函数栈桢空间
0040D790 pop ebp //将ebp重新指向main函数栈底,并使得esp向下偏移
0040D791 ret
9:
10: int main()
11: { //开辟0x4c大小的函数栈桢空间并循环赋值为0xcccccccc
00401050 push ebp
00401051 mov ebp,esp
00401053 sub esp,4Ch
00401056 push ebx //三个保存main函数现场的寄存器ebx、 esi、edi
00401057 push esi
00401058 push edi
00401059 lea edi,[ebp-4Ch]
0040105C mov ecx,13h
00401061 mov eax,0CCCCCCCCh
00401066 rep stos dword ptr [edi] //局部变量申请空间并初始化
12: int a = 10;
00401068 mov dword ptr [ebp-4],0Ah
13: int b = 20;
0040106F mov dword ptr [ebp-8],14h
14: int tmp = 0;
00401076 mov dword ptr [ebp-0Ch],0
15: tmp = sum(a, b); //调用sum函数
0040107D mov eax,dword ptr [ebp-8] //压实参
00401080 push eax
00401081 mov ecx,dword ptr [ebp-4]
00401084 push ecx
00401085 call @ILT+0(_sum) (00401005) //跳转到sum函数入口
0040108A add esp,8 //清理实参空间
0040108D mov dword ptr [ebp-0Ch],eax
16:
17: printf("tmp = %d\n", tmp); //调用printf函数
00401090 mov edx,dword ptr [ebp-0Ch]
00401093 push edx
00401094 push offset string "ret = %d\n" (0042201c)
00401099 call printf (004010d0)
0040109E add esp,8
18: return 0;
004010A1 xor eax,eax
19: } //清理主函数栈桢空间
004010A3 pop edi
004010A4 pop esi
004010A5 pop ebx
004010A6 add esp,4Ch //释放main函数栈桢空间
004010A9 cmp ebp,esp
004010AB call __chkesp (00401150)
004010B0 mov esp,ebp
004010B2 pop ebp
004010B3 ret
函数在开辟栈桢空间的时候是通过两个指针即栈顶指针esp和栈底指针ebp来完成的。
总结:开辟过程
-
开辟形参内存并初始化
-
压入下一行指令地址
-
压入ebp(调用方栈底指针的值)
-
开辟局部变量所需要的栈空间并初始化为0xcccc cccc
总结:清栈过程
-
清空开辟局部变量所需要的栈空间 mov esp ebp
-
回退到调用方的栈底pop ebp
-
将下一行指令地址放入pc
-
清理形参内存
了解了开辟和回退的过程,现在来解决刚开始提出的几个问题
形参开不开辟空,在哪开辟?
形参开辟空间,且由调用方开辟。
下图是形参开辟在main函数上的反汇编代码和图示:
-
形参的入栈顺序?
从做向右一次入栈。由图可以看出a 的位置是ebp-4,b的位置为ebp-8,执行到tmp = sum(a,b)时将b 放入eax中,将a放入ecx中。
main函数
-
返回值由谁带出?
0字节<返回值<=4字节 由一个寄存器带出
4字节<返回值<=8字节 由两个寄存器带出
返回值>8字节 通过临时变量带出
这个例子中返回值为4个字节则通过一个寄存器eax返回
-
函数调用完成后怎样知道返回到main函数之中?
通过call指令会把调用方的栈底指针压栈
call指令:
-
先压入下一行指令的地址
-
跳转得到被调用函数
-
怎样控制调用完成之后,继续执行下一句?
会将下一行地址放在pc中
ret指令:有一个pop pc 的过程即pc = pop( );