Linux C语言基于FP寄存器进行栈回溯
文章目录
- 前言
- 一、简介
- 二、C语言例子
- 2.1 demo1
- 2.2 demo2
- 2.3 demo3
前言
前置知识:
https://xiaolizai.blog.csdn.net/article/details/136690200
https://xiaolizai.blog.csdn.net/article/details/138706194
一、简介
基于帧指针可以进行栈回溯:
在X86中,通常使用RBP寄存器作为帧指针使用,RBP寄存器所指向的栈单元中保存的是前一个RBP寄存器的值,通常也就是caller函数的RBP值。帧指针不仅对函数中的代码起到定位变量和参数的参照物作用,而且将栈中的一个个栈帧串联在一起,形成了一个可以遍历所有栈帧的链条,这也就是栈回溯的基本原理。
main() -> funa -> funb -> func
如下图所示:
二、C语言例子
2.1 demo1
代码如下:
#define _GNU_SOURCE // 启用GNU扩展功能(如dladdr的扩展信息)#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>#include <link.h>
#include <dlfcn.h>static int get_stack_range(unsigned long *start, unsigned long *end) {FILE *maps;char line[1024];maps = fopen("/proc/self/maps", "r");if (!maps) {perror("fopen");return -1;}while (fgets(line, sizeof(line), maps)) {if (strstr(line, "[stack]")) {sscanf(line, "%lx-%lx", start, end);break;}}fclose(maps);return 0;
}/* 检查指针是否可能合法 */
static bool is_valid_address(void *p) {uintptr_t addr = (uintptr_t)p;unsigned long stack_start, stack_end;get_stack_range(&stack_start, &stack_end);/* 16字节对齐检查 */if(addr >= stack_start && addr <= stack_end && (addr & 0xF) == 0)return true;elsereturn false;
}void my_backtrace() {void **rbp;asm volatile ("mov %%rbp, %0" : "=r"(rbp)); // 读取当前rbp寄存器的值(当前栈帧的基址)printf("Backtrace:\n");int frame_count = 0;int max_frames = 20; // 防止无限循环while (rbp && max_frames-- && is_valid_address(rbp)) {printf("rbp = %p\n", rbp);void *ret_addr = *(rbp + 1); //rbp+1存储返回地址(调用者的下一条指令)printf("#%-2d ret_addr = %p\n", frame_count++, ret_addr);rbp = (void **)*rbp; // 移动到上一个栈帧的rbp(栈帧链表的下一个节点)}
}// 测试函数
__attribute__((noinline)) void func3() { my_backtrace(); }
__attribute__((noinline)) void func2() { func3(); }
__attribute__((noinline)) void func1() { func2(); }int main() {func1();return 0;
}
rbp 寄存器存的是 当前栈帧基址。
[rbp] 存的是 调用者的 rbp(上一帧的基址)。
[rbp+8] 存的是 返回地址(call 指令压栈的地址)。
双指针 void **rbp,因为 RBP 寄存器里存的就是上一个栈帧的基址地址(一个指针),而我们要访问它指向的内容(返回地址、上一个 RBP),这就需要“指向指针的指针”。
$ gcc backtrace.c -o backtrace
$ ./backtrace
Backtrace:
rbp = 0x7ffc45ed1700
#0 ret_addr = 0x55b233d39382
rbp = 0x7ffc45ed1710
#1 ret_addr = 0x55b233d39393
rbp = 0x7ffc45ed1720
#2 ret_addr = 0x55b233d393a4
rbp = 0x7ffc45ed1730
#3 ret_addr = 0x55b233d393b5
rbp = 0x7ffc45ed1740
#4 ret_addr = 0x7f7e84a4a24a
2.2 demo2
给栈回溯增加符号:
#define _GNU_SOURCE#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>#include <link.h>
#include <dlfcn.h>static int get_stack_range(unsigned long *start, unsigned long *end) {FILE *maps;char line[1024];maps = fopen("/proc/self/maps", "r");if (!maps) {perror("fopen");return -1;}while (fgets(line, sizeof(line), maps)) {if (strstr(line, "[stack]")) {sscanf(line, "%lx-%lx", start, end);break;}}fclose(maps);return 0;
}/* 检查指针是否可能合法 */
static bool is_valid_address(void *p) {uintptr_t addr = (uintptr_t)p;unsigned long stack_start, stack_end;get_stack_range(&stack_start, &stack_end);/* 16字节对齐检查 */if(addr >= stack_start && addr <= stack_end && (addr & 0xF) == 0)return true;elsereturn false;
}/* 符号解析函数 */
static void resolve_symbol(void *addr) {Dl_info info;if (dladdr(addr, &info)) {if (info.dli_sname) {printf(" %s()", info.dli_sname);if (info.dli_fname) {printf(" in %s", info.dli_fname);}// 尝试获取偏移量unsigned long offset = (unsigned long)addr - (unsigned long)info.dli_saddr;if (offset < 0x1000) { // 只显示小偏移量printf("+0x%lx", offset);}} else {printf(" ?? [%p]", addr);}} else {printf(" ?? [%p]", addr);}
}void my_backtrace() {void **rbp;asm volatile ("mov %%rbp, %0" : "=r"(rbp));printf("Backtrace:\n");int frame_count = 0;int max_frames = 20; // 防止无限循环while (rbp && max_frames-- && is_valid_address(rbp)) {//返回地址是 call 的下一条指令void *ret_addr = *(rbp + 1);printf("#%-2d ", frame_count++);resolve_symbol(ret_addr);printf("\n");rbp = (void **)*rbp;}
}// 测试函数
__attribute__((noinline)) void func3() { my_backtrace(); }
__attribute__((noinline)) void func2() { func3(); }
__attribute__((noinline)) void func1() { func2(); }int main() {// 编译时需要加上 -rdynamic 选项以导出符号// 例如: gcc -rdynamic backtrace.c -o backtracefunc1();return 0;
}
$ gcc -rdynamic 2.c -o backtrace
$ ./backtrace
Backtrace:
#0 func3() in ./backtrace+0xe
#1 func2() in ./backtrace+0xe
#2 func1() in ./backtrace+0xe
#3 main() in ./backtrace+0xe
#4 ?? [0x7f89ceed024a]
2.3 demo3
增加.so:
#define _GNU_SOURCE#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <dlfcn.h>
#include <link.h> // 用于获取库基地址#define MAX_FRAMES 20// 获取栈内存范围
static int get_stack_range(unsigned long *start, unsigned long *end) {FILE *maps = fopen("/proc/self/maps", "r");if (!maps) {perror("fopen");return -1;}char line[1024];while (fgets(line, sizeof(line), maps)) {if (strstr(line, "[stack]")) {sscanf(line, "%lx-%lx", start, end);break;}}fclose(maps);return 0;
}// 检查指针是否合法
static bool is_valid_address(void *p) {uintptr_t addr = (uintptr_t)p;unsigned long stack_start, stack_end;if (get_stack_range(&stack_start, &stack_end) != 0) {return false;}return (addr >= stack_start && addr <= stack_end && (addr & 0xF) == 0);
}// 增强版符号解析
/*
Dl_info 结构体:用于存储 dladdr 解析出的符号信息,包含以下关键字段:
dli_sname:符号名(如函数名 func3)。
dli_fname:符号所在的文件名(如 ./a.out 或 libc.so.6)。
dli_fbase:符号所在文件的加载基地址(动态库的起始地址)。
dli_saddr:符号对应的内存地址(函数入口地址)。
*/
static void resolve_symbol(void *addr) {Dl_info info;if (dladdr(addr, &info)) {// 基本符号信息printf(" 0x%016lx: ", (unsigned long)addr);//解析并打印符号名(函数名)if (info.dli_sname) {printf("%s()", info.dli_sname);} else {printf("??");}// 库信息if (info.dli_fname) {printf(" in %s", info.dli_fname);// 如果是动态库,显示基地址和偏移量if (strstr(info.dli_fname, ".so")) {printf(" (base: 0x%lx, offset: 0x%lx)",(unsigned long)info.dli_fbase,(unsigned long)addr - (unsigned long)info.dli_fbase);}}// 如果是在主程序中,显示偏移量if (info.dli_saddr && !strstr(info.dli_fname ? info.dli_fname : "", ".so")) {printf("+0x%lx", (unsigned long)addr - (unsigned long)info.dli_saddr);}} else {printf(" 0x%016lx: ?? [未知地址]", (unsigned long)addr);}
}// 打印所有已加载的库(调试用)
void print_loaded_libs() {printf("\n已加载的库:\n");struct link_map *lm = _r_debug.r_map;while (lm) {printf(" 0x%016lx %s\n", (unsigned long)lm->l_addr, lm->l_name);lm = lm->l_next;}
}void backtrace() {void **rbp;asm volatile ("mov %%rbp, %0" : "=r"(rbp));printf("调用栈回溯:\n");int frame_count = 0;while (rbp && frame_count < MAX_FRAMES && is_valid_address(rbp)) {void *ret_addr = *(rbp + 1);printf("#%-2d ", frame_count++);resolve_symbol(ret_addr);printf("\n");rbp = (void **)*rbp;}print_loaded_libs(); // 可选:显示加载的库
}// 测试函数
__attribute__((noinline)) void func3() { backtrace(); }
__attribute__((noinline)) void func2() { func3(); }
__attribute__((noinline)) void func1() { func2(); }int main() {printf("=== 开始栈回溯测试 ===\n");func1();printf("=== 测试结束 ===\n");return 0;
}
$ gcc -rdynamic 3.c -o backtrace
$ ./backtrace
=== 开始栈回溯测试 ===
调用栈回溯:
#0 0x000055cae91a0563: func3() in ./backtrace+0xe
#1 0x000055cae91a0574: func2() in ./backtrace+0xe
#2 0x000055cae91a0585: func1() in ./backtrace+0xe
#3 0x000055cae91a05a5: main() in ./backtrace+0x1d
#4 0x00007f9e75d3f24a: ?? in /lib/x86_64-linux-gnu/libc.so.6 (base: 0x7f9e75d18000, offset: 0x2724a)已加载的库:0x000055cae919f0000x00007ffe819fe000 linux-vdso.so.10x00007f9e75d18000 /lib/x86_64-linux-gnu/libc.so.60x00007f9e75f21000 /lib64/ld-linux-x86-64.so.2
=== 测试结束 ===