《深入理解Linux:高效崩溃分析与实时栈回溯技巧》

【backtrace导读】Linux系统中,backtrace是一种在程序崩溃或异常时,捕获函数调用堆栈的调试工具,帮助开发者快速定位问题根源。假如我们在外网采集到的munidump不可用,我们就没法获取到正确的堆栈信息了。也许backtrace可以帮助到您。

backtrace非常依赖信号处理机制,我们需要在程序中注册好指定的信号,比如:常见的SIGSEGV、SIGILL、SIGFPE等,在响应信号的回调函数中获取到用户空间的上下文信息。backtrace在Linux上主要有三个函数。

复制代码
#include <execinfo.h>int backtrace(void **buffer, int size);char **backtrace_symbols(void *const *buffer, int size);void backtrace_symbols_fd(void *const *buffer, int size, int fd);

backtrace: 获取当前栈帧,需要传递两个参数, buffer是一个指针数组,size是栈帧回溯的深度,必须确保数组的大小和size都要大于当前栈帧的层数,方可回溯完整的栈帧。

**backtrace_symbols:**将backtrace获取到的buffer的内容转换成字符串通过返回值返回回来,返回值是个char**二级指针,也是指向了一个指针数组,数组中每个元素是一个地址,指向一块内存区域,存储转换后的符号信息。

**backtrace_symbols_fd:**该函数同第二个,但是直接把转换后的符号信息写到文件中,建议使用,fd就是指向某个文件的文件描述符,当然你也可以把fd定义成1,直接以标准输出的方式打印出来。

tips: backtrace尽量不要使用mallloc,或使用线程非安全的函数。

需要收集的信息

导致崩溃的信号、信号发生的原因、崩溃的地址、 寄存器信息(上下文信息)、栈回溯的栈帧。

Linux信号处理框架直接提供到用户的只有信号,需要我们手动解析信号发生的原因,地址和寄存器信息。

崩溃的原因&地址

信号处理函数提供的信息中,第二个参数siginfo_t,记录了为什么发生崩溃,崩溃发生的地址等信息,但比较麻烦的一点是,这个参数没有具体的系统api可以操作,靠我们翻源码来分析,要用到的两个系统头文件是:

复制代码
/usr/include/bits/siginfo-consts.h/usr/include/bits/types/siginfo_t.h

上述两个头文件详细的介绍了哪些成员变量获取哪些内容,我们手动发送signal 11(segment fault),它也有详细的记录。例如:其中的si_code成员变量就是发生崩溃的原因,大于0是kernel发送给用户态的信号,此时需要记录pid和uid就好,崩溃的地址不存在就会输出(nil)。

寄存器

我们需要从ucontext中获取寄存器的信息,该功能对于崩溃现场是一个非常非常有用的。但需要注意,这个功能我们要启用GUN的特性才可使用。需要在整个源码的开头加入以下宏定义。

复制代码
#ifndef _GNU_SOURCE#define _GNU_SOURCE#endif#include <ucontext.h>

backtrace栈回溯演练

我们在Linux平台下,可以按照如下方式写一个小程序backtrace.c,验证backtrace的能力。

cpp 复制代码
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif

#include <signal.h>
#include <execinfo.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <ucontext.h>
#include <sys/ucontext.h>

#define BACKTRACE_SIZE 100  // 栈回溯深度
#define DPRINTF(fd, format, ...) \
	if (fd > STDERR_FILENO) \
		dprintf(fd, format, ##__VA_ARGS__); \
		dprintf(STDERR_FILENO, format, ##__VA_ARGS__)

// 信号处理程序
void signal_handler(int sig, siginfo_t *info, void *ucontext) {
    void *buffer[BACKTRACE_SIZE];
    int nptrs = BACKTRACE_SIZE;
	FILE *log_file = fopen("crash.log", "w");
	int fd = fileno(log_file);
	if (fd < 0) {
		perror("open log file");
		fd = STDERR_FILENO;
        }
    // 打印信号信息
    DPRINTF(fd, "========== crash report ========\n");
    DPRINTF(fd, "Error: signal %d:\n", sig);
    // 打印导致崩溃的原因地址
    DPRINTF(fd, "Fault address: %p\n", info->si_addr);
    DPRINTF(fd, "Fault code: %d\n", info->si_code);
    DPRINTF(fd, "========== register info ========\n");
    DPRINTF(fd, "register\thex value\t\tdec value\n");
    // 打印寄存器信息
    ucontext_t *uc = (ucontext_t *)ucontext;
#if defined(__x86_64__)
    DPRINTF(fd, "RIP:\t%16llx\t%20lld\n", (unsigned long long)uc->uc_mcontext.gregs[REG_RIP], (unsigned long long)uc->uc_mcontext.gregs[REG_RIP]);
    DPRINTF(fd, "RSP:\t%16llx\t%20lld\n", (unsigned long long)uc->uc_mcontext.gregs[REG_RSP], (unsigned long long)uc->uc_mcontext.gregs[REG_RSP]);
    DPRINTF(fd, "RBP:\t%16llx\t%20lld\n", (unsigned long long)uc->uc_mcontext.gregs[REG_RBP], (unsigned long long)uc->uc_mcontext.gregs[REG_RBP]);
    DPRINTF(fd, "RAX:\t%16llx\t%20lld\n", (unsigned long long)uc->uc_mcontext.gregs[REG_RAX], (unsigned long long)uc->uc_mcontext.gregs[REG_RAX]);
    DPRINTF(fd, "RBX:\t%16llx\t%20lld\n", (unsigned long long)uc->uc_mcontext.gregs[REG_RBX], (unsigned long long)uc->uc_mcontext.gregs[REG_RBX]);
 DPRINTF(fd, "RCX:\t%16llx\t%20lld\n", (unsigned long long)uc->uc_mcontext.gregs[REG_RCX], (unsigned long long)uc->uc_mcontext.gregs[REG_RCX]);
    DPRINTF(fd, "RDX:\t%16llx\t%20lld\n", (unsigned long long)uc->uc_mcontext.gregs[REG_RDX], (unsigned long long)uc->uc_mcontext.gregs[REG_RDX]);
    DPRINTF(fd, "RSI:\t%16llx\t%20lld\n", (unsigned long long)uc->uc_mcontext.gregs[REG_RSI], (unsigned long long)uc->uc_mcontext.gregs[REG_RSI]);
    DPRINTF(fd, "RDI:\t%16llx\t%20lld\n", (unsigned long long)uc->uc_mcontext.gregs[REG_RDI], (unsigned long long)uc->uc_mcontext.gregs[REG_RDI]);
    DPRINTF(fd, "R8:\t%16llx\t%20lld\n", (unsigned long long)uc->uc_mcontext.gregs[REG_R8], (unsigned long long)uc->uc_mcontext.gregs[REG_R8]);
    DPRINTF(fd, "R9:\t%16llx\t%20lld\n", (unsigned long long)uc->uc_mcontext.gregs[REG_R9], (unsigned long long)uc->uc_mcontext.gregs[REG_R9]);
    DPRINTF(fd, "R10:\t%16llx\t%20lld\n", (unsigned long long)uc->uc_mcontext.gregs[REG_R10], (unsigned long long)uc->uc_mcontext.gregs[REG_R10]);
    DPRINTF(fd, "R11:\t%16llx\t%20lld\n", (unsigned long long)uc->uc_mcontext.gregs[REG_R11], (unsigned long long)uc->uc_mcontext.gregs[REG_R11]);
    DPRINTF(fd, "R12:\t%16llx\t%20lld\n", (unsigned long long)uc->uc_mcontext.gregs[REG_R12], (unsigned long long)uc->uc_mcontext.gregs[REG_R12]);
    DPRINTF(fd, "R13:\t%16llx\t%20lld\n", (unsigned long long)uc->uc_mcontext.gregs[REG_R13], (unsigned long long)uc->uc_mcontext.gregs[REG_R13]);
    DPRINTF(fd, "R14:\t%16llx\t%20lld\n", (unsigned long long)uc->uc_mcontext.gregs[REG_R14], (unsigned long long)uc->uc_mcontext.gregs[REG_R14]);
    DPRINTF(fd, "R15:\t%16llx\t%20lld\n", (unsigned long long)uc->uc_mcontext.gregs[REG_R15], (unsigned long long)uc->uc_mcontext.gregs[REG_R15]);
#elif defined(__aarch64__)
    DPRINTF(fd, "PC: \t%16llx\t%20lld\n", (unsigned long long)uc->uc_mcontext.pc, (unsigned long long)uc->uc_mcontext.pc);
    DPRINTF(fd, "SP: \t%16llx\t%20lld\n", (unsigned long long)uc->uc_mcontext.sp, (unsigned long long)uc->uc_mcontext.sp);
    DPRINTF(fd, "FP: \t%16llx\t%20lld\n", (unsigned long long)uc->uc_mcontext.regs[29], (unsigned long long)uc->uc_mcontext.regs[29]);
    for (int i = 0; i < 31; i++) {
 for (int i = 0; i < 31; i++) {
        DPRINTF(fd, "X%d: \t%16llx\t%20lld\n", i, (unsigned long long)uc->uc_mcontext.regs[i], (unsigned long long)uc->uc_mcontext.regs[i]);
    }
#endif
    
    DPRINTF(fd, "\n========== backtrace info ========\n");
    nptrs = backtrace(buffer, BACKTRACE_SIZE);
    if (nptrs == 0) {
	DPRINTF(fd, "backtrace failed\n");
    }
    // 打印堆栈跟踪信息
    backtrace_symbols_fd(buffer, nptrs, STDERR_FILENO);
    if (log_file) {
	backtrace_symbols_fd(buffer, nptrs, fd);
    }
    fclose(log_file);
    // 退出程序
    _exit(EXIT_FAILURE);
}

// 注册信号处理器
void register_signal_handlers() {
    struct sigaction sa;
    sa.sa_sigaction = signal_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_SIGINFO | SA_RESTART;  // 关键标志
    // 注册常见崩溃信号
    sigaction(SIGSEGV, &sa, NULL);  // 段错误
sigaction(SIGILL,  &sa, NULL);  // 非法指令
    sigaction(SIGFPE,  &sa, NULL);  // 算术异常
    sigaction(SIGABRT, &sa, NULL);  // 主动终止
}

// 测试函数(触发崩溃)
void trigger_crash() {
    volatile int *p = NULL;
    *p = 42;  // 触发 SIGSEGV
}

int main() {
    register_signal_handlers();
    trigger_crash();
    return 0;
}

编译成二进制程序backtrace,记得带上符号:

运行backtrace:

可以很清晰地看到,程序崩溃一刻地寄存器信息rbp、rsp、rip都能采集到,backtrace info下罗列出了每个栈帧中具体访问了哪个模块中哪个虚拟地址,通过addr2line可以把指定地符号位置给还原回来。

相关推荐
不光头强3 小时前
spring cloud知识总结
后端·spring·spring cloud
SPC的存折5 小时前
1、Redis数据库基础
linux·运维·服务器·数据库·redis·缓存
爱学习的小囧6 小时前
VMware ESXi 6.7U3v 新版特性、驱动集成教程和资源包、部署教程及高频问答详情
运维·服务器·虚拟化·esxi6.7·esxi蟹卡驱动
小疙瘩6 小时前
只是记录自己发布若依分离系统到linux过程中遇到的问题
linux·运维·服务器
GetcharZp6 小时前
告别 Python 依赖!用 LangChainGo 打造高性能大模型应用,Go 程序员必看!
后端
dldw7777 小时前
IE无法正常登录windows2000server的FTP服务器
运维·服务器·网络
阿里加多7 小时前
第 4 章:Go 线程模型——GMP 深度解析
java·开发语言·后端·golang
小小李程序员7 小时前
Langchain4j工具调用获取不到ThreadLocal
java·后端·ai