Assemble函数栈帧结构详解

November 1st, 2013 by JasonLe's Tech Leave a reply »

我们在C语言中调用一个函数,直接写出函数名与参数就可以一直没有深入了解这一过程的汇编实现,今天晚上木有妹子,正好仔细研习了这一块知识。
IA32中程序栈的声明是从高向低处生长,也就是说%esp会一直向底地址处生长。我们先看《深入理解计算机系统》上面的一幅图。
Unnamed QQ Screenshot20131101212326

首先我们先声明几个CPU中的寄存器,%ebp %ebx %esp

EAX 是”累加器”(accumulator), 它是很多加法乘法指令的缺省寄存器。
EBX 是”基地址”(base)寄存器, 在内存寻址时存放基地址。
ECX 是计数器(counter), 是重复(REP)前缀指令和LOOP指令的内定计数器。
EDX 则总是被用来放整数除法产生的余数。

比如我们要call一个函数,编译器会先:

pushl  %ebp
movl   %esp, %ebp
pushl   %ebx //返回值
subl   $16, %esp//跳开一段空区域,因为这段空区域放入函数中声明的变量

%ebp 与 %esp是程序的栈底与栈顶,这个相当于一个定界符。%esp会随着临时变量的增加向小地址增加。

递归调用指的是就是将函数连续压栈,一直压到符合退出条件为止。然后连续pop %ebp  pop %ebx  leave,直到调用结束,我们要明白%ebx经常当做返回值使用。

我编写的函数,用gcc -S 生成汇编代码后,会生成很多CFI。
我从网上查了一下一些资料.http://larmbr.me/2013/08/20/x86-assembly-call-frame-and-dwarf-CFI-introduction/

CFI: 调用框架指令
在了解了调用框架之后, 就能明白CFI的作用了。CFI全称是Call Frame Instrctions, 即调用框架指令。

CFI提供的调用框架信息, 为实现堆栈回绕(stack unwiding)或异常处理(exception handling)提供了方便, 它在汇编指令中插入指令符(directive), 以生成DWARF[3]可用的堆栈回绕信息。这里列有gas(GNU Assembler)支持的CFI指令符。

接下来分析上面的例子中出现了几个CFI指令符。

在一个汇编函数的开头和结尾, 分别有.cfi_startproc和.cfi_endproc标示着函数的起止。被包围的函数将在最终生成的程序的.eh_frame段中包含该函数的CFI记录, 详细请看这里。

1 .cfi_def_cfa_offset 8一句。该指令表示: 此处距离CFA地址为8字节。这有两个信息点:

CFA(Canonical Frame Address)是标准框架地址, 它被定义为在前一个调用框架中调用当前函数时的栈顶指针。结合本例看, CFA如图所示:

高地址  :          :
   |   |    42    |
   |   +----------+    |   |    37    |
   |   +----------+    |   | 返回地址  |
   |   +----------+
   |   | 旧的ebp   |
低地址  +----------+ 

至于此处指的是esp寄存器指向的位置。在push %ebp后, esp显然距离CFA为8字节。

2 .cfi_offset 5, -8一句。

把第5号寄存器[4]原先的值保存在距离CFA – 8的位置, 结合上图, 意义明显。

3 .cfi_def_cfa_register 5一句。

该指令是位于movl %esp, %ebp之后。它的意思是: 从这里开始, 使用ebp作为计算CFA的基址寄存器(前面用的是esp)。

4 .cfi_restore 5和.cfi_def_cfa 4, 4这两句结合起来看。注意它们位于leave指令之后。

前一句意思是: 现在第5号寄存器恢复到函数开始的样子。第二句意思则是: 现在重新定义CFA, 它的值是第4号寄存器(esp)所指位置加4字节。这两句其实表述的就是尾声指令所做的事。意义亦很明显。