imprimer la pile d'appels en C ou C ++

120

Existe-t-il un moyen de vider la pile d'appels dans un processus en cours d'exécution en C ou C ++ chaque fois qu'une certaine fonction est appelée? Ce que j'ai à l'esprit est quelque chose comme ceci:

void foo()
{
   print_stack_trace();

   // foo's body

   return
}

print_stack_tracefonctionne de la même manière callerqu'en Perl.

Ou quelque chose comme ça:

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

    // etc...
}

register_stack_trace_functionmet une sorte de point d'arrêt interne qui provoquera l'impression d'une trace de pile à chaque fooappel.

Est-ce que quelque chose comme ça existe dans une bibliothèque C standard?

Je travaille sous Linux, en utilisant GCC.


Contexte

J'ai un test qui se comporte différemment en fonction de certains commutateurs de ligne de commande qui ne devraient pas affecter ce comportement. Mon code a un générateur de nombres pseudo-aléatoires qui, je suppose, est appelé différemment en fonction de ces commutateurs. Je veux pouvoir exécuter le test avec chaque ensemble de commutateurs et voir si le générateur de nombres aléatoires est appelé différemment pour chacun.

Nathan Fellman
la source
1
@Armen, connaissez-vous l'un d'entre eux?
Nathan Fellman
1
@Nathan: Si votre débogueur est gdb, il peut gérer ce cas . Je ne peux pas vous parler des autres, mais je suppose que gdb n'est pas le seul à avoir cette fonctionnalité. A part: je viens de regarder mon commentaire précédent. :: gag :: s/easier/either/Comment diable est-ce arrivé?
dmckee --- chaton ex-modérateur
2
@dmckee: En fait, ça devrait l'être s/either/easier. Ce que j'aurais besoin de faire avec gdb, c'est d'écrire un script qui interrompt cette fonction et imprime la trace de la pile, puis continue. Maintenant que j'y pense, il est peut-être temps pour moi d'en apprendre davantage sur les scripts gdb.
Nathan Fellman
1
Gah! Je vais dormir.
Vraiment
1
Version C uniquement: stackoverflow.com/questions/105659/…
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

Réponses:

79

Pour une solution Linux uniquement, vous pouvez utiliser backtrace (3) qui renvoie simplement un tableau de void *(en fait, chacun de ces points pointe vers l'adresse de retour du cadre de pile correspondant). Pour les traduire en quelque chose d'utile, il y a backtrace_symbols (3) .

Faites attention à la section des notes dans backtrace (3) :

Les noms de symboles peuvent ne pas être disponibles sans l'utilisation d'options spéciales de l'éditeur de liens. Pour les systèmes utilisant l'éditeur de liens GNU, il est nécessaire d'utiliser l'option -rdynamic linker. Notez que les noms des fonctions "statiques" ne sont pas exposés et ne seront pas disponibles dans la trace arrière.

Idan K
la source
10
FWIW, cette fonctionnalité existe également sur Mac OS X: developer.apple.com/library/mac/#documentation/Darwin/Reference
...
9
Windows a CaptureStackBackTrace
bobobobo
2
Apple a changé le lien developer.apple.com/legacy/library/documentation/Darwin/...
Ben Ylvisaker
Sur Linux avec glibc, malheureusement, les backtrace_symbolsfonctions ne fournissent pas le nom de la fonction, le nom du fichier source et le numéro de ligne.
Maxim Egorushkin
En plus d'utiliser -rdynamic, vérifiez également que votre système de construction n'ajoute pas d' -fvisibility=hiddenoption! (car il supprimera complètement l'effet de -rdynamic)
Dima Litvinov
38

Boost stacktrace

Documenté à: https://www.boost.org/doc/libs/1_66_0/doc/html/stacktrace/getting_started.html#stacktrace.getting_started.how_to_print_current_call_stack

C'est l'option la plus pratique que j'ai vue jusqu'à présent, car elle:

  • peut en fait imprimer les numéros de ligne.

    Il vient en fait des appels à addr2linecependant , ce qui est laid et pourrait être lent si votre prennent trop de traces.

  • démêle par défaut

  • Boost est uniquement en-tête, donc pas besoin de modifier votre système de construction très probablement

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

Malheureusement, cela semble être un ajout plus récent, et le package libboost-stacktrace-devn'est pas présent dans Ubuntu 16.04, seulement 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

Nous devons ajouter -ldlà la fin sinon la compilation échoue.

Production:

 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

La sortie et est expliquée plus en détail dans la section "Glibc Backtrace" ci-dessous, qui est analogue.

Notez comment my_func_1(int)et my_func_1(float), qui sont mutilés en raison d'une surcharge de fonction , ont été bien démêlés pour nous.

Notez que les premiers intappels sont coupés d'une ligne (28 au lieu de 27 et le second de deux lignes (27 au lieu de 29). Il a été suggéré dans les commentaires que cela est dû au fait que l'adresse d'instruction suivante est considérée, ce qui fait que 27 devient 28 et 29 saute de la boucle et devient 27.

On observe alors qu'avec -O3, la sortie est complètement mutilée:

 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

Les backtraces sont en général irrémédiablement mutilées par les optimisations. L'optimisation des appels de queue en est un exemple notable: qu'est-ce que l'optimisation des appels de queue?

Benchmark exécuté sur -O3:

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

Production:

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

Donc, comme prévu, nous voyons que cette méthode est extrêmement lente pour les appels externes addr2line, et ne sera faisable que si un nombre limité d'appels est effectué.

Chaque impression de trace arrière semble prendre des centaines de millisecondes, alors soyez averti que si une trace arrière se produit très souvent, les performances du programme en souffriront considérablement.

Testé sur Ubuntu 19.10, GCC 9.2.1, boost 1.67.0.

glibc backtrace

Documenté sur: https://www.gnu.org/software/libc/manual/html_node/Backtraces.html

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

Compiler:

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

-rdynamic est l'option clé requise.

Courir:

./main.out

Les sorties:

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

Nous voyons donc immédiatement qu'une optimisation en ligne s'est produite, et certaines fonctions ont été perdues de la trace.

Si nous essayons d'obtenir les adresses:

addr2line -e main.out 0x4008f9 0x4008fe

on obtient:

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

qui est complètement éteint.

Si nous faisons la même chose avec à la -O0place, ./main.outdonne la trace complète correcte:

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

puis:

addr2line -e main.out 0x400a74 0x400a79

donne:

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

donc les lignes sont décalées d'un seul, TODO pourquoi? Mais cela pourrait encore être utilisable.

Conclusion: les backtraces ne peuvent apparaître parfaitement qu'avec -O0. Avec les optimisations, la trace d'origine est fondamentalement modifiée dans le code compilé.

Je n'ai pas pu trouver un moyen simple de démêler automatiquement les symboles C ++ avec ceci cependant, voici quelques hacks:

Testé sur Ubuntu 16.04, GCC 6.4.0, libc 2.23.

glibc backtrace_symbols_fd

Cet assistant est un peu plus pratique que backtrace_symbols, et produit une sortie fondamentalement identique:

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

Testé sur Ubuntu 16.04, GCC 6.4.0, libc 2.23.

glibc backtraceavec piratage de démêlage C ++ 1: -export-dynamic+dladdr

Adapté de: https://gist.github.com/fmela/591333/c64f4eb86037bb237862a8283df70cdfc25f01d3

C'est un "hack" car il faut changer l'ELF avec -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);
}

Compilez et exécutez:

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 

production:

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]

Testé sur Ubuntu 18.04.

glibc backtraceavec C ++ demangling hack 2: analyse de la sortie de la trace de retour

Montré à: https://panthema.net/2008/0901-stacktrace-demangled/

Ceci est un hack car il nécessite une analyse.

TODO le compiler et le montrer ici.

libunwind

TODO est-ce que cela a un avantage sur le backtrace glibc? Une sortie très similaire, nécessite également de modifier la commande build, mais ne fait pas partie de la glibc donc nécessite une installation de package supplémentaire.

Code adapté de: https://eli.thegreenplace.net/2015/programmatic-access-to-the-call-stack-in-c/

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

Compilez et exécutez:

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

Soit il #define _XOPEN_SOURCE 700faut être au top, soit il faut utiliser -std=gnu99:

Courir:

./main.out

Production:

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

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

et:

addr2line -e main.out 0x4007db 0x4007e2

donne:

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

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

et:

addr2line -e main.out 0x4009f3 0x4009f8

donne:

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

Testé sur Ubuntu 16.04, GCC 6.4.0, libunwind 1.1.

libunwind avec démêlage de nom C ++

Code adapté de: https://eli.thegreenplace.net/2015/programmatic-access-to-the-call-stack-in-c/

dérouler.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);
}

Compilez et exécutez:

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

Production:

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)

et puis nous pouvons trouver les lignes de my_func_2et my_func_1(int)avec:

addr2line -e unwind.out 0x400c80 0x400cb7

qui donne:

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

TODO: pourquoi les lignes sont-elles une par une?

Testé sur Ubuntu 18.04, GCC 7.4.0, libunwind 1.2.1.

Automatisation GDB

Nous pouvons également faire cela avec GDB sans recompiler en utilisant: Comment faire une action spécifique quand un certain point d'arrêt est atteint dans GDB?

Bien que si vous envisagez d'imprimer beaucoup la trace arrière, ce sera probablement moins rapide que les autres options, mais peut-être que nous pouvons atteindre des vitesses natives avec compile code, mais je suis paresseux de le tester maintenant: Comment appeler l'assembly dans 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

Compilez et exécutez:

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

Production:

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 Je voulais faire cela avec juste à -expartir de la ligne de commande pour ne pas avoir à créer main.gdbmais je ne pouvais pas faire commandsfonctionner le.

Testé dans Ubuntu 19.04, GDB 8.2.

Noyau Linux

Comment imprimer la trace actuelle de la pile de threads dans le noyau Linux?

libdwfl

Cela a été mentionné à l'origine sur: https://stackoverflow.com/a/60713161/895245 et c'est peut-être la meilleure méthode, mais je dois évaluer un peu plus, mais veuillez voter pour cette réponse.

TODO: J'ai essayé de minimiser le code de cette réponse, qui fonctionnait, à une seule fonction, mais il s'agit de segfaulting, faites-moi savoir si quelqu'un peut trouver pourquoi.

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

Compilez et exécutez:

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

Production:

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

Analyse comparative:

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

Production:

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

Nous voyons donc que cette méthode est 10 fois plus rapide que le stacktrace de Boost, et pourrait donc être applicable à plus de cas d'utilisation.

Testé dans Ubuntu 19.10 amd64, libdw-dev 0.176-1.1.

Voir également

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
la source
1
Tous les "TODO: lignes décalées d'un" sont dus au fait que le numéro de ligne est pris à partir du début de l'expression suivante.
SS Anne
6

Il n'y a pas de moyen normalisé de le faire. Pour Windows, la fonctionnalité est fournie dans la bibliothèque DbgHelp

Paul Michalik
la source
6

Existe-t-il un moyen de vider la pile d'appels dans un processus en cours d'exécution en C ou C ++ chaque fois qu'une certaine fonction est appelée?

Vous pouvez utiliser une fonction macro au lieu d'une instruction return dans la fonction spécifique.

Par exemple, au lieu d'utiliser return,

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

    ... do something ...

    return 0
}

Vous pouvez utiliser une fonction macro.

#include "c-callstack.h"

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

    ... do something ...

    NL_RETURN(0);
}

Chaque fois qu'une erreur se produit dans une fonction, vous verrez une pile d'appels de style Java comme indiqué ci-dessous.

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)

Le code source complet est disponible ici.

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

NullPointerException
la source
6

Une autre réponse à un vieux fil.

Lorsque j'ai besoin de faire cela, j'utilise généralement system()etpstack

Donc quelque chose comme ça:

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

Cette sortie

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

Cela devrait fonctionner sous Linux, FreeBSD et Solaris. Je ne pense pas que macOS ait pstack ou un simple équivalent, mais ce fil semble avoir une alternative .

Si vous utilisez C, vous devrez utiliser Cdes fonctions de chaîne.

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

J'ai utilisé 7 pour le nombre maximum de chiffres dans le PID, basé sur ce post .

Paul Floyd
la source
Bon point, puisque le sujet demande du C. Non, il faudrait adapter, puisque std :: string est uniquement C ++. Je mettrai à jour ma réponse avec une version C.
Paul Floyd
6

Spécifique à Linux, TLDR:

  1. backtracein glibcproduit des traces de pile précises uniquement lorsqu'il -lunwindest lié (fonctionnalité non documentée spécifique à la plateforme).
  2. Pour afficher le nom de la fonction , le fichier source et le numéro de ligne, utilisez #include <elfutils/libdwfl.h>(cette bibliothèque n'est documentée que dans son fichier d'en-tête). backtrace_symbolset backtrace_symbolsd_fdsont les moins informatifs.

Sur Linux moderne, vous pouvez obtenir les adresses stacktrace en utilisant function backtrace. Le moyen non documenté de backtraceproduire des adresses plus précises sur les plates-formes populaires consiste à établir un lien avec -lunwind( libunwind-devsur Ubuntu 18.04) (voir l'exemple de sortie ci-dessous). backtraceutilise la fonction _Unwind_Backtraceet par défaut celle-ci provient libgcc_s.so.1et cette implémentation est la plus portable. Quand -lunwindest lié, il fournit une version plus précise de _Unwind_Backtracemais cette bibliothèque est moins portable (voir les architectures prises en charge dans libunwind/src).

Malheureusement, le compagnon backtrace_symbolsdet les backtrace_symbols_fdfonctions n'ont pas été en mesure de résoudre les adresses stacktrace en noms de fonction avec le nom du fichier source et le numéro de ligne depuis probablement une décennie maintenant (voir l'exemple de sortie ci-dessous).

Cependant, il existe une autre méthode pour résoudre les adresses en symboles et elle produit les traces les plus utiles avec le nom de la fonction , le fichier source et le numéro de ligne . La méthode consiste à #include <elfutils/libdwfl.h>établir un lien avec -ldw( libdw-devsur Ubuntu 18.04).

Exemple de travail 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");
}

Compilé sur Ubuntu 18.04.4 LTS avec 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

Les sorties:

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

Quand no -lunwindest lié, cela produit un stacktrace moins précis:

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

À titre de comparaison, la backtrace_symbols_fdsortie pour le même stacktrace est la moins informative:

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

Dans une version de production (ainsi que la version de langue C) , vous pouvez vous faire ce code supplémentaire robuste en remplaçant boost::core::demangle, std::stringet std::coutavec leurs appels sous - jacents.

Vous pouvez également remplacer __cxa_throwpour capturer le stacktrace lorsqu'une exception est levée et l'imprimer lorsque l'exception est interceptée. Au moment où il entre dans le catchbloc, la pile a été déroulée, il est donc trop tard pour appeler backtrace, et c'est pourquoi la pile doit être capturée sur throwlaquelle est implémentée par fonction __cxa_throw. Notez que dans un programme multi-thread __cxa_throwpeut être appelé simultanément par plusieurs threads, de sorte que s'il capture le stacktrace dans un tableau global qui doit être thread_local.

Maxim Egorushkin
la source
1
Bonne réponse! Bien documenté aussi.
SS Anne
@SSAnne Très gentil, merci. Ce -lunwindproblème a été découvert lors de la création de cet article, je l'utilisais auparavant libunwinddirectement pour obtenir le stacktrace et j'allais le publier, mais le backtracefait pour moi quand il -lunwindest lié.
Maxim Egorushkin
1
@SSAnne Peut-être parce que l'auteur original de la bibliothèque David Mosberger s'est concentré sur IA-64 au départ, mais ensuite la bibliothèque a obtenu plus de traction nongnu.org/libunwind/people.html . gccn'expose pas l'API, n'est-ce pas?
Maxim Egorushkin
3

Vous pouvez implémenter vous-même la fonctionnalité:

Utilisez une pile globale (chaîne) et au début de chaque fonction, poussez le nom de la fonction et d'autres valeurs (par exemple, des paramètres) sur cette pile; à la sortie de la fonction, le pop à nouveau.

Écrivez une fonction qui imprimera le contenu de la pile lorsqu'elle sera appelée et utilisez-la dans la fonction où vous voulez voir la pile d'appels.

Cela peut sembler beaucoup de travail mais c'est assez utile.

slashmais
la source
2
Je ne ferais pas ça. Au lieu de cela, je créerais un wrapper qui utilise les API spécifiques à la plate-forme sous-jacente (voir ci-dessous). La quantité de travail serait probablement la même, mais l'investissement devrait porter ses fruits plus rapidement.
Paul Michalik
3
@paul: votre réponse se réfère à Windows lorsque l'OP spécifie clairement Linux ... mais pourrait être utile pour les gars de Windows qui apparaissent ici.
slashmais
Bon, j'ai oublié ça ... Hm, c'est la dernière phrase de la question, alors peut-être que l'affiche devrait modifier sa demande pour mentionner sa plate-forme cible dans un endroit plus visible.
Paul Michalik
1
Ce serait une bonne idée, sauf que ma base de code comprend quelques dizaines de fichiers contenant quelques centaines (voire quelques milliers) de fichiers, ce n'est donc pas faisable.
Nathan Fellman
peut-être pas si vous hackez un script sed / perl à ajouter après chaque déclaration de fonction call_registror MY_SUPERSECRETNAME(__FUNCTION__);qui pousse l'argument dans son constructeur et apparaît dans son destructeur FUNCTION représente toujours le nom de la fonction courante.
volé
2

Bien sûr, la question suivante est: cela suffira-t-il?

Le principal inconvénient de stack-traces est que pourquoi vous avez la fonction précise appelée, vous n'avez rien d'autre, comme la valeur de ses arguments, ce qui est très utile pour le débogage.

Si vous avez accès à gcc et gdb, je vous suggère d'utiliser assertpour vérifier une condition spécifique et de produire un vidage de la mémoire si elle n'est pas remplie. Bien sûr, cela signifie que le processus s'arrêtera, mais vous aurez un rapport complet au lieu d'une simple trace de pile.

Si vous souhaitez un moyen moins intrusif, vous pouvez toujours utiliser la journalisation. Il existe des installations d'exploitation forestière très efficaces, comme Pantheios par exemple. Ce qui, une fois de plus, pourrait vous donner une image beaucoup plus précise de ce qui se passe.

Matthieu M.
la source
1
Bien sûr, cela peut ne pas suffire, mais si je peux voir que la fonction est appelée en place avec une configuration et pas avec l'autre, alors c'est un très bon point de départ.
Nathan Fellman
2

Vous pouvez utiliser Poppy pour cela. Il est normalement utilisé pour rassembler la trace de la pile lors d'un crash, mais il peut également le générer pour un programme en cours d'exécution.

Maintenant, voici la bonne partie: il peut générer les valeurs de paramètres réelles pour chaque fonction de la pile, et même des variables locales, des compteurs de boucle, etc.

Orlin Georgiev
la source
2

Je sais que ce fil est vieux, mais je pense qu'il peut être utile pour d'autres personnes. Si vous utilisez gcc, vous pouvez utiliser ses fonctions d'instrument (option -finstrument-functions) pour enregistrer n'importe quel appel de fonction (entrée et sortie). Jetez un œil à ceci pour plus d'informations: http://hacktalks.blogspot.fr/2013/08/gcc-instrument-functions.html

Vous pouvez ainsi, par exemple, pousser et insérer tous les appels dans une pile, et lorsque vous voulez l'imprimer, vous n'avez qu'à regarder ce que vous avez dans votre pile.

Je l'ai testé, il fonctionne parfaitement et est très pratique

MISE À JOUR: vous pouvez également trouver des informations sur l'option de compilation -finstrument-functions dans la doc GCC concernant les options d'Instrumentation: https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html

François
la source
Vous devriez également créer un lien vers les documents GCC au cas où l'article tomberait en panne.
HolyBlackCat
Merci, vous avez raison. J'ai donc ajouté une MISE À JOUR dans mon post avec un lien vers la doc gcc
François
2

Vous pouvez utiliser les bibliothèques Boost pour imprimer la pile d'appels actuelle.

#include <boost/stacktrace.hpp>

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

Homme ici: https://www.boost.org/doc/libs/1_65_1/doc/html/stacktrace.html

Barkles
la source
J'ai une erreur cannot locate SymEnumSymbolsExW at C:\Windows\SYSTEM32\dbgeng.dllsur Win10.
zwcloud
0

Vous pouvez utiliser le profileur GNU. Il montre également le graphe d'appel! la commande est gprofet vous devez compiler votre code avec une option.

Saurabh Shah
la source
-6

Existe-t-il un moyen de vider la pile d'appels dans un processus en cours d'exécution en C ou C ++ chaque fois qu'une certaine fonction est appelée?

Non, il n'y en a pas, bien que des solutions dépendant de la plate-forme puissent exister.

sbi
la source