能否让内核解析用户态崩溃栈跟踪中的地址符号?
嘿,你问的这个问题很常见——内核本身是不会帮你把用户态地址解析成函数名和行号的,原因其实很简单:内核的职责是管理系统资源,它不会主动去读取用户程序里的调试符号(比如DWARF信息、符号表),一来是安全考虑(内核没必要碰用户空间的调试数据),二来是解析符号会带来额外的性能开销,不符合内核的设计理念。
不过别担心,我们有好几种办法可以事后或者实时拿到带函数名和行号的栈跟踪,结合你的例子给你详细讲讲:
方法1:用addr2line手动解析地址
这是最直接的方式,只要你编译程序时加了-g参数保留了调试信息(你已经这么做了),addr2line就能从二进制文件里把地址转换成函数名和行号。
比如你的内核日志里给出的PC地址是0x557e8a395c,直接运行:
addr2line -e /tmp/prog 0x557e8a395c
结合你的代码,这个地址应该对应到main函数里*(char *)s = 'H';这一行——毕竟你试图修改只读的字符串字面量,这就是触发段错误的原因。
如果遇到ASLR(地址空间随机化)导致加载地址变化的情况,你可以用内核输出的内存映射信息计算偏移:
557e8a3000-557e8a4000 r-xp 00000000 00:13 267223 /tmp/prog
这里代码段的加载基址是0x557e8a3000,用实际地址减去基址得到编译时的偏移:0x557e8a395c - 0x557e8a3000 = 0x95c,再用这个偏移解析:
addr2line -e /tmp/prog 0x95c
方法2:用gdb加载core文件自动生成带符号的栈跟踪
如果你的系统开启了core dump功能(可以先运行ulimit -c unlimited开启),程序崩溃时会生成一个core文件,用gdb加载这个文件就能直接看到完整的、带函数名和行号的栈回溯:
gdb /tmp/prog core
进入gdb后输入bt命令,你会看到类似这样的输出:
#0 0x000000557e8a395c in main () at prog.c:3
直接就定位到了出错的代码行,非常直观。
方法3:让程序崩溃时自动输出带符号的栈跟踪
如果你希望程序崩溃时自己就能输出带符号的信息,不用事后工具,可以自己实现一个SIGSEGV信号处理函数,借助backtrace()和backtrace_symbols()函数(编译时需要加-rdynamic参数):
修改你的代码如下:
#include <stdio.h> #include <signal.h> #include <execinfo.h> #include <stdlib.h> void sigsegv_handler(int sig) { void *stack_frames[20]; int frame_count = backtrace(stack_frames, 20); char **symbols = backtrace_symbols(stack_frames, frame_count); printf("Segmentation fault! Stack trace:\n"); for (int i = 0; i < frame_count; i++) { printf("%s\n", symbols[i]); } free(symbols); exit(1); } int main() { signal(SIGSEGV, sigsegv_handler); const char *s = "hello world"; *(char *)s = 'H'; return 0; }
用gcc -g -rdynamic prog.c -o prog编译后,程序崩溃时会输出:
Segmentation fault! Stack trace: ./prog(sigsegv_handler+0x3c) [0x557e8a39f4] /lib/libc-2.26.so(+0x37b6c) [0x7f8ae97b6c] ./prog(main+0x2c) [0x557e8a395c] /lib/libc-2.26.so(__libc_start_main+0xe4) [0x7f8ae7b1a90] ./prog(_start+0x24) [0x557e8a3840]
虽然这里只能显示函数名和偏移,要拿到行号还是得结合addr2line,但已经比纯地址友好太多了。
总的来说,内核输出的栈跟踪已经给了你足够的信息(地址、内存映射),只要你保留了带调试信息的二进制文件,就可以通过上面的工具轻松还原出函数名和行号。
内容的提问来源于stack exchange,提问作者yy7k




