Linux内核分析(一)之简单的C语言调用栈分析

2019-07-14 11:43发布

前言

本文的内容来自于《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)。这两项任务都是由被调用函数完成的。