стек вызовов печати в C или C ++ - PullRequest
93 голосов
/ 10 октября 2010

Есть ли способ сбросить стек вызовов в запущенном процессе на 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.

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

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


* * Фон тысяча двадцать-один * * тысяча двадцать-дв

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

Ответы [ 12 ]

71 голосов
/ 10 октября 2010

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

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

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

6 голосов

Boost stacktrace

Документировано по адресу: 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, что уродливо и может быть медленным, если вы берете слишком много трасс.

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

  • Ускорение - только заголовок, поэтому скорее всего не нужно изменять систему сборки

main.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) {
    my_func_2();
}

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

int main() {
    my_func_1(1);   /* line 19 */
    my_func_1(2.0); /* line 20 */
}

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

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

У нас естьдобавить -ldl в конце, иначе сборка не удалась.

Затем:

./main.out

дает:

 0# my_func_2() at /root/lkmc/main.cpp:7
 1# my_func_1(int) at /root/lkmc/main.cpp:16
 2# main at /root/lkmc/main.cpp:20
 3# __libc_start_main in /lib/x86_64-linux-gnu/libc.so.6
 4# _start in ./main.out

 0# my_func_2() at /root/lkmc/main.cpp:7
 1# my_func_1(double) at /root/lkmc/main.cpp:12
 2# main at /root/lkmc/main.cpp:21
 3# __libc_start_main in /lib/x86_64-linux-gnu/libc.so.6
 4# _start in ./main.out

И с -O3:

 0# my_func_2() at /usr/include/boost/stacktrace/stacktrace.hpp:217
 1# my_func_1(double) at /root/lkmc/main.cpp:11
 2# __libc_start_main in /lib/x86_64-linux-gnu/libc.so.6
 3# _start in ./main.out

 0# my_func_2() at /usr/include/boost/stacktrace/stacktrace.hpp:217
 1# main at /root/lkmc/main.cpp:21
 2# __libc_start_main in /lib/x86_64-linux-gnu/libc.so.6
 3# _start in ./main.out

Имейте в виду, что следы в целом непоправимо изуродованы оптимизацией.Оптимизация Tail Call является ярким примером этого: Что такое оптимизация Tail Call?

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

Протестировано на Ubuntu 18.04, GCC 7.3.0, boost 1.65.1.

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 является ключевым обязательным параметром.

Run:

./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.С оптимизацией исходная обратная трассировка кардинально модифицируется в скомпилированном коде.

Протестировано в 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.

libunwind

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

Код адаптирован из: 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.

C ++ demangling

Можно сделать с помощью abi::__cxa_demangle для glibc backtrace и libunwind, см. Пример по адресу: https://eli.thegreenplace.net/2015/programmatic-access-to-the-call-stack-in-c/

Ядро Linux

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

См. Также

5 голосов
/ 01 сентября 2013

Есть ли способ сбросить стек вызовов в работающем процессе на 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

5 голосов
/ 10 октября 2010

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

4 голосов
/ 15 января 2018

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

Когда мне нужно это сделать, я обычно просто использую 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 или простой эквивалент, но этот поток , кажется, имеет альтернативу .

2 голосов
/ 08 января 2019

Вы можете использовать библиотеки 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

2 голосов
/ 23 октября 2014

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

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

2 голосов
/ 10 октября 2010

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

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

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

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

1 голос
/ 15 января 2018

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

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

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

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

1 голос
/ 10 октября 2010

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

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

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

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

...