函数堆栈调用

首先我们先提出几个问题:

  1. 形参开不开辟空,在哪开辟?

  2. 形参的入栈顺序?

  3. 返回值由谁带出?

  4. 函数调用完成后怎样知道返回到main函数之中?

  5. 怎样控制调用完成之后,继续执行下一句?

要解决这些问题,我们就要从汇编的角度切入。通过汇编代码能够使我们更加清晰地掌握函数的堆栈调用。 汇编分为两种形式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来完成的。

总结:开辟过程

  1. 开辟形参内存并初始化

  2. 压入下一行指令地址

  3. 压入ebp(调用方栈底指针的值)

  4. 开辟局部变量所需要的栈空间并初始化为0xcccc cccc

总结:清栈过程

  1. 清空开辟局部变量所需要的栈空间  mov esp ebp

  2. 回退到调用方的栈底pop ebp

  3. 将下一行指令地址放入pc

  4. 清理形参内存

了解了开辟和回退的过程,现在来解决刚开始提出的几个问题

形参开不开辟空,在哪开辟?

形参开辟空间,且由调用方开辟。

下图是形参开辟在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指令:

  1. 先压入下一行指令的地址

  2. 跳转得到被调用函数

  • 怎样控制调用完成之后,继续执行下一句?

会将下一行地址放在pc中

ret指令:有一个pop pc 的过程即pc = pop( );

猜你喜欢

转载自blog.csdn.net/cyy_0802/article/details/83961080
今日推荐