wypisuje stos wywołań w C lub C ++


120

Czy istnieje sposób na zrzucenie stosu wywołań w działającym procesie w C lub C ++ za każdym razem, gdy wywoływana jest określona funkcja? Mam na myśli coś takiego:

void foo()
{
   print_stack_trace();

   // foo's body

   return
}

Gdzie print_stack_tracedziała podobnie jak callerw Perlu.

Lub coś w tym stylu:

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

    // etc...
}

gdzie register_stack_trace_functionumieszcza jakiś wewnętrzny punkt przerwania, który spowoduje wydrukowanie śladu stosu przy każdym foowywołaniu.

Czy coś takiego istnieje w jakiejś standardowej bibliotece C?

Pracuję na Linuksie, używając GCC.


tło

Mam test, który zachowuje się inaczej w zależności od niektórych przełączników wiersza poleceń, które nie powinny wpływać na to zachowanie. Mój kod ma generator liczb pseudolosowych, który, jak przypuszczam, jest nazywany inaczej na podstawie tych przełączników. Chcę móc uruchomić test z każdym zestawem przełączników i sprawdzić, czy generator liczb losowych jest wywoływany inaczej dla każdego z nich.


1
@Armen, czy znasz któryś z nich?
Nathan Fellman

1
@Nathan: Jeśli twój debugger to gdb, może obsłużyć ten przypadek . Nie mogę ci powiedzieć o innych, ale zakładam, że gdb nie jest sam w posiadaniu tej funkcjonalności. Na bok: właśnie spojrzałem na mój wcześniejszy komentarz. :: gag :: s/easier/either/jak do diabła to się stało?
dmckee --- ex-moderator kitten

2
@dmckee: Właściwie powinno być s/either/easier. To, co musiałbym zrobić z gdb, to napisać skrypt, który przerwie tę funkcję i wydrukuje ślad stosu, a następnie będzie kontynuował. Teraz, kiedy o tym myślę, może czas, żebym się nauczył o skryptach gdb.
Nathan Fellman

1
Gah! Idę się przespać. Już niedługo ...
dmckee --- ex-moderator kitten

Odpowiedzi:


79

W przypadku rozwiązania tylko dla systemu Linux możesz użyć funkcji backtrace (3), która po prostu zwraca tablicę void *(w rzeczywistości każdy z tych punktów wskazuje na adres zwrotny z odpowiedniej ramki stosu). Aby przetłumaczyć je na coś użytecznego, istnieje backtrace_symbols (3) .

Zwróć uwagę na sekcję notatek w backtrace (3) :

Nazwy symboli mogą być niedostępne bez użycia specjalnych opcji konsolidatora. W przypadku systemów używających konsolidatora GNU konieczne jest użycie opcji -rdynamic linker. Zauważ, że nazwy funkcji „statycznych” nie są ujawniane i nie będą dostępne w śledzeniu wstecznym.


10
FWIW, ta funkcja istnieje również w systemie Mac OS X: developer.apple.com/library/mac/#documentation/Darwin/Reference/…
EmeryBerger



glibcNiestety w systemie Linux backtrace_symbolsfunkcje nie zapewniają nazwy funkcji, nazwy pliku źródłowego i numeru wiersza.
Maxim Egorushkin

Oprócz używania -rdynamicsprawdź również, czy twój system kompilacji nie dodaje -fvisibility=hiddenopcji! (ponieważ całkowicie odrzuci efekt -rdynamic)
Dima Litvinov

38

Zwiększ śledzenie stosu

Udokumentowane pod adresem : https://www.boost.org/doc/libs/1_66_0/doc/html/stacktrace/getting_started.html#stacktrace.getting_started.how_to_print_current_call_stack

To najwygodniejsza opcja, jaką do tej pory widziałem, ponieważ:

  • może faktycznie wydrukować numery linii.

    To właśnie sprawia, że połączenia do addr2linejednak , co jest brzydkie i może być powolne jeśli biorą zbyt wiele śladów.

  • domyślnie demontuje

  • Boost to tylko nagłówek, więc najprawdopodobniej nie ma potrzeby modyfikowania systemu kompilacji

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
    }
}

Niestety wydaje się, że jest to nowszy dodatek, a pakietu libboost-stacktrace-devnie ma w Ubuntu 16.04, tylko 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

Musimy dodać -ldlna końcu albo kompilacja się nie powiedzie.

Wynik:

 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

Dane wyjściowe i są dalej wyjaśnione w sekcji „śledzenie wstecznego glibc” poniżej, która jest analogiczna.

Zwróć uwagę, jak my_func_1(int)i my_func_1(float), które są zniekształcone z powodu przeciążenia funkcji , zostały dla nas ładnie zdemontowane.

Należy pamiętać, że pierwsze introzmowy jest wyłączone przez jedną linię (28 zamiast 27, a drugi jest wyłączony przez dwie linie (27 zamiast 29). To było sugerowane w komentarzach , że to dlatego, że następujący adres instrukcji jest rozważane, które sprawia, że ​​27 staje się 28, a 29 wyskakuje z pętli i staje się 27.

Następnie obserwujemy, że w -O3przypadku wyjście jest całkowicie okaleczone:

 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

Mechanizmy śledzenia wstecznego są na ogół nieodwracalnie niszczone przez optymalizacje. Optymalizacja wywołań końcowych jest tego godnym uwagi przykładem: Co to jest optymalizacja połączeń końcowych?

Benchmark działa na -O3:

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

Wynik:

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

Tak więc, zgodnie z oczekiwaniami, widzimy, że ta metoda jest niezwykle powolna, prawdopodobnie w przypadku połączeń zewnętrznych addr2line, i będzie wykonalna tylko wtedy, gdy wykonywana jest ograniczona liczba połączeń.

Wydaje się, że każdy wydruk śladu zajmie setki milisekund, więc ostrzegamy, że jeśli ślad śladu zdarza się bardzo często, wydajność programu znacznie się pogorszy.

Testowano na Ubuntu 19.10, GCC 9.2.1, boost 1.67.0.

glibc backtrace

Dokumentacja dostępna pod adresem : 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;
}

Skompilować:

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

-rdynamic jest kluczową wymaganą opcją.

Biegać:

./main.out

Wyjścia:

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

Od razu widzimy więc, że nastąpiła optymalizacja inlining i niektóre funkcje zostały utracone ze śledzenia.

Jeśli spróbujemy uzyskać adresy:

addr2line -e main.out 0x4008f9 0x4008fe

otrzymujemy:

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

który jest całkowicie wyłączony.

Jeśli -O0zamiast tego zrobimy to samo z , ./main.outdaje prawidłowy pełny ślad:

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

i wtedy:

addr2line -e main.out 0x400a74 0x400a79

daje:

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

więc linie są przesunięte tylko o jeden, TODO dlaczego? Ale nadal może być użyteczne.

Wniosek: ślady wsteczne mogą się doskonale wyświetlać tylko z -O0. Dzięki optymalizacjom oryginalny ślad śledzenia jest zasadniczo modyfikowany w skompilowanym kodzie.

Nie mogłem znaleźć prostego sposobu na automatyczne rozróżnianie symboli C ++ za pomocą tego, jednak oto kilka hacków:

Testowane na Ubuntu 16.04, GCC 6.4.0, libc 2.23.

glibc backtrace_symbols_fd

Ten pomocnik jest nieco wygodniejszy niż backtrace_symbolsi generuje zasadniczo identyczne dane wyjściowe:

/* 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("");
}

Testowane na Ubuntu 16.04, GCC 6.4.0, libc 2.23.

glibc backtracez C ++ demangling hack 1: -export-dynamic+dladdr

Na podstawie: https://gist.github.com/fmela/591333/c64f4eb86037bb237862a8283df70cdfc25f01d3

To jest „hack”, ponieważ wymaga zmiany ELF z -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);
}

Skompiluj i uruchom:

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 

wynik:

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]

Testowane na Ubuntu 18.04.

glibc backtracez C ++ demangling hack 2: parse backtrace output

Pokazano na: https://panthema.net/2008/0901-stacktrace-demangled/

To jest hack, ponieważ wymaga analizy.

DO ZROBIENIA skompilować i pokazać tutaj.

libunwind

ZROBIĆ, czy ma to jakąś przewagę nad śledzeniem wstecznym glibc? Bardzo podobny wynik, również wymaga zmodyfikowania polecenia budowania, ale nie jest częścią glibc, więc wymaga dodatkowej instalacji pakietu.

Kod zaadaptowano z: 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;
}

Skompiluj i uruchom:

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

Albo #define _XOPEN_SOURCE 700musi być na górze, albo musimy użyć -std=gnu99:

Biegać:

./main.out

Wynik:

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

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

i:

addr2line -e main.out 0x4007db 0x4007e2

daje:

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

Z -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)

i:

addr2line -e main.out 0x4009f3 0x4009f8

daje:

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

Testowane na Ubuntu 16.04, GCC 6.4.0, libunwind 1.1.

libunwind z demanglingiem nazw w C ++

Kod zaadaptowano z: https://eli.thegreenplace.net/2015/programmatic-access-to-the-call-stack-in-c/

relax.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);
}

Skompiluj i uruchom:

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

Wynik:

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)

a następnie możemy znaleźć linie my_func_2i my_func_1(int)z:

addr2line -e unwind.out 0x400c80 0x400cb7

co daje:

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

DO ZROBIENIA: dlaczego linie są oddzielone o jeden?

Testowano na Ubuntu 18.04, GCC 7.4.0, libunwind 1.2.1.

Automatyzacja GDB

Możemy to również zrobić z GDB bez ponownej kompilacji, używając: Jak wykonać określoną akcję, gdy określony punkt przerwania zostanie osiągnięty w GDB?

Chociaż jeśli zamierzasz dużo drukować śladu wstecznego, prawdopodobnie będzie to mniej szybkie niż inne opcje, ale może możemy osiągnąć natywne prędkości z compile code, ale jestem leniwy, aby to teraz przetestować: Jak wywołać assembler w 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

Skompiluj i uruchom:

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

Wynik:

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 Chciałem to zrobić tylko -exz wiersza poleceń, aby nie musieć tworzyć, main.gdbale nie mogłem commandstam pracować.

Testowane w Ubuntu 19.04, GDB 8.2.

Jądro Linux

Jak wydrukować bieżący ślad stosu wątków w jądrze Linuksa?

libdwfl

Pierwotnie wspomniano o tym na stronie : https://stackoverflow.com/a/60713161/895245 i może to być najlepsza metoda, ale muszę trochę przetestować test, ale proszę, zagłosuj za tą odpowiedzią.

TODO: Próbowałem zminimalizować kod w tej odpowiedzi, który działał, do jednej funkcji, ale jest to segfaulting, daj mi znać, jeśli ktoś może znaleźć przyczynę.

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);
    }
}

Skompiluj i uruchom:

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

Wynik:

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

Test porównawczy:

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

Wynik:

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

Widzimy więc, że ta metoda jest 10 razy szybsza niż śledzenie stosu Boost i dlatego może mieć zastosowanie w większej liczbie przypadków użycia.

Testowane w Ubuntu 19.10 amd64, libdw-dev 0.176-1.1.

Zobacz też


1
Wszystkie „TODO: wiersze oddzielone o jeden” wynikają z tego, że numer wiersza jest pobierany od początku następnego wyrażenia.
SS Anne


6

Czy istnieje sposób na zrzucenie stosu wywołań w działającym procesie w C lub C ++ za każdym razem, gdy wywoływana jest określona funkcja?

Możesz użyć funkcji makra zamiast instrukcji return w określonej funkcji.

Na przykład zamiast używać zwrotu,

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

    ... do something ...

    return 0
}

Możesz użyć funkcji makro.

#include "c-callstack.h"

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

    ... do something ...

    NL_RETURN(0);
}

Ilekroć wystąpi błąd w funkcji, zobaczysz stos wywołań w stylu Java, jak pokazano poniżej.

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)

Pełny kod źródłowy jest dostępny tutaj.

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


6

Kolejna odpowiedź na stary wątek.

Kiedy muszę to zrobić, zwykle używam system()ipstack

Więc coś takiego:

#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();
}

To wychodzi

#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 ()

Powinno to działać w systemach Linux, FreeBSD i Solaris. Nie sądzę, że macOS ma pstack lub prosty odpowiednik, ale ten wątek wydaje się mieć alternatywę .

Jeśli używasz C, będziesz musiał użyć Cfunkcji tekstowych.

#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);
}

Użyłem 7 dla maksymalnej liczby cyfr w PID, na podstawie tego postu .


Dobra uwaga, ponieważ podmiot pyta o C. Nie, wymagałoby to dostosowania, ponieważ std :: string jest tylko w C ++. Zaktualizuję moją odpowiedź wersją C.
Paul Floyd

6

Specyficzne dla systemu Linux, TLDR:

  1. backtracein glibctworzy dokładne ślady stosu tylko wtedy, gdy-lunwind jest połączony (nieudokumentowana funkcja specyficzna dla platformy).
  2. Aby wyprowadzić nazwę funkcji , plik źródłowy i numer linii użyj #include <elfutils/libdwfl.h>(ta biblioteka jest udokumentowana tylko w pliku nagłówkowym). backtrace_symbolsi backtrace_symbolsd_fdsą najmniej pouczające.

W nowoczesnym Linuksie adresy śledzenia stosu można uzyskać za pomocą funkcji backtrace. Nieudokumentowanym sposobem backtracetworzenia dokładniejszych adresów na popularnych platformach jest linkowanie z -lunwind( libunwind-devna Ubuntu 18.04) (zobacz przykładowe dane wyjściowe poniżej). backtraceużywa funkcji _Unwind_Backtracei domyślnie ta ostatnia pochodzi z libgcc_s.so.1i ta implementacja jest najbardziej przenośna. Po -lunwindpołączeniu zapewnia dokładniejszą wersję, _Unwind_Backtraceale ta biblioteka jest mniej przenośna (zobacz obsługiwane architektury w libunwind/src).

Niestety, program towarzyszący backtrace_symbolsdi backtrace_symbols_fdfunkcje nie były w stanie przekształcić adresów stosu śledzenia na nazwy funkcji z nazwą pliku źródłowego i numerem linii prawdopodobnie od dekady (zobacz przykładowe dane wyjściowe poniżej).

Istnieje jednak inna metoda rozwiązywania adresów do symboli i tworzy najbardziej przydatne ślady z nazwą funkcji , plikiem źródłowym i numerem linii . Metoda polega na #include <elfutils/libdwfl.h>połączeniu z -ldw( libdw-devw systemie Ubuntu 18.04).

Działający przykład w 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");
}

Skompilowany na Ubuntu 18.04.4 LTS z 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

Wyjścia:

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

Kiedy nie -lunwindjest połączone, generuje mniej dokładny ślad stosu:

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

Dla porównania, backtrace_symbols_fdwynik dla tego samego śladu stosu jest najmniej informacyjny:

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

W wersji produkcyjnej (a także C wersji językowej), może chcesz, aby ten kod extra wytrzymałe zastępując boost::core::demangle, std::stringastd::cout ich połączeń bazowych.

Możesz także przesłonić, __cxa_throwaby przechwycić ślad stosu, gdy zostanie zgłoszony wyjątek, i wydrukować go, gdy zostanie przechwycony. Zanim wejdzie do catchbloku, stos został rozwinięty, więc jest już za późno na wywołanie backtracei dlatego stos musi zostać przechwycony, na throwktórym jest zaimplementowana funkcja __cxa_throw. Zauważ, że program wielowątkowy __cxa_throwmoże być wywoływany jednocześnie przez wiele wątków, więc jeśli przechwytuje stos śledzenia do globalnej tablicy, która musi być thread_local.


1
Niezła odpowiedź! Dobrze zbadane.
SS Anne

@SSAnne Bardzo miło, dziękuję. Ten -lunwindproblem został odkryty podczas tworzenia tego posta, wcześniej użyłem go libunwindbezpośrednio do uzyskania śledzenia stosu i zamierzałem go opublikować, ale backtracerobi to za mnie, gdy -lunwindjest połączony.
Maxim Egorushkin

1
@SSAnne Być może dlatego, że pierwotny autor biblioteki David Mosberger początkowo koncentrował się na IA-64, ale później biblioteka zyskała większą przyczepność nongnu.org/libunwind/people.html . gccnie ujawnia API, czy to prawda?
Maxim Egorushkin

3

Funkcjonalność możesz wdrożyć samodzielnie:

Użyj stosu globalnego (łańcuchowego) i na początku każdej funkcji umieść nazwę funkcji i inne wartości (np. Parametry) na ten stos; przy wyjściu z funkcji wyskakuj ponownie.

Napisz funkcję, która wydrukuje zawartość stosu, gdy zostanie wywołana, i użyj jej w funkcji, w której chcesz zobaczyć stos wywołań.

Może się to wydawać dużo pracy, ale jest całkiem przydatne.


2
Nie zrobiłbym tego. Zamiast tego utworzyłbym opakowanie, które używa interfejsów API specyficznych dla platformy (patrz poniżej). Nakład pracy byłby prawdopodobnie taki sam, ale inwestycja powinna się szybciej zwrócić.
Paul Michalik

3
@paul: twoja odpowiedź odnosi się do okien, gdy OP wyraźnie określa linux ... ale może być przydatna dla facetów od Windows, którzy się tutaj pojawiają.
slashmais

Racja, przeoczyłem to… Hm, to ostatnie zdanie pytania, więc może plakat powinien zmodyfikować swoją prośbę, aby wspomnieć o swojej platformie docelowej w bardziej widocznym miejscu.
Paul Michalik

1
Byłby to dobry pomysł, poza tym, że moja baza kodu zawiera kilkadziesiąt plików zawierających kilkaset (jeśli nie kilka tysięcy) plików, więc jest to niewykonalne.
Nathan Fellman

może nie, jeśli zhakujesz skrypt sed / perl, który zostanie dodany po każdej deklaracji funkcji, call_registror MY_SUPERSECRETNAME(__FUNCTION__);która odkłada argument w konstruktorze i pojawia się w jego destruktorze FUNKCJA zawsze reprezentuje nazwę bieżącej funkcji.
flownt

2

Oczywiście następne pytanie brzmi: czy to wystarczy?

Główną wadą śledzenia stosu jest to, że dlaczego wywoływana jest precyzyjna funkcja, nie masz nic innego, jak wartość jej argumentów, co jest bardzo przydatne do debugowania.

Jeśli masz dostęp do gcc i gdb, sugerowałbym użycie assert do sprawdzenia określonego warunku i utworzenia zrzutu pamięci, jeśli nie jest on spełniony. Oczywiście oznacza to, że proces się zatrzyma, ale zamiast zwykłego śladu stosu będziesz mieć pełny raport.

Jeśli chcesz mniej uciążliwy sposób, zawsze możesz skorzystać z logowania. Istnieją bardzo wydajne urządzenia do pozyskiwania drewna, takie jak na przykład Pantheios . Co jeszcze raz może dać ci znacznie dokładniejszy obraz tego, co się dzieje.


1
Oczywiście może to nie wystarczyć, ale jeśli widzę, że funkcja jest wywoływana w miejscu z jedną konfiguracją, a nie z drugą, to jest to całkiem dobre miejsce na początek.
Nathan Fellman

2

Możesz do tego użyć Poppy . Zwykle jest używany do zbierania śladów stosu podczas awarii, ale może również wyświetlać je dla uruchomionego programu.

Teraz dobra część: może wyprowadzać rzeczywiste wartości parametrów dla każdej funkcji na stosie, a nawet zmienne lokalne, liczniki pętli itp.


2

Wiem, że ten wątek jest stary, ale myślę, że może być przydatny dla innych osób. Jeśli używasz gcc, możesz użyć jego funkcji instrumentu (opcja -finstrument-functions) do logowania dowolnego wywołania funkcji (wejścia i wyjścia). Spójrz na to, aby uzyskać więcej informacji: http://hacktalks.blogspot.fr/2013/08/gcc-instrument-functions.html

Możesz więc na przykład push i wrzucić wszystkie wywołania do stosu, a kiedy chcesz je wydrukować, po prostu spójrz na to, co masz w swoim stosie.

Przetestowałem to, działa doskonale i jest bardzo poręczne

AKTUALIZACJA: możesz również znaleźć informacje o opcji kompilacji -finstrument-functions w dokumencie GCC dotyczącym opcji Instrumentation: https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html


Powinieneś także linkować do dokumentów GCC na wypadek, gdyby artykuł został przerwany.
HolyBlackCat

Dziękuję, masz rację. W ten sposób dodałem UPDATE w moim poście z linkiem do dokumentu gcc
François


0

Możesz użyć profilera GNU. Pokazuje również wykres połączeń! komendą jest gprofi musisz skompilować swój kod z jakąś opcją.


-6

Czy istnieje sposób na zrzucenie stosu wywołań w działającym procesie w C lub C ++ za każdym razem, gdy wywoływana jest określona funkcja?

Nie, nie ma, chociaż mogą istnieć rozwiązania zależne od platformy.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.