IT TIP

프로그램 내부에서 gdb를 호출하여 스택 추적을 인쇄하는 가장 좋은 방법은 무엇입니까?

itqueen 2020. 12. 11. 21:07
반응형

프로그램 내부에서 gdb를 호출하여 스택 추적을 인쇄하는 가장 좋은 방법은 무엇입니까?


다음과 같은 기능 사용 :

#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>

void print_trace() {
    char pid_buf[30];
    sprintf(pid_buf, "--pid=%d", getpid());
    char name_buf[512];
    name_buf[readlink("/proc/self/exe", name_buf, 511)]=0;
    int child_pid = fork();
    if (!child_pid) {           
        dup2(2,1); // redirect output to stderr
        fprintf(stdout,"stack trace for %s pid=%s\n",name_buf,pid_buf);
        execlp("gdb", "gdb", "--batch", "-n", "-ex", "thread", "-ex", "bt", name_buf, pid_buf, NULL);
        abort(); /* If gdb failed to start */
    } else {
        waitpid(child_pid,NULL,0);
    }
}

출력에서 print_trace의 세부 사항을 봅니다.

다른 방법은 무엇입니까?


내 다른 답변 (현재 삭제 됨)에서 줄 번호도보고 싶다고 언급했습니다. 응용 프로그램 내부에서 gdb를 호출 할 때 어떻게해야할지 모르겠습니다.

하지만 gdb를 사용하지 않고 함수 이름과 각각의 줄 번호로 간단한 스택 트레이스를 인쇄하는 몇 가지 방법을 알려 드리겠습니다 . 대부분은 Linux Journal매우 멋진 기사에서 나왔습니다 .

  • 방법 # 1 :

첫 번째 방법은 실행 경로를 정확히 파악하기 위해 인쇄 및 로그 메시지와 함께 배포하는 것입니다. 복잡한 프로그램에서이 옵션은 일부 GCC 특정 매크로의 도움으로 약간 단순화 될 수 있더라도 번거롭고 지루할 수 있습니다. 예를 들어 다음과 같은 디버그 매크로를 고려하십시오.

 #define TRACE_MSG fprintf(stderr, __FUNCTION__     \
                          "() [%s:%d] here I am\n", \
                          __FILE__, __LINE__)

이 매크로를 잘라내어 붙여 넣어 프로그램 전체에 빠르게 전파 할 수 있습니다. 더 이상 필요하지 않으면 no-op으로 정의하여 간단히 끄십시오.

  • 방법 # 2 : (줄 번호에 대해서는 언급하지 않지만 방법 4에서 수행합니다.)

그러나 스택 역 추적을 얻는 더 좋은 방법은 glibc에서 제공하는 특정 지원 기능 중 일부를 사용하는 것입니다. 핵심은 backtrace ()로, 호출 지점에서 프로그램 시작까지 스택 프레임을 탐색하고 반환 주소 배열을 제공합니다. 그런 다음 nm 명령을 사용하여 개체 파일을 살펴봄으로써 각 주소를 코드의 특정 함수 본문에 매핑 할 수 있습니다. 또는 backtrace_symbols ()를 사용하여 더 간단한 방법으로 수행 할 수 있습니다. 이 함수는 backtrace ()에 의해 반환 된 반환 주소 목록을 문자열 목록으로 변환합니다. 각 목록에는 함수 내의 함수 이름 오프셋과 반환 주소가 포함됩니다. 문자열 목록은 힙 공간에서 할당되므로 (malloc ()을 호출 한 것처럼), 작업이 끝나면 바로 해제 ()해야합니다.

페이지에 소스 코드 예제 가 있으므로 읽어 보시기 바랍니다 . 주소를 함수 이름으로 변환하려면 -rdynamic 옵션을 사용하여 응용 프로그램을 컴파일해야합니다 .

  • 방법 # 3 : (방법 2를 수행하는 더 나은 방법)

이 기술에 대한 훨씬 더 유용한 응용 프로그램은 신호 핸들러 내부에 스택 역 추적을 넣고 후자가 프로그램이 수신 할 수있는 모든 "나쁜"신호 (SIGSEGV, SIGBUS, SIGILL, SIGFPE 등)를 포착하도록하는 것입니다. 이렇게하면 불행하게도 프로그램이 충돌하고 디버거로 실행하지 않은 경우 스택 추적을 얻고 오류가 발생한 위치를 알 수 있습니다. 이 기술은 또한 프로그램이 응답을 멈출 경우 프로그램이 반복되는 위치를 이해하는 데 사용할 수 있습니다

이 기술의 구현은 여기에서 사용할 수 있습니다 .

  • 방법 # 4 :

줄 번호를 인쇄하기 위해 방법 # 3에서 약간 개선했습니다. 이것은 방법 # 2에서도 작동하도록 복사 할 수 있습니다.

기본적으로, 팁 다음에 사용 하면 addr2line를 로를

주소를 파일 이름과 줄 번호로 변환합니다.

아래 소스 코드는 모든 로컬 함수에 대한 줄 번호를 인쇄합니다. 다른 라이브러리의 함수가 호출되면 ??:0파일 이름 대신 몇 가지가 표시 될 수 있습니다 .

#include <stdio.h>
#include <signal.h>
#include <stdio.h>
#include <signal.h>
#include <execinfo.h>

void bt_sighandler(int sig, struct sigcontext ctx) {

  void *trace[16];
  char **messages = (char **)NULL;
  int i, trace_size = 0;

  if (sig == SIGSEGV)
    printf("Got signal %d, faulty address is %p, "
           "from %p\n", sig, ctx.cr2, ctx.eip);
  else
    printf("Got signal %d\n", sig);

  trace_size = backtrace(trace, 16);
  /* overwrite sigaction with caller's address */
  trace[1] = (void *)ctx.eip;
  messages = backtrace_symbols(trace, trace_size);
  /* skip first stack frame (points here) */
  printf("[bt] Execution path:\n");
  for (i=1; i<trace_size; ++i)
  {
    printf("[bt] #%d %s\n", i, messages[i]);

    /* find first occurence of '(' or ' ' in message[i] and assume
     * everything before that is the file name. (Don't go beyond 0 though
     * (string terminator)*/
    size_t p = 0;
    while(messages[i][p] != '(' && messages[i][p] != ' '
            && messages[i][p] != 0)
        ++p;

    char syscom[256];
    sprintf(syscom,"addr2line %p -e %.*s", trace[i], p, messages[i]);
        //last parameter is the file name of the symbol
    system(syscom);
  }

  exit(0);
}


int func_a(int a, char b) {

  char *p = (char *)0xdeadbeef;

  a = a + b;
  *p = 10;  /* CRASH here!! */

  return 2*a;
}


int func_b() {

  int res, a = 5;

  res = 5 + func_a(a, 't');

  return res;
}


int main() {

  /* Install our signal handler */
  struct sigaction sa;

  sa.sa_handler = (void *)bt_sighandler;
  sigemptyset(&sa.sa_mask);
  sa.sa_flags = SA_RESTART;

  sigaction(SIGSEGV, &sa, NULL);
  sigaction(SIGUSR1, &sa, NULL);
  /* ... add any other signal here */

  /* Do something */
  printf("%d\n", func_b());
}

이 코드는 다음과 같이 컴파일해야합니다. gcc sighandler.c -o sighandler -rdynamic

프로그램은 다음을 출력합니다.

Got signal 11, faulty address is 0xdeadbeef, from 0x8048975
[bt] Execution path:
[bt] #1 ./sighandler(func_a+0x1d) [0x8048975]
/home/karl/workspace/stacktrace/sighandler.c:44
[bt] #2 ./sighandler(func_b+0x20) [0x804899f]
/home/karl/workspace/stacktrace/sighandler.c:54
[bt] #3 ./sighandler(main+0x6c) [0x8048a16]
/home/karl/workspace/stacktrace/sighandler.c:74
[bt] #4 /lib/tls/i686/cmov/libc.so.6(__libc_start_main+0xe6) [0x3fdbd6]
??:0
[bt] #5 ./sighandler() [0x8048781]
??:0

최신 Linux 커널 버전에 대한 2012/04/28 업데이트 , 위 sigaction서명은 더 이상 사용되지 않습니다. 또한 이 답변 에서 실행 가능한 이름을 잡아서 조금 개선했습니다 . 다음은 최신 버전입니다 .

char* exe = 0;

int initialiseExecutableName() 
{
    char link[1024];
    exe = new char[1024];
    snprintf(link,sizeof link,"/proc/%d/exe",getpid());
    if(readlink(link,exe,sizeof link)==-1) {
        fprintf(stderr,"ERRORRRRR\n");
        exit(1);
    }
    printf("Executable name initialised: %s\n",exe);
}

const char* getExecutableName()
{
    if (exe == 0)
        initialiseExecutableName();
    return exe;
}

/* get REG_EIP from ucontext.h */
#define __USE_GNU
#include <ucontext.h>

void bt_sighandler(int sig, siginfo_t *info,
                   void *secret) {

  void *trace[16];
  char **messages = (char **)NULL;
  int i, trace_size = 0;
  ucontext_t *uc = (ucontext_t *)secret;

  /* Do something useful with siginfo_t */
  if (sig == SIGSEGV)
    printf("Got signal %d, faulty address is %p, "
           "from %p\n", sig, info->si_addr, 
           uc->uc_mcontext.gregs[REG_EIP]);
  else
    printf("Got signal %d\n", sig);

  trace_size = backtrace(trace, 16);
  /* overwrite sigaction with caller's address */
  trace[1] = (void *) uc->uc_mcontext.gregs[REG_EIP];

  messages = backtrace_symbols(trace, trace_size);
  /* skip first stack frame (points here) */
  printf("[bt] Execution path:\n");
  for (i=1; i<trace_size; ++i)
  {
    printf("[bt] %s\n", messages[i]);

    /* find first occurence of '(' or ' ' in message[i] and assume
     * everything before that is the file name. (Don't go beyond 0 though
     * (string terminator)*/
    size_t p = 0;
    while(messages[i][p] != '(' && messages[i][p] != ' '
            && messages[i][p] != 0)
        ++p;

    char syscom[256];
    sprintf(syscom,"addr2line %p -e %.*s", trace[i] , p, messages[i] );
           //last parameter is the filename of the symbol
    system(syscom);

  }
  exit(0);
}

다음과 같이 초기화하십시오.

int main() {

  /* Install our signal handler */
  struct sigaction sa;

  sa.sa_sigaction = (void *)bt_sighandler;
  sigemptyset (&sa.sa_mask);
  sa.sa_flags = SA_RESTART | SA_SIGINFO;

  sigaction(SIGSEGV, &sa, NULL);
  sigaction(SIGUSR1, &sa, NULL);
  /* ... add any other signal here */

  /* Do something */
  printf("%d\n", func_b());

}

Linux를 사용하는 경우 표준 C 라이브러리에는 backtrace프레임의 반환 주소로 배열을 채우는라는 함수 backtrace_symbols주소를 가져와 backtrace해당 함수 이름을 조회하는 라는 또 다른 함수 가 포함되어 있습니다 . 이것들은 GNU C Library 매뉴얼에 문서화되어 있습니다 .

Those won't show argument values, source lines, and the like, and they only apply to the calling thread. However, they should be a lot faster (and perhaps less flaky) than running GDB that way, so they have their place.


nobar posted a fantastic answer. In short;

So you want a stand-alone function that prints a stack trace with all of the features that gdb stack traces have and that doesn't terminate your application. The answer is to automate the launch of gdb in a non-interactive mode to perform just the tasks that you want.

This is done by executing gdb in a child process, using fork(), and scripting it to display a stack-trace while your application waits for it to complete. This can be performed without the use of a core-dump and without aborting the application.

I believe that this is what you are looking for, @Vi


Isn't abort() simpler?

That way if it happens in the field the customer can send you the core file (I don't know many users who are involved enough in my application to want me to force them to debug it).

참고URL : https://stackoverflow.com/questions/3151779/best-way-to-invoke-gdb-from-inside-program-to-print-its-stacktrace

반응형