Application에서의 backtrace(call stack) dump 방법 (비정상 종료에 대한 디버깅)
- 작성자
- 고친과정
2018년 1월 12일 : 처음씀
1.1. 개요
Application에 문제가 있는 코드로 인하여 비정상 종료되는 경우 어디가 문제인지를 확인하기 매우 어려울 때가 있습니다.
비정상적인 종료가 될 때 도대체 어느 부분을 호출한 시점에서 죽었는지 확인하기 위한 방법에는 여러가지가 있으나 그 중에서 backtrace 기법을 소개하고 정리해보았습니다.
비정상적인 종료가 될 때 도대체 어느 부분을 호출한 시점에서 죽었는지 확인하기 위한 방법에는 여러가지가 있으나 그 중에서 backtrace 기법을 소개하고 정리해보았습니다.
1.2. Compile & Link option (Symbol 확인을 위해서 필요한 옵션입니다.)
- 주) CPPFLAGS는 전처리기 옵션, CFLAGS는 컴파일러 옵션, LDFLAGS는 링커 옵션을 의미합니다.
# 모든 함수의 Stack frame (call 규칙)에서 frame-pointer 를 최적화에 따른 비정규 frame 방식을 사용하지 않겠다는 의미 CFLAGS += -fno-omit-frame-pointer # 필수 # 코드를 재배치 가능한 형식으로 만들겠다는 의미 (만약 이 옵션으로 컴파일에 문제가 있다면 빼도 됩니다.) CFLAGS += -fPIC # 선택사항 # Thread 를 프로그램 내에서 사용한다면 다음과 같은 재진입성 향상 옵션을 추가하세요. CPPFLAGS += -D_REENTRANT # 선택사항 # 만약 64bit file size를 접근한다면 다음과 같은 호환 확장 옵션을 추가해볼 수 있습니다. CPPFLAGS += -D_LARGEFILE_SOURCE -D_LARGEFILE64_SOURCE -D_FILE_OFFSET_BITS=64 # 선택사항 # 빌드 환경과 런타임 환경의 glibc 가 다를 수 있다면 glibc 호환성 향상 옵션을 사용하세요. CPPFLAGS += -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 # 선택사항 # 자신의 주소에 대한 심볼명을 동적으로 확인하기 위하여 필요 LDFLAGS += -rdynamic # 권장사항 (심볼명 dump를 원하면 필수사항) 위 option들은 Symbol명을 확인하기 위해서 필요합니다. 하지만 필수는 아닙니다. 위 옵션이 주어지지 않는다면 주소만 표시될 것이며 이를 addr2line 명령어로 심볼을 확인할 수도 있기 때문입니다.
1.3. 프로그램의 main 함수 근처에 다음과 같은 함수를 삽입합니다
#include <execinfo.h> void dump_backtrace(void) { /* IMPORTANT gcc need compile option "-fno-omit-frame-pointer" gcc optional linker option "-rdynamic" */ void *s_backtrace_buffer[16]; char **s_backtrace_symbols; int s_backtrace_size; int s_backtrace_index; s_backtrace_size = backtrace( (void **)(&s_backtrace_buffer[0]), (int)(sizeof(s_backtrace_buffer) / sizeof(void *)) ); if(s_backtrace_size <= 0) { s_backtrace_symbols = (char **)0; } else { s_backtrace_symbols = backtrace_symbols( (void * const *)(&s_backtrace_buffer[0]), s_backtrace_size ); } (void)fprintf(stderr, "backtrace() returned %d addresses\n", s_backtrace_size); for(s_backtrace_index = 0;s_backtrace_index < s_backtrace_size;s_backtrace_index++) { (void)fprintf( stderr, "%02d - %p - %s\n", s_backtrace_index + 1, s_backtrace_buffer[s_backtrace_index], (s_backtrace_symbols == ((char **)0)) ? "<unknown symbol>" : s_backtrace_symbols[s_backtrace_index] ); } free((void *)s_backtrace_symbols); }
1.4. 삽입했던 함수를 Signal handler 에서 호출되도록 main 함수내에 추가합니다.
#include <signal.h> void my_signal_handler(int s_signal) { switch(s_signal) { case SIGILL: case SIGABRT: case SIGBUS: case SIGSTKFLT: case SIGFPE: case SIGSEGV: dump_backtrace(); break; } signal(s_signal, my_signal_handler); /* 자기자신의 Signal 을 재귀적으로 처리하기 위해서 */ } int main(int s_argc, char **s_argv) { ... /* 주요 비정상 종료와 관련한 Signal에 handler를 등록합니다. */ signal(SIGILL, my_signal_handler); signal(SIGABRT, my_signal_handler); signal(SIGBUS, my_signal_handler); signal(SIGSTKFLT, my_signal_handler); signal(SIGFPE, my_signal_handler); signal(SIGSEGV, my_signal_handler); ... }
1.5. 이제 프로그램에 버그가 있어 비정상 종료되는 경우 함수의 호출위치가 dump 되는 것을 확인할 수 있을겁니다.
세상에 버그없는 프로그램만 만드는 사람은 없습니다.
꼭 위 방법 적용해두시면 적어도 버그 잡는 시간을 줄일 수 있습니다.
본 방법을 사용하시길 강력히 권장합니다.
꼭 위 방법 적용해두시면 적어도 버그 잡는 시간을 줄일 수 있습니다.
본 방법을 사용하시길 강력히 권장합니다.