C 또는 C ++로 호출 스택 인쇄


120

특정 함수가 호출 될 때마다 C 또는 C ++에서 실행중인 프로세스에서 호출 스택을 덤프하는 방법이 있습니까? 내가 염두에 두는 것은 다음과 같습니다.

void foo()
{
   print_stack_trace();

   // foo's body

   return
}

Perl print_stack_trace과 유사하게 작동하는 곳 caller.

또는 다음과 같이 :

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

    // etc...
}

어디 register_stack_trace_function스택 추적의 원인이됩니다 내부 중단 어떤 종류의 인쇄 할 둔다는 때마다 foo호출된다.

이와 같은 것이 일부 표준 C 라이브러리에 있습니까?

GCC를 사용하여 Linux에서 작업하고 있습니다.


배경

이 동작에 영향을주지 않아야하는 일부 명령 줄 스위치에 따라 다르게 동작하는 테스트 실행이 있습니다. 내 코드에는 이러한 스위치에 따라 다르게 호출되는 의사 난수 생성기가 있습니다. 각 스위치 세트로 테스트를 실행하고 난수 생성기가 각 스위치마다 다르게 호출되는지 확인하고 싶습니다.


1
@Armen, 당신은 이것들에 대해 잘 알고 있습니까?
Nathan Fellman

1
@Nathan : 디버거가 gdb이면 해당 케이스를 처리 할 수 있습니다 . 다른 사람에 대해 말할 수는 없지만이 기능을 가진 것은 gdb만이 아니라고 생각합니다. 곁에 : 나는 방금 내 이전 코멘트를 보았다 . :: gag :: s/easier/either/도대체 어떻게 된거 야 ?
dmckee --- 전 중재자 새끼 고양이

2
@dmckee : 사실 s/either/easier. gdb로해야 할 일은 해당 함수를 중단하고 스택 추적을 인쇄하는 스크립트를 작성한 다음 계속하는 것입니다. 이제 생각해 보았으니 gdb 스크립팅에 대해 배울 때가되었을 것입니다.
Nathan Fellman

1
아! 잠을 좀 자러 간다. 곧
진짜로

답변:


79

리눅스 전용 솔루션의 경우 단순히 배열을 반환하는 backtrace (3) 를 사용할 수 있습니다 (void * 사실 이들 각각은 해당 스택 프레임의 반환 주소를 가리킴). 이것을 유용한 것으로 번역하려면 backtrace_symbols (3)이 있습니다.

backtrace (3)notes 섹션에주의하십시오 .

특수 링커 옵션을 사용하지 않으면 기호 이름을 사용할 수 없습니다. GNU 링커를 사용하는 시스템의 경우 -rdynamic 링커 옵션을 사용해야합니다. "정적"함수의 이름은 노출되지 않으며 역 추적에서 사용할 수 없습니다.





glibc안타깝게도를 사용하는 Linux 에서는 backtrace_symbols함수가 함수 이름, 소스 파일 이름 및 줄 번호를 제공하지 않습니다.
Maxim Egorushkin

을 사용하는 것 외에도 -rdynamic빌드 시스템이 -fvisibility=hidden옵션을 추가하지 않는지 확인하십시오 ! (의 효과를 완전히 폐기하므로 -rdynamic)
Dima Litvinov

37

스택 트레이스 향상

문서화 : https://www.boost.org/doc/libs/1_66_0/doc/html/stacktrace/getting_started.html#stacktrace.getting_started.how_to_print_current_call_stack

이것은 지금까지 본 것 중 가장 편리한 옵션입니다.

  • 실제로 줄 번호를 인쇄 할 수 있습니다.

    그것은 단지 에 대한 호출을 addr2line하지만 못생긴하고 너무 많은 흔적을 복용하는 경우 느린 될 수있는.

  • 기본적으로 꺾임

  • Boost는 헤더 전용이므로 빌드 시스템을 수정할 필요가 없습니다.

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

불행히도 더 최근에 추가 된 것으로 보이며 패키지 libboost-stacktrace-dev는 Ubuntu 16.04에는없고 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

-ldl마지막 에 추가해야 합니다. 그렇지 않으면 컴파일이 실패합니다.

산출:

 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

출력과 유사한 내용은 아래의 "glibc 역 추적"섹션에 자세히 설명되어 있습니다.

참고 방법 my_func_1(int)my_func_1(float), 인해 기능 과부하로 엉망이되어 , 정중하게 우리를 위해 분해 해제했다.

첫 번째 int호출은 한 줄 (27 개 대신 28 개 , 두 번째 호출은 두 줄 (29 개 대신 27 개)) 떨어져 있습니다. 이는 다음 명령 주소를 고려하고 있기 때문이라는 의견에서 제안되었습니다 . 27은 28이되고 29는 루프에서 뛰어 내려 27이됩니다.

그런 다음을 사용 -O3하면 출력이 완전히 절단됩니다.

 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

역 추적은 일반적으로 최적화에 의해 복구 불가능하게 절단됩니다. 테일 콜 최적화는 그 대표적인 예입니다. 테일 콜 최적화 란 무엇입니까?

벤치 마크 실행 -O3:

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

산출:

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

따라서 예상대로이 메서드는 외부 호출에 대해 매우 느리고 addr2line제한된 수의 호출이 수행되는 경우에만 실행 가능할 것입니다.

각 역 추적 인쇄에는 수백 밀리 초가 걸리는 것처럼 보이므로 역 추적이 자주 발생하면 프로그램 성능이 크게 저하 될 수 있습니다.

Ubuntu 19.10, GCC 9.2.1, 부스트 1.67.0에서 테스트되었습니다.

glibc backtrace

문서화 : https://www.gnu.org/software/libc/manual/html_node/Backtraces.html

main.c

#include <stdio.h>
#include <stdlib.h>

/* Paste this on the file you want to debug. */
#include <stdio.h>
#include <execinfo.h>
void print_trace(void) {
    char **strings;
    size_t i, size;
    enum Constexpr { MAX_SIZE = 1024 };
    void *array[MAX_SIZE];
    size = backtrace(array, MAX_SIZE);
    strings = backtrace_symbols(array, size);
    for (i = 0; i < size; i++)
        printf("%s\n", strings[i]);
    puts("");
    free(strings);
}

void my_func_3(void) {
    print_trace();
}

void my_func_2(void) {
    my_func_3();
}

void my_func_1(void) {
    my_func_3();
}

int main(void) {
    my_func_1(); /* line 33 */
    my_func_2(); /* line 34 */
    return 0;
}

엮다:

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

-rdynamic 핵심 필수 옵션입니다.

운영:

./main.out

출력 :

./main.out(print_trace+0x2d) [0x400a3d]
./main.out(main+0x9) [0x4008f9]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x7f35a5aad830]
./main.out(_start+0x29) [0x400939]

./main.out(print_trace+0x2d) [0x400a3d]
./main.out(main+0xe) [0x4008fe]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x7f35a5aad830]
./main.out(_start+0x29) [0x400939]

따라서 인라인 최적화가 발생하고 일부 기능이 추적에서 손실되었음을 즉시 알 수 있습니다.

주소를 얻으려고하면 :

addr2line -e main.out 0x4008f9 0x4008fe

우리는 다음을 얻습니다.

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

완전히 꺼져 있습니다.

-O0대신 동일한 작업을 수행하면 ./main.out올바른 전체 추적을 제공합니다.

./main.out(print_trace+0x2e) [0x4009a4]
./main.out(my_func_3+0x9) [0x400a50]
./main.out(my_func_1+0x9) [0x400a68]
./main.out(main+0x9) [0x400a74]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x7f4711677830]
./main.out(_start+0x29) [0x4008a9]

./main.out(print_trace+0x2e) [0x4009a4]
./main.out(my_func_3+0x9) [0x400a50]
./main.out(my_func_2+0x9) [0x400a5c]
./main.out(main+0xe) [0x400a79]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x7f4711677830]
./main.out(_start+0x29) [0x4008a9]

그리고:

addr2line -e main.out 0x400a74 0x400a79

제공합니다 :

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

그래서 줄이 하나만 떨어져 있습니다. TODO 왜? 그러나 이것은 여전히 ​​사용할 수 있습니다.

결론 : 역 추적은 -O0. 최적화를 통해 원래 역 추적은 컴파일 된 코드에서 근본적으로 수정됩니다.

이것으로 C ++ 기호를 자동으로 손상시키는 간단한 방법을 찾을 수 없었지만 여기에 몇 가지 해킹이 있습니다.

Ubuntu 16.04, GCC 6.4.0, libc 2.23에서 테스트되었습니다.

glibc backtrace_symbols_fd

이 도우미는보다 편리 backtrace_symbols하며 기본적으로 동일한 출력을 생성합니다.

/* Paste this on the file you want to debug. */
#include <execinfo.h>
#include <stdio.h>
#include <unistd.h>
void print_trace(void) {
    size_t i, size;
    enum Constexpr { MAX_SIZE = 1024 };
    void *array[MAX_SIZE];
    size = backtrace(array, MAX_SIZE);
    backtrace_symbols_fd(array, size, STDOUT_FILENO);
    puts("");
}

Ubuntu 16.04, GCC 6.4.0, libc 2.23에서 테스트되었습니다.

backtraceC ++ demangling hack 1이있는 glibc : -export-dynamic+dladdr

수정 출처 : https://gist.github.com/fmela/591333/c64f4eb86037bb237862a8283df70cdfc25f01d3

.NET Framework를 사용하여 ELF를 변경해야하기 때문에 이것은 "해킹"입니다 -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);
}

컴파일 및 실행 :

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 

산출:

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]

Ubuntu 18.04에서 테스트되었습니다.

backtraceC ++ demangling hack 2가있는 glibc 2 : 역 추적 출력 구문 분석

표시 : https://panthema.net/2008/0901-stacktrace-demangled/

파싱이 필요하기 때문에 해킹입니다.

TODO를 컴파일하고 여기에 표시하십시오.

libunwind

TODO가 glibc 역 추적보다 이점이 있습니까? 매우 유사한 출력으로 빌드 명령을 수정해야하지만 glibc의 일부가 아니므로 추가 패키지 설치가 필요합니다.

코드 수정 : https://eli.thegreenplace.net/2015/programmatic-access-to-the-call-stack-in-c/

main.c

/* This must be on top. */
#define _XOPEN_SOURCE 700

#include <stdio.h>
#include <stdlib.h>

/* Paste this on the file you want to debug. */
#define UNW_LOCAL_ONLY
#include <libunwind.h>
#include <stdio.h>
void print_trace() {
    char sym[256];
    unw_context_t context;
    unw_cursor_t cursor;
    unw_getcontext(&context);
    unw_init_local(&cursor, &context);
    while (unw_step(&cursor) > 0) {
        unw_word_t offset, pc;
        unw_get_reg(&cursor, UNW_REG_IP, &pc);
        if (pc == 0) {
            break;
        }
        printf("0x%lx:", pc);
        if (unw_get_proc_name(&cursor, sym, sizeof(sym), &offset) == 0) {
            printf(" (%s+0x%lx)\n", sym, offset);
        } else {
            printf(" -- error: unable to obtain symbol name for this frame\n");
        }
    }
    puts("");
}

void my_func_3(void) {
    print_trace();
}

void my_func_2(void) {
    my_func_3();
}

void my_func_1(void) {
    my_func_3();
}

int main(void) {
    my_func_1(); /* line 46 */
    my_func_2(); /* line 47 */
    return 0;
}

컴파일 및 실행 :

sudo apt-get install libunwind-dev
gcc -fno-pie -ggdb3 -O3 -no-pie -o main.out -std=c99 \
  -Wall -Wextra -pedantic-errors main.c -lunwind

어느 쪽 #define _XOPEN_SOURCE 700상단에 있어야합니다, 또는 우리가 사용해야합니다 -std=gnu99:

운영:

./main.out

산출:

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

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

과:

addr2line -e main.out 0x4007db 0x4007e2

제공합니다 :

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

와 함께 -O0:

0x4009cf: (my_func_3+0xe)
0x4009e7: (my_func_1+0x9)
0x4009f3: (main+0x9)
0x7f7b84ad7830: (__libc_start_main+0xf0)
0x4007d9: (_start+0x29)

0x4009cf: (my_func_3+0xe)
0x4009db: (my_func_2+0x9)
0x4009f8: (main+0xe)
0x7f7b84ad7830: (__libc_start_main+0xf0)
0x4007d9: (_start+0x29)

과:

addr2line -e main.out 0x4009f3 0x4009f8

제공합니다 :

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

Ubuntu 16.04, GCC 6.4.0, libunwind 1.1에서 테스트되었습니다.

C ++ 이름 디망 글링을 사용하는 libunwind

코드 수정 : 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);
}

컴파일 및 실행 :

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

산출:

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)

그리고 우리의 라인을 찾을 수 my_func_2my_func_1(int)와를 :

addr2line -e unwind.out 0x400c80 0x400cb7

다음을 제공합니다.

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

TODO : 라인이 하나 떨어져있는 이유는 무엇입니까?

Ubuntu 18.04, GCC 7.4.0, libunwind 1.2.1에서 테스트되었습니다.

GDB 자동화

또한 다음을 사용하여 재 컴파일하지 않고도 GDB로이를 수행 할 수 있습니다. GDB 에서 특정 중단 점에 도달했을 때 특정 작업을 수행하는 방법은 무엇입니까?

역 추적을 많이 인쇄하려는 경우 다른 옵션보다 빠르 겠지만을 사용하여 기본 속도에 도달 할 수 compile code있지만 지금 테스트하기가 게으르다. 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

컴파일 및 실행 :

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

산출:

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 -ex생성 할 필요가 없도록 명령 줄 에서이 작업을 수행하고 싶었지만 거기에서 작업 main.gdb할 수 없었 commands습니다.

Ubuntu 19.04, GDB 8.2에서 테스트되었습니다.

Linux 커널

Linux 커널 내에서 현재 스레드 스택 추적을 인쇄하는 방법은 무엇입니까?

libdwfl

이것은 원래 https://stackoverflow.com/a/60713161/895245 에서 언급되었으며 가장 좋은 방법 일 수 있지만 조금 더 벤치마킹해야하지만 그 대답을 위로 투표하십시오.

TODO : 나는 작동하는 그 대답의 코드를 단일 기능으로 최소화하려고 노력했지만 segfaulting입니다. 누군가가 이유를 찾을 수 있으면 알려주십시오.

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

컴파일 및 실행 :

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

산출:

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

벤치 마크 실행 :

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

산출:

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

따라서이 방법은 Boost의 스택 추적보다 10 배 빠르므로 더 많은 사용 사례에 적용 할 수 있습니다.

Ubuntu 19.10 amd64, libdw-dev 0.176-1.1에서 테스트되었습니다.

또한보십시오


1
모든 "TODO : 줄을 하나씩 벗어남"은 줄 번호가 다음 식의 시작 부분에서 가져 오기 때문입니다.
SS Anne

6

이를 수행하는 표준화 된 방법은 없습니다. Windows의 경우 DbgHelp 라이브러리 에서 기능이 제공됩니다.


6

특정 함수가 호출 될 때마다 C 또는 C ++에서 실행중인 프로세스에서 호출 스택을 덤프하는 방법이 있습니까?

특정 함수에서 return 문 대신 매크로 함수를 사용할 수 있습니다.

예를 들어 return을 사용하는 대신

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

    ... do something ...

    return 0
}

매크로 기능을 사용할 수 있습니다.

#include "c-callstack.h"

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

    ... do something ...

    NL_RETURN(0);
}

함수에서 오류가 발생할 때마다 아래와 같이 Java 스타일의 호출 스택이 표시됩니다.

Error(code:-1) at : so_topless_ranking_server (sample.c:23)
Error(code:-1) at : nanolat_database (sample.c:31)
Error(code:-1) at : nanolat_message_queue (sample.c:39)
Error(code:-1) at : main (sample.c:47)

전체 소스 코드는 여기에서 확인할 수 있습니다.

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


6

오래된 스레드에 대한 또 다른 대답.

나는이 작업을 수행 할 필요가있을 때, 나는 대개 사용 system()하고pstack

그래서 다음과 같이 :

#include <sys/types.h>
#include <unistd.h>
#include <string>
#include <sstream>
#include <cstdlib>

void f()
{
    pid_t myPid = getpid();
    std::string pstackCommand = "pstack ";
    std::stringstream ss;
    ss << myPid;
    pstackCommand += ss.str();
    system(pstackCommand.c_str());
}

void g()
{
   f();
}


void h()
{
   g();
}

int main()
{
   h();
}

이 출력

#0  0x00002aaaab62d61e in waitpid () from /lib64/libc.so.6
#1  0x00002aaaab5bf609 in do_system () from /lib64/libc.so.6
#2  0x0000000000400c3c in f() ()
#3  0x0000000000400cc5 in g() ()
#4  0x0000000000400cd1 in h() ()
#5  0x0000000000400cdd in main ()

이것은 Linux, FreeBSD 및 Solaris에서 작동합니다. 나는 macOS에 pstack 또는 간단한 동등한 기능이 있다고 생각하지 않지만이 스레드에는 대안이있는 것 같습니다 .

당신이 사용하는 경우 C에, 당신은 사용해야합니다 C문자열 함수를.

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

이 게시물을 기반으로 PID의 최대 자릿수로 7을 사용 했습니다 .


주제가 C를 요구하기 때문에 좋은 점입니다. std :: string은 C ++ 전용이므로 적응이 필요하지 않습니다. C 버전으로 답변을 업데이트하겠습니다.
Paul Floyd

6

Linux 전용, TLDR :

  1. backtracein 은 링크 된 glibc경우에만 정확한 스택 트레이스 를 생성합니다 -lunwind(문서화되지 않은 플랫폼 별 기능).
  2. 출력에 함수 이름 , 소스 파일줄 번호 사용 #include <elfutils/libdwfl.h>(이 라이브러리는 헤더 파일에 설명되어 있습니다). backtrace_symbols그리고 backtrace_symbolsd_fd적어도 정보입니다.

최신 Linux에서는 function을 사용하여 스택 추적 주소를 가져올 수 있습니다 backtrace. backtrace널리 사용되는 플랫폼에서 더 정확한 주소를 생성 하는 문서화되지 않은 방법 은 -lunwind( libunwind-devUbuntu 18.04에서) 와 연결하는 것입니다 ( 아래 예제 출력 참조). backtrace기능을 사용 _Unwind_Backtrace하고 기본적으로 후자는 유래 libgcc_s.so.1하고 그 구현은 가장 이식성이 있습니다. -lunwind링크 되면 보다 정확한 버전을 제공 _Unwind_Backtrace하지만이 라이브러리는 이식성이 떨어집니다 (에서 지원되는 아키텍처 참조 libunwind/src).

불행히도 컴패니언 backtrace_symbolsdbacktrace_symbols_fd함수는 아마도 10 년 동안 소스 파일 이름과 줄 번호가있는 함수 이름으로 스택 트레이스 주소를 확인할 수 없었습니다 (아래 예제 출력 참조).

그러나 주소를 기호로 확인하는 또 다른 방법이 있으며 함수 이름 , 소스 파일줄 번호를 사용 하여 가장 유용한 추적을 생성합니다 . 방법은 ( Ubuntu 18.04에서)에 #include <elfutils/libdwfl.h>연결하는 것 입니다.-ldwlibdw-dev

작동하는 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");
}

gcc-8.3을 사용하여 Ubuntu 18.04.4 LTS에서 컴파일되었습니다.

g++ -o test.o -c -m{arch,tune}=native -std=gnu++17 -W{all,extra,error} -g -Og -fstack-protector-all test.cc
g++ -o test -g test.o -ldw -lunwind

출력 :

Fatal exception std::runtime_error: test exception.
Stacktrace of 7 frames:
0: 0x55f3837c1a8c terminate_with_stacktrace() at /home/max/src/test/test.cc:76
1: 0x7fbc1c845ae5 <unknown>
2: 0x7fbc1c845b20 std::terminate()
3: 0x7fbc1c845d53 __cxa_throw
4: 0x55f3837c1a43 main at /home/max/src/test/test.cc:103
5: 0x7fbc1c3e3b96 __libc_start_main at ../csu/libc-start.c:310
6: 0x55f3837c17e9 _start

-lunwind링크가 없으면 덜 정확한 스택 추적이 생성됩니다.

0: 0x5591dd9d1a4d terminate_with_stacktrace() at /home/max/src/test/test.cc:76
1: 0x7f3c18ad6ae6 <unknown>
2: 0x7f3c18ad6b21 <unknown>
3: 0x7f3c18ad6d54 <unknown>
4: 0x5591dd9d1a04 main at /home/max/src/test/test.cc:103
5: 0x7f3c1845cb97 __libc_start_main at ../csu/libc-start.c:344
6: 0x5591dd9d17aa _start

비교 backtrace_symbols_fd를 위해 동일한 스택 추적의 출력은 정보가 가장 적습니다.

/home/max/src/test/debug/gcc/test(+0x192f)[0x5601c5a2092f]
/usr/lib/x86_64-linux-gnu/libstdc++.so.6(+0x92ae5)[0x7f95184f5ae5]
/usr/lib/x86_64-linux-gnu/libstdc++.so.6(_ZSt9terminatev+0x10)[0x7f95184f5b20]
/usr/lib/x86_64-linux-gnu/libstdc++.so.6(__cxa_throw+0x43)[0x7f95184f5d53]
/home/max/src/test/debug/gcc/test(+0x1ae7)[0x5601c5a20ae7]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xe6)[0x7f9518093b96]
/home/max/src/test/debug/gcc/test(+0x1849)[0x5601c5a20849]

프로덕션 버전 (및 C 언어 버전)에서 boost::core::demangle, std::stringstd::cout기본 호출 을 대체하여이 코드를 더욱 강력하게 만들 수 있습니다 .

__cxa_throw예외가 발생할 때 스택 추적을 캡처하고 예외가 발견되면 인쇄하도록 재정의 할 수도 있습니다 . catch블록에 들어갈 때 스택이 풀렸으므로을 호출하기에는 너무 늦었습니다. backtrace이것이 스택 throw이 함수에 의해 구현되는 캡처되어야하는 이유 __cxa_throw입니다. 다중 스레드 프로그램 __cxa_throw에서 다중 스레드가 동시에 호출 할 수 있으므로 스택 추적을 thread_local.


1
좋은 대답입니다! 또한 잘 연구되었습니다.
SS Anne

@SSAnne 매우 친절합니다. 감사합니다. -lunwind이 게시물을 작성하는 동안 그 문제가 발견되었으며 이전 libunwind에 스택 트레이스를 가져 오는 데 직접 사용 했고 게시하려고했지만 링크 backtrace되면 나를 위해 수행합니다 -lunwind.
Maxim Egorushkin

1
@SSAnne David Mosberger 도서관의 원저자 가 처음에 IA-64에 초점을 맞추 었기 때문일 수 있지만 그 후 도서관이 더 많은 견인력을 얻었습니다 . nongnu.org/libunwind/people.html . gccAPI를 노출하지 않습니다. 맞습니까?
Maxim Egorushkin

3

기능을 직접 구현할 수 있습니다.

전역 (문자열) 스택을 사용하고 각 함수가 시작될 때 함수 이름과 다른 값 (예 : 매개 변수)을이 스택에 푸시합니다. 기능 종료시 다시 팝하십시오.

호출 될 때 스택 내용을 출력하는 함수를 작성하고이를 호출 스택을보고자하는 함수에서 사용하십시오.

이것은 많은 작업처럼 들릴 수 있지만 매우 유용합니다.


2
나는 그렇게하지 않을 것입니다. 오히려 기본 플랫폼 별 API를 사용하는 래퍼를 만들 것입니다 (아래 참조). 작업량은 아마도 동일 할 것이지만 투자는 더 빨리 갚아야합니다.
Paul Michalik

3
@paul : 당신의 대답은 OP가 리눅스를 명확하게 지정했을 때의 창을 말합니다.하지만 여기에 나타나는 창가들에게 유용 할 수 있습니다.
slashmais 2010

맞아, 간과 했어 .. 음, 질문의 마지막 문장이라서 포스터가 자신의 타겟 플랫폼을 더 눈에 잘 띄는 곳에 언급하도록 요청을 수정해야 할지도 모릅니다.
Paul Michalik

1
내 코드베이스에 수백 개 (수천 개는 아니지만) 파일을 포함하는 수십 개의 파일이 포함되어 있다는 점을 제외하면 이것은 좋은 생각 일 것이므로 실행 불가능합니다.
Nathan Fellman

call_registror MY_SUPERSECRETNAME(__FUNCTION__);생성자에서 인수를 푸시하고 소멸자에서 팝업되는 각 함수 선언 뒤에 추가 할 sed / perl 스크립트를 해킹하면 아닐 수도 있습니다. FUNCTION은 항상 현재 함수의 이름을 나타냅니다.
비행

2

물론 다음 질문은 이것으로 충분할까요?

스택 추적의 주요 단점은 정확한 함수를 호출하는 이유는 디버깅에 매우 유용한 인수 값과 같은 다른 것이 없다는 것입니다.

gcc 및 gdb에 액세스 할 수있는 경우 assert특정 조건을 확인하고 충족되지 않으면 메모리 덤프를 생성하는 데 사용 하는 것이 좋습니다 . 물론 이것은 프로세스가 중지된다는 것을 의미하지만 단순한 스택 추적 대신 완전한 보고서를 얻을 수 있습니다.

덜 거슬리는 방법을 원하면 항상 로깅을 사용할 수 있습니다. 예를 들어 Pantheios 와 같은 매우 효율적인 로깅 기능이 있습니다 . 다시 한번 더 정확한 이미지를 얻을 수 있습니다.


1
물론 충분하지 않을 수도 있지만 함수가 다른 구성이 아닌 한 구성으로 제자리에서 호출되는 것을 볼 수 있다면 시작하기에 꽤 좋은 곳입니다.
Nathan Fellman

2

이를 위해 Poppy 를 사용할 수 있습니다 . 일반적으로 크래시 중에 스택 추적을 수집하는 데 사용되지만 실행중인 프로그램에 대해서도 출력 할 수 있습니다.

이제 여기에 좋은 부분이 있습니다. 스택의 각 함수에 대한 실제 매개 변수 값과 지역 변수, 루프 카운터 등을 출력 할 수 있습니다.


2

이 스레드가 오래되었다는 것을 알고 있지만 다른 사람들에게 유용 할 수 있다고 생각합니다. gcc를 사용하는 경우 계측기 기능 (-finstrument-functions 옵션)을 사용하여 모든 함수 호출 (시작 및 종료)을 기록 할 수 있습니다. 자세한 내용은 http://hacktalks.blogspot.fr/2013/08/gcc-instrument-functions.html 을 참조하십시오.

따라서 예를 들어 모든 호출을 스택으로 푸시하고 팝할 수 있으며 인쇄 할 때 스택에있는 항목 만 볼 수 있습니다.

나는 그것을 테스트했고 완벽하게 작동하고 매우 편리합니다.

업데이트 : 계측 옵션에 관한 GCC 문서에서 -finstrument-functions 컴파일 옵션에 대한 정보를 찾을 수도 있습니다 : https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html


기사가 다운되는 경우에도 GCC 문서에 링크해야합니다.
HolyBlackCat

감사합니다, 당신이 옳습니다. 그래서 나는 gcc 문서에 대한 링크와 함께 내 게시물에 업데이트를 추가했습니다
François

2

Boost 라이브러리를 사용하여 현재 호출 스택을 인쇄 할 수 있습니다.

#include <boost/stacktrace.hpp>

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

여기 남자 : https://www.boost.org/doc/libs/1_65_1/doc/html/stacktrace.html


cannot locate SymEnumSymbolsExW at C:\Windows\SYSTEM32\dbgeng.dllWin10 에서 오류가 발생 했습니다.
zwcloud

0

GNU 프로파일 러를 사용할 수 있습니다. 콜 그래프도 보여줍니다! 명령은 gprof이며 몇 가지 옵션을 사용하여 코드를 컴파일해야합니다.


-6

특정 함수가 호출 될 때마다 C 또는 C ++에서 실행중인 프로세스에서 호출 스택을 덤프하는 방법이 있습니까?

플랫폼에 따른 솔루션이 존재할 수 있지만 없습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.