C语言函数调用栈浅析

C语言函数调用栈浅析

一直在做较为底层的编程,很早以前就想写一篇关于C语言调用栈相关的文章。正好这一次和一个同事一起做debug tool。 其中要做一个call stack的功能。于是我又重新打开IA32的文档,把C调用栈的原理记录如下。

Table of Contents

  • 1 为什么要有汇编?
  • 2 将要用到的寄存器
  • 3 函数调用前后,ESP的变化
  • 4 如何获取Call Stack?
  • 5 实例分析
    • 5.1 源代码
    • 5.2 调用foo()之前的寄存器数值
    • 5.3 调用foo()之后寄存器的数值
    • 5.4 栈里面的内容

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()之前的寄存器数值

C语言函数调用栈浅析_第1张图片

5.3 调用foo()之后寄存器的数值

  • 将断点设在函数调用后的第一条汇编语句。此时,函数还没有压入EBP,ESP从0xfaf958变成了0xfaf94c。少了12个字节。 再看foo函数的定义,一共有两个参数,加上压入的EIP刚好12字节。

C语言函数调用栈浅析_第2张图片

5.4 栈里面的内容

  • 由下图可以看出,此时还未压入EBP

C语言函数调用栈浅析_第3张图片

  • 压入EBP前栈内的内容:EIP (0x002d1417), 参数a = 1, 参数b = 2。

C语言函数调用栈浅析_第4张图片

  • 压入EBP之后栈内的内容
    • 寄存器的变化, 之前的EBP入了栈,ESP(0x00faf948) => EBP

  • 栈的变化, 栈中多了EBP (0x00fafa24)

Date: 2014-07-20 13:29:59

HTML generated by org-mode 6.31a in emacs 23

你可能感兴趣的:(C语言函数调用栈浅析)