Aufrufstapel in C oder C ++ drucken


120

Gibt es eine Möglichkeit, den Aufrufstapel in einem laufenden Prozess in C oder C ++ bei jedem Aufruf einer bestimmten Funktion zu sichern? Was ich vorhabe, ist ungefähr so:

void foo()
{
   print_stack_trace();

   // foo's body

   return
}

Wo print_stack_tracefunktioniert ähnlich wie callerin Perl.

Oder sowas:

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

    // etc...
}

Dabei wird register_stack_trace_functioneine Art interner Haltepunkt eingefügt, der bewirkt, dass bei jedem fooAufruf eine Stapelverfolgung gedruckt wird.

Gibt es so etwas in einer Standard-C-Bibliothek?

Ich arbeite unter Linux mit GCC.


Hintergrund

Ich habe einen Testlauf, der sich basierend auf einigen Befehlszeilenschaltern, die dieses Verhalten nicht beeinflussen sollten, anders verhält. Mein Code hat einen Pseudozufallszahlengenerator, von dem ich annehme, dass er basierend auf diesen Schaltern anders aufgerufen wird. Ich möchte in der Lage sein, den Test mit jedem Satz von Schaltern durchzuführen und zu sehen, ob der Zufallszahlengenerator für jeden unterschiedlich aufgerufen wird.


1
@Armen, kennst du eines davon?
Nathan Fellman

1
@ Nathan: Wenn Ihr Debugger GDB ist, kann er diesen Fall behandeln . Ich kann Ihnen nichts über andere erzählen, aber ich gehe davon aus, dass gdb mit dieser Funktionalität nicht allein ist. Nebenbei: ich gerade sah an meinem früheren Kommentar. :: gag :: s/easier/either/wie zum Teufel ist das passiert?
dmckee --- Ex-Moderator Kätzchen

2
@dmckee: In der Tat sollte es sein s/either/easier. Was ich mit gdb tun müsste, ist ein Skript zu schreiben, das diese Funktion unterbricht, den Stack-Trace druckt und dann fortfährt. Jetzt, wo ich darüber nachdenke, ist es vielleicht Zeit für mich, etwas über GDB-Scripting zu lernen.
Nathan Fellman

1
Gah! Ich werde etwas schlafen gehen.
Sehr

Antworten:


79

Für eine Nur-Linux-Lösung können Sie Backtrace (3) verwenden , das einfach ein Array von zurückgibt void *(tatsächlich zeigt jeder dieser Punkte auf die Rücksprungadresse des entsprechenden Stapelrahmens). Um diese in etwas Nützliches zu übersetzen, gibt es backtrace_symbols (3) .

Beachten Sie den Abschnitt mit den Notizen in Rückverfolgung (3) :

Die Symbolnamen sind möglicherweise ohne die Verwendung spezieller Linkeroptionen nicht verfügbar. Für Systeme, die den GNU-Linker verwenden, muss die Option -rdynamic Linker verwendet werden. Beachten Sie, dass Namen von "statischen" Funktionen nicht verfügbar sind und im Backtrace nicht verfügbar sind.


10
FWIW, diese Funktionalität gibt es auch unter Mac OS X: developer.apple.com/library/mac/#documentation/Darwin/Reference/…
EmeryBerger



Unter Linux mit bieten Funktionen glibcleider backtrace_symbolskeinen Funktionsnamen, Quellendateinamen und Zeilennummer.
Maxim Egorushkin

-rdynamicÜberprüfen Sie zusätzlich zur Verwendung auch, ob Ihr Build-System keine -fvisibility=hiddenOption hinzufügt ! (da es die Wirkung von vollständig verwerfen wird -rdynamic)
Dima Litvinov

36

Boost Stacktrace

Dokumentiert unter: https://www.boost.org/doc/libs/1_66_0/doc/html/stacktrace/getting_started.html#stacktrace.getting_started.how_to_print_current_call_stack

Dies ist die bequemste Option, die ich bisher gesehen habe, weil sie:

  • kann tatsächlich die Zeilennummern ausdrucken.

    Es werden jedoch nur Anrufe addr2linegetätigt , was hässlich ist und langsam sein kann, wenn Sie zu viele Spuren verfolgen.

  • entwirrt standardmäßig

  • Boost ist nur ein Header, sodass Sie Ihr Build-System höchstwahrscheinlich nicht ändern müssen

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

Leider scheint es sich um eine neuere Erweiterung zu handeln, und das Paket libboost-stacktrace-devist in Ubuntu 16.04 nicht vorhanden, nur in 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

Wir müssen -ldlam Ende hinzufügen, sonst schlägt die Kompilierung fehl.

Ausgabe:

 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

Die Ausgabe und wird im Abschnitt "glibc backtrace" weiter unten erläutert, der analog ist.

Beachten Sie, wie my_func_1(int)und my_func_1(float), die aufgrund von Funktionsüberlastung verstümmelt wurden, für uns schön entwirrt wurden.

Beachten Sie, dass der erste intAnruf um eine Zeile unterbrochen ist (28 statt 27 und der zweite um zwei Zeilen (27 statt 29). In den Kommentaren wurde vorgeschlagen, dass dies darauf zurückzuführen ist, dass die folgende Anweisungsadresse berücksichtigt wird macht 27 zu 28 und 29 springen von der Schleife und werden zu 27.

Wir beobachten dann, dass mit -O3die Ausgabe vollständig verstümmelt ist:

 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

Backtraces werden im Allgemeinen durch Optimierungen irreparabel verstümmelt. Die Tail-Call-Optimierung ist ein bemerkenswertes Beispiel dafür: Was ist die Tail-Call-Optimierung?

Benchmark-Lauf auf -O3:

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

Ausgabe:

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

Wie erwartet stellen wir fest, dass diese Methode bei externen Anrufen äußerst langsam addr2lineist und nur dann durchführbar ist, wenn eine begrenzte Anzahl von Anrufen getätigt wird.

Jeder Backtrace-Druck scheint Hunderte von Millisekunden zu dauern. Seien Sie also gewarnt, dass die Programmleistung erheblich beeinträchtigt wird, wenn ein Backtrace sehr häufig auftritt.

Getestet unter Ubuntu 19.10, GCC 9.2.1, Boost 1.67.0.

glibc backtrace

Dokumentiert unter: https://www.gnu.org/software/libc/manual/html_node/Backtraces.html

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

Kompilieren:

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

-rdynamic ist die Schlüsseloption.

Lauf:

./main.out

Ausgänge:

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

Wir sehen also sofort, dass eine Inlining-Optimierung stattgefunden hat und einige Funktionen aus der Ablaufverfolgung verloren gegangen sind.

Wenn wir versuchen, die Adressen zu bekommen:

addr2line -e main.out 0x4008f9 0x4008fe

wir erhalten:

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

das ist völlig aus.

Wenn wir -O0stattdessen dasselbe tun , erhalten Sie ./main.outdie richtige vollständige Ablaufverfolgung:

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

und dann:

addr2line -e main.out 0x400a74 0x400a79

gibt:

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

Also sind die Leitungen nur um eine, TODO warum? Dies könnte jedoch noch verwendbar sein.

Fazit: Backtraces können nur möglicherweise perfekt mit zeigen -O0. Mit Optimierungen wird die ursprüngliche Rückverfolgung im kompilierten Code grundlegend geändert.

Ich konnte keinen einfachen Weg finden, um C ++ - Symbole damit automatisch zu entwirren. Hier sind einige Hacks:

Getestet unter Ubuntu 16.04, GCC 6.4.0, libc 2.23.

glibc backtrace_symbols_fd

Dieser Helfer ist etwas praktischer als backtrace_symbolsund erzeugt im Grunde eine identische Ausgabe:

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

Getestet unter Ubuntu 16.04, GCC 6.4.0, libc 2.23.

glibc backtracemit C ++ Demangling Hack 1: -export-dynamic+dladdr

Angepasst von: https://gist.github.com/fmela/591333/c64f4eb86037bb237862a8283df70cdfc25f01d3

Dies ist ein "Hack", da dafür die ELF geändert werden muss -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);
}

Kompilieren und ausführen:

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 

Ausgabe:

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]

Getestet unter Ubuntu 18.04.

glibc backtracemit C ++ Demangling Hack 2: Backtrace-Ausgabe analysieren

Dargestellt unter: https://panthema.net/2008/0901-stacktrace-demangled/

Dies ist ein Hack, da er analysiert werden muss.

TODO bringt es zum Kompilieren und zeigt es hier.

libunwind

TODO hat dies einen Vorteil gegenüber Glibc Backtrace? Eine sehr ähnliche Ausgabe erfordert auch das Ändern des Build-Befehls, ist jedoch nicht Teil von glibc und erfordert daher eine zusätzliche Paketinstallation.

Code angepasst von: https://eli.thegreenplace.net/2015/programmatic-access-to-the-call-stack-in-c/

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

Kompilieren und ausführen:

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

Entweder #define _XOPEN_SOURCE 700muss oben sein, oder wir müssen verwenden -std=gnu99:

Lauf:

./main.out

Ausgabe:

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

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

und:

addr2line -e main.out 0x4007db 0x4007e2

gibt:

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

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

und:

addr2line -e main.out 0x4009f3 0x4009f8

gibt:

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

Getestet unter Ubuntu 16.04, GCC 6.4.0, libunwind 1.1.

libunwind mit C ++ - Namensentflechtung

Code angepasst von: https://eli.thegreenplace.net/2015/programmatic-access-to-the-call-stack-in-c/

unwind.cpp

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

void backtrace() {
  unw_cursor_t cursor;
  unw_context_t context;

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

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

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

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

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

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

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

Kompilieren und ausführen:

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

Ausgabe:

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)

und dann können wir die Zeilen von my_func_2und my_func_1(int)mit finden:

addr2line -e unwind.out 0x400c80 0x400cb7

was gibt:

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

TODO: Warum sind die Zeilen um eins entfernt?

Getestet unter Ubuntu 18.04, GCC 7.4.0, libunwind 1.2.1.

GDB-Automatisierung

Wir können dies auch mit GDB tun, ohne es neu zu kompilieren, indem wir Folgendes verwenden: Wie wird eine bestimmte Aktion ausgeführt, wenn ein bestimmter Haltepunkt in GDB erreicht wird?

Wenn Sie die Rückverfolgung häufig drucken, ist dies wahrscheinlich weniger schnell als die anderen Optionen, aber vielleicht können wir mit native Geschwindigkeiten erreichen compile code , aber ich bin faul, es jetzt zu testen: Wie rufe ich Assembly in GDB auf?

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

Kompilieren und ausführen:

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

Ausgabe:

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 Ich wollte dies nur -exüber die Kommandozeile tun , um nicht erstellen zu müssen, main.gdbaber ich konnte das nicht bekommencommands zum Laufen bringen.

Getestet in Ubuntu 19.04, GDB 8.2.

Linux Kernel

Wie drucke ich den aktuellen Thread-Stack-Trace im Linux-Kernel?

libdwfl

Dies wurde ursprünglich erwähnt unter: https://stackoverflow.com/a/60713161/895245 und es ist vielleicht die beste Methode, aber ich muss ein bisschen mehr Benchmarking durchführen, aber bitte stimmen Sie dieser Antwort zu.

TODO: Ich habe versucht, den Code in dieser funktionierenden Antwort auf eine einzige Funktion zu minimieren, aber es ist ein Fehler. Lassen Sie mich wissen, ob jemand herausfinden kann, warum.

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

Kompilieren und ausführen:

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

Ausgabe:

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

Benchmark-Lauf:

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

Ausgabe:

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

Wir sehen also, dass diese Methode 10x schneller ist als Boosts Stacktrace und daher möglicherweise auf mehr Anwendungsfälle anwendbar ist.

Getestet in Ubuntu 19.10 amd64, libdw-dev 0.176-1.1.

Siehe auch


1
Alle "TODO: Zeilen um eins" sind darauf zurückzuführen, dass die Zeilennummer vom Anfang des nächsten Ausdrucks übernommen wird.
SS Anne

6

Es gibt keinen standardisierten Weg, dies zu tun. Für Windows wird die Funktionalität in der DbgHelp- Bibliothek bereitgestellt


6

Gibt es eine Möglichkeit, den Aufrufstapel in einem laufenden Prozess in C oder C ++ bei jedem Aufruf einer bestimmten Funktion zu sichern?

Sie können in der jeweiligen Funktion eine Makrofunktion anstelle der return-Anweisung verwenden.

Zum Beispiel, anstatt return zu verwenden,

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

    ... do something ...

    return 0
}

Sie können eine Makrofunktion verwenden.

#include "c-callstack.h"

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

    ... do something ...

    NL_RETURN(0);
}

Wenn in einer Funktion ein Fehler auftritt, wird der Aufrufstapel im Java-Stil angezeigt (siehe unten).

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)

Den vollständigen Quellcode finden Sie hier.

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


6

Eine andere Antwort auf einen alten Thread.

Wenn ich das tun muss, benutze ich normalerweise nur system() undpstack

Also so etwas wie das:

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

Dies gibt aus

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

Dies sollte unter Linux, FreeBSD und Solaris funktionieren. Ich denke nicht, dass macOS pstack oder ein einfaches Äquivalent hat, aber dieser Thread scheint eine Alternative zu haben .

Wenn Sie verwenden C, müssen Sie CZeichenfolgenfunktionen verwenden.

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

Ich habe 7 für die maximale Anzahl von Stellen in der PID verwendet, basierend auf diesem Beitrag .


Guter Punkt, da das Thema nach C fragt. Nein, es müsste angepasst werden, da std :: string nur C ++ ist. Ich werde meine Antwort mit einer C-Version aktualisieren.
Paul Floyd

5

Linux-spezifisch, TLDR:

  1. backtracein glibcerzeugt nur dann genaue Stacktraces, wenn -lunwindeine Verknüpfung besteht (undokumentierte plattformspezifische Funktion).
  2. Zur Ausgabe von Funktionsnamen , Quelldatei und Zeilennummer verwenden #include <elfutils/libdwfl.h>(diese Bibliothek dokumentierte nur in der Header - Datei). backtrace_symbolsund backtrace_symbolsd_fdsind am wenigsten informativ.

Unter modernem Linux können Sie die Stacktrace-Adressen mithilfe der Funktion abrufen backtrace. Die undokumentierte Möglichkeit, backtraceauf gängigen Plattformen genauere Adressen zu erstellen, besteht in der Verknüpfung mit -lunwind( libunwind-devunter Ubuntu 18.04) (siehe Beispielausgabe unten). backtraceverwendet die Funktion _Unwind_Backtraceund standardmäßig stammt diese von libgcc_s.so.1und diese Implementierung ist am portabelsten. Wenn -lunwindes verknüpft ist, bietet es eine genauere Version von, _Unwind_Backtraceaber diese Bibliothek ist weniger portabel (siehe unterstützte Architekturen in libunwind/src).

Leider konnten der Begleiter backtrace_symbolsdund die backtrace_symbols_fdFunktionen die Stacktrace-Adressen seit wahrscheinlich einem Jahrzehnt nicht mehr in Funktionsnamen mit Quelldateinamen und Zeilennummer auflösen (siehe Beispielausgabe unten).

Es gibt jedoch eine andere Methode, um Adressen in Symbole aufzulösen, und sie erzeugt die nützlichsten Spuren mit Funktionsname , Quelldatei und Zeilennummer . Die Methode ist zu #include <elfutils/libdwfl.h>und verknüpfen mit -ldw( libdw-devunter Ubuntu 18.04).

Arbeits C ++ Beispiel ( 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");
}

Kompiliert unter Ubuntu 18.04.4 LTS mit 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

Ausgänge:

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

Wenn no -lunwindverknüpft ist, wird eine weniger genaue Stapelverfolgung erstellt:

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

Zum Vergleich ist die backtrace_symbols_fdAusgabe für denselben Stacktrace am wenigsten informativ:

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

In einer Produktionsversion (sowie C Sprachversion) können Sie diesen Code besonders robust zu machen , durch den Austausch boost::core::demangle, std::stringundstd::cout mit ihren zugrunde liegenden Anrufen.

Sie können auch überschreiben __cxa_throw, um die Stapelverfolgung zu erfassen, wenn eine Ausnahme ausgelöst wird, und sie zu drucken, wenn die Ausnahme abgefangen wird. Zum Zeitpunkt des Eintritts in den catchBlock wurde der Stapel abgewickelt, sodass es zu spät ist, ihn aufzurufen backtrace. Aus diesem Grund muss der Stapel erfasst werden, auf throwdem die Funktion implementiert ist __cxa_throw. Beachten Sie, dass in einem Multithread-Programm __cxa_throwmehrere Threads gleichzeitig aufrufen können, sodass dies der Fall sein muss, wenn der Stacktrace in einem globalen Array erfasst wird thread_local.


1
Gute Antwort! Auch gut recherchiert.
SS Anne

@SSAnne Sehr nett, danke. Dieses -lunwindProblem wurde beim Erstellen dieses Beitrags entdeckt. Ich habe es zuvor libunwinddirekt verwendet, um den Stacktrace zu erhalten, und wollte es veröffentlichen, backtracemache es aber für mich, wenn -lunwindes verlinkt ist.
Maxim Egorushkin

1
@SSAnne Vielleicht, weil der ursprüngliche Autor der Bibliothek, David Mosberger, sich ursprünglich auf IA-64 konzentrierte, aber dann bekam die Bibliothek mehr Traktion nongnu.org/libunwind/people.html . gccmacht die API nicht verfügbar, stimmt das?
Maxim Egorushkin

3

Sie können die Funktionalität selbst implementieren:

Verwenden Sie einen globalen (String-) Stapel und drücken Sie zu Beginn jeder Funktion den Funktionsnamen und solche anderen Werte (z. B. Parameter) auf diesen Stapel. beim Beenden der Funktion erneut einfügen.

Schreiben Sie eine Funktion, die den Stapelinhalt beim Aufruf ausdruckt, und verwenden Sie diese in der Funktion, in der Sie den Aufrufstapel anzeigen möchten.

Das mag nach viel Arbeit klingen, ist aber sehr nützlich.


2
Ich würde das nicht tun. Ich würde vielmehr einen Wrapper erstellen, der die zugrunde liegenden plattformspezifischen APIs verwendet (siehe unten). Der Arbeitsaufwand wäre wahrscheinlich der gleiche, aber die Investition sollte sich schneller auszahlen.
Paul Michalik

3
@paul: Ihre Antwort bezieht sich auf Windows, wenn das OP Linux eindeutig spezifiziert ... könnte aber für Windows-Leute nützlich sein, die hier auftauchen.
Slashmais

Richtig, ich habe das übersehen. Hm, es ist der letzte Satz der Frage. Vielleicht sollte das Poster seine Anfrage ändern, um seine Zielplattform an einer prominenteren Stelle zu erwähnen.
Paul Michalik

1
Dies wäre eine gute Idee, außer dass meine Codebasis einige Dutzend Dateien enthält, die einige hundert (wenn nicht einige tausend) Dateien enthalten. Dies ist also nicht möglich.
Nathan Fellman

Möglicherweise nicht, wenn Sie ein sed / perl-Skript hacken, das nach jeder Funktionsdeklaration hinzugefügt werden soll call_registror MY_SUPERSECRETNAME(__FUNCTION__); die das Argument in ihrem Konstruktor drückt und in seinem Destruktor erscheint. FUNCTION repräsentiert immer den Namen der aktuellen Funktion.
Flownt

2

Die nächste Frage ist natürlich: Wird das ausreichen?

Der Hauptnachteil von Stack-Traces besteht darin, dass Sie, warum Sie die genaue Funktion haben, die aufgerufen wird, nichts anderes haben, wie den Wert der Argumente, was für das Debuggen sehr nützlich ist.

Wenn Sie Zugriff auf gcc und gdb haben, würde ich empfehlen, zu verwenden assert, um nach einer bestimmten Bedingung zu suchen und einen Speicherauszug zu erstellen, wenn dieser nicht erfüllt ist. Dies bedeutet natürlich, dass der Prozess gestoppt wird, aber Sie haben einen vollständigen Bericht anstelle einer bloßen Stapelverfolgung.

Wenn Sie einen weniger aufdringlichen Weg wünschen, können Sie jederzeit die Protokollierung verwenden. Es gibt sehr effiziente Protokollierungsmöglichkeiten, wie zum Beispiel Pantheios . Dies könnte Ihnen wieder ein viel genaueres Bild davon geben, was vor sich geht.


1
Natürlich mag es nicht genug sein, aber wenn ich sehe, dass die Funktion mit einer Konfiguration und nicht mit der anderen aufgerufen wird, dann ist das ein ziemlich guter Anfang.
Nathan Fellman

2

Sie können Poppy dafür verwenden. Es wird normalerweise verwendet, um den Stack-Trace während eines Absturzes zu erfassen, kann ihn aber auch für ein laufendes Programm ausgeben.

Hier ist der gute Teil: Es kann die tatsächlichen Parameterwerte für jede Funktion auf dem Stapel und sogar lokale Variablen, Schleifenzähler usw. ausgeben.


2

Ich weiß, dass dieser Thread alt ist, aber ich denke, dass er für andere Leute nützlich sein kann. Wenn Sie gcc verwenden, können Sie die Gerätefunktionen (Option -finstrument-functions) verwenden, um jeden Funktionsaufruf (Ein- und Ausstieg) zu protokollieren. Weitere Informationen finden Sie hier: http://hacktalks.blogspot.fr/2013/08/gcc-instrument-functions.html

So können Sie beispielsweise jeden Aufruf in einen Stapel schieben und einfügen, und wenn Sie ihn drucken möchten, sehen Sie sich nur an, was sich in Ihrem Stapel befindet.

Ich habe es getestet, es funktioniert perfekt und ist sehr praktisch

UPDATE: Informationen zur Kompilierungsoption -finstrument-functions finden Sie im GCC-Dokument zu den Instrumentierungsoptionen: https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html


Sie sollten auch auf GCC-Dokumente verlinken, falls der Artikel ausfällt.
HolyBlackCat

Danke, du hast recht. Ich habe daher ein UPDATE in meinem Beitrag mit einem Link zum gcc doc hinzugefügt
François

2

Mit den Boost-Bibliotheken können Sie den aktuellen Callstack drucken.

#include <boost/stacktrace.hpp>

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

Mann hier: https://www.boost.org/doc/libs/1_65_1/doc/html/stacktrace.html


Ich habe einen Fehler cannot locate SymEnumSymbolsExW at C:\Windows\SYSTEM32\dbgeng.dllunter Win10 erhalten.
zwcloud

0

Sie können den GNU-Profiler verwenden. Es zeigt auch das Anrufdiagramm! Der Befehl lautet gprofund Sie müssen Ihren Code mit einer Option kompilieren.


-6

Gibt es eine Möglichkeit, den Aufrufstapel in einem laufenden Prozess in C oder C ++ bei jedem Aufruf einer bestimmten Funktion zu sichern?

Nein, gibt es nicht, obwohl plattformabhängige Lösungen existieren könnten.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.