前言
本文的内容来自于《Linux内核分析》MOOC课程。文中以一段简单的C语言代码为例解释了C语言函数调用栈的结构。
实验环境
操作系统Ubuntu 14.04 LTS,gcc版本4.8.4
程序代码
本小节中的代码如下所示:
int g(int x) {
return x + 1;
}
int f(int x) {
return g(x);
}
int main(void) {
return f(1) + 1;
}
生成汇编代码
利用gcc编译main.c生成main.s汇编文件
$ gcc -S -o main.s main.c -m32
生成后的汇编文件中有很多以.开头的symbol,这些symbol都是与链接相关的,不在本文讨论的范围内。在把汇编文件中的链接符号删除后,得到程序执行过程中最核心的汇编代码。
g:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %eax
addl $1, %eax
popl %ebp
ret
f:
pushl %ebp
movl %esp, %ebp
subl $4, %esp
movl 8(%ebp), %eax
movl %eax, (%esp)
call g
leave
ret
main:
pushl %ebp
movl %esp, %ebp
subl $4, %esp
movl $1, (%esp)
call f
addl $1, %eax
leave
ret
汇编代码分析
接下来将对汇编代码的工作过程中堆栈和重要寄存器值的变化做详细的分析。
注意:在x86架构中,栈是负增长的。为了方便讨论,笔者在这里将栈的大小设为0x100个字节。同时,假设代码段的起始地址为0,每一个指令对应的地址为其在上文中汇编代码的行数。
程序的入口是main函数,从第18行开始。
step1
18 pushl %ebp
语义
将调用main的进程的ebp压栈
函数调用栈
地址 | 内容 | 栈指针 |
---|---|---|
0x100 | 调用main函数进程的ebp | esp |
0x0FC |
相关寄存器的值
寄存器 | 值 |
---|---|
eax | 未知 |
eip | 19 |
step2
19 movl %esp, %ebp
语义
将esp的值赋给ebp
函数调用栈
地址 | 内容 | 栈指针 |
---|---|---|
0x100 | 调用main函数进程的ebp | esp ebp |
0x0FC |
相关寄存器的值
寄存器 | 值 |
---|---|
eax | 未知 |
eip | 20 |
step3
20 subl $4, %esp
语义
将esp内的值减4,即栈顶指针增加一个单元
函数调用栈
地址 | 内容 | 栈指针 |
---|---|---|
0x100 | 调用main函数进程的ebp | ebp |
0x0FC | esp |