imprimir pilha de chamadas em C ou C ++


120

Existe alguma maneira de despejar a pilha de chamadas em um processo em execução em C ou C ++ toda vez que uma determinada função é chamada? O que tenho em mente é algo assim:

void foo()
{
   print_stack_trace();

   // foo's body

   return
}

Onde print_stack_tracefunciona de forma semelhante ao callerPerl.

Ou algo parecido com isto:

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

    // etc...
}

onde register_stack_trace_functioncoloca algum tipo de ponto de interrupção interno que fará com que um rastreamento de pilha seja impresso sempre que foofor chamado.

Algo assim existe em alguma biblioteca C padrão?

Estou trabalhando no Linux, usando GCC.


fundo

Eu tenho um teste executado que se comporta de maneira diferente com base em algumas opções de linha de comando que não devem afetar esse comportamento. Meu código tem um gerador de números pseudo-aleatórios que suponho que seja chamado de maneira diferente com base nessas opções. Quero ser capaz de executar o teste com cada conjunto de opções e ver se o gerador de números aleatórios é chamado de maneira diferente para cada um.


1
@Armen, você está familiarizado com algum deles?
Nathan Fellman,

1
@Nathan: Se o seu depurador for gdb, ele pode lidar com esse caso . Não posso falar sobre outros, mas presumo que o gdb não seja o único a ter essa funcionalidade. À parte: acabei de ler meu comentário anterior. :: gag :: s/easier/either/como diabos isso aconteceu?
dmckee --- ex-moderador gatinho

2
@dmckee: Na verdade, deveria ser s/either/easier. O que eu precisaria fazer com o gdb é escrever um script que interrompa essa função e imprima o rastreamento da pilha e, em seguida, continue. Agora que pensei sobre isso, talvez seja a hora de aprender sobre scripts gdb.
Nathan Fellman,

1
Gah! Vou dormir um pouco. Muito em breve agora ...
dmckee --- gatinho ex-moderador

Respostas:


79

Para uma solução somente Linux, você pode usar backtrace (3) que simplesmente retorna um array de void *(na verdade, cada um deles aponta para o endereço de retorno do frame de pilha correspondente). Para traduzir isso em algo útil, há backtrace_symbols (3) .

Preste atenção à seção de notas em backtrace (3) :

Os nomes dos símbolos podem estar indisponíveis sem o uso de opções especiais do vinculador. Para sistemas que usam o linker GNU, é necessário usar a opção -rdynamic linker. Observe que os nomes das funções "estáticas" não são expostos e não estarão disponíveis no backtrace.


10
FWIW, essa funcionalidade também existe no Mac OS X: developer.apple.com/library/mac/#documentation/Darwin/Reference/…
EmeryBerger

8
O Windows tem CaptureStackBackTrace
bobobobo


No Linux com glibc, infelizmente, as backtrace_symbolsfunções não fornecem o nome da função, o nome do arquivo de origem e o número da linha.
Maxim Egorushkin

Além de usar -rdynamic, verifique também se o seu sistema de compilação não adiciona -fvisibility=hiddenopções! (pois descartará completamente o efeito de -rdynamic)
Dima Litvinov

36

Boost stacktrace

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

Esta é a opção mais conveniente que vi até agora, porque:

  • pode realmente imprimir os números das linhas.

    No entanto , ele apenas faz ligações paraaddr2line , o que é feio e pode ser lento se você estiver registrando muitos rastros.

  • demangles por padrão

  • Boost é apenas o cabeçalho, então não há necessidade de modificar seu sistema de compilação provavelmente

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

Infelizmente, parece ser uma adição mais recente, e o pacote libboost-stacktrace-dev não está presente no Ubuntu 16.04, apenas 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

Temos que adicionar -ldlno final ou então a compilação falhará.

Resultado:

 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

A saída é explicada com mais detalhes na seção "glibc backtrace" abaixo, que é análoga.

Observe como my_func_1(int)e my_func_1(float), que estão mutilados devido à sobrecarga de função , foram bem demangled para nós.

Observe que a primeira intchamada está desativada por uma linha (28 em vez de 27 e a segunda está desativada por duas linhas (27 em vez de 29). Foi sugerido nos comentários que isso ocorre porque o seguinte endereço de instrução está sendo considerado, o que faz com que 27 se tornem 28, e 29 pulem do loop e se tornem 27.

Em seguida, observamos que com -O3, a saída é completamente mutilada:

 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 são em geral irreparavelmente mutilados por otimizações. A otimização da chamada final é um exemplo notável disso: O que é a otimização da chamada final?

Teste de referência executado em -O3:

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

Resultado:

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

Como esperado, vemos que esse método é extremamente lento para chamadas externas addr2line, e só será viável se um número limitado de chamadas estiver sendo feito.

Cada impressão de backtrace parece levar centenas de milissegundos, portanto, esteja avisado de que se um backtrace acontecer com muita frequência, o desempenho do programa sofrerá significativamente.

Testado no Ubuntu 19.10, GCC 9.2.1, boost 1.67.0.

glibc backtrace

Documentado em: 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;
}

Compilar:

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

-rdynamic é a opção chave necessária.

Corre:

./main.out

Saídas:

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

Assim, vemos imediatamente que uma otimização inlining aconteceu e algumas funções foram perdidas no rastreamento.

Se tentarmos obter os endereços:

addr2line -e main.out 0x4008f9 0x4008fe

nós obtemos:

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

que está completamente desligado.

Se fizermos o mesmo com -O0, ./main.outfornece o rastreamento completo correto:

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

e depois:

addr2line -e main.out 0x400a74 0x400a79

dá:

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

então as linhas estão erradas por apenas uma, TODO por quê? Mas ainda pode ser usado.

Conclusão: backtraces só podem ser mostrados perfeitamente com -O0. Com otimizações, o backtrace original é fundamentalmente modificado no código compilado.

Não consegui encontrar uma maneira simples de remover automaticamente os símbolos C ++ com isso, no entanto, aqui estão alguns truques:

Testado no Ubuntu 16.04, GCC 6.4.0, libc 2.23.

glibc backtrace_symbols_fd

Este auxiliar é um pouco mais conveniente backtrace_symbolse produz uma saída basicamente idêntica:

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

Testado no Ubuntu 16.04, GCC 6.4.0, libc 2.23.

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

Adaptado de: https://gist.github.com/fmela/591333/c64f4eb86037bb237862a8283df70cdfc25f01d3

Este é um "hack" porque requer a troca do ELF com -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);
}

Compile e execute:

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 

resultado:

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]

Testado no Ubuntu 18.04.

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

Exibido em: https://panthema.net/2008/0901-stacktrace-demangled/

Este é um hack porque requer análise.

TODO pegue para compilar e mostre aqui.

Libunwind

TODO isso tem alguma vantagem sobre o backtrace glibc? Uma saída muito semelhante também requer a modificação do comando build, mas não faz parte da glibc, portanto, requer uma instalação de pacote extra.

Código adaptado de: 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;
}

Compile e execute:

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

Ou #define _XOPEN_SOURCE 700deve estar no topo, ou devemos usar -std=gnu99:

Corre:

./main.out

Resultado:

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

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

e:

addr2line -e main.out 0x4007db 0x4007e2

dá:

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

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

e:

addr2line -e main.out 0x4009f3 0x4009f8

dá:

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

Testado no Ubuntu 16.04, GCC 6.4.0, libunwind 1.1.

libunwind com eliminação de nomes C ++

Código adaptado de: https://eli.thegreenplace.net/2015/programmatic-access-to-the-call-stack-in-c/

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

Compile e execute:

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

Resultado:

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)

e então podemos encontrar as linhas de my_func_2e my_func_1(int)com:

addr2line -e unwind.out 0x400c80 0x400cb7

que dá:

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

TODO: por que as linhas estão desligadas por um?

Testado no Ubuntu 18.04, GCC 7.4.0, libunwind 1.2.1.

Automação GDB

Também podemos fazer isso com GDB sem recompilar usando: Como fazer uma ação específica quando um determinado ponto de interrupção é atingido no GDB?

Embora se você for imprimir muito o backtrace, provavelmente será menos rápido do que as outras opções, mas talvez possamos alcançar velocidades nativas com compile code, mas estou com preguiça de testá-lo agora: Como chamar assembly no 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

Compile e execute:

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

Resultado:

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 Eu queria fazer isso apenas -exna linha de comando para não precisar criar, main.gdbmas não consegui fazer o commandsfuncionar lá.

Testado no Ubuntu 19.04, GDB 8.2.

Kernel Linux

Como imprimir o rastreamento de pilha de thread atual dentro do kernel Linux?

libdwfl

Isso foi mencionado originalmente em: https://stackoverflow.com/a/60713161/895245 e pode ser o melhor método, mas eu tenho que fazer um benchmark um pouco mais, mas vá votar a favor dessa resposta.

TODO: Eu tentei minimizar o código dessa resposta, que estava funcionando, para uma única função, mas é segfaulting, me diga se alguém descobrir o porquê.

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

Compile e execute:

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

Resultado:

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

Teste de referência:

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

Resultado:

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

Portanto, vemos que esse método é 10 vezes mais rápido do que o rastreamento de pilha do Boost e, portanto, pode ser aplicável a mais casos de uso.

Testado no Ubuntu 19.10 amd64, libdw-dev 0.176-1.1.

Veja também


1
Todas as "TODO: linhas desligadas por um" são porque o número da linha é retirado do início da próxima expressão.
SS Anne,

6

Não existe uma maneira padronizada de fazer isso. Para Windows, a funcionalidade é fornecida na biblioteca DbgHelp


6

Existe alguma maneira de despejar a pilha de chamadas em um processo em execução em C ou C ++ toda vez que uma determinada função é chamada?

Você pode usar uma função macro em vez da instrução de retorno na função específica.

Por exemplo, em vez de usar return,

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

    ... do something ...

    return 0
}

Você pode usar uma função macro.

#include "c-callstack.h"

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

    ... do something ...

    NL_RETURN(0);
}

Sempre que ocorrer um erro em uma função, você verá a pilha de chamadas no estilo Java, conforme mostrado abaixo.

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)

O código-fonte completo está disponível aqui.

C-callstack em https://github.com/Nanolat


6

Outra resposta a um velho tópico.

Quando preciso fazer isso, normalmente uso system()epstack

Então, algo assim:

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

Isso produz

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

Isso deve funcionar no Linux, FreeBSD e Solaris. Não acho que o macOS tenha pstack ou um equivalente simples, mas este tópico parece ter uma alternativa .

Se estiver usando C, você precisará usar Cfunções de string.

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

Eu usei 7 para o número máximo de dígitos no PID, com base neste post .


Bom ponto, já que o assunto pede C. Não, ele precisaria ser adaptado, já que std :: string é apenas C ++. Vou atualizar minha resposta com uma versão C.
Paul Floyd

5

Específico para Linux, TLDR:

  1. backtracein glibcproduz rastreamentos de pilha precisos apenas quando -lunwindestá vinculado (recurso específico da plataforma não documentado).
  2. Para produzir o nome da função , o arquivo de origem e o número da linha, use #include <elfutils/libdwfl.h>(esta biblioteca está documentada apenas em seu arquivo de cabeçalho). backtrace_symbolse backtrace_symbolsd_fdsão menos informativos.

No Linux moderno, você pode obter os endereços de rastreamento de pilha usando a função backtrace. A maneira não documentada de backtraceproduzir endereços mais precisos em plataformas populares é vincular com -lunwind( libunwind-devno Ubuntu 18.04) (veja o exemplo de saída abaixo). backtraceusa a função _Unwind_Backtracee, por padrão, a última vem libgcc_s.so.1e essa implementação é mais portátil. Quando -lunwindestá vinculado, ele fornece uma versão mais precisa do, _Unwind_Backtracemas esta biblioteca é menos portátil (consulte as arquiteturas suportadas em libunwind/src).

Infelizmente, o companheiro backtrace_symbolsdebacktrace_symbols_fd funções não foram capazes de resolver os endereços de rastreamento de pilha para nomes de função com nome de arquivo de origem e número de linha por provavelmente uma década (veja o exemplo de saída abaixo).

No entanto, existe outro método para resolver endereços para símbolos e produz os traços mais úteis com o nome da função , arquivo de origem e número da linha . O método é para #include <elfutils/libdwfl.h>e link com -ldw( libdw-devno Ubuntu 18.04).

Exemplo funcional de 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");
}

Compilado no Ubuntu 18.04.4 LTS com 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

Saídas:

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

Quando nenhum -lunwindestá vinculado, ele produz um rastreamento de pilha menos preciso:

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

Para comparação, a backtrace_symbols_fdsaída para o mesmo rastreamento de pilha é menos informativa:

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

Em uma versão de produção (bem como a versão em linguagem C), você pode querer tornar este código mais robusto, substituindo boost::core::demangle, std::stringestd::cout com as suas chamadas subjacentes.

Você também pode substituir __cxa_throwpara capturar o rastreamento de pilha quando uma exceção é lançada e imprimi-lo quando a exceção é detectada. No momento em que ele entra no catchbloco, a pilha foi desfeita, então é tarde demais para chamar backtrace, e é por isso que a pilha deve ser capturada throwe implementada por função __cxa_throw. Observe que em um programa multi-threaded __cxa_throwpode ser chamado simultaneamente por vários threads, de modo que se ele captura o stacktrace em um array global que deve ser thread_local.


1
Boa resposta! Bem pesquisado também.
SS Anne

@SSAnne Muito gentil, obrigado. Esse -lunwindproblema foi descoberto ao fazer este post, eu usei anteriormente libunwinddiretamente para obter o stacktrace e ia postá-lo, mas backtracefaz isso para mim quando -lunwindestá vinculado.
Maxim Egorushkin,

1
@SSAnne Pode ser porque o autor original da biblioteca David Mosberger estava focado em IA-64 inicialmente, mas depois a biblioteca ganhou mais força nongnu.org/libunwind/people.html . gccnão expõe a API, certo?
Maxim Egorushkin

3

Você mesmo pode implementar a funcionalidade:

Use uma pilha global (string) e no início de cada função coloque o nome da função e outros valores (por exemplo, parâmetros) nesta pilha; ao sair da função, abra-o novamente.

Escreva uma função que imprimirá o conteúdo da pilha quando for chamada e use-a na função onde deseja ver a pilha de chamadas.

Isso pode parecer muito trabalhoso, mas é bastante útil.


2
Eu não faria isso. Em vez disso, eu criaria um wrapper que usa APIs específicas da plataforma subjacente (veja abaixo). A quantidade de trabalho provavelmente seria a mesma, mas o investimento deve pagar mais rápido.
Paul Michalik

3
@paul: sua resposta se refere a janelas quando o OP especifica claramente o linux ... mas pode ser útil para os caras do Windows que aparecem aqui.
slashmais de

Certo, esqueci isso ... Hum, é a última frase da pergunta, então talvez o autor da postagem deva modificar sua solicitação para mencionar sua plataforma de destino em um local mais proeminente.
Paul Michalik

1
Isso seria uma boa ideia, exceto que minha base de código inclui algumas dezenas de arquivos contendo algumas centenas (senão alguns milhares) de arquivos, então isso é inviável.
Nathan Fellman,

talvez não se você hackear um script sed / perl para adicionar após cada declaração de função call_registror MY_SUPERSECRETNAME(__FUNCTION__);que empurra o argumento em seu construtor e ativa seu destruidor FUNCTION sempre representa o nome da função atual.
voou em

2

Claro que a próxima pergunta é: isso será suficiente?

A principal desvantagem dos rastreamentos de pilha é que porque você tem a função precisa sendo chamada, você não tem mais nada, como o valor de seus argumentos, que é muito útil para depuração.

Se você tiver acesso a gcc e gdb, sugiro usar assertpara verificar uma condição específica e produzir um despejo de memória se não for atendido. Claro, isso significa que o processo será interrompido, mas você terá um relatório completo em vez de um mero rastreamento de pilha.

Se desejar uma maneira menos intrusiva, você sempre pode usar o registro. Existem instalações de extração de madeira muito eficientes, como a Pantheios, por exemplo. O que, mais uma vez, pode dar uma imagem muito mais precisa do que está acontecendo.


1
Claro que pode não ser suficiente, mas se eu posso ver que a função é chamada no lugar com uma configuração e não com a outra, então esse é um bom lugar para começar.
Nathan Fellman,

2

Você pode usar Poppy para isso. Normalmente é usado para coletar o rastreamento de pilha durante uma falha, mas também pode gerá-lo para um programa em execução.

Agora, aqui está a parte boa: ele pode gerar os valores reais dos parâmetros para cada função na pilha e até mesmo variáveis ​​locais, contadores de loop, etc.


2

Eu sei que este tópico é antigo, mas acho que pode ser útil para outras pessoas. Se estiver usando o gcc, você pode usar os recursos do instrumento (opção -finstrument-functions) para registrar qualquer chamada de função (entrada e saída). Dê uma olhada nisso para obter mais informações: http://hacktalks.blogspot.fr/2013/08/gcc-instrument-functions.html

Você pode, assim, por exemplo, empurrar e colocar todas as chamadas em uma pilha e, quando quiser imprimi-lo, basta olhar o que tem em sua pilha.

Eu testei, funciona perfeitamente e é muito útil

ATUALIZAÇÃO: você também pode encontrar informações sobre a opção de compilação -finstrument-functions no documento GCC sobre as opções de Instrumentação: https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html


Você também deve criar um link para os documentos do GCC, caso o artigo caia.
HolyBlackCat

Obrigado, você tem razão. Assim, adicionei uma ATUALIZAÇÃO em meu post com um link para o documento gcc
François

2

Você pode usar as bibliotecas Boost para imprimir a pilha de chamadas atual.

#include <boost/stacktrace.hpp>

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

Homem aqui: https://www.boost.org/doc/libs/1_65_1/doc/html/stacktrace.html


Recebi um erro cannot locate SymEnumSymbolsExW at C:\Windows\SYSTEM32\dbgeng.dllno Win10.
zwcloud

0

Você pode usar o GNU Profiler. Também mostra o gráfico de chamadas! o comando é gprofe você precisa compilar seu código com alguma opção.


-6

Existe alguma maneira de despejar a pilha de chamadas em um processo em execução em C ou C ++ toda vez que uma determinada função é chamada?

Não, não há, embora possam existir soluções dependentes de plataforma.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.