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

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

void foo()
{
   print_stack_trace();

   // foo's body

   return
}

куда print_stack_trace работает аналогично caller в перл.

Или как то так:

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.


Фон

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

14 ответов

Решение

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

Обратите внимание на раздел заметок в backtrace(3):

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

Boost Stacktrace

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

Если вы уже на темной стороне, это лучший вариант, который я видел до сих пор, потому что он:

  • может на самом деле распечатать номера строк. Он просто звонит 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

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

Бежать:

./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

так что линии отключены только на один, почему? Но это все еще может быть полезным.

Вывод: этот метод работает только с -O0что делает меня более склонным просто использовать вместо GDB, как -O0 теряет много производительности и может сделать отладку слишком медленной.

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

Glibcbacktrace_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?

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

В C ++ 23 будет <stacktrace>, а затем вы можете:

      #include <stacktrace>

/* ... */

std::cout << std::stacktrace::current();

Дополнительные сведения:
  • https://en.cppreference.com/w/cpp/header/stacktrace
  • https://en.cppreference.com/w/cpp/utility/basic_stacktrace/operator_ltlt

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

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

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

  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.

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

Вы можете использовать макро-функцию вместо оператора 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)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Другие вопросы по тегам