← Back to Index

backtrace的使用

下面这段代码展示的是backtrace函数的使用示例。通常我们用它来打印函数的调用栈。调用栈本身是存储在栈的二进制地址信息。backtrace通过定位这类地址,并结合符号表打印出适合人类查看的信息。

    \#include "stdio.h"
    \#include "execinfo.h"
    
    \#define MAX_LEVEL 4
    
    void test2() {
      int i = 0;
      void *buffer[MAX_LEVEL] = {0};
      int size = backtrace(buffer, MAX_LEVEL);
      for (i = 0; i < size; ++i)
        printf("called by %p", buffer[i]);
    
      char **strings;
      strings = backtrace_symbols(buffer, size);
      for (int j = 0; j < size; j++)
        printf("%s", strings[j]);
    }
    void test1() {
      test2();
      return;
    }
    
    int main() {
      test1();
      return 0;
    }

gcc -g -Wall -rdynamic backtrace.cpp -o backtrace

采用解态编译的方式,这样可以得到运行时的虚拟地址。我们使用addr2line就可以对这个地址进行解析,从而得到对应的行号

image.png

下面开始正文。

基础知识

(1)ESP:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶,顶的意思是栈最新位置。

(2)EBP:基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部,底的意思是栈最老位置。

当栈发生变化时,比如发生函数调用,中断等,ESP会将其存放的值放到EBP中。在该子函数运行过程中,EBP值是不会变化的。

查看下面函数main中定义的array大小。可以看出编译器在配置ESP时是按照4字节来分配的。所有该函数中的本地变量,位于ESP和EBP指向的栈空间。在58行我们可以看到mov rbp, rsp。就是用来保存栈基址。rbp=ebp, rsp=esp,分别对应64bit和32bit,两个寄存器内容是一样的。

栈是从高地址往低地址生长的。

image.png

函数的入栈

对于一个3层调用的函数,代码如下

funct1(){funct1DoSomething()}
funct2(){funct1();}
funct3(){funct2();}
main()
{
    int a,b;
    funct3();
    return;
}

那么当函数执行到funct1DoSomething时,它的栈分布是这样的

===frame 1============

变量a

变量b

EIP //main 函数return位置

EBP //main函数的栈base位置(这里指的是main函数栈最先入栈的那个地址)

===frame 2============下面是funct3函数,此时EBP会变

funct3中的变量

EIP

EBP //==记录funct3 的栈base位置入栈,然后新的funct2 EBP指向这个位置(backtrace的关键)==

===frame 3============

funct2中的变量

EIP

EBP//funct2 的栈base位置入栈,然后新的funct1 EBP指向这个位置

====================

此时EBP指向funct1的栈base,但当前的EBP没有入栈。

这样,我们可以通过定位EBP的位置来找到这个栈上所有的函数调用。在这个例子中,我们最先得到的是funct3中的EBP,这个EBP指向当前函数的base地址,也就是EBP记录了一个地址,地址存放的是上一个函数的EBP,上一个 EBP记录一个地址,那个地址存放的是上上一个函数的EBP,依此类推。

至于EIP,我们认为它是近似于函数的调用位置的,所以backtrace直接取EIP为函数的调用栈。而EIP和EBP在栈上的相对位置是固定的,比较好定位。

64 bit CPU for x86 backtrace函数实现

\#include "execinfo.h"
\#include "stdio.h"

typedef unsigned long long uint64_t;

\#define MAX_LEVEL 4

//自己实现的backtrace
void mybacktrace(int size) {
  register uint64_t rbp asm("rbp");//得到当前backtrace函数的rbp
  // printf("RBP value 0x%llx in func \n", rbp);

  uint64_t rbpVal = rbp;//这个变量地址不可变,赋值给另一个变量
  for (auto i = 0; i < size; ++i) {
    uint64_t *tmp = (uint64_t *)rbpVal;//用指针来操作地址,上一个frame的rbp->tmp[0], rip->tmp[1]
    printf("mybactrace: called by 0x%llx\n", tmp[1]);
    rbpVal = tmp[0];
  }
}

void test2() {
  int i = 0;
  void *buffer[MAX_LEVEL] = {0};
  int size = backtrace(buffer, MAX_LEVEL);
  for (i = 0; i < size; ++i)
    printf("backtrace: called by %p\n", buffer[i]);

  mybacktrace(size);
}

void test1() {
  test2();
  return;
}

int main() {
  test1();
  return 0;
}

运行结果,调用的层级关系是一样的,不过由于backtrace和mybacktrace在代码中的位置不一样,所以运行栈的第一个位置不同。

image.png

backtrace的小问题

在前面的实现中,我们有一个假设“至于EIP,我们认为它是近似于函数的调用位置”。实际上backtrace出来的代码位置并不是代码真正调用的位置,而是EIP的位置。那么我们怎么才能得到真正的调用位置呢?

每条c语言对应的指令大小对了不同的字节数,实际上我们只要把EIP的位置减1就可以得到上一条指令的地址了。

image.png