C语言过程(函数)的机器级表示

     一个函数调用包括将数据(包括参数和返回值)和控制从代码一部分传到另一部分。还包括对函数内局部变量分配空间,并在退出时释放空间。  其中,转移控制到过程 和 从过程转移出控制——使用指令;局部变量的分配和释放通过 程序栈 来实现。

1.栈帧结构

   栈由高地址向低地址方向增长。对单个过程分配的栈称为 栈帧。以两个指针来界定:帧指针%ebp和栈指针%esp.栈指针是不断变化的,所以大多数信息基于帧指针%ebp.(注意在我的电脑上,帧指针是%esp,所以在汇编时总是由 movl 8(%esp) %eax来得到参数)。

image

   从上图的栈帧结构中看到,假设P调用Q。

  • P栈帧部分参数为传入Q的参数;
  • P的返回地址形成P栈帧的末尾;
  • Q在%ebp+4+4i的位置上得到其参数;
  • 栈向低地址增长,故栈指针减小——分配空间;栈指针增大——释放空间。

2.转移控制

一个简单的sum调用函数:

int accum = 0;

int sum(int x, int y)
{
    int t = x + y;
    accum += t;
    return t;
}

int main()
{
    return sum(1,3);
}

该函数使用gcc –o sum sum.c –O1得到可执行文件sum。对该可执行文件使用objdump –d sum 得到反汇编代码如下:

080483b4 <sum>:
 80483b4:	8b 44 24 08          	mov    0x8(%esp),%eax
 80483b8:	03 44 24 04          	add    0x4(%esp),%eax
 80483bc:	01 05 18 a0 04 08    	add    %eax,0x804a018
 80483c2:	c3                   	ret    

080483c3 <main>:
 80483c3:	83 ec 08             	sub    $0x8,%esp
 80483c6:	c7 44 24 04 03 00 00 	movl   $0x3,0x4(%esp)
 80483cd:	00 
 80483ce:	c7 04 24 01 00 00 00 	movl   $0x1,(%esp)
 80483d5:	e8 da ff ff ff       	call   80483b4 <sum>
 80483da:	83 c4 08             	add    $0x8,%esp
 80483dd:	c3                   	ret  

【汇编的一个习惯技巧】

对下面的一个代码片段:

	call next
next:
	popl %eax

这是一个汇编代码的习惯用法,结果是把popl指令地址放入%eax中——将程序计数器值放入整数寄存器的唯一方法。

这不是一个过程调用,因为跳转的结果与指令顺序相同:

执行call next前,PC设为popl指令的地址(PC始终设置为下条指令地址);

执行call next后,%esp被设置为popl指令地址(因为它是调用者的返回地址);程序同时也跳转到了popl指令,(此时帧指针和栈指针是重合的,栈指针%esp只有在声明新局部变量时会增大,所以%esp还存储的是popl的地址),根据: %eax = M[R[%esp]],所以popl指令地址放入了%eax中

3.寄存器使用惯例

寄存器组是被所有过程共享的资源,但同一时刻只有一个过程激活,所以需要保证被调用者不会影响调用者在寄存器中的值。

将%eax, %edx, %ecx作为调用者保存寄存器,被调用者可以覆盖这些寄存器;

将%ebx, %ebi, %edi作为被调用者保存寄存器,需要在过程开始前pushl,在过程结束后popl到寄存器。

4.过程示例

int swap_add(int *xp, int *yp)
{
    int x = *xp;
    int y = *yp;
    *xp = y;
    *yp = x;
    return x+y;
}

int caller()
{
    int arg1 = 534;
    int arg2 = 1057;
    int sum = swap_add(&arg1, &arg2);
    return sum;
}

汇编代码为:

swap_add:
.LFB0:
	.cfi_startproc
	pushl	%ebx
	.cfi_def_cfa_offset 8
	.cfi_offset 3, -8
	movl	8(%esp), %ebx
	movl	12(%esp), %ecx
	movl	(%ebx), %edx
	movl	(%ecx), %eax
	movl	%eax, (%ebx)
	movl	%edx, (%ecx)
	addl	%edx, %eax
	popl	%ebx
	.cfi_def_cfa_offset 4
	.cfi_restore 3
	ret
	.cfi_endproc
caller:
.LFB1:
	.cfi_startproc
	subl	$24, %esp
	.cfi_def_cfa_offset 28
	movl	$534, 16(%esp)
	movl	$1057, 20(%esp)
	leal	20(%esp), %eax
	movl	%eax, 4(%esp)
	leal	16(%esp), %eax
	movl	%eax, (%esp)
	call	swap_add
	addl	$24, %esp
	.cfi_def_cfa_offset 4
	ret
	.cfi_endproc

对caller栈帧,分配如下:

共32个字节:

+24  (一个空闲位,在书上该位用来存放了“保存的%ebp")

+20 arg2

+16 arg1

……

+8

+4 &arg2

&arg1(%esp保存该地址)

返回地址(from ret)

    对swap_add栈帧,%esp从上面的返回地址后面的地址开始,由于首先pushl %edx, %esp变为下一个地址:

……

&arg1

返回地址(call调用会存储该地址)

Initial %ebx(%esp保存该地址,由于没有”保存的%ebp“,所以该位置就类似于书上的%ebp位置。)

所以可以从%esp+8处取得参数值,注意此处与书上有所不同,因为一直没有用到%ebp帧地址,书上使用%edp帧地址,所以在每次初始部分多了:

pushl %ebp

movl %esp %ebp  (这一步结束,就将%ebp指向了”保存的%ebp“的地址。)

在结束部分,要恢复%ebp的值:

popl %ebp

ret

练习3.32: 不论从书上还是实际试验都可以看到,前面位置的参数里%ebp越近(%ebp+4+4i为第i个参数的地址)。

练习3.33:不要犯下面三个错误:(1)注意地址相减表示由高位到低位的扩展;(2)注意做十六进制相加(每个寄存器4字节);(3)C对应12,E对应14.

5.递归过程

栈规则的关键是:每次函数调用它都会保存自己的私有信息(包括返回地址、栈指针%ebp、被调用者保存寄存器值%ebp等,如果需要还有其他局部变量)。

posted @ 2013-01-04 23:22  dandingyy  阅读(2879)  评论(0编辑  收藏  举报