有时候,线上环境没有开启coredump,这时把进程复位时的调用栈打印出来就非常有利于问题定位了。怎么做呢?也很简单,我们在程序中接管SGISEGV信号,在信号处理函数打印调用栈信息即可。
信号处理可以看:UNIX环境高级编程
内核机制可以看:Linux内核源代码情景分析
这两本书虽老了点,但写的确实很不错,相关系统API使用可以配合可能Linux man手册。
接管段错误信号后,在信号处理函数中打印当前调用栈即可。此时的栈信息可以回溯到之前发生异常时的函数栈帧。整个过程大致是这样的:用户太程序在运行,堆栈寄存器指向用户栈,访问非法内存,中断陷入内核,内核掉注册的信号处理函数,又切回到了用户态复用之前的栈。理解压栈原理,很容易从当前栈基址往前回溯整个站的,原理可以看:Linux C函数调用栈帧结构。并且,Linux也有API可以直接调用来打印调用栈。当然,如果栈内存被破坏了,就还原不出来了。
一个例子如下,记得编译的时候加上 -rdynamic 参数,可以打出符号名称。代码非常简单:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <execinfo.h>
#include <unistd.h>
void ShowStack()
{
int i;
void *buffer[1024];
int n = backtrace(buffer, 1024);
char **symbols = backtrace_symbols(buffer, n);
for (i = 0; i < n; i++) {
printf("%s\n", symbols[i]);
}
}
void SigSegvProc(int signo) {
if (signo == SIGSEGV) {
printf("Receive SIGSEGV signal\n");
printf("-----call stack-----\n");
ShowStack();
exit(-1);
} else {
printf("this is sig %d", signo);
}
}
void RegSig() {
signal(SIGSEGV, SigSegvProc);
}
void fun3() {
printf("this is fun3\n");
*(char*)0 = 1; // read nullptr
}
void fun2() {
printf("this is fun2\n");
fun3();
}
void fun1() {
printf("this is fun1\n");
fun2();
}
int main() {
RegSig();
fun1();
return 0;
}
其运行结果如下:
➜ sig gcc -g -rdynamic main.c
➜ sig ./a.out
this is fun1
this is fun2
this is fun3
Receive SIGSEGV signal
-----call stack-----
./a.out(ShowStack+0x2e) [0x55d6574ebb38]
./a.out(SigSegvProc+0x33) [0x55d6574ebbec]
/lib/x86_64-linux-gnu/libc.so.6(+0x3ef20) [0x14a39f418f20]
./a.out(fun3+0x15) [0x55d6574ebc3c]
./a.out(fun2+0x1a) [0x55d6574ebc5c]
./a.out(fun1+0x1a) [0x55d6574ebc79]
./a.out(main+0x18) [0x55d6574ebc94]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xe7) [0x14a39f3fbb97]
./a.out(_start+0x2a) [0x55d6574eba2a]
如果没有开coredump,那么不妨就自己接管段错误SIGSEGV信号,打印非法访问内存时的调用栈。