You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.01 13:31:01