下面这段代码展示的是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就可以对这个地址进行解析,从而得到对应的行号
下面开始正文。
(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,两个寄存器内容是一样的。
栈是从高地址往低地址生长的。
对于一个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在栈上的相对位置是固定的,比较好定位。
\#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在代码中的位置不一样,所以运行栈的第一个位置不同。
在前面的实现中,我们有一个假设“至于EIP,我们认为它是近似于函数的调用位置”。实际上backtrace出来的代码位置并不是代码真正调用的位置,而是EIP的位置。那么我们怎么才能得到真正的调用位置呢?
每条c语言对应的指令大小对了不同的字节数,实际上我们只要把EIP的位置减1就可以得到上一条指令的地址了。