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入れなくても色々遊べそうな予感。