前言
本文的内容来自于
《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
相关寄存器的值
寄存器 |
值 |
eax
未知
eip
21
step4
21
movl $1, (%esp)
语义
将1(调用函数f的参数)赋给esp所指向的内存地址
函数调用栈
地址 |
内容 |
栈指针 |
0x100
调用main函数进程的ebp
ebp
0x0FC
1
esp
相关寄存器的值
寄存器 |
值 |
eax
未知
eip
22
step5
22
call f
语义
调用函数f,所做的工作包括将eip压栈,并将eip指向f的起始地址
函数调用栈
地址 |
内容 |
栈指针 |
0x100
调用main函数进程的ebp
ebp
0x0FC
1
0x0F8
23
esp
相关寄存器的值
寄存器 |
值 |
eax
未知
eip
9
step6
9
pushl %ebp
语义
将main函数的ebp压栈
函数调用栈
地址 |
内容 |
栈指针 |
0x100
调用main函数进程的ebp
ebp
0x0FC
1
0x0F8
23
0x0F8
0x100
esp
相关寄存器的值
寄存器 |
值 |
eax
未知
eip
10
step7
10
movl %esp, %ebp
语义
将esp的值赋给ebp
函数调用栈
地址 |
内容 |
栈指针 |
0x100
调用main函数进程的ebp
0x0FC
1
0x0F8
23
0x0F4
0x100
esp ebp
相关寄存器的值
寄存器 |
值 |
eax
未知
eip
11
step8
11
subl $4, %esp
语义
将esp的值减4
函数调用栈
地址 |
内容 |
栈指针 |
0x100
调用main函数进程的ebp
0x0FC
1
0x0F8
23
0x0F4
0x100
ebp
0x0F0
esp
相关寄存器的值
寄存器 |
值 |
eax
未知
eip
12
step9
12
movl 8(%ebp), %eax
语义
将ebp + 8所指向的地址单元(0xFC)中的值赋给eax,即把1赋给eax
函数调用栈
地址 |
内容 |
栈指针 |
0x100
调用main函数进程的ebp
0x0FC
1
0x0F8
23
0x0F4
0x100
ebp
0x0F0
esp
相关寄存器的值
寄存器 |
值 |
eax
1
eip
13
step10
13
movl %eax, (%esp)
语义
将eax中的值赋给esp所指向的地址单元(0xF0)
函数调用栈
地址 |
内容 |
栈指针 |
0x100
调用main函数进程的ebp
0x0FC
1
0x0F8
23
0x0F4
0x100
ebp
0x0F0
1
esp
相关寄存器的值
寄存器 |
值 |
eax
1
eip
14
step11
14
call g
语义
调用函数g,所做的操作包括将eip压栈,并将eip指向g的起始地址
函数调用栈
地址 |
内容 |
栈指针 |
0x100
调用main函数进程的ebp
0x0FC
1
0x0F8
23
0x0F4
0x100
ebp
0x0F0
1
0x0F0
15
esp
相关寄存器的值
寄存器 |
值 |
eax
1
eip
2
step12
2
pushl %ebp
语义
将main函数的ebp压栈
函数调用栈
地址 |
内容 |
栈指针 |
0x100
调用main函数进程的ebp
0x0FC
1
0x0F8
23
0x0F4
0x100
ebp
0x0F0
1
0x0F0
15
0x0F0
0X0F4
esp
相关寄存器的值
寄存器 |
值 |
eax
1
eip
3
step13
3
movl %esp, %ebp
语义
将esp的值赋给ebp
函数调用栈
地址 |
内容 |
栈指针 |
0x100
调用main函数进程的ebp
0x0FC
1
0x0F8
23
0x0F4
0x100
0x0F0
1
0x0F0
15
0x0F0
0X0F4
esp ebp
相关寄存器的值
寄存器 |
值 |
eax
1
eip
4
step14
4
movl 8(%ebp), %eax
语义
将ebp + 8所指向的地址单元(0xF0)中的值赋给eax,即把1赋给eax
函数调用栈
地址 |
内容 |
栈指针 |
0x100
调用main函数进程的ebp
0x0FC
1
0x0F8
23
0x0F4
0x100
0x0F0
1
0x0F0
15
0x0F0
0X0F4
esp ebp
相关寄存器的值
寄存器 |
值 Vjj |
eax
1
eip
5
step15
5
addl $1, %eax
语义
将eax中的值加1后存入eax,即eax <- eax + 1
函数调用栈
地址 |
内容 |
栈指针 |
0x100
调用main函数进程的ebp
0x0FC
1
0x0F8
23
0x0F4
0x100
0x0F0
1
0x0F0
15
0x0F0
0X0F4
esp ebp
相关寄存器的值
寄存器 |
值 |
eax
2
eip
6
step16
6
popl %ebp
语义
将栈顶元素弹栈,并赋给ebp
函数调用栈
地址 |
内容 |
栈指针 |
0x100
调用main函数进程的ebp
0x0FC
1
0x0F8
23
ebp
0x0F4
0x100
0x0F0
1
0x0F0
15
0x0F0
0X0F4
esp
相关寄存器的值
寄存器 |
值 |
eax
2
eip
7
step17
7
ret
语义
函数返回,将栈顶元素弹栈,并赋给eip
函数调用栈
地址 |
内容 |
栈指针 |
0x100
调用main函数进程的ebp
0x0FC
1
0x0F8
23
ebp
0x0F4
0x100
0x0F0
1
esp
相关寄存器的值
寄存器 |
值 |
eax
2
eip
15
step18
15
leave
语义
作用相当于
movl %ebp, %esp
popl %ebp
函数调用栈
地址 |
内容 |
栈指针 |
0x100
调用main函数进程的ebp
ebp
0x0FC
1
0x0F8
23
esp
相关寄存器的值
寄存器 |
值 |
eax
2
eip
16
step19
16
ret
语义
函数返回
函数调用栈
地址 |
内容 |
栈指针 |
0x100
调用main函数进程的ebp
ebp
0x0FC
1
esp
相关寄存器的值
寄存器 |
值 |
eax
2
eip
23
step20
23
addl $1, %eax
语义
函数返回
函数调用栈
地址 |
内容 |
栈指针 |
0x100
调用main函数进程的ebp
ebp
0x0FC
1
esp
相关寄存器的值
寄存器 |
值 |
eax
3
eip
24
step24
24
leave
语义
作用相当于
movl %ebp, %esp
popl %ebp
函数调用栈
地址 |
内容 |
栈指针 |
0x100
调用main函数进程的ebp
esp
相关寄存器的值
寄存器 |
值 |
eax
3
eip
25
step25
25
ret
语义
函数返回
函数调用栈
地址 |
内容 |
栈指针 |
0x100
相关寄存器的值
寄存器 |
值 |
eax
2
eip
调用main函数的进程的下一条命令
总结
- 在冯诺依曼体系结构中,程序代码和数据代码都存储在内存中。在上文分析的成程序中,代码存储在代码段,栈存储在堆栈段中。
- 在程序的执行是顺序执行,eip中存储着下一条执行的指令地址。
- 在调用函数的时候,首先需要传参数。传参的顺序是按照参数列表从右向左依次压栈。其次,需要保存当时的程序运行情况,包括程序调用返回后应该执行的命令的地址(eip)和栈的基址地址(ebp)。其中,eip由调用函数(caller)保存,ebp由被调用函数(callee)保存。
- 在函数调用返回时,首先需要传递返回值。返回值保存在eax中。其次,需要恢复到函数调用之前的执行状况。包括恢复栈基址地址(ebp)和下一条执行的指令地址(eip)。这两项任务都是由被调用函数完成的。