C

거믄마루 2013. 1. 11. 11:25
호출 스택은 함수가 함수를 부르는 프로그램 실행을 처리하기 위한 구조이다.
gdb 등 디버거에서 어떤 프로세스의 실행 이미지에 대해서 bt 명령을 치면 호출 스택의 정보가 나오게 된다.
일반 프로세스들에서도 이러한 호출 스택 정보를 알아낼 수 있는 방법이 있는데 바로 backtrace 함수이다.

현재의 호출 스택을 알아내는 man 페이지의 예제는 다음과 같다.

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

       void
       myfunc3(void)
       {
           int j, nptrs;
       #define SIZE 100
           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);
       }

       static void   /* "static" means don't export the symbol... */
       myfunc2(void)
       {
           myfunc3();
       }

       void
       myfunc(int ncalls)
       {
           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);
           }

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


좀 복잡해서 다른 시스템(OSX)의 더 간단한 예도 같이 들어보겠다.


 EXAMPLE
         #include <execinfo.h>
         #include <stdio.h>
         ...
         void* callstack[128];
         int i, frames = backtrace(callstack, 128);
         char** strs = backtrace_symbols(callstack, frames);
         for (i = 0; i < frames; ++i) {
             printf("%s\n", strs[i]);
         }
         free(strs);
         ...


즉, backtrace 함수로 호출 스택 구조와 호출 스택의 깊이를 알아내고,
backtrace_symbols 함수에서는 backtrace 함수에서 만들어진 자료구조를 다음과 같이 문자열로 표현한다.


           $ cc -rdynamic prog.c -o prog
           $ ./prog 3
           backtrace() returned 8 addresses
           ./prog(myfunc3+0x5c) [0x80487f0]
           ./prog [0x8048871]
           ./prog(myfunc+0x21) [0x8048894]
           ./prog(myfunc+0x1a) [0x804888d]
           ./prog(myfunc+0x1a) [0x804888d]
           ./prog(main+0x65) [0x80488fb]
           /lib/libc.so.6(__libc_start_main+0xdc) [0xb7e38f9c]
           ./prog [0x8048711]


그런데 컴파일시 -rdynamic 이라는 옵션을 줘야하는게 함정이다.
이 옵션을 주지 않으면 다음과 같이 괄호안에 호출된 함수명이 나오지 않게 된다.
(실제 실행결과는 아니고 위의 예를 직접 편집한 것이다.)

 

$ cc prog.c -o prog $ ./prog 3 backtrace() returned 8 addresses ./prog() [0x80487f0] ./prog [0x8048871] ./prog() [0x8048894] ./prog() [0x804888d] ./prog() [0x804888d] ./prog() [0x80488fb] /lib/libc.so.6() [0xb7e38f9c] ./prog [0x8048711]


어떤 시스템에서는 괄호자체도 없어져서 다음과 같은 포맷으로 나오는 경우도 있다.

(이것도 실제 실행결과가 아닌 편집화면이다.)

 
           $ cc prog.c -o prog
           $ ./prog 3
           backtrace() returned 8 addresses
           ./prog [0x80487f0]
           ./prog [0x8048871]
           ./prog [0x8048894]
           ./prog [0x804888d]
           ./prog [0x804888d]
           ./prog [0x80488fb]
           /lib/libc.so.6 [0xb7e38f9c]
           ./prog [0x8048711]


어찌되었든 모든 경우에 프로세스명은 호출 스택 배열의 첫번째 항목의 맨 앞에 나오게 되며 그 끝은 좌측괄호기호 또는 공백문자가 된다는 것이다.
그리고 프로세스명에 슬래시(/) 가 포함된 경우 마지막 슬래시 다음부터 프로세스명이라는 것이다.
이를 함수로 직접 구현해보면 다음과 같다.

 
int
get_process_name(char *buf, int bufsz)
{
    void *callstack[1];
    int frames;
    char **strs;
    char *ptr;

    if (buf == NULL)
    {
        return FAIL;
    }

    if (bufsz < 1)
    {
        return FAIL;
    }

    buf[0] = 0;

    frames = backtrace(callstack, 1);

    if (frames != 1)
    {
        return FAIL;
    }

    strs = backtrace_symbols(callstack, frames);

    if (strs == NULL || *strs == NULL)
    {
        return FAIL;
    }

    ptr = strchr(strs[0], '(');

    if (ptr)
    {
        *ptr = 0;
    }
    else
    {
        ptr = strchr(strs[0], ' ');

        if (ptr)
        {
            *ptr = 0;
        }
        else
        {
            return FAIL;
        }
    }

    ptr = strrchr(strs[0], '/');

    if (ptr)
    {
        strcpy(strs[0], strs[0] + (ptr + 1 - strs[0]));
    }

    snprintf(buf, bufsz, "%s", strs[0]);

    free(strs);

    return SUCCESS;
}


보통 SUCCESS 는 (0) 으로 FAIL 은 (-1) 로 정의해서 사용한다.

반환값이 SUCCESS 이면 출력버퍼 buf 에 프로세스명이 출력된다. bufsz 는 출력버퍼의 크기이다.

뭐 필요하신분들은 잘 쓰시기 바란다. 출처를 명시하는 것도 잊지마시고...