函数调用的栈与帧

  花了两个星期的时间,终于把《深入理解计算机系统》的第三章(程序的机器级表示)看完了,收获不小,尤其是对于函数帧栈的理解。这里简单地对IA-32与x86-64 代码中函数调用的栈与帧作一点总结。

  IA-32程序用栈来支持过程调用。机器用栈来传递过程参数、存储返回信息、保存寄存器用于以后恢复,以及本地存储。为单个过程分配的那部分栈称为栈帧(stack frame)。栈帧可以由两个指针界定,在IA-32中,寄存器%ebp为帧指针,%esp为栈指针。当程序执行时,栈指针可以移动,因此大多数信息的访问都是相对于帧指针的。

  一个简单的整型数交换的函数,C语言源程序代码如下:

void swap(int *a, int *b)
{
    int temp = *a;
    *a = *b;
    *b = temp;
}
 
int main()
{
    int x = 0, y =1;
    swap(&x,&y);
    return 0;
}


  这段代码没有使用任何C/C++语言的库函数,汇编生成的代码阅读起来相对比较容易。

Dump of assembler code for function main:
   0x00001f80<+0>:    push   %ebp                 //将调用者帧指针压栈
   0x00001f81<+1>:    mov    %esp,%ebp            //将当前栈指针赋为帧指针
   0x00001f83<+3>:    sub    $0x18,%esp           //开辟栈空间,共24字节
   0x00001f86<+6>:    lea    -0x8(%ebp),%eax      //取得x的地址
   0x00001f89<+9>:    lea    -0xc(%ebp),%ecx      //取得y的地址
   0x00001f8c<+12>:   movl   $0x0,-0x4(%ebp)      //???
   0x00001f93<+19>:   movl   $0x0,-0x8(%ebp)      //给x赋0
   0x00001f9a<+26>:   movl   $0x1,-0xc(%ebp)      //给y赋1
   0x00001fa1<+33>:   mov    %eax,(%esp)          //将x地址存入%esp指向内存
   0x00001fa4<+36>:   mov    %ecx,0x4(%esp)       //将y地址存入相应内存
   0x00001fa8<+40>:   call   0x1f40 <swap>        //调用swap函数
   0x00001fad<+45>:   mov    $0x0,%eax            //%eax存储main的返回值
   0x00001fb2<+50>:   add    $0x18,%esp           //收回栈空间
   0x00001fb5<+53>:   pop    %ebp                 //返回保存的%ebp
   0x00001fb6<+54>:   ret                         //返回
End of assembler dump.
Dump of assembler code for function swap:
   0x00001f40<+0>:   push   %ebp             //保存调用者帧指针
   0x00001f41<+1>:   mov    %esp,%ebp        //设置当前帧指针
   0x00001f43 <+3>:   sub   $0xc,%esp        //分配栈空间,共12bytes
   0x00001f46<+6>:   mov    0xc(%ebp),%eax  //get b
   0x00001f49<+9>:   mov    0x8(%ebp),%ecx  //get a
   0x00001f4c<+12>:  mov    %ecx,-0x4(%ebp) //sa = a
   0x00001f4f<+15>:  mov    %eax,-0x8(%ebp) //sb = b
   0x00001f52<+18>:  mov    -0x4(%ebp),%eax //get sa
   0x00001f55<+21>:  mov    (%eax),%eax     //get sb
   0x00001f57<+23>:  mov    %eax,-0xc(%ebp) //temp = *sa
   0x00001f5a<+26>:  mov    -0x8(%ebp),%eax //get sb
   0x00001f5d<+29>:  mov    (%eax),%eax     //get*sb
   0x00001f5f<+31>:  mov    -0x4(%ebp),%ecx //get sa
   0x00001f62<+34>:  mov    %eax,(%ecx)      //*sa = *sb, *a = *b
   0x00001f64<+36>:  mov    -0xc(%ebp),%eax  //get temp
   0x00001f67<+39>:  mov    -0x8(%ebp),%ecx  //get sb
   0x00001f6a <+42>:  mov   %eax,(%ecx)      //*sb = temp, *b= *a
   0x00001f6c<+44>:  add    $0xc,%esp        //收回栈空间
   0x00001f6f<+47>:  pop    %ebp              //恢复帧指针
   0x00001f70<+48>:  ret                      //返回调用者
End of assembler dump.


  Main函数与swap函数栈空间分配如下

%ebp->

Saved %ebp

-4

0

-8

X

-12

Y

-16

 

-20

&y

-24

&x

-28

Return address

 

  Swap函数栈空间分配如下

%ebp->

Saved %ebp

-4

Sa

-8

Sb

-12

temp

 

  值得指出的是,main函数栈空间并没有使用完全,所占的32个字节中,有8个字节并没有使用。这是因为gcc坚持一个x86编程指导方针,一个函数所使用的所有栈空间必须是16字节的整数倍。这样,包括saved %ebp的4个字节和返回地址的4个字节,整好是32个字节。同理,swap函数的栈空间则不存在空闲的问题,恰好占16个字节。

  由于编译时没有使用任何优化,两个函数的汇编代码里过度地使用了栈空间,而没有能够完全利用寄存器。IA-32对寄存器的使用惯例是,%eax、%edx和%ecx为调用者保存寄存器。即当过程P调用过程Q时,Q可以覆盖这些寄存器,而不会存坏P所需要的数据。另一方面,%ebx、%esi和%edi则被划分为调用者保存寄存器,这意味着Q必须在覆盖这些寄存器前,先把它们保存到栈中,并在返回前恢复它们。

  假如使用-O1级优化来编译代码,所得到的swap函数的汇编代码将大大简化。Main函数传进来的参数,将不再被保存在栈中,而是直接保存在寄存器%eax和%edx中。

Dump of assembler code for function swap:
   0x00001f60<+0>:  push   %ebp
   0x00001f61<+1>:  mov    %esp,%ebp
   0x00001f63<+3>:  push   %esi
   0x00001f64<+4>:  mov    0x8(%ebp),%eax
   0x00001f67<+7>:  mov    (%eax),%ecx
   0x00001f69<+9>:  mov    0xc(%ebp),%edx
   0x00001f6c<+12>: mov    (%edx),%esi
   0x00001f6e<+14>: mov    %esi,(%eax)
   0x00001f70<+16>: mov    %ecx,(%edx)
   0x00001f72<+18>: pop    %esi
   0x00001f73<+19>: pop    %ebp
   0x00001f74<+20>: ret   
End of assembler dump.

  以上是对IA-32的栈帧结构的一点总结。IA-32的后继版本x86-64对寄存器进行了扩展,寄存器数量增加至16个,每个寄存器长度增加一倍至64位。寄存器的扩展为汇编代码的优化提供了更为广阔的空间。大量的堆栈开销可以省略,而直接用寄存器来存储原本需要存储在栈空间中的局部变量。

  同样用上面swap函数的例子来说明该IA-32与x86-64生成汇编代码的不同。执行gcc –m64 –fomit-frame-pointer –O1 test.c,得到的汇编代码如下所示。

Dump of assembler code for function swap:
0x0000000100000f20 <+0>: mov (%rdi),%eax
0x0000000100000f22 <+2>: mov (%rsi),%ecx
0x0000000100000f24 <+4>: mov %ecx,(%rdi)
0x0000000100000f26 <+6>: mov %eax,(%rsi)
0x0000000100000f28 <+8>: retq
0x0000000100000f29 <+9>: nopl 0x0(%rax)
End of assembler dump.

  这段代码使用了-fomit-frame-pointer编译选项,意思是省略帧指针。《深入理解计算机系统》一书中文版187页中的例子并没这个优化选项,实际上我在MacOS X 10.9与gcc4.6.1测试时,-O1并没有包含-fomit-frame-pointer。

  以上代码与IA-32代码的不同之处在于:

  (1)  寄存器名称的变化,看到了64位寄存器——%rsi,%rdi等;

  (2)  函数调用的参数传递不再依赖于%ebp的位置,在P调用Q前,参数将被事先存在寄存器中,而不是栈中。事实上,x86-64支持最多6个寄存器来传递参数,如果多于6个,还是要利用栈来传递参数,此时多余的参数将通过与 %rsp的相对位置来指定,而不是帧指针。


  从IA-32到x86-64是一个飞跃,x86-64完全兼容IA-32,并且提供了比IA-32更多的寄存器长度和空间。这样内存的寻址范围一下从4GB扩展到了256TB,支持更高精度的运算,拓展的寄存器数量使得函数调用的参数传递变得更为简单和直接。值得一提的是,x86-64最初并不是由IC行业的霸主Intel提出,而是出自追赶者AMD。而Intel自己的64位架构IA-64则由于缺乏对IA-32的兼容,并未被市场接纳。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值