x86函数调用过程与栈帧

x86函数调用过程与栈帧

x86与x86-64在函数调用约定上有相当的不同,因此分开来讲

栈帧(stack frame)

先说一下栈帧的概念
函数每次被调用时,要在调用栈(call stack)上占用一段空间,
在这段空间上保存调用者栈帧的基址(ebp)、本函数的局部变量、调用其他函数时的返回地址,
并在需要时保存调用者使用的寄存器值,
被调函数结束后esp上移表示释放这段空间,然后回到调用者的占用的空间与代码位置继续执行,
函数运行阶段在调用栈上占用的这段空间就叫做栈帧,是编译原理运行时空间组织中活动记录(activation record)的一种实现

栈帧主要通过 ebp、esp 两个寄存器维护,ebp 始终指向栈底,esp 始终指向栈顶

每个函数被调用时执行下面两条命令

pushl	%ebp			; ebp入栈,保存调用者的栈帧基址,以便返回
movl	%esp, %ebp		; 将当前 esp 的位置作为当前栈帧的基址

这样在当前栈帧向上一栈帧退回时,只需要取出之前压栈的基址
另一方面,调用过程的指令 call

call a_func

会将 call 指令的下一条指令地址压栈,a_func 函数返回时执行指令

leave		; 相当于 movl %ebp %esp   popl %ebp
ret			; 相当于 popl %eip, 返回到 call 压栈保存的地址, 即调用 a_func 的函数中

这样被调函数返回调用函数前就可以将 ebp、esp 置回调用函数的栈帧位置
并返回 call 指令的下一条指令执行


此外,在 call 指令前,主调函数会将被调函数的参数保存到栈上
因此栈帧的图像如下图所示
在这里插入图片描述
注意几乎所有的机器与操作系统上栈都是由高地址向低地址生长的

栈边界的字节对齐


在现代处理器中,GCC会将堆栈默认对齐为16字节对齐

因为 SSE2 指令集(Streaming SIMD Extensions,单指令多数据扩展指令集)具有16字节大小的和 XMM 寄存器,

因此,在进行函数调用时,它会自动对齐到16个字节,而在函数外部保持为 8 字节对齐

常常可以见到 andl $-16, %esp 或者 andl 0xFFFFFFF0, %esp,即是 esp 向下移动到 16 字节对齐处


栈帧示例

下面是用 gcc -m32 -S hello.c命令编译出的 hello.s
hello.c 文件内容如下

#include<stdio.h>

int func(int n){
	int i, res = 1;
	for(i = 2;i<=n;i++){
		res *= i;
	}
	return res;
}

int main(){
	int n;
	scanf("%d", &n);
	printf("%d", func(n));
}

我们来看一下 hello.s 里的函数调用过程

	.file	"hello.c"
	.text
	.globl	_func
	.def	_func;	.scl	2;	.type	32;	.endef
_func:
	pushl	%ebp			; 保存上个栈帧的 ebp
	movl	%esp, %ebp		; 设置 ebp 为当前栈帧的基址
	subl	$16, %esp		; 为栈帧分配 16 Byte 的空间
	movl	$1, -8(%ebp)	; 在 ebp-8 的位置存放 1, res
	movl	$2, -4(%ebp)	; 在 ebp-4 的位置存放 2, i
	jmp	L2					; 转到L2
L3:
	movl	-8(%ebp), %eax	; eax = res
	imull	-4(%ebp), %eax	; eax = res * i
	movl	%eax, -8(%ebp)	; res = eax
	addl	$1, -4(%ebp)	; i++
L2:
	movl	-4(%ebp), %eax	; 将 i 读入eax
	cmpl	8(%ebp), %eax	; 比较 i 与 n 的大小
	jle	L3					; i<n goto L3
	movl	-8(%ebp), %eax	; 将res放入eax 作为返回值
	leave					; 恢复栈帧指针
	ret						; 返回到main
	.def	___main;	.scl	2;	.type	32;	.endef
	.section .rdata,"dr"
LC0:
	.ascii "%d\0"
	.text
	.globl	_main
	.def	_main;	.scl	2;	.type	32;	.endef
_main:
	pushl	%ebp			;
	movl	%esp, %ebp		;
	andl	$-16, %esp		; 将 esp 下移到16字节对齐处
	subl	$32, %esp		; 为栈帧分配 32 字节的空间
	call	___main			;
	leal	28(%esp), %eax	; eax = esp+28
	movl	%eax, 4(%esp)	; esp+4 处存放, 可以看出 esp+28 处存放变量 n
	movl	$LC0, (%esp)	; esp 处存放“%d”
	call	_scanf			;
	movl	28(%esp), %eax	; eax = n
	movl	%eax, (%esp)	; esp 处存放 n
	call	_func			;
	movl	%eax, 4(%esp)	; func 返回值放在 esp+4 处
	movl	$LC0, (%esp)	; esp 处存放“%d”
	call	_printf			;
	leave					;
	ret						;
	.ident	"GCC: (tdm64-1) 4.9.2"
	.def	_scanf;	.scl	2;	.type	32;	.endef
	.def	_printf;	.scl	2;	.type	32;	.endef

结构体变量和结构体指针作为参数

在这里插入图片描述
该代码对应的x86汇编是

_main:
	pushl	%ebp
	movl	%esp, %ebp
	andl	$-16, %esp
	subl	$48, %esp
	call	___main
	movl	$1, 24(%esp)
	movl	$2, 28(%esp)
	movl	$3, 32(%esp)
	movl	$4, 36(%esp)
	movl	$5, 40(%esp)
	movl	$6, 44(%esp)
	leal	36(%esp), %eax			; 从左至右
	movl	%eax, 12(%esp)			; 结构体 {4,5,6} 指针压栈
	movl	24(%esp), %eax			;
	movl	%eax, (%esp)			; 1,2,3 分别压栈
	movl	28(%esp), %eax			;
	movl	%eax, 4(%esp)			; 2
	movl	32(%esp), %eax			;
	movl	%eax, 8(%esp)			; 3
	call	_print
	movl	$0, %eax
	leave
	ret

将代码稍作修改
在这里插入图片描述

_main:
	pushl	%ebp
	movl	%esp, %ebp
	andl	$-16, %esp
	subl	$32, %esp
	call	___main
	movl	$1, 16(%esp)
	movl	$2, 20(%esp)
	movl	$3, 24(%esp)
	leal	16(%esp), %eax			; 取得t的地址放入eax
	movl	%eax, 28(%esp)			; t的地址放入指针 esp+28 处 即指针p
	movl	28(%esp), %eax			;
	movl	%eax, 12(%esp)			; 将指针p压栈
	movl	16(%esp), %eax			; 1,2,3 分别压栈
	movl	%eax, (%esp)			;
	movl	20(%esp), %eax			;
	movl	%eax, 4(%esp)			;
	movl	24(%esp), %eax			;
	movl	%eax, 8(%esp)			;
	call	_print
	movl	$0, %eax
	leave
	ret

可以看出,结构体变量作为参数时,会将结构体拆开,将每个成员按成员顺序压栈
而结构体指针就直接作为参数


结构体变量和结构体指针作为返回值

在这里插入图片描述
以上代码生成的x86汇编如下

_getType:
	pushl	%ebp				; pushl ebp 后, esp 下移 8,
	movl	%esp, %ebp			; 没有开辟栈帧
	movl	8(%ebp), %eax		; 8(%ebp) 存放的值是 _main 中的 16(%esp),即传递过来的参数
	movl	$1, (%eax)			; 以8(%ebp)为基址, 依次填充1,2,3, 
	movl	8(%ebp), %eax		;
	movl	$2, 4(%eax)			;
	movl	8(%ebp), %eax		;
	movl	$3, 8(%eax)			;
	movl	8(%ebp), %eax		; 返回了结构体变量的地址
	popl	%ebp
	ret

_getTypePointer:
	pushl	%ebp
	movl	%esp, %ebp
	subl	$40, %esp
	movl	$12, (%esp)			; 结构体字节数作为 _malloc 的参数
	call	_malloc
	movl	%eax, -12(%ebp)		; _malloc 返回值存放在指针t中
	movl	-12(%ebp), %eax		; 以t为基址, 填充4,5,6
	movl	$4, (%eax)
	movl	-12(%ebp), %eax
	movl	$5, 4(%eax)
	movl	-12(%ebp), %eax
	movl	$6, 8(%eax)
	movl	-12(%ebp), %eax		; 返回t
	leave
	ret

_main:
	pushl	%ebp
	movl	%esp, %ebp
	andl	$-16, %esp
	subl	$32, %esp
	call	___main
	leal	16(%esp), %eax
	movl	%eax, (%esp)		; 将 16(%esp) 作为参数, 传递给 _getType
	call	_getType
	call	_getTypePointer
	movl	%eax, 28(%esp)		; 返回的指针保存在 28(%esp) 处, 即变量 tp
	movl	$0, %eax
	leave
	ret

可以看出,
函数结构体时,其空间在调用者的栈帧上开辟,
并且调用者将其地址作为参数传递给被调函数,
同时被调函数也返回这个地址,即结构体变量的指针

函数返回指针时,就是普通的返回变量


进一步了解:字节对齐

2019/12/20

posted @ 2019-12-20 23:29  kafm  阅读(985)  评论(1编辑  收藏  举报