| 검색 | ?

1.1. 개요

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

비정상적인 종료가 될 때 도대체 어느 부분을 호출한 시점에서 죽었는지 확인하기 위한 방법에는 여러가지가 있으나 그 중에서 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 되는 것을 확인할 수 있을겁니다.

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

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

본 방법을 사용하시길 강력히 권장합니다.


Copyright ⓒ MINZKN.COM
All Rights Reserved.