C语言函数调用栈浅析
一直在做较为底层的编程,很早以前就想写一篇关于C语言调用栈相关的文章。正好这一次和一个同事一起做debug tool。 其中要做一个call stack的功能。于是我又重新打开IA32的文档,把C调用栈的原理记录如下。
Table of Contents
1 为什么bootloader和bios需要汇编?
- 其实这是一个经常被问到的问题。为什么我们不能只用c语言呢?能不能不要用汇编,所有的bios和boot loader 代码都用c来写? 这个问题常被问到,但却很少有人去思考。答案当然是no。
- 为什么呢?这就和我们今天所要讲的内容息息相关了。c语言的运行是离不开栈的。没有栈,临时变量放哪里?参数怎么传? 但汇编却不需要,在汇编程序中我们可以直接操作寄存器。
2 寄存器介绍
- IA32有很多寄存器,我们这里主要要用到的寄存器如下:
- EIP: Instruction Pointer 程序计数器,记录当前程序所运行的地址
- EBP: Base pointer 基址寄存器
- ESP: Stack pointer 栈指针寄存器
3 函数调用前后,栈指针的变化
- 栈的增长方式是从倒着的,即从高地址向低地址增长,所以当函数调用后,会先将 调用参数从右至左压栈,这样做的好处是,当我们弹出栈的时候,可以得到正序的 参数。而且获取参数时不必知道参数的个数。试想,如果我们将参数从左到右压栈。 那么,当我们进入函数后,要想得到arg1,就得先知道一共有多少个参数,因为 此时参数是倒序的。
- 压完参数后,接着是压EIP,即调用该函数的下一条指令的地址。这样当被调用的 函数执行完成之后才知道返回到哪里,从哪里继续执行。
Low Address ---> +---------+ | ... | | EIP | <--- New ESP | Arg1 | | Arg2 | | Arg3 | | ... | | Argn | | ... | <--- Old ESP | ... | | ... | High Address ---> +---------+
- 进入到C语言的函数后,系统紧接着会做另一件事,就是压EBP入栈。然后再把当前的ESP=>EBP。
Low Address ---> +---------+ | ... | EBP ---> | Old EBP | <--- New ESP | EIP | <--- Old ESP | Arg1 | | Arg2 | | Arg3 | | ... | | Argn | | ... | | ... | High Address ---> +---------+
4 如何获取函数的Call Stack?
- 通过前面的分析,我们可以知道,当函数调用完成之后,EBP所指向的位置存储着函数调用前EBP 的值。(EBP+4)的位置就是程序的返回地址 (EIP)。所以通过EBP我们可以不断地获取到上 一次的EBP的位置,也就能不断地获取到上一次程序调用的返回地址。
Low Address ---> +---------+ +---------+ +---------+ | ... | | ... | | ... | EBP ---> | Old EBP | --> | Old EBP | --> | Old EBP | | EIP | | EIP | | EIP | | Arg1 | | Arg1 | | Arg1 | | Arg2 | | Arg2 | | Arg2 | | Arg3 | | Arg3 | | Arg3 | | ... | | ... | | ... | | Argn | | Argn | | Argn | | ... | | ... | | ... | | ... | | ... | | ... | High Address ---> +---------+ +---------+ +---------+
5 实例分析
下面的代码很简单,主要想用一个实际的例子来检验以上所讲的理论。
5.1 源代码
foo函数被定义成 int foo(int a, int b)。有两个输入参数。被main函数调用。下面将主要分析foo函数的调用过程。
#include "stdafx.h" int foo(int a, int b) { int sum = 0; sum = a + b; return sum; } int _tmain(int argc, _TCHAR* argv[]) { foo(1, 2); return 0; }
5.2 调用foo()之前的寄存器数值
5.3 调用foo()之后寄存器的数值
- 将断点设在函数调用后的第一条汇编语句。此时,函数还没有压入EBP,ESP从0xfaf958变成了0xfaf94c。少了12个字节。 再看foo函数的定义,一共有两个参数,加上压入的EIP刚好12字节。
5.4 栈里面的内容
- 由下图可以看出,此时还未压入EBP
- 压入EBP前栈内的内容:EIP (0x002d1417), 参数a = 1, 参数b = 2。
- 压入EBP之后栈内的内容
- 寄存器的变化, 之前的EBP入了栈,ESP(0x00faf948) => EBP
- 栈的变化, 栈中多了EBP (0x00fafa24)
HTML generated by org-mode 6.31a in emacs 23