glibcのbacktrace
glibc付属のbacktraceでシグナルハンドラ内からbacktraceを呼んだ場合
中途半端な結果しか得られないのは何故かと思い調べてみた。
サンプルコード
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <signal.h> #include <execinfo.h> int (*func_ptr)() = (void *)NULL; int func1(); int func2(); void segv_handler(int signum); int main(int argc,char ** argv){ struct sigaction sa; memset(&sa,0,sizeof(struct sigaction)); sigemptyset(&sa.sa_mask); sa.sa_handler = segv_handler; sigaction(SIGSEGV,&sa,NULL); func1(); return 0; } int func1(){ func2(); return 0; } int func2(){ func_ptr(); return 0; } void segv_handler(int signum){ int cnt; void **arr = alloca(256 * sizeof(void *)); cnt = backtrace(arr, 256); backtrace_symbols_fd(arr,cnt,1); abort(); }
実行結果は以下、func2のスタックが見えていない。(0xc5b420は__kernel_sigreturn)
流れとしてはfunc2内部でcallした後NULLに飛んでSEGVする。
$ ./a.out ./a.out(segv_handler+0x3b)[0x8048799] [0xc5b420] ./a.out(func1+0xb)[0x8048743] ./a.out(main+0x73)[0x8048727] /lib/libc.so.6(__libc_start_main+0xdc)[0x395dec] ./a.out[0x8048631] Aborted (core dumped)
glibcのbacktrace(sysdeps/i386/backtrace.c)あたりを読んだところ、libgccのunwindを使用して
%ebpを拾いチェインを辿っていくような動作をしている感じ。
func2からNULLに飛んだ時点で関数の初期化処理push %ebp, mov %esp,%ebpによるフレーム
操作が行われていないので、call NULLでスタックにリターンアドレスだけ積んだ状態で
シグナルでとばされると思われる。
確かにこれだとfunc2の分が待避されてないので辿れないよなーと思い、gdbに食わせてみたら
以下の通り普通に全部表示された。gdbはフレーム単位で追っていくようだけど、理解不足と
読み切れてないので正確なところは分からず。
#0 0x00c5b402 in __kernel_vsyscall () #1 0x003a8c00 in raise () from /lib/libc.so.6 #2 0x003aa451 in abort () from /lib/libc.so.6 #3 0x080487bb in segv_handler () #4 <signal handler called> #5 0x00000000 in ?? () #6 0x08048757 in func2 () #7 0x08048743 in func1 () #8 0x08048727 in main ()
うーん、kernelのsignalによるスタック操作周りがいまいち不明瞭なので後で調べよう。
あと、大体標準で入っているlibgccにunwind関係の関数が入ってるので
libunwind入れなくても色々遊べそうな予感。