друкувати стек виклику в C або C ++


120

Чи є спосіб скинути стек викликів у запущеному процесі на C або C ++ кожен раз, коли викликається певна функція? Я маю на увазі щось таке:

void foo()
{
   print_stack_trace();

   // foo's body

   return
}

Де print_stack_traceпрацює аналогічно callerв Perl.

Або щось подібне:

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

    // etc...
}

де register_stack_trace_functionставиться якась внутрішня точка розриву, яка призведе до друку сліду стека щоразу, коли fooвикликається.

Чи є щось подібне в якійсь стандартній бібліотеці С?

Я працюю над 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) .

Зверніть увагу на розділ приміток у фоновому режимі (3) :

Назви символів можуть бути недоступними без використання спеціальних параметрів лінкера. Для систем, що використовують лінкер GNU, необхідно використовувати опцію -rdynamic linker. Зауважте, що назви "статичних" функцій не піддаються впливу, і вони не будуть доступні у зворотному сліді.


10
FWIW, ця функціональність існує і на Mac OS X: developer.apple.com/library/mac/#documentation/Darwin/Reference/…
EmeryBerger

8
У Windows є CaptureStackBackTrace
bobobobo


У Linux з glibc, на жаль, backtrace_symbolsфункціями не передбачено ім'я функції, ім'я вихідного файла та номер рядка.
Максим Єгорушкін

Окрім використання -rdynamic, також переконайтесь, що система складання не додає -fvisibility=hiddenпараметр! (як це повністю відкине ефект -rdynamic)
Діма Литвинов

37

Підвищити стек-трек

Документовано за адресою: 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

Вихід і далі пояснюється в розділі "backlice glibc" нижче, що є аналогом.

Зверніть увагу на те, як 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, прискорення 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. За допомогою оптимізацій оригінальний backtrace принципово змінюється у складеному коді.

Не вдалося знайти простий спосіб автоматичного демонтажу символів 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 backtrace? Дуже схожий вихід, також вимагає зміни команди збірки, але не є частиною 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/

unind.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, повороту 1.2.1.

Автоматизація ГДБ

Ми також можемо це зробити за допомогою 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: Я намагався мінімізувати код у цій відповіді, яка працювала, до однієї функції, але це сегментарно, дайте мені знати, якщо хтось може знайти чому.

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 разів швидший, ніж стек-трек Boost, і, отже, може бути застосований для більшої кількості випадків використання.

Випробувано в Ubuntu 19.10 amd64, libdw-dev 0.176-1.1.

Дивитися також


1
Усі "TODO: рядки відключені одним" є тому, що номер рядка береться з початку наступного виразу.
СС Енн

6

Не існує стандартизованого способу зробити це. Для Windows функціонал передбачений у бібліотеці 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 ++. Я оновлю свою відповідь версією С.
Пол Флойд

6

Linux, специфічний TLDR:

  1. backtraceу 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
Гарна відповідь! Добре досліджені теж.
СС Енн

@SSAnne Дуже добрий, дякую. Цю -lunwindпроблему було виявлено під час створення цієї публікації, я раніше використовував libunwindбезпосередньо для отримання стек-трек і збирався опублікувати його, але backtraceробить це для мене, коли -lunwindзв’язується.
Максим Єгорушкін

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

3

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

Використовуйте глобальний (рядковий) стек і на початку кожної функції натисніть на цю функцію ім'я функції та інші значення (наприклад, параметри); при виході з функції повторно натисніть його.

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

Це може здатися великою роботою, але цілком корисно.


2
Я б цього не робив. Швидше я б створив обгортку, яка використовує основні API для платформи (див. Нижче). Обсяг роботи був би, мабуть, однаковим, але інвестиції повинні окуплятися швидше.
Пол Міхалик

3
@paul: Ваша відповідь стосується windows, коли OP чітко вказує Linux ... але може бути корисним для хлопців з Windows, які з’являються тут.
slashmais

Правильно, я не помітив, що ... Гм, це останнє речення питання, тому, можливо, плакат повинен змінити його прохання згадати про свою / її цільову платформу на більш помітному місці.
Пол Міхалик

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

може бути, ні, якщо ви зламаєте скрипт sed / perl для додавання після кожної декларації функції, call_registror MY_SUPERSECRETNAME(__FUNCTION__);яка штовхає аргумент у його конструкторі та з'являється у його деструкторі FUNCTION завжди представляє ім’я поточної функції.
течія

2

Звичайно, наступне питання: чи цього буде достатньо?

Основний недолік слідів стека полягає в тому, що тому, що ви називаєте точну функцію, у вас немає нічого іншого, як значення його аргументів, що дуже корисно для налагодження.

Якщо у вас є доступ до gcc та gdb, я б запропонував використовувати assertдля перевірки конкретного стану та створення дампа пам'яті, якщо він не виконаний. Звичайно, це означає, що процес зупиниться, але ви матимете повноцінний звіт замість простого сліду стека.

Якщо ви бажаєте менш нав'язливого способу, ви завжди можете використовувати журнал. Там є дуже ефективні засоби для лісозаготівлі, як, наприклад, Пантеос . Що ще раз може дати вам набагато точніше уявлення про те, що відбувається.


1
Звичайно, це може бути недостатньо, але якщо я бачу, що функція викликається на місці з однією конфігурацією, а не з іншою, то це досить непогане місце для початку.
Натан Фелман

2

Ви можете використовувати для цього мак . Зазвичай він використовується для збору сліду стека під час аварії, але він також може вивести його для запущеної програми.

Тепер ось хороша частина: вона може виводити фактичні значення параметрів для кожної функції на стек і навіть локальні змінні, лічильники циклу тощо.


2

Я знаю, що ця нитка стара, але я думаю, що може бути корисною для інших людей. Якщо ви використовуєте gcc, ви можете використовувати його функції інструменту (опція -finstrument-function) для реєстрації будь-якого виклику функції (входу та виходу). Перегляньте це для отримання додаткової інформації: http://hacktalks.blogspot.fr/2013/08/gcc-instrument-functions.html

Таким чином, ви можете, наприклад, натискати та переносити всі дзвінки в стек, а коли ви хочете роздрукувати їх, ви просто дивитесь, що у вас є у стеку.

Я перевірив це, він відмінно працює і дуже зручний

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


Ви також маєте посилання на документи GCC, якщо стаття знижується.
HolyBlackCat

Дякую, ви праві. Таким чином я додав ОНОВЛЕННЯ у своєму дописі із посиланням на gcc doc
Франсуа

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.