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