티스토리 뷰


디버깅 방법에는 많은 종류가 있지만, 사건이 발생 했을때 바로 무언가를 남겨주는 것이 최고의 디버그 메시지가 아닌가 싶다. 관련해서 backtrace addr2line 을 소개하도록 한다.


일단 관련 된 소스는 아래와 같다. 소스의 95%가 vim에서 제공되는 backtrace의 man page 내용이다. 해당 소스는 문제가 있고 죽도록 설계(?)되어 있다. (man page의 오리지널 소스가 그렇다는 것은 아님)

#include <execinfo.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#include <signal.h>
/* sig ==> signal number */
void calltrace(int sig)
{
    int j, nptrs;
#define SIZE 3 
    void *buffer[100];
    char **strings;

    nptrs = backtrace(buffer, SIZE);
    printf("backtrace() returned %d addresses\n", nptrs);

    /* The call backtrace_symbols_fd(buffer, nptrs, STDOUT_FILENO)
       would produce similar output to the following: */

    strings = backtrace_symbols(buffer, nptrs);
    if (strings == NULL) {
        perror("backtrace_symbols");
        exit(EXIT_FAILURE);
    }


    for (j = 0; j < nptrs; j++)
        printf(">>> %s\n", strings[j]);

    free(strings);
    exit(255);
}

void
myfunc3(void)
{
    fprintf(stderr, "%s \n", __func__);

    int *p = NULL;
    *p = 1;
}

static void   /* "static" means don't export the symbol... */
myfunc2(void)
{
    fprintf(stderr, "%s \n", __func__);
    myfunc3();
}

void
myfunc(int ncalls)
{
    fprintf(stderr, "%s \n", __func__);
    if (ncalls > 1)
        myfunc(ncalls - 1);
    else
        myfunc2();
}

int
main(int argc, char *argv[])
{
    if (argc != 2) {
        fprintf(stderr, "%s num-calls\n", argv[0]);
        exit(EXIT_FAILURE);
    }
    (void) signal (SIGSEGV,  calltrace);

    myfunc(atoi(argv[1]));
    exit(EXIT_SUCCESS);
}

위 소스에는 man 페이지에서 제공되지 않는 항목이 두가지 있다. 첫번째는 시그널 등록이다. 개발하는 프로세스가 언제 어떤 이유로든 죽을 수 있기 때문에 발생하는 시그널을 잡도록 한다.


통상 SIGSEGV(Segmentation Fault)이 많이 발생되므로 예제에서는 해당 시그널만 등록했다.

(void) signal (SIGSEGV,  calltrace);


의미는 SIGSEGV가 발생되면 calltrace라는 함수를 호출하라는 뜻이고, calltrace 함수 역할은 backtrace를 통해 특정 깊이 만큼의 함수 trace를 출력해준다.


테스트를 해보자. 일단 아래와 같이 일반적인 방법으로 컴파일을 하고 실행하도록 한다.

$ gcc prog.c -o prog


실행해서 결과를 확인한다.



무언가 메모리 주소로 보이는 것이 출력된다. 하지만 이것만으로는 그 어떤것도 확인할 수가 없다. (지금 우리 실력으로는 말이다.)


그래서 이번에는 gcc에 -rdynamic 이라는 옵션을 줘서 컴파일 하도록 한다.

$ gcc -rdynamic prog.c -o prog




오! 이번에는 죽는 위치의 힌트(?) 정도를 제공 받게 되었다. 


myfunc3 이라는 함수의 0x2b 만큼 떨어진 곳에서 문제가 발생된 모양이다. 그게 어딘데?


그걸 찾을 수 있는 방법에는 컴파일시에 System.map을 생성하거나 nm 명령어 등을 사용할 수 있지만 우리는 이번 포스팅의 목적에 맞도록 addr2line를 사용하도록 한다. addr2line에 인자로 죽은 위치로 추정되는 0x8048923을 넣어보자.


prog 프로세스가 죽는 정확한 위치를 찍어줄 것이다!


$ addr2line -e prog 0x8048923



??:?


출력되지 않았다. 당황하지 말고.. 컴파일시에 추가적으로 -g 옵션을 주도록 하자.


$ gcc -g -rdynamic prog.c -o prog


그리고 다시 확인해보도록 하자.


정확하게 문제가 되는 소스의 위치를 찾았다.

/root/bin/oops/prog.c:39


해당 소스의 위치는 아래와 같다.

void
myfunc3(void)
{
    fprintf(stderr, "%s \n", __func__);

    int *p = NULL;
    *p = 1;  // HERE
}


예제 소스에서는 print로 죽는 위치를 찍어놨지만, 실제 운영할때는 별도의 파일 디버깅 등을 통해 유연하게 사용하도록 하자.



댓글
  • 프로필사진 비밀댓글입니다 2016.07.27 23:45
  • 프로필사진 BlogIcon Jaeyeon Baek 해당 바이너리가 -g 옵션 없이 컴파일 된 것으로 보입니다. 혹은 CPU 타입에 따라 addr2line이 아닌 다른 접근을 해야 할 수도 있습니다.. 2016.07.28 08:50 신고
  • 프로필사진 이재원 많은 도움이 되었습니다~
    추가적으로 addr2line 호출시 함수명을 보여주는 f 옵션을 추가하면 -g 옵션 없이 컴파일된 경우에도
    함수명은 나오는 것 같네요!
    2017.12.13 14:57
  • 프로필사진 BlogIcon Jaeyeon Baek 함수가 길고 복잡한 경우에는 함수이름 출력되는게 큰 도움이 안됩니다. :-) 뭐 그것조차 안나오는 것 보다는 낫겠지만요. ㅎㅎ 2017.12.13 18:14 신고
댓글쓰기 폼