Как получить трассировку стека для C ++, используя gcc с информацией о номере строки? - PullRequest
56 голосов
/ 09 января 2011

Мы используем трассировку стека в проприетарном макросе assert для обнаружения ошибок разработчика - при обнаружении ошибки выводится трассировка стека.

Я нахожу пары gcc backtrace() / backtrace_symbols() методами недостаточно:

  1. Имена искажены
  2. Нет информации о строке

1-ю проблему можно решить с помощью abi :: __ cxa_demangle .

Однако 2-я проблема более сложная.Я нашел замену для backtrace_symbols () .Это лучше, чем gtra's backtrace_symbols (), так как он может извлекать номера строк (если они скомпилированы с -g) и вам не нужно компилировать с -rdynamic.

Hoverer, этот код лицензирован GNU, поэтому IMHO яне может использовать его в коммерческом коде.

Любое предложение?

PS

GDB способен распечатывать аргументы, передаваемые функциям.Наверное, это уже слишком много, чтобы просить:)

PS 2

Подобный вопрос (спасибо nobar)

Ответы [ 12 ]

37 голосов
/ 19 января 2011

Итак, вам нужна автономная функция, которая печатает трассировку стека со всеми функциями, которые есть у трассировки стека GDB, и которая не завершает работу вашего приложения.Ответ заключается в том, чтобы автоматизировать запуск GDB в неинтерактивном режиме, чтобы выполнять только те задачи, которые вы хотите.

Это делается путем выполнения GDB в дочернем процессе с использованием fork () и сценарием его дляотображать трассировку стека, пока ваше приложение ожидает его завершения.Это может быть выполнено без использования дампа памяти и без прерывания работы приложения.Изучив этот вопрос, я узнал, как это сделать: Как лучше вызывать gdb из программы для печати его стековой трассировки?

Пример, опубликованный с этим вопросом, не работал для меня точнокак написано, вот моя «исправленная» версия (я запустил ее в Ubuntu 9.04).

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

void print_trace() {
    char pid_buf[30];
    sprintf(pid_buf, "%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);
    }
}

Как показано в указанном вопросе, gdb предоставляет дополнительные опции, которые вы можете использовать.Например, использование «bt full» вместо «bt» создает еще более подробный отчет (локальные переменные включаются в вывод).Справочные страницы для GDB довольно просты, но полная документация доступна здесь .

Поскольку это основано на GDB, вывод включает в себя выделенные имена , номера строк , аргументы функции и, необязательно, даже локальные переменные .Кроме того, GDB поддерживает потоки, поэтому вы должны иметь возможность извлекать некоторые специфичные для потока метаданные.

Вот пример типа трассировки стека, который я вижу с помощью этого метода.

0x00007f97e1fc2925 in waitpid () from /lib/libc.so.6
[Current thread is 0 (process 15573)]
#0  0x00007f97e1fc2925 in waitpid () from /lib/libc.so.6
#1  0x0000000000400bd5 in print_trace () at ./demo3b.cpp:496
2  0x0000000000400c09 in recursive (i=2) at ./demo3b.cpp:636
3  0x0000000000400c1a in recursive (i=1) at ./demo3b.cpp:646
4  0x0000000000400c1a in recursive (i=0) at ./demo3b.cpp:646
5  0x0000000000400c46 in main (argc=1, argv=0x7fffe3b2b5b8) at ./demo3b.cpp:70

Примечание: я обнаружил, что это несовместимо с использованием valgrind (возможно, из-за использования виртуальной машины Valgrind).Он также не работает, когда вы запускаете программу внутри сеанса GDB (невозможно применить второй экземпляр «ptrace» к процессу).

28 голосов
/ 13 января 2011

Не так давно Я ответил на аналогичный вопрос . Вы должны взглянуть на исходный код, доступный для метода # 4, который также печатает номера строк и имена файлов.

  • Метод № 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
11 голосов
/ 17 января 2011

По этому же вопросу ведется активное обсуждение: Как создать трассировку стека при сбое моего приложения gcc C ++ . Предоставлено много предложений, включая множество обсуждений о том, как генерировать трассировки стека во время выполнения.

Мой личный любимый ответ из этой цепочки состоял в том, чтобы включить дампы ядра , что позволяет просматривать полное состояние приложения во время сбоя (включая аргументы функций, номера строк и не исправленные имена). Дополнительным преимуществом этого подхода является то, что он работает не только для утверждений , но также для ошибок сегментации и необработанных исключений .

Разные оболочки Linux используют разные команды для включения дампов ядра, но вы можете сделать это из кода своего приложения с помощью чего-то подобного ...

#include <sys/resource.h>
...
struct rlimit core_limit = { RLIM_INFINITY, RLIM_INFINITY };
assert( setrlimit( RLIMIT_CORE, &core_limit ) == 0 ); // enable core dumps for debug builds

После сбоя запустите ваш любимый отладчик, чтобы проверить состояние программы.

$ kdbg executable core

Вот пример вывода ...

alt text

Также можно извлечь трассировку стека из дампа памяти в командной строке.

$ ( CMDFILE=$(mktemp); echo "bt" >${CMDFILE}; gdb 2>/dev/null --batch -x ${CMDFILE} temp.exe core )
Core was generated by `./temp.exe'.
Program terminated with signal 6, Aborted.
[New process 22857]
#0  0x00007f4189be5fb5 in raise () from /lib/libc.so.6
#0  0x00007f4189be5fb5 in raise () from /lib/libc.so.6
#1  0x00007f4189be7bc3 in abort () from /lib/libc.so.6
#2  0x00007f4189bdef09 in __assert_fail () from /lib/libc.so.6
#3  0x00000000004007e8 in recursive (i=5) at ./demo1.cpp:18
#4  0x00000000004007f3 in recursive (i=4) at ./demo1.cpp:19
#5  0x00000000004007f3 in recursive (i=3) at ./demo1.cpp:19
#6  0x00000000004007f3 in recursive (i=2) at ./demo1.cpp:19
#7  0x00000000004007f3 in recursive (i=1) at ./demo1.cpp:19
#8  0x00000000004007f3 in recursive (i=0) at ./demo1.cpp:19
#9  0x0000000000400849 in main (argc=1, argv=0x7fff2483bd98) at ./demo1.cpp:26
6 голосов
/ 13 января 2011

Поскольку лицензионный код GPL предназначен для помощи вам во время разработки, вы можете просто не включать его в конечный продукт.GPL ограничивает вас от распространения кода лицензий GPL, связанного с не-GPL-совместимым кодом.Пока вы используете только код GPL, все будет в порядке.

6 голосов
/ 09 января 2011

Используйте библиотеку Google Glog для этого.У него есть новая лицензия BSD.

Содержит функцию GetStackTrace в файле stacktrace.h.

РЕДАКТИРОВАТЬ

Я нашел здесь http://blog.bigpixel.ro/2010/09/09/stack-unwinding-stack-trace-with-gcc/ что существует утилита addr2line, которая переводит программные адреса в имена файлов и номера строк.

http://linuxcommand.org/man_pages/addr2line1.html

5 голосов
/ 18 января 2011

Вот альтернативный подход. Макрос debug_assert () программно устанавливает условную точку останова . Если вы работаете в отладчике, вы достигнете точки останова, когда выражение assert равно false - и вы можете проанализировать живой стек (программа не завершается). Если вы не работаете в отладчике, сбой debug_assert () приводит к прерыванию работы программы и вы получаете дамп ядра, из которого вы можете проанализировать стек (см. Мой предыдущий ответ).

Преимущество этого подхода по сравнению с обычными утверждениями заключается в том, что вы можете продолжить выполнение программы после запуска debug_assert (при запуске в отладчике). Другими словами, debug_assert () немного более гибок, чем assert ().

   #include <iostream>
   #include <cassert>
   #include <sys/resource.h> 

// note: The assert expression should show up in
// stack trace as parameter to this function
void debug_breakpoint( char const * expression )
   {
   asm("int3"); // x86 specific
   }

#ifdef NDEBUG
   #define debug_assert( expression )
#else
// creates a conditional breakpoint
   #define debug_assert( expression ) \
      do { if ( !(expression) ) debug_breakpoint( #expression ); } while (0)
#endif

void recursive( int i=0 )
   {
   debug_assert( i < 5 );
   if ( i < 10 ) recursive(i+1);
   }

int main( int argc, char * argv[] )
   {
   rlimit core_limit = { RLIM_INFINITY, RLIM_INFINITY };
   setrlimit( RLIMIT_CORE, &core_limit ); // enable core dumps
   recursive();
   }

Примечание. Иногда настройка «условных точек останова» в отладчиках может быть медленной. Программно устанавливая точку останова, производительность этого метода должна быть эквивалентна производительности обычного assert ().

Примечание. Как написано, это относится к архитектуре Intel x86 - другие процессоры могут иметь разные инструкции для генерации точки останова.

4 голосов
/ 02 ноября 2011

Немного поздно, но вы можете использовать libbfb для извлечения имени файла и белья, как это делает refdbg в symsnarf.c . libbfb внутренне используется addr2line и gdb

2 голосов
/ 01 марта 2013

Вы можете использовать DeathHandler - небольшой класс C ++, который делает все для вас, надежно.

2 голосов
/ 13 января 2011

Одно из решений - запустить gdb со скриптом "bt" в обработчике сбоя assert.Не так просто интегрировать такой запуск gdb, но он даст вам как backtrace и args, так и demangle имена (или вы можете передать вывод gdb через программу c ++ filta).

Обе программы (gdb иc ++ fille) не будет связан с вашим приложением, поэтому GPL не потребует от вас полного завершения приложения с открытым исходным кодом.

Тот же подход (кроме программы GPL), который вы можете использовать с символами возврата.Просто сгенерируйте ascii список% eip и карту exec-файла (/ proc / self / maps) и передайте его в отдельный двоичный файл.

1 голос
/ 18 января 2011

Вот мой третий ответ - все еще пытаюсь воспользоваться дампами ядра.

В вопросе не совсем ясно, должны ли «assert-like» макросы завершать приложение (как это делает assert) или они должны были продолжать выполняться после генерации трассировки стека.

В этом ответе я рассматриваю случай, когда вы хотите показать трассировку стека и продолжить выполнение. Я написал функцию coredump () ниже, чтобы создать дамп ядра, автоматически извлекает из него трассировку стека , а затем продолжает выполнение программы.

Использование такое же, как у assert (). Разница, конечно, в том, что assert () завершает программу, а coredump_assert () - нет.

   #include <iostream>
   #include <sys/resource.h> 
   #include <cstdio>
   #include <cstdlib>
   #include <boost/lexical_cast.hpp>
   #include <string>
   #include <sys/wait.h>
   #include <unistd.h>

   std::string exename;

// expression argument is for diagnostic purposes (shows up in call-stack)
void coredump( char const * expression )
   {

   pid_t childpid = fork();

   if ( childpid == 0 ) // child process generates core dump
      {
      rlimit core_limit = { RLIM_INFINITY, RLIM_INFINITY };
      setrlimit( RLIMIT_CORE, &core_limit ); // enable core dumps
      abort(); // terminate child process and generate core dump
      }

// give each core-file a unique name
   if ( childpid > 0 ) waitpid( childpid, 0, 0 );
   static int count=0;
   using std::string;
   string pid = boost::lexical_cast<string>(getpid());
   string newcorename = "core-"+boost::lexical_cast<string>(count++)+"."+pid;
   string rawcorename = "core."+boost::lexical_cast<string>(childpid);
   int rename_rval = rename(rawcorename.c_str(),newcorename.c_str()); // try with core.PID
   if ( rename_rval == -1 ) rename_rval = rename("core",newcorename.c_str()); // try with just core
   if ( rename_rval == -1 ) std::cerr<<"failed to capture core file\n";

  #if 1 // optional: dump stack trace and delete core file
   string cmd = "( CMDFILE=$(mktemp); echo 'bt' >${CMDFILE}; gdb 2>/dev/null --batch -x ${CMDFILE} "+exename+" "+newcorename+" ; unlink ${CMDFILE} )";
   int system_rval = system( ("bash -c '"+cmd+"'").c_str() );
   if ( system_rval == -1 ) std::cerr.flush(), perror("system() failed during stack trace"), fflush(stderr);
   unlink( newcorename.c_str() );
  #endif

   }

#ifdef NDEBUG
   #define coredump_assert( expression ) ((void)(expression))
#else
   #define coredump_assert( expression ) do { if ( !(expression) ) { coredump( #expression ); } } while (0)
#endif

void recursive( int i=0 )
   {
   coredump_assert( i < 2 );
   if ( i < 4 ) recursive(i+1);
   }

int main( int argc, char * argv[] )
   {
   exename = argv[0]; // this is used to generate the stack trace
   recursive();
   }

Когда я запускаю программу, она отображает три следа стека ...

Core was generated by `./temp.exe'.                                         
Program terminated with signal 6, Aborted.
[New process 24251]
#0  0x00007f2818ac9fb5 in raise () from /lib/libc.so.6
#0  0x00007f2818ac9fb5 in raise () from /lib/libc.so.6
#1  0x00007f2818acbbc3 in abort () from /lib/libc.so.6
#2  0x0000000000401a0e in coredump (expression=0x403303 "i < 2") at ./demo3.cpp:29
#3  0x0000000000401f5f in recursive (i=2) at ./demo3.cpp:60
#4  0x0000000000401f70 in recursive (i=1) at ./demo3.cpp:61
#5  0x0000000000401f70 in recursive (i=0) at ./demo3.cpp:61
#6  0x0000000000401f8b in main (argc=1, argv=0x7fffc229eb98) at ./demo3.cpp:66
Core was generated by `./temp.exe'.
Program terminated with signal 6, Aborted.
[New process 24259]
#0  0x00007f2818ac9fb5 in raise () from /lib/libc.so.6
#0  0x00007f2818ac9fb5 in raise () from /lib/libc.so.6
#1  0x00007f2818acbbc3 in abort () from /lib/libc.so.6
#2  0x0000000000401a0e in coredump (expression=0x403303 "i < 2") at ./demo3.cpp:29
#3  0x0000000000401f5f in recursive (i=3) at ./demo3.cpp:60
#4  0x0000000000401f70 in recursive (i=2) at ./demo3.cpp:61
#5  0x0000000000401f70 in recursive (i=1) at ./demo3.cpp:61
#6  0x0000000000401f70 in recursive (i=0) at ./demo3.cpp:61
#7  0x0000000000401f8b in main (argc=1, argv=0x7fffc229eb98) at ./demo3.cpp:66
Core was generated by `./temp.exe'.
Program terminated with signal 6, Aborted.
[New process 24267]
#0  0x00007f2818ac9fb5 in raise () from /lib/libc.so.6
#0  0x00007f2818ac9fb5 in raise () from /lib/libc.so.6
#1  0x00007f2818acbbc3 in abort () from /lib/libc.so.6
#2  0x0000000000401a0e in coredump (expression=0x403303 "i < 2") at ./demo3.cpp:29
#3  0x0000000000401f5f in recursive (i=4) at ./demo3.cpp:60
#4  0x0000000000401f70 in recursive (i=3) at ./demo3.cpp:61
#5  0x0000000000401f70 in recursive (i=2) at ./demo3.cpp:61
#6  0x0000000000401f70 in recursive (i=1) at ./demo3.cpp:61
#7  0x0000000000401f70 in recursive (i=0) at ./demo3.cpp:61
#8  0x0000000000401f8b in main (argc=1, argv=0x7fffc229eb98) at ./demo3.cpp:66
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...