基于最简单的代码示例,一步步分析每一段汇编代码,对于堆栈变化
一、编译C代码
int g(int x) {return x + 3;
}
int f(int x) {return g(x);
}
int main(void) {return f(8) + 1;
}
编译
gcc -S -o main.s main.c -m32
得到汇编代码
g:pushl %ebpmovl %esp, %ebpmovl 8(%ebp), %eaxaddl $3, %eaxpopl %ebpretf:pushl %ebpmovl %esp, %ebpsubl $4, %espmovl 8(%ebp), %eaxmovl %eax, (%esp)call gleaveret
main:pushl %ebpmovl %esp, %ebpsubl $4, %espmovl $8, (%esp)call faddl $1, %eaxleaveret
二、关键信息
%ebp:栈帧基址,用于指向当前函数的栈帧基址,使得函数内部可以方便地访问参数和局部变量。
%esp:栈顶指针,始终指向当前栈顶的位置
%eax:寄存器
leave:恢复调用者的栈帧
movl %ebp, %esp
popl %ebp
ret:将 %eax 中的值作为返回值。
三、压栈出栈详细步骤
第一步:进入main函数
pushl %ebp:将基指%ebp 压入栈,保存上一层栈帧基址
堆栈内容
[调用者的 %ebp]
movl %esp, %ebp:将当前栈顶(%esp)存入 %ebp,设定新的栈帧基址
[调用者的 %ebp]
subl $4, %esp:栈顶指针%esp向下移动 4 字节,为局部变量分配空间
[调用者的 %ebp]
[局部变量空间:4字节]
movl $8, (%esp):将立即数 8 压入栈顶,为 f 函数调用提供参数
[调用者的 %ebp]
[参数 x = 8]
call f:调用 f,请看f的堆栈变化,将返回值存入 %eax,栈在跳转到 f 时变化
addl $1, %eax
leave
ret
第二步:f 函数堆栈变化
pushl %ebp:保存调用者的 %ebp 到栈中
[调用者的 %ebp]
[参数 x = 8]
[main 的 %ebp]
movl %esp, %ebp:设置新的栈帧基址,将%esp赋给%ebp
[调用者的 %ebp]
[参数 x = 8]
[main 的 %ebp]
subl $4, %esp:为局部变量分配 4 字节的栈空间
[调用者的 %ebp]
[参数 x = 8]
[main 的 %ebp]
[局部变量空间:4字节]
movl 8(%ebp), %eax:将%ebp的值 x(即 8)加载到 寄存器%eax
movl %eax, (%esp):将 %eax(x = 8)压入栈,为 g 函数调用提供参数
[调用者的 %ebp]
[参数 x = 8]
[main 的 %ebp]
[局部变量空间]
[参数 x = 8]
call g:调用 g,栈跳转到 g 的堆栈变化,可以获得%eax=11
leave
ret
第三步:g 函数堆栈变化
pushl %ebp:保存调用者的 %ebp
[调用者的 %ebp]
[参数 x = 8]
[main 的 %ebp]
[局部变量空间]
[参数 x = 8]
[f 的 %ebp]
movl %esp, %ebp:设置新的栈帧基址 堆栈内容不变
[调用者的 %ebp]
[参数 x = 8]
[main 的 %ebp]
[局部变量空间]
[参数 x = 8]
[f 的 %ebp]
movl 8(%ebp), %eax:从栈中取出参数 x(8),存入 %eax
addl $3, %eax:将 3 加到 %eax,计算 x + 3 = 8 + 3 = 11
popl %ebp:恢复调用者的 %ebp,清理当前栈帧 %ebp。
[调用者的 %ebp]
[参数 x = 8]
[main 的 %ebp]
[局部变量空间]
[参数 x = 8]
ret:返回调用者,%eax 中的值 11 作为返回值,看回f
第四步:回到 f 函数堆栈变化
Call g :返回值 11 存入 %eax,并释放掉x,恢复调用者的栈帧
[调用者的 %ebp]
[参数 x = 8]
[main 的 %ebp]
[局部变量空间]
leave:恢复调用者的栈帧。
[调用者的 %ebp]
[参数 x = 8]
ret:返回调用者,%eax 中的值 11 作为返回值
第五步:回到 main 函数堆栈变化
call f 返回:f 返回值 11 存入 %eax。
[调用者的 %ebp]
addl $1, %eax:将 1 加到 %eax 中,计算 f(8) + 1 = 11 + 1 = 12。
leave:恢复调用者的栈帧。
堆栈内容为空
ret:返回调用者,%eax 中的值 12 作为程序最终返回值。
最终结果:程序返回值为 12,存储在寄存器 %eax 中。