x86-64 Linux下从指令指针还原函数及自定义栈回溯问题
x86-64 Linux下信号触发后的无libc依赖栈回溯实现需求与问题
需求背景
在x86-64 Linux环境下,需实现:程序收到信号后执行栈遍历并打印指令指针;发布版本为剥离符号的优化二进制,希望在构建机器上将收集到的RIP地址转换为对应函数名(优化后行号大概率不可用,考虑过保留调试版本用于符号解析)。
当前问题
自定义栈回溯代码无法跳过信号处理函数的栈帧,但libc的backtrace可以实现该逻辑,现需在不依赖libc的前提下完成栈回溯并跳过信号处理函数。现有代码如下:
#include <signal.h> #include <cstdio> typedef unsigned long long u64; int mybacktrace(); #include <execinfo.h> #include <unistd.h> void print_stacktrace(void) { size_t size; enum Constexpr { MAX_SIZE = 1024 }; void *array[MAX_SIZE]; size = backtrace(array, MAX_SIZE); backtrace_symbols_fd(array, size, STDOUT_FILENO); } void mysig(int signo) { mybacktrace(); _exit(1); } int mybacktrace() { u64*p; p = (u64*)((u64)&p + 16); //seems to work correctly for (int i = 0; i < 10 && (u64)p >= 1<<16; i++) { printf("%d %p\n", i, p[1]); p = (u64*)(p[0]); } print_stacktrace(); return 0; return 0; } int test() { return mybacktrace(); } int main(int argc, char *argv[]) { signal(SIGILL, mysig); test(); __builtin_trap(); return 0; }
解决方案
1. 理解信号栈帧结构
x86-64 Linux中,信号触发时内核会在栈上构建信号上下文结构,其中包含被打断时的寄存器状态(原RIP、RBP等)。信号处理函数的栈帧与普通函数调用栈帧不同,需识别并跳过这部分栈帧,直接从信号触发前的上下文继续回溯。
2. 修改自定义回溯函数(无libc依赖)
自行定义Linux信号上下文相关结构(避免依赖libc头文件),在信号处理函数中定位到该上下文,提取信号触发前的RBP和RIP,以此为起点进行栈回溯:
#include <signal.h> #include <stdio.h> typedef unsigned long long u64; // 自行定义x86-64 Linux信号上下文结构(与内核定义一致) struct sigcontext { u64 r8; u64 r9; u64 r10; u64 r11; u64 r12; u64 r13; u64 r14; u64 r15; u64 rdi; u64 rsi; u64 rbp; u64 rbx; u64 rdx; u64 rax; u64 rcx; u64 rsp; u64 rip; // 省略其他不相关字段 }; // 信号处理函数:通过SA_SIGINFO获取上下文 void mysig(int signo, siginfo_t *info, void *ucontext) { struct sigcontext *sc = ((struct ucontext_t*)ucontext)->uc_mcontext; // 提取信号触发前的RBP和RIP u64 *rbp = (u64*)sc->rbp; u64 rip = sc->rip; printf("Signal triggered, original RIP: %p\n", (void*)rip); printf("Stack backtrace (skip signal handler):\n"); // 从原RBP开始栈回溯 for (int i = 0; i < 10 && (u64)rbp >= (1 << 16); i++) { // rbp[1]是当前栈帧的返回地址(RIP) printf("%d: %p\n", i, (void*)rbp[1]); // 移动到上一级栈帧的RBP rbp = (u64*)rbp[0]; } _exit(1); } int test() { // 触发SIGILL __builtin_trap(); return 0; } int main(int argc, char *argv[]) { struct sigaction sa; sa.sa_sigaction = mysig; sa.sa_flags = SA_SIGINFO; // 必须设置此标志才能获取上下文参数 sigemptyset(&sa.sa_mask); sigaction(SIGILL, &sa, NULL); test(); return 0; }
3. 符号解析方案
发布版本用strip剥离符号,但构建阶段保留带-g调试信息的二进制。在构建机器上,使用addr2line工具将RIP地址转换为函数名:
addr2line -e your_program.debug -f <rip_address>
关键说明
- 必须设置
SA_SIGINFO标志安装信号处理函数,才能获取包含寄存器上下文的ucontext_t指针。 - 自行定义的
struct sigcontext需与目标内核版本的定义一致,不同内核版本可能略有差异,需对应调整。 - 栈回溯时需检查RBP的有效性(比如是否在合法用户栈地址范围内),避免非法内存访问。
内容的提问来源于stack exchange,提问作者Stan
相关产品推荐
相关产品推荐

