zoukankan      html  css  js  c++  java
  • 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"
    	.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

  • 相关阅读:
    专用学习笔记
    百度地图API试用--(初次尝试)
    学习进度条
    AAAA
    HBase集成(准备篇)
    软件工程学期总结
    【操作系统】实验四 主存空间的分配和回收
    《构建之法》8、9、10章
    金融计算器app的下载情况
    操作系统 实验三 进程调度模拟程序
  • 原文地址:https://www.cnblogs.com/kafm/p/12721789.html
Copyright © 2011-2022 走看看