Application에서의 backtrace(call stack) dump 방법 (비정상 종료에 대한 디버깅)

대문 / 프로그래밍 / Application에서의 backtrace(call stack) dump 방법 (비정상 종료에 대한 디버깅)

Application에서의 backtrace(call stack) dump 방법 (비정상 종료에 대한 디버깅)


개요

Application에 문제가 있는 코드로 인하여 비정상 종료되는 경우 어디가 문제인지를 확인하기 매우 어려울 때가 있습니다.

비정상적인 종료가 될 때 도대체 어느 부분을 호출한 시점에서 죽었는지 확인하기 위한 방법에는 여러가지가 있으나 그 중에서 backtrace 기법을 소개하고 정리해보았습니다.

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 명령어로 심볼을 확인할 수도 있기 때문입니다.
    


프로그램의 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);
}


삽입했던 함수를 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);

    ...
}


이제 프로그램에 버그가 있어 비정상 종료되는 경우 함수의 호출위치가 dump 되는 것을 확인할 수 있을겁니다.

세상에 버그없는 프로그램만 만드는 사람은 없습니다.

꼭 위 방법 적용해두시면 적어도 버그 잡는 시간을 줄일 수 있습니다.

본 방법을 사용하시길 강력히 권장합니다.
Retrieved from https://www.minzkn.com:443/moniwiki/wiki.php/backtrace
last modified 2018-11-30 04:38:52