стек вызовов печати на C или C ++


120

Есть ли способ сбрасывать стек вызовов в запущенном процессе на C или C ++ каждый раз, когда вызывается определенная функция? Я имею в виду примерно следующее:

void foo()
{
   print_stack_trace();

   // foo's body

   return
}

Где print_stack_traceработает аналогично callerPerl.

Или что-то вроде этого:

int main (void)
{
    // will print out debug info every time foo() is called
    register_stack_trace_function(foo); 

    // etc...
}

где register_stack_trace_functionставит какую-то внутреннюю точку останова, которая будет вызывать печать трассировки стека при каждом fooвызове.

Есть ли что-нибудь подобное в какой-нибудь стандартной библиотеке C?

Я работаю над Linux, использую GCC.


Задний план

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


1
@Armen, тебе что-нибудь знакомо?
Натан Феллман,

1
@Nathan: Если ваш отладчик - gdb, он может справиться с этим случаем . Я не могу рассказать вам о других, но полагаю, что не только GDB обладает такой функциональностью. В сторону: я только что просмотрел свой предыдущий комментарий. :: gag :: s/easier/either/как, черт возьми, это случилось?
dmckee --- котенок экс-модератора

2
@dmckee: На самом деле так и должно быть s/either/easier. Что мне нужно сделать с gdb, так это написать сценарий, который прерывает эту функцию и распечатывает трассировку стека, а затем продолжает. Теперь, когда я думаю об этом, возможно, мне пора узнать о сценариях gdb.
Натан Феллман

1
Г! Собираюсь немного поспать.
Уже совсем

Ответы:


79

Для решения только для Linux вы можете использовать backtrace (3), который просто возвращает массив void *(фактически каждый из них указывает на адрес возврата из соответствующего кадра стека). Чтобы преобразовать их во что-то полезное, есть backtrace_symbols (3) .

Обратите внимание на раздел примечаний в backtrace (3) :

Имена символов могут быть недоступны без использования специальных опций компоновщика. Для систем, использующих компоновщик GNU, необходимо использовать параметр компоновщика -rdynamic. Обратите внимание, что имена «статических» функций не отображаются и не будут доступны в трассировке.


10
FWIW, эта функция также существует в Mac OS X: developer.apple.com/library/mac/#documentation/Darwin/Reference/…
EmeryBerger

9
В Windows есть CaptureStackBackTrace
bobobobo,


К glibcсожалению, в Linux backtrace_symbolsфункции не предоставляют имя функции, имя исходного файла и номер строки.
Максим Егорушкин

Помимо использования -rdynamic, также убедитесь, что ваша система сборки не добавляет -fvisibility=hiddenпараметр! (так как это полностью отбросит эффект -rdynamic)
Дима Литвинов

38

Ускорение трассировки стека

Документировано по адресу: https://www.boost.org/doc/libs/1_66_0/doc/html/stacktrace/getting_started.html#stacktrace.getting_started.how_to_print_current_call_stack

Это самый удобный вариант, который я видел до сих пор, потому что он:

  • действительно может распечатать номера строк.

    Он просто делает вызовы , addr2lineоднако , что это некрасиво и может быть медленным , если ваши принимают слишком много следов.

  • разоблачает по умолчанию

  • Boost - это только заголовок, поэтому, скорее всего, нет необходимости изменять вашу систему сборки

boost_stacktrace.cpp

#include <iostream>

#define BOOST_STACKTRACE_USE_ADDR2LINE
#include <boost/stacktrace.hpp>

void my_func_2(void) {
    std::cout << boost::stacktrace::stacktrace() << std::endl;
}

void my_func_1(double f) {
    (void)f;
    my_func_2();
}

void my_func_1(int i) {
    (void)i;
    my_func_2();
}

int main(int argc, char **argv) {
    long long unsigned int n;
    if (argc > 1) {
        n = strtoul(argv[1], NULL, 0);
    } else {
        n = 1;
    }
    for (long long unsigned int i = 0; i < n; ++i) {
        my_func_1(1);   // line 28
        my_func_1(2.0); // line 29
    }
}

К сожалению, это кажется более поздним дополнением, и пакет libboost-stacktrace-devотсутствует в Ubuntu 16.04, только 18.04:

sudo apt-get install libboost-stacktrace-dev
g++ -fno-pie -ggdb3 -O0 -no-pie -o boost_stacktrace.out -std=c++11 \
  -Wall -Wextra -pedantic-errors boost_stacktrace.cpp -ldl
./boost_stacktrace.out

Мы должны добавить -ldlв конце, иначе компиляция не удастся.

Вывод:

 0# boost::stacktrace::basic_stacktrace<std::allocator<boost::stacktrace::frame> >::basic_stacktrace() at /usr/include/boost/stacktrace/stacktrace.hpp:129
 1# my_func_1(int) at /home/ciro/test/boost_stacktrace.cpp:18
 2# main at /home/ciro/test/boost_stacktrace.cpp:29 (discriminator 2)
 3# __libc_start_main in /lib/x86_64-linux-gnu/libc.so.6
 4# _start in ./boost_stacktrace.out

 0# boost::stacktrace::basic_stacktrace<std::allocator<boost::stacktrace::frame> >::basic_stacktrace() at /usr/include/boost/stacktrace/stacktrace.hpp:129
 1# my_func_1(double) at /home/ciro/test/boost_stacktrace.cpp:13
 2# main at /home/ciro/test/boost_stacktrace.cpp:27 (discriminator 2)
 3# __libc_start_main in /lib/x86_64-linux-gnu/libc.so.6
 4# _start in ./boost_stacktrace.out

Вывод и поясняется далее в разделе «glibc backtrace» ниже, который аналогичен.

Обратите внимание на то, как my_func_1(int)и my_func_1(float), искаженные из-за перегрузки функций , были хорошо разобраны для нас.

Обратите внимание, что первые intвызовы отключены на одну строку (28 вместо 27, а второй - на две строки (27 вместо 29). В комментариях было высказано предположение, что это связано с тем, что рассматривается следующий адрес инструкции, который превращает 27 в 28, а 29 выпрыгивает из цикла и становится 27.

Затем мы видим, что с -O3, вывод полностью искажен:

 0# boost::stacktrace::basic_stacktrace<std::allocator<boost::stacktrace::frame> >::size() const at /usr/include/boost/stacktrace/stacktrace.hpp:215
 1# my_func_1(double) at /home/ciro/test/boost_stacktrace.cpp:12
 2# __libc_start_main in /lib/x86_64-linux-gnu/libc.so.6
 3# _start in ./boost_stacktrace.out

 0# boost::stacktrace::basic_stacktrace<std::allocator<boost::stacktrace::frame> >::size() const at /usr/include/boost/stacktrace/stacktrace.hpp:215
 1# main at /home/ciro/test/boost_stacktrace.cpp:31
 2# __libc_start_main in /lib/x86_64-linux-gnu/libc.so.6
 3# _start in ./boost_stacktrace.out

Обратные трассировки обычно безвозвратно искажаются оптимизацией. Оптимизация хвостового вызова является ярким примером этого: что такое оптимизация хвостового вызова?

Бенчмарк запущен -O3:

time  ./boost_stacktrace.out 1000 >/dev/null

Вывод:

real    0m43.573s
user    0m30.799s
sys     0m13.665s

Итак, как и ожидалось, мы видим, что этот метод чрезвычайно медленен для внешних вызовов addr2lineи будет возможен только в том случае, если выполняется ограниченное количество вызовов.

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

Проверено на Ubuntu 19.10, GCC 9.2.1, boost 1.67.0.

Glibc backtrace

Документировано по адресу: https://www.gnu.org/software/libc/manual/html_node/Backtraces.html

main.c

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

/* Paste this on the file you want to debug. */
#include <stdio.h>
#include <execinfo.h>
void print_trace(void) {
    char **strings;
    size_t i, size;
    enum Constexpr { MAX_SIZE = 1024 };
    void *array[MAX_SIZE];
    size = backtrace(array, MAX_SIZE);
    strings = backtrace_symbols(array, size);
    for (i = 0; i < size; i++)
        printf("%s\n", strings[i]);
    puts("");
    free(strings);
}

void my_func_3(void) {
    print_trace();
}

void my_func_2(void) {
    my_func_3();
}

void my_func_1(void) {
    my_func_3();
}

int main(void) {
    my_func_1(); /* line 33 */
    my_func_2(); /* line 34 */
    return 0;
}

Обобщение:

gcc -fno-pie -ggdb3 -O3 -no-pie -o main.out -rdynamic -std=c99 \
  -Wall -Wextra -pedantic-errors main.c

-rdynamic является ключевым обязательным параметром.

Бегать:

./main.out

Выходы:

./main.out(print_trace+0x2d) [0x400a3d]
./main.out(main+0x9) [0x4008f9]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x7f35a5aad830]
./main.out(_start+0x29) [0x400939]

./main.out(print_trace+0x2d) [0x400a3d]
./main.out(main+0xe) [0x4008fe]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x7f35a5aad830]
./main.out(_start+0x29) [0x400939]

Итак, мы сразу видим, что произошла оптимизация встраивания, и некоторые функции были потеряны из трассировки.

Если мы попытаемся получить адреса:

addr2line -e main.out 0x4008f9 0x4008fe

мы получаем:

/home/ciro/main.c:21
/home/ciro/main.c:36

который полностью выключен.

Если мы сделаем то же самое с -O0вместо этого, ./main.outдаст правильную полную трассировку:

./main.out(print_trace+0x2e) [0x4009a4]
./main.out(my_func_3+0x9) [0x400a50]
./main.out(my_func_1+0x9) [0x400a68]
./main.out(main+0x9) [0x400a74]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x7f4711677830]
./main.out(_start+0x29) [0x4008a9]

./main.out(print_trace+0x2e) [0x4009a4]
./main.out(my_func_3+0x9) [0x400a50]
./main.out(my_func_2+0x9) [0x400a5c]
./main.out(main+0xe) [0x400a79]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x7f4711677830]
./main.out(_start+0x29) [0x4008a9]

а потом:

addr2line -e main.out 0x400a74 0x400a79

дает:

/home/cirsan01/test/main.c:34
/home/cirsan01/test/main.c:35

так что линии расходятся только на одну, TODO, почему? Но это все еще можно использовать.

Вывод: обратные трассировки могут идеально отображаться только с -O0. При оптимизации исходная трассировка коренным образом изменяется в скомпилированном коде.

Я не смог найти простой способ автоматически разобрать символы C ++ с этим, однако вот несколько уловок:

Проверено на Ubuntu 16.04, GCC 6.4.0, libc 2.23.

Glibc backtrace_symbols_fd

Этот помощник немного удобнее, чем backtrace_symbolsи производит в основном идентичный вывод:

/* Paste this on the file you want to debug. */
#include <execinfo.h>
#include <stdio.h>
#include <unistd.h>
void print_trace(void) {
    size_t i, size;
    enum Constexpr { MAX_SIZE = 1024 };
    void *array[MAX_SIZE];
    size = backtrace(array, MAX_SIZE);
    backtrace_symbols_fd(array, size, STDOUT_FILENO);
    puts("");
}

Проверено на Ubuntu 16.04, GCC 6.4.0, libc 2.23.

glibc backtraceс C ++ разборчивым хаком 1: -export-dynamic+dladdr

Адаптировано из: https://gist.github.com/fmela/591333/c64f4eb86037bb237862a8283df70cdfc25f01d3

Это "взлом", потому что он требует изменения ELF с помощью -export-dynamic.

glibc_ldl.cpp

#include <dlfcn.h>     // for dladdr
#include <cxxabi.h>    // for __cxa_demangle

#include <cstdio>
#include <string>
#include <sstream>
#include <iostream>

// This function produces a stack backtrace with demangled function & method names.
std::string backtrace(int skip = 1)
{
    void *callstack[128];
    const int nMaxFrames = sizeof(callstack) / sizeof(callstack[0]);
    char buf[1024];
    int nFrames = backtrace(callstack, nMaxFrames);
    char **symbols = backtrace_symbols(callstack, nFrames);

    std::ostringstream trace_buf;
    for (int i = skip; i < nFrames; i++) {
        Dl_info info;
        if (dladdr(callstack[i], &info)) {
            char *demangled = NULL;
            int status;
            demangled = abi::__cxa_demangle(info.dli_sname, NULL, 0, &status);
            std::snprintf(
                buf,
                sizeof(buf),
                "%-3d %*p %s + %zd\n",
                i,
                (int)(2 + sizeof(void*) * 2),
                callstack[i],
                status == 0 ? demangled : info.dli_sname,
                (char *)callstack[i] - (char *)info.dli_saddr
            );
            free(demangled);
        } else {
            std::snprintf(buf, sizeof(buf), "%-3d %*p\n",
                i, (int)(2 + sizeof(void*) * 2), callstack[i]);
        }
        trace_buf << buf;
        std::snprintf(buf, sizeof(buf), "%s\n", symbols[i]);
        trace_buf << buf;
    }
    free(symbols);
    if (nFrames == nMaxFrames)
        trace_buf << "[truncated]\n";
    return trace_buf.str();
}

void my_func_2(void) {
    std::cout << backtrace() << std::endl;
}

void my_func_1(double f) {
    (void)f;
    my_func_2();
}

void my_func_1(int i) {
    (void)i;
    my_func_2();
}

int main() {
    my_func_1(1);
    my_func_1(2.0);
}

Скомпилируйте и запустите:

g++ -fno-pie -ggdb3 -O0 -no-pie -o glibc_ldl.out -std=c++11 -Wall -Wextra \
  -pedantic-errors -fpic glibc_ldl.cpp -export-dynamic -ldl
./glibc_ldl.out 

вывод:

1             0x40130a my_func_2() + 41
./glibc_ldl.out(_Z9my_func_2v+0x29) [0x40130a]
2             0x40139e my_func_1(int) + 16
./glibc_ldl.out(_Z9my_func_1i+0x10) [0x40139e]
3             0x4013b3 main + 18
./glibc_ldl.out(main+0x12) [0x4013b3]
4       0x7f7594552b97 __libc_start_main + 231
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xe7) [0x7f7594552b97]
5             0x400f3a _start + 42
./glibc_ldl.out(_start+0x2a) [0x400f3a]

1             0x40130a my_func_2() + 41
./glibc_ldl.out(_Z9my_func_2v+0x29) [0x40130a]
2             0x40138b my_func_1(double) + 18
./glibc_ldl.out(_Z9my_func_1d+0x12) [0x40138b]
3             0x4013c8 main + 39
./glibc_ldl.out(main+0x27) [0x4013c8]
4       0x7f7594552b97 __libc_start_main + 231
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xe7) [0x7f7594552b97]
5             0x400f3a _start + 42
./glibc_ldl.out(_start+0x2a) [0x400f3a]

Проверено на Ubuntu 18.04.

glibc backtraceс деманглингом C ++, хак 2: анализ вывода обратной трассировки

Показано по адресу: https://panthema.net/2008/0901-stacktrace-demangled/

Это взлом, потому что он требует синтаксического анализа.

TODO - скомпилировать и показать здесь.

libunwind

TODO есть ли у этого преимущества перед трассировкой glibc? Очень похожий вывод, также требует изменения команды сборки, но не является частью glibc, поэтому требуется установка дополнительного пакета.

Код адаптирован из: https://eli.thegreenplace.net/2015/programmatic-access-to-the-call-stack-in-c/

main.c

/* This must be on top. */
#define _XOPEN_SOURCE 700

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

/* Paste this on the file you want to debug. */
#define UNW_LOCAL_ONLY
#include <libunwind.h>
#include <stdio.h>
void print_trace() {
    char sym[256];
    unw_context_t context;
    unw_cursor_t cursor;
    unw_getcontext(&context);
    unw_init_local(&cursor, &context);
    while (unw_step(&cursor) > 0) {
        unw_word_t offset, pc;
        unw_get_reg(&cursor, UNW_REG_IP, &pc);
        if (pc == 0) {
            break;
        }
        printf("0x%lx:", pc);
        if (unw_get_proc_name(&cursor, sym, sizeof(sym), &offset) == 0) {
            printf(" (%s+0x%lx)\n", sym, offset);
        } else {
            printf(" -- error: unable to obtain symbol name for this frame\n");
        }
    }
    puts("");
}

void my_func_3(void) {
    print_trace();
}

void my_func_2(void) {
    my_func_3();
}

void my_func_1(void) {
    my_func_3();
}

int main(void) {
    my_func_1(); /* line 46 */
    my_func_2(); /* line 47 */
    return 0;
}

Скомпилируйте и запустите:

sudo apt-get install libunwind-dev
gcc -fno-pie -ggdb3 -O3 -no-pie -o main.out -std=c99 \
  -Wall -Wextra -pedantic-errors main.c -lunwind

Либо #define _XOPEN_SOURCE 700должно быть сверху, либо мы должны использовать -std=gnu99:

Бегать:

./main.out

Вывод:

0x4007db: (main+0xb)
0x7f4ff50aa830: (__libc_start_main+0xf0)
0x400819: (_start+0x29)

0x4007e2: (main+0x12)
0x7f4ff50aa830: (__libc_start_main+0xf0)
0x400819: (_start+0x29)

и:

addr2line -e main.out 0x4007db 0x4007e2

дает:

/home/ciro/main.c:34
/home/ciro/main.c:49

С -O0:

0x4009cf: (my_func_3+0xe)
0x4009e7: (my_func_1+0x9)
0x4009f3: (main+0x9)
0x7f7b84ad7830: (__libc_start_main+0xf0)
0x4007d9: (_start+0x29)

0x4009cf: (my_func_3+0xe)
0x4009db: (my_func_2+0x9)
0x4009f8: (main+0xe)
0x7f7b84ad7830: (__libc_start_main+0xf0)
0x4007d9: (_start+0x29)

и:

addr2line -e main.out 0x4009f3 0x4009f8

дает:

/home/ciro/main.c:47
/home/ciro/main.c:48

Проверено на Ubuntu 16.04, GCC 6.4.0, libunwind 1.1.

libunwind с распознаванием имен C ++

Код адаптирован из: https://eli.thegreenplace.net/2015/programmatic-access-to-the-call-stack-in-c/

unwind.cpp

#define UNW_LOCAL_ONLY
#include <cxxabi.h>
#include <libunwind.h>
#include <cstdio>
#include <cstdlib>
#include <iostream>

void backtrace() {
  unw_cursor_t cursor;
  unw_context_t context;

  // Initialize cursor to current frame for local unwinding.
  unw_getcontext(&context);
  unw_init_local(&cursor, &context);

  // Unwind frames one by one, going up the frame stack.
  while (unw_step(&cursor) > 0) {
    unw_word_t offset, pc;
    unw_get_reg(&cursor, UNW_REG_IP, &pc);
    if (pc == 0) {
      break;
    }
    std::printf("0x%lx:", pc);

    char sym[256];
    if (unw_get_proc_name(&cursor, sym, sizeof(sym), &offset) == 0) {
      char* nameptr = sym;
      int status;
      char* demangled = abi::__cxa_demangle(sym, nullptr, nullptr, &status);
      if (status == 0) {
        nameptr = demangled;
      }
      std::printf(" (%s+0x%lx)\n", nameptr, offset);
      std::free(demangled);
    } else {
      std::printf(" -- error: unable to obtain symbol name for this frame\n");
    }
  }
}

void my_func_2(void) {
    backtrace();
    std::cout << std::endl; // line 43
}

void my_func_1(double f) {
    (void)f;
    my_func_2();
}

void my_func_1(int i) {
    (void)i;
    my_func_2();
}  // line 54

int main() {
    my_func_1(1);
    my_func_1(2.0);
}

Скомпилируйте и запустите:

sudo apt-get install libunwind-dev
g++ -fno-pie -ggdb3 -O0 -no-pie -o unwind.out -std=c++11 \
  -Wall -Wextra -pedantic-errors unwind.cpp -lunwind -pthread
./unwind.out

Вывод:

0x400c80: (my_func_2()+0x9)
0x400cb7: (my_func_1(int)+0x10)
0x400ccc: (main+0x12)
0x7f4c68926b97: (__libc_start_main+0xe7)
0x400a3a: (_start+0x2a)

0x400c80: (my_func_2()+0x9)
0x400ca4: (my_func_1(double)+0x12)
0x400ce1: (main+0x27)
0x7f4c68926b97: (__libc_start_main+0xe7)
0x400a3a: (_start+0x2a)

а затем мы можем найти строки my_func_2и my_func_1(int)с:

addr2line -e unwind.out 0x400c80 0x400cb7

который дает:

/home/ciro/test/unwind.cpp:43
/home/ciro/test/unwind.cpp:54

TODO: почему строки расходятся по одной?

Проверено на Ubuntu 18.04, GCC 7.4.0, libunwind 1.2.1.

GDB автоматизация

Мы также можем сделать это с GDB без перекомпиляции, используя: Как выполнить определенное действие при достижении определенной точки останова в GDB?

Хотя, если вы собираетесь много печатать трассировку, это, вероятно, будет менее быстрым, чем другие варианты, но, возможно, мы сможем достичь собственных скоростей compile code, но мне лень тестировать это сейчас: как вызвать сборку в gdb?

main.cpp

void my_func_2(void) {}

void my_func_1(double f) {
    my_func_2();
}

void my_func_1(int i) {
    my_func_2();
}

int main() {
    my_func_1(1);
    my_func_1(2.0);
}

main.gdb

start
break my_func_2
commands
  silent
  backtrace
  printf "\n"
  continue
end
continue

Скомпилируйте и запустите:

g++ -ggdb3 -o main.out main.cpp
gdb -nh -batch -x main.gdb main.out

Вывод:

Temporary breakpoint 1 at 0x1158: file main.cpp, line 12.

Temporary breakpoint 1, main () at main.cpp:12
12          my_func_1(1);
Breakpoint 2 at 0x555555555129: file main.cpp, line 1.
#0  my_func_2 () at main.cpp:1
#1  0x0000555555555151 in my_func_1 (i=1) at main.cpp:8
#2  0x0000555555555162 in main () at main.cpp:12

#0  my_func_2 () at main.cpp:1
#1  0x000055555555513e in my_func_1 (f=2) at main.cpp:4
#2  0x000055555555516f in main () at main.cpp:13

[Inferior 1 (process 14193) exited normally]

TODO Я хотел сделать это -exиз командной строки, чтобы не создавать, main.gdbно я не мог заставить commandsработать там.

Протестировано в Ubuntu 19.04, GDB 8.2.

Ядро Linux

Как распечатать текущую трассировку стека потока внутри ядра Linux?

libdwfl

Первоначально об этом упоминалось по адресу: https://stackoverflow.com/a/60713161/895245, и это может быть лучший метод, но мне нужно провести еще немного тестов, но, пожалуйста, проголосуйте за этот ответ.

TODO: Я попытался свести к минимуму код в этом ответе, который работал, до одной функции, но это segfault, дайте мне знать, если кто-нибудь сможет понять, почему.

dwfl.cpp

#include <cassert>
#include <iostream>
#include <memory>
#include <sstream>
#include <string>

#include <cxxabi.h> // __cxa_demangle
#include <elfutils/libdwfl.h> // Dwfl*
#include <execinfo.h> // backtrace
#include <unistd.h> // getpid

// /programming/281818/unmangling-the-result-of-stdtype-infoname
std::string demangle(const char* name) {
    int status = -4;
    std::unique_ptr<char, void(*)(void*)> res {
        abi::__cxa_demangle(name, NULL, NULL, &status),
        std::free
    };
    return (status==0) ? res.get() : name ;
}

std::string debug_info(Dwfl* dwfl, void* ip) {
    std::string function;
    int line = -1;
    char const* file;
    uintptr_t ip2 = reinterpret_cast<uintptr_t>(ip);
    Dwfl_Module* module = dwfl_addrmodule(dwfl, ip2);
    char const* name = dwfl_module_addrname(module, ip2);
    function = name ? demangle(name) : "<unknown>";
    if (Dwfl_Line* dwfl_line = dwfl_module_getsrc(module, ip2)) {
        Dwarf_Addr addr;
        file = dwfl_lineinfo(dwfl_line, &addr, &line, nullptr, nullptr, nullptr);
    }
    std::stringstream ss;
    ss << ip << ' ' << function;
    if (file)
        ss << " at " << file << ':' << line;
    ss << std::endl;
    return ss.str();
}

std::string stacktrace() {
    // Initialize Dwfl.
    Dwfl* dwfl = nullptr;
    {
        Dwfl_Callbacks callbacks = {};
        char* debuginfo_path = nullptr;
        callbacks.find_elf = dwfl_linux_proc_find_elf;
        callbacks.find_debuginfo = dwfl_standard_find_debuginfo;
        callbacks.debuginfo_path = &debuginfo_path;
        dwfl = dwfl_begin(&callbacks);
        assert(dwfl);
        int r;
        r = dwfl_linux_proc_report(dwfl, getpid());
        assert(!r);
        r = dwfl_report_end(dwfl, nullptr, nullptr);
        assert(!r);
        static_cast<void>(r);
    }

    // Loop over stack frames.
    std::stringstream ss;
    {
        void* stack[512];
        int stack_size = ::backtrace(stack, sizeof stack / sizeof *stack);
        for (int i = 0; i < stack_size; ++i) {
            ss << i << ": ";

            // Works.
            ss << debug_info(dwfl, stack[i]);

#if 0
            // TODO intended to do the same as above, but segfaults,
            // so possibly UB In above function that does not blow up by chance?
            void *ip = stack[i];
            std::string function;
            int line = -1;
            char const* file;
            uintptr_t ip2 = reinterpret_cast<uintptr_t>(ip);
            Dwfl_Module* module = dwfl_addrmodule(dwfl, ip2);
            char const* name = dwfl_module_addrname(module, ip2);
            function = name ? demangle(name) : "<unknown>";
            // TODO if I comment out this line it does not blow up anymore.
            if (Dwfl_Line* dwfl_line = dwfl_module_getsrc(module, ip2)) {
              Dwarf_Addr addr;
              file = dwfl_lineinfo(dwfl_line, &addr, &line, nullptr, nullptr, nullptr);
            }
            ss << ip << ' ' << function;
            if (file)
                ss << " at " << file << ':' << line;
            ss << std::endl;
#endif
        }
    }
    dwfl_end(dwfl);
    return ss.str();
}

void my_func_2() {
    std::cout << stacktrace() << std::endl;
    std::cout.flush();
}

void my_func_1(double f) {
    (void)f;
    my_func_2();
}

void my_func_1(int i) {
    (void)i;
    my_func_2();
}

int main(int argc, char **argv) {
    long long unsigned int n;
    if (argc > 1) {
        n = strtoul(argv[1], NULL, 0);
    } else {
        n = 1;
    }
    for (long long unsigned int i = 0; i < n; ++i) {
        my_func_1(1);
        my_func_1(2.0);
    }
}

Скомпилируйте и запустите:

sudo apt install libdw-dev
g++ -fno-pie -ggdb3 -O0 -no-pie -o dwfl.out -std=c++11 -Wall -Wextra -pedantic-errors dwfl.cpp -ldw
./dwfl.out

Вывод:

0: 0x402b74 stacktrace[abi:cxx11]() at /home/ciro/test/dwfl.cpp:65
1: 0x402ce0 my_func_2() at /home/ciro/test/dwfl.cpp:100
2: 0x402d7d my_func_1(int) at /home/ciro/test/dwfl.cpp:112
3: 0x402de0 main at /home/ciro/test/dwfl.cpp:123
4: 0x7f7efabbe1e3 __libc_start_main at ../csu/libc-start.c:342
5: 0x40253e _start at ../csu/libc-start.c:-1

0: 0x402b74 stacktrace[abi:cxx11]() at /home/ciro/test/dwfl.cpp:65
1: 0x402ce0 my_func_2() at /home/ciro/test/dwfl.cpp:100
2: 0x402d66 my_func_1(double) at /home/ciro/test/dwfl.cpp:107
3: 0x402df1 main at /home/ciro/test/dwfl.cpp:121
4: 0x7f7efabbe1e3 __libc_start_main at ../csu/libc-start.c:342
5: 0x40253e _start at ../csu/libc-start.c:-1

Тестовый прогон:

g++ -fno-pie -ggdb3 -O3 -no-pie -o dwfl.out -std=c++11 -Wall -Wextra -pedantic-errors dwfl.cpp -ldw
time ./dwfl.out 1000 >/dev/null

Вывод:

real    0m3.751s
user    0m2.822s
sys     0m0.928s

Итак, мы видим, что этот метод в 10 раз быстрее, чем stacktrace Boost, и поэтому может быть применим для большего количества вариантов использования.

Протестировано в Ubuntu 19.10 amd64, libdw-dev 0.176-1.1.

Смотрите также


1
Все «TODO: строки разделены на одну» потому, что номер строки берется из начала следующего выражения.
SS Anne,

6

Стандартного способа сделать это не существует. Для окон функциональность предоставляется в библиотеке DbgHelp.


6

Есть ли способ сбрасывать стек вызовов в запущенном процессе на C или C ++ каждый раз, когда вызывается определенная функция?

Вы можете использовать макрос-функцию вместо оператора return в конкретной функции.

Например, вместо использования return,

int foo(...)
{
    if (error happened)
        return -1;

    ... do something ...

    return 0
}

Вы можете использовать функцию макроса.

#include "c-callstack.h"

int foo(...)
{
    if (error happened)
        NL_RETURN(-1);

    ... do something ...

    NL_RETURN(0);
}

Всякий раз, когда в функции происходит ошибка, вы увидите стек вызовов в стиле Java, как показано ниже.

Error(code:-1) at : so_topless_ranking_server (sample.c:23)
Error(code:-1) at : nanolat_database (sample.c:31)
Error(code:-1) at : nanolat_message_queue (sample.c:39)
Error(code:-1) at : main (sample.c:47)

Полный исходный код доступен здесь.

c-callstack на https://github.com/Nanolat


6

Еще один ответ на старую ветку.

Когда мне нужно это сделать, я обычно просто использую system()иpstack

Так что примерно так:

#include <sys/types.h>
#include <unistd.h>
#include <string>
#include <sstream>
#include <cstdlib>

void f()
{
    pid_t myPid = getpid();
    std::string pstackCommand = "pstack ";
    std::stringstream ss;
    ss << myPid;
    pstackCommand += ss.str();
    system(pstackCommand.c_str());
}

void g()
{
   f();
}


void h()
{
   g();
}

int main()
{
   h();
}

Это выводит

#0  0x00002aaaab62d61e in waitpid () from /lib64/libc.so.6
#1  0x00002aaaab5bf609 in do_system () from /lib64/libc.so.6
#2  0x0000000000400c3c in f() ()
#3  0x0000000000400cc5 in g() ()
#4  0x0000000000400cd1 in h() ()
#5  0x0000000000400cdd in main ()

Это должно работать в Linux, FreeBSD и Solaris. Я не думаю, что в macOS есть pstack или простой эквивалент, но у этого потока, похоже, есть альтернатива .

Если вы используете C, вам нужно будет использовать Cстроковые функции.

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

void f()
{
    pid_t myPid = getpid();
    /*
      length of command 7 for 'pstack ', 7 for the PID, 1 for nul
    */
    char pstackCommand[7+7+1];
    sprintf(pstackCommand, "pstack %d", (int)myPid);
    system(pstackCommand);
}

Я использовал 7 для максимального количества цифр в PID, основываясь на этом сообщении .


Хороший момент, поскольку субъект запрашивает C. Нет, это потребует адаптации, поскольку std :: string - это только C ++. Я обновлю свой ответ версией C.
Пол Флойд

6

Специфично для Linux, TL; DR:

  1. backtracein glibcсоздает точные трассировки стека, только когда -lunwindон связан (недокументированная функция, специфичная для платформы).
  2. Для вывода имени функции , исходного файла и номера строки использования #include <elfutils/libdwfl.h>(эта библиотека задокументированы только в заголовочном файле). backtrace_symbolsи backtrace_symbolsd_fdнаименее информативны.

В современном Linux вы можете получить адреса трассировки стека с помощью функции backtrace. Недокументированный способ backtraceсоздания более точных адресов на популярных платформах - это установить ссылку на -lunwind( libunwind-devв Ubuntu 18.04) (см. Пример вывода ниже). backtraceиспользует функцию, _Unwind_Backtraceи по умолчанию последняя исходит из, libgcc_s.so.1и эта реализация наиболее переносима. Когда -lunwindона связана, она предоставляет более точную версию, _Unwind_Backtraceно эта библиотека менее переносима (см. Поддерживаемые архитектуры в libunwind/src).

К сожалению, товарищ backtrace_symbolsdиbacktrace_symbols_fd функции не могли преобразовать адреса трассировки стека в имена функций с именем исходного файла и номером строки, вероятно, уже десять лет (см. Пример вывода ниже).

Однако есть другой метод преобразования адресов в символы, и он дает наиболее полезные трассировки с именем функции , исходным файлом и номером строки . Метод заключается в #include <elfutils/libdwfl.h>подключении -ldw( libdw-devв Ubuntu 18.04).

Рабочий пример C ++ ( test.cc):

#include <stdexcept>
#include <iostream>
#include <cassert>
#include <cstdlib>
#include <string>

#include <boost/core/demangle.hpp>

#include <execinfo.h>
#include <elfutils/libdwfl.h>

struct DebugInfoSession {
    Dwfl_Callbacks callbacks = {};
    char* debuginfo_path = nullptr;
    Dwfl* dwfl = nullptr;

    DebugInfoSession() {
        callbacks.find_elf = dwfl_linux_proc_find_elf;
        callbacks.find_debuginfo = dwfl_standard_find_debuginfo;
        callbacks.debuginfo_path = &debuginfo_path;

        dwfl = dwfl_begin(&callbacks);
        assert(dwfl);

        int r;
        r = dwfl_linux_proc_report(dwfl, getpid());
        assert(!r);
        r = dwfl_report_end(dwfl, nullptr, nullptr);
        assert(!r);
        static_cast<void>(r);
    }

    ~DebugInfoSession() {
        dwfl_end(dwfl);
    }

    DebugInfoSession(DebugInfoSession const&) = delete;
    DebugInfoSession& operator=(DebugInfoSession const&) = delete;
};

struct DebugInfo {
    void* ip;
    std::string function;
    char const* file;
    int line;

    DebugInfo(DebugInfoSession const& dis, void* ip)
        : ip(ip)
        , file()
        , line(-1)
    {
        // Get function name.
        uintptr_t ip2 = reinterpret_cast<uintptr_t>(ip);
        Dwfl_Module* module = dwfl_addrmodule(dis.dwfl, ip2);
        char const* name = dwfl_module_addrname(module, ip2);
        function = name ? boost::core::demangle(name) : "<unknown>";

        // Get source filename and line number.
        if(Dwfl_Line* dwfl_line = dwfl_module_getsrc(module, ip2)) {
            Dwarf_Addr addr;
            file = dwfl_lineinfo(dwfl_line, &addr, &line, nullptr, nullptr, nullptr);
        }
    }
};

std::ostream& operator<<(std::ostream& s, DebugInfo const& di) {
    s << di.ip << ' ' << di.function;
    if(di.file)
        s << " at " << di.file << ':' << di.line;
    return s;
}

void terminate_with_stacktrace() {
    void* stack[512];
    int stack_size = ::backtrace(stack, sizeof stack / sizeof *stack);

    // Print the exception info, if any.
    if(auto ex = std::current_exception()) {
        try {
            std::rethrow_exception(ex);
        }
        catch(std::exception& e) {
            std::cerr << "Fatal exception " << boost::core::demangle(typeid(e).name()) << ": " << e.what() << ".\n";
        }
        catch(...) {
            std::cerr << "Fatal unknown exception.\n";
        }
    }

    DebugInfoSession dis;
    std::cerr << "Stacktrace of " << stack_size << " frames:\n";
    for(int i = 0; i < stack_size; ++i) {
        std::cerr << i << ": " << DebugInfo(dis, stack[i]) << '\n';
    }
    std::cerr.flush();

    std::_Exit(EXIT_FAILURE);
}

int main() {
    std::set_terminate(terminate_with_stacktrace);
    throw std::runtime_error("test exception");
}

Скомпилировано на Ubuntu 18.04.4 LTS с gcc-8.3:

g++ -o test.o -c -m{arch,tune}=native -std=gnu++17 -W{all,extra,error} -g -Og -fstack-protector-all test.cc
g++ -o test -g test.o -ldw -lunwind

Выходы:

Fatal exception std::runtime_error: test exception.
Stacktrace of 7 frames:
0: 0x55f3837c1a8c terminate_with_stacktrace() at /home/max/src/test/test.cc:76
1: 0x7fbc1c845ae5 <unknown>
2: 0x7fbc1c845b20 std::terminate()
3: 0x7fbc1c845d53 __cxa_throw
4: 0x55f3837c1a43 main at /home/max/src/test/test.cc:103
5: 0x7fbc1c3e3b96 __libc_start_main at ../csu/libc-start.c:310
6: 0x55f3837c17e9 _start

Когда нет -lunwindпривязки, он дает менее точную трассировку стека:

0: 0x5591dd9d1a4d terminate_with_stacktrace() at /home/max/src/test/test.cc:76
1: 0x7f3c18ad6ae6 <unknown>
2: 0x7f3c18ad6b21 <unknown>
3: 0x7f3c18ad6d54 <unknown>
4: 0x5591dd9d1a04 main at /home/max/src/test/test.cc:103
5: 0x7f3c1845cb97 __libc_start_main at ../csu/libc-start.c:344
6: 0x5591dd9d17aa _start

Для сравнения, backtrace_symbols_fdвывод для той же трассировки стека наименее информативен:

/home/max/src/test/debug/gcc/test(+0x192f)[0x5601c5a2092f]
/usr/lib/x86_64-linux-gnu/libstdc++.so.6(+0x92ae5)[0x7f95184f5ae5]
/usr/lib/x86_64-linux-gnu/libstdc++.so.6(_ZSt9terminatev+0x10)[0x7f95184f5b20]
/usr/lib/x86_64-linux-gnu/libstdc++.so.6(__cxa_throw+0x43)[0x7f95184f5d53]
/home/max/src/test/debug/gcc/test(+0x1ae7)[0x5601c5a20ae7]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xe6)[0x7f9518093b96]
/home/max/src/test/debug/gcc/test(+0x1849)[0x5601c5a20849]

В производственной версии (а также в версии на языке C) вы можете сделать этот код более надежным, заменив boost::core::demangle, std::stringиstd::cout их базовые вызовы.

Вы также можете переопределить, __cxa_throwчтобы захватить трассировку стека при возникновении исключения и распечатать ее при обнаружении исключения. К тому времени, когда он входит в catchблок, стек уже размотан, поэтому вызывать его уже поздно backtrace, и именно поэтому необходимо захватить стек, throwкоторый реализуется функцией __cxa_throw. Обратите внимание, что в многопоточной программе __cxa_throwможет одновременно вызываться несколько потоков, так что если она захватывает трассировку стека в глобальный массив, который должен быть thread_local.


1
Хороший ответ! Тоже хорошо проработано.
SS Anne,

@SSAnne Очень любезно, спасибо. Эта -lunwindпроблема была обнаружена при libunwindнаписании этого сообщения, я ранее использовал напрямую для получения трассировки стека и собирался опубликовать его, но backtraceделает это за меня, когда -lunwindон связан.
Максим Егорушкин

1
@SSAnne Может быть потому, что первоначальный автор библиотеки Дэвид Мосбергер изначально был ориентирован на IA-64, но затем библиотека получила больше внимания nongnu.org/libunwind/people.html . gccне предоставляет API, верно?
Максим Егорушкин

3

Вы можете реализовать функционал самостоятельно:

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

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

Это может показаться трудоемким, но весьма полезным.


2
Я бы не стал этого делать. Скорее, я бы создал оболочку, которая использует API-интерфейсы базовой платформы (см. Ниже). Объем работы, вероятно, будет таким же, но вложения должны окупиться быстрее.
Пол Михалик

3
@paul: ваш ответ относится к окнам, когда OP явно указывает Linux ... но может быть полезен для тех, кто появляется здесь.
slashmais

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

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

возможно, нет, если вы взломаете скрипт sed / perl для добавления после каждого объявления функции, call_registror MY_SUPERSECRETNAME(__FUNCTION__);который подталкивает аргумент в своем конструкторе и всплывает в своем деструкторе. FUNCTION всегда представляет имя текущей функции.
прилетел

2

Конечно, следующий вопрос: хватит ли этого?

Основным недостатком трассировки стека является то, что почему у вас есть конкретная вызываемая функция, у вас нет ничего другого, например значения ее аргументов, что очень полезно для отладки.

Если у вас есть доступ к gcc и gdb, я бы посоветовал использовать assertдля проверки определенного условия и создания дампа памяти, если оно не выполняется. Конечно, это означает, что процесс остановится, но у вас будет полноценный отчет вместо простой трассировки стека.

Если вы хотите менее навязчивый способ, вы всегда можете использовать ведение журнала. Существуют очень эффективные лесозаготовительные предприятия, например, Pantheios . Что еще раз может дать вам гораздо более точное представление о том, что происходит.


1
Конечно, этого может быть недостаточно, но если я вижу, что функция вызывается на месте с одной конфигурацией, а не с другой, тогда это довольно хорошее место для начала.
Натан Феллман,

2

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

А теперь самое интересное: он может выводить фактические значения параметров для каждой функции в стеке и даже локальные переменные, счетчики циклов и т. Д.


2

Я знаю, что эта ветка устарела, но думаю, что она может быть полезна другим людям. Если вы используете gcc, вы можете использовать его возможности инструмента (параметр -finstrument-functions) для регистрации любого вызова функции (вход и выход). Взгляните на это для получения дополнительной информации: http://hacktalks.blogspot.fr/2013/08/gcc-instrument-functions.html

Таким образом, вы можете, например, помещать и вставлять все вызовы в стек, а когда вы хотите его распечатать, вы просто смотрите, что у вас есть в стеке.

Протестировал, работает отлично и очень удобно

ОБНОВЛЕНИЕ: вы также можете найти информацию о параметре компиляции -finstrument-functions в документе GCC, касающемся параметров инструментария: https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html


Вы также должны ссылаться на документы GCC, если статья не работает.
HolyBlackCat

Спасибо, вы правы. Таким образом, я добавил ОБНОВЛЕНИЕ в свой пост со ссылкой на документ gcc
François

2

Вы можете использовать библиотеки Boost для печати текущего стека вызовов.

#include <boost/stacktrace.hpp>

// ... somewhere inside the `bar(int)` function that is called recursively:
std::cout << boost::stacktrace::stacktrace();

Человек здесь: https://www.boost.org/doc/libs/1_65_1/doc/html/stacktrace.html


У меня cannot locate SymEnumSymbolsExW at C:\Windows\SYSTEM32\dbgeng.dllна Win10 ошибка .
zwcloud

0

Вы можете использовать профилировщик GNU. Он также показывает график звонков! команда есть, gprofи вам нужно скомпилировать свой код с какой-либо опцией.


-6

Есть ли способ сбрасывать стек вызовов в запущенном процессе на C или C ++ каждый раз, когда вызывается определенная функция?

Нет, хотя решения, зависящие от платформы, могут существовать.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.