Czy istnieje sposób na zrzucenie stosu wywołań w działającym procesie w C lub C ++ za każdym razem, gdy wywoływana jest określona funkcja? Mam na myśli coś takiego:
void foo()
{
print_stack_trace();
// foo's body
return
}
Gdzie print_stack_trace
działa podobnie jak caller
w Perlu.
Lub coś w tym stylu:
int main (void)
{
// will print out debug info every time foo() is called
register_stack_trace_function(foo);
// etc...
}
gdzie register_stack_trace_function
umieszcza jakiś wewnętrzny punkt przerwania, który spowoduje wydrukowanie śladu stosu przy każdym foo
wywołaniu.
Czy coś takiego istnieje w jakiejś standardowej bibliotece C?
Pracuję na Linuksie, używając GCC.
tło
Mam test, który zachowuje się inaczej w zależności od niektórych przełączników wiersza poleceń, które nie powinny wpływać na to zachowanie. Mój kod ma generator liczb pseudolosowych, który, jak przypuszczam, jest nazywany inaczej na podstawie tych przełączników. Chcę móc uruchomić test z każdym zestawem przełączników i sprawdzić, czy generator liczb losowych jest wywoływany inaczej dla każdego z nich.
s/easier/either/
jak do diabła to się stało?s/either/easier
. To, co musiałbym zrobić z gdb, to napisać skrypt, który przerwie tę funkcję i wydrukuje ślad stosu, a następnie będzie kontynuował. Teraz, kiedy o tym myślę, może czas, żebym się nauczył o skryptach gdb.Odpowiedzi:
W przypadku rozwiązania tylko dla systemu Linux możesz użyć funkcji backtrace (3), która po prostu zwraca tablicę
void *
(w rzeczywistości każdy z tych punktów wskazuje na adres zwrotny z odpowiedniej ramki stosu). Aby przetłumaczyć je na coś użytecznego, istnieje backtrace_symbols (3) .Zwróć uwagę na sekcję notatek w backtrace (3) :
źródło
glibc
Niestety w systemie Linuxbacktrace_symbols
funkcje nie zapewniają nazwy funkcji, nazwy pliku źródłowego i numeru wiersza.-rdynamic
sprawdź również, czy twój system kompilacji nie dodaje-fvisibility=hidden
opcji! (ponieważ całkowicie odrzuci efekt-rdynamic
)Zwiększ śledzenie stosu
Udokumentowane pod adresem : https://www.boost.org/doc/libs/1_66_0/doc/html/stacktrace/getting_started.html#stacktrace.getting_started.how_to_print_current_call_stack
To najwygodniejsza opcja, jaką do tej pory widziałem, ponieważ:
może faktycznie wydrukować numery linii.
To właśnie sprawia, że połączenia do
addr2line
jednak , co jest brzydkie i może być powolne jeśli biorą zbyt wiele śladów.domyślnie demontuje
Boost to tylko nagłówek, więc najprawdopodobniej nie ma potrzeby modyfikowania systemu kompilacji
boost_stacktrace.cpp
Niestety wydaje się, że jest to nowszy dodatek, a pakietu
libboost-stacktrace-dev
nie ma w Ubuntu 16.04, tylko 18.04:Musimy dodać
-ldl
na końcu albo kompilacja się nie powiedzie.Wynik:
Dane wyjściowe i są dalej wyjaśnione w sekcji „śledzenie wstecznego glibc” poniżej, która jest analogiczna.
Zwróć uwagę, jak
my_func_1(int)
imy_func_1(float)
, które są zniekształcone z powodu przeciążenia funkcji , zostały dla nas ładnie zdemontowane.Należy pamiętać, że pierwsze
int
rozmowy jest wyłączone przez jedną linię (28 zamiast 27, a drugi jest wyłączony przez dwie linie (27 zamiast 29). To było sugerowane w komentarzach , że to dlatego, że następujący adres instrukcji jest rozważane, które sprawia, że 27 staje się 28, a 29 wyskakuje z pętli i staje się 27.Następnie obserwujemy, że w
-O3
przypadku wyjście jest całkowicie okaleczone:Mechanizmy śledzenia wstecznego są na ogół nieodwracalnie niszczone przez optymalizacje. Optymalizacja wywołań końcowych jest tego godnym uwagi przykładem: Co to jest optymalizacja połączeń końcowych?
Benchmark działa na
-O3
:Wynik:
Tak więc, zgodnie z oczekiwaniami, widzimy, że ta metoda jest niezwykle powolna, prawdopodobnie w przypadku połączeń zewnętrznych
addr2line
, i będzie wykonalna tylko wtedy, gdy wykonywana jest ograniczona liczba połączeń.Wydaje się, że każdy wydruk śladu zajmie setki milisekund, więc ostrzegamy, że jeśli ślad śladu zdarza się bardzo często, wydajność programu znacznie się pogorszy.
Testowano na Ubuntu 19.10, GCC 9.2.1, boost 1.67.0.
glibc
backtrace
Dokumentacja dostępna pod adresem : https://www.gnu.org/software/libc/manual/html_node/Backtraces.html
main.c
Skompilować:
-rdynamic
jest kluczową wymaganą opcją.Biegać:
Wyjścia:
Od razu widzimy więc, że nastąpiła optymalizacja inlining i niektóre funkcje zostały utracone ze śledzenia.
Jeśli spróbujemy uzyskać adresy:
otrzymujemy:
który jest całkowicie wyłączony.
Jeśli
-O0
zamiast tego zrobimy to samo z ,./main.out
daje prawidłowy pełny ślad:i wtedy:
daje:
więc linie są przesunięte tylko o jeden, TODO dlaczego? Ale nadal może być użyteczne.
Wniosek: ślady wsteczne mogą się doskonale wyświetlać tylko z
-O0
. Dzięki optymalizacjom oryginalny ślad śledzenia jest zasadniczo modyfikowany w skompilowanym kodzie.Nie mogłem znaleźć prostego sposobu na automatyczne rozróżnianie symboli C ++ za pomocą tego, jednak oto kilka hacków:
Testowane na Ubuntu 16.04, GCC 6.4.0, libc 2.23.
glibc
backtrace_symbols_fd
Ten pomocnik jest nieco wygodniejszy niż
backtrace_symbols
i generuje zasadniczo identyczne dane wyjściowe:Testowane na Ubuntu 16.04, GCC 6.4.0, libc 2.23.
glibc
backtrace
z C ++ demangling hack 1:-export-dynamic
+dladdr
Na podstawie: https://gist.github.com/fmela/591333/c64f4eb86037bb237862a8283df70cdfc25f01d3
To jest „hack”, ponieważ wymaga zmiany ELF z
-export-dynamic
.glibc_ldl.cpp
Skompiluj i uruchom:
wynik:
Testowane na Ubuntu 18.04.
glibc
backtrace
z C ++ demangling hack 2: parse backtrace outputPokazano na: https://panthema.net/2008/0901-stacktrace-demangled/
To jest hack, ponieważ wymaga analizy.
DO ZROBIENIA skompilować i pokazać tutaj.
libunwind
ZROBIĆ, czy ma to jakąś przewagę nad śledzeniem wstecznym glibc? Bardzo podobny wynik, również wymaga zmodyfikowania polecenia budowania, ale nie jest częścią glibc, więc wymaga dodatkowej instalacji pakietu.
Kod zaadaptowano z: https://eli.thegreenplace.net/2015/programmatic-access-to-the-call-stack-in-c/
main.c
Skompiluj i uruchom:
Albo
#define _XOPEN_SOURCE 700
musi być na górze, albo musimy użyć-std=gnu99
:Biegać:
Wynik:
i:
daje:
Z
-O0
:i:
daje:
Testowane na Ubuntu 16.04, GCC 6.4.0, libunwind 1.1.
libunwind z demanglingiem nazw w C ++
Kod zaadaptowano z: https://eli.thegreenplace.net/2015/programmatic-access-to-the-call-stack-in-c/
relax.cpp
Skompiluj i uruchom:
Wynik:
a następnie możemy znaleźć linie
my_func_2
imy_func_1(int)
z:co daje:
DO ZROBIENIA: dlaczego linie są oddzielone o jeden?
Testowano na Ubuntu 18.04, GCC 7.4.0, libunwind 1.2.1.
Automatyzacja GDB
Możemy to również zrobić z GDB bez ponownej kompilacji, używając: Jak wykonać określoną akcję, gdy określony punkt przerwania zostanie osiągnięty w GDB?
Chociaż jeśli zamierzasz dużo drukować śladu wstecznego, prawdopodobnie będzie to mniej szybkie niż inne opcje, ale może możemy osiągnąć natywne prędkości z
compile code
, ale jestem leniwy, aby to teraz przetestować: Jak wywołać assembler w gdb?main.cpp
main.gdb
Skompiluj i uruchom:
Wynik:
TODO Chciałem to zrobić tylko
-ex
z wiersza poleceń, aby nie musieć tworzyć,main.gdb
ale nie mogłemcommands
tam pracować.Testowane w Ubuntu 19.04, GDB 8.2.
Jądro Linux
Jak wydrukować bieżący ślad stosu wątków w jądrze Linuksa?
libdwfl
Pierwotnie wspomniano o tym na stronie : https://stackoverflow.com/a/60713161/895245 i może to być najlepsza metoda, ale muszę trochę przetestować test, ale proszę, zagłosuj za tą odpowiedzią.
TODO: Próbowałem zminimalizować kod w tej odpowiedzi, który działał, do jednej funkcji, ale jest to segfaulting, daj mi znać, jeśli ktoś może znaleźć przyczynę.
dwfl.cpp
Skompiluj i uruchom:
Wynik:
Test porównawczy:
Wynik:
Widzimy więc, że ta metoda jest 10 razy szybsza niż śledzenie stosu Boost i dlatego może mieć zastosowanie w większej liczbie przypadków użycia.
Testowane w Ubuntu 19.10 amd64, libdw-dev 0.176-1.1.
Zobacz też
źródło
Nie ma na to znormalizowanego sposobu. W przypadku systemu Windows funkcjonalność jest dostępna w bibliotece DbgHelp
źródło
Możesz użyć funkcji makra zamiast instrukcji return w określonej funkcji.
Na przykład zamiast używać zwrotu,
Możesz użyć funkcji makro.
Ilekroć wystąpi błąd w funkcji, zobaczysz stos wywołań w stylu Java, jak pokazano poniżej.
Pełny kod źródłowy jest dostępny tutaj.
c-callstack pod adresem https://github.com/Nanolat
źródło
Kolejna odpowiedź na stary wątek.
Kiedy muszę to zrobić, zwykle używam
system()
ipstack
Więc coś takiego:
To wychodzi
Powinno to działać w systemach Linux, FreeBSD i Solaris. Nie sądzę, że macOS ma pstack lub prosty odpowiednik, ale ten wątek wydaje się mieć alternatywę .
Jeśli używasz
C
, będziesz musiał użyćC
funkcji tekstowych.Użyłem 7 dla maksymalnej liczby cyfr w PID, na podstawie tego postu .
źródło
Specyficzne dla systemu Linux, TLDR:
backtrace
inglibc
tworzy dokładne ślady stosu tylko wtedy, gdy-lunwind
jest połączony (nieudokumentowana funkcja specyficzna dla platformy).#include <elfutils/libdwfl.h>
(ta biblioteka jest udokumentowana tylko w pliku nagłówkowym).backtrace_symbols
ibacktrace_symbolsd_fd
są najmniej pouczające.W nowoczesnym Linuksie adresy śledzenia stosu można uzyskać za pomocą funkcji
backtrace
. Nieudokumentowanym sposobembacktrace
tworzenia dokładniejszych adresów na popularnych platformach jest linkowanie z-lunwind
(libunwind-dev
na Ubuntu 18.04) (zobacz przykładowe dane wyjściowe poniżej).backtrace
używa funkcji_Unwind_Backtrace
i domyślnie ta ostatnia pochodzi zlibgcc_s.so.1
i ta implementacja jest najbardziej przenośna. Po-lunwind
połączeniu zapewnia dokładniejszą wersję,_Unwind_Backtrace
ale ta biblioteka jest mniej przenośna (zobacz obsługiwane architektury wlibunwind/src
).Niestety, program towarzyszący
backtrace_symbolsd
ibacktrace_symbols_fd
funkcje nie były w stanie przekształcić adresów stosu śledzenia na nazwy funkcji z nazwą pliku źródłowego i numerem linii prawdopodobnie od dekady (zobacz przykładowe dane wyjściowe poniżej).Istnieje jednak inna metoda rozwiązywania adresów do symboli i tworzy najbardziej przydatne ślady z nazwą funkcji , plikiem źródłowym i numerem linii . Metoda polega na
#include <elfutils/libdwfl.h>
połączeniu z-ldw
(libdw-dev
w systemie Ubuntu 18.04).Działający przykład w C ++ (
test.cc
):Skompilowany na Ubuntu 18.04.4 LTS z gcc-8.3:
Wyjścia:
Kiedy nie
-lunwind
jest połączone, generuje mniej dokładny ślad stosu:Dla porównania,
backtrace_symbols_fd
wynik dla tego samego śladu stosu jest najmniej informacyjny:W wersji produkcyjnej (a także C wersji językowej), może chcesz, aby ten kod extra wytrzymałe zastępując
boost::core::demangle
,std::string
astd::cout
ich połączeń bazowych.Możesz także przesłonić,
__cxa_throw
aby przechwycić ślad stosu, gdy zostanie zgłoszony wyjątek, i wydrukować go, gdy zostanie przechwycony. Zanim wejdzie docatch
bloku, stos został rozwinięty, więc jest już za późno na wywołaniebacktrace
i dlatego stos musi zostać przechwycony, nathrow
którym jest zaimplementowana funkcja__cxa_throw
. Zauważ, że program wielowątkowy__cxa_throw
może być wywoływany jednocześnie przez wiele wątków, więc jeśli przechwytuje stos śledzenia do globalnej tablicy, która musi byćthread_local
.źródło
-lunwind
problem został odkryty podczas tworzenia tego posta, wcześniej użyłem golibunwind
bezpośrednio do uzyskania śledzenia stosu i zamierzałem go opublikować, alebacktrace
robi to za mnie, gdy-lunwind
jest połączony.gcc
nie ujawnia API, czy to prawda?Funkcjonalność możesz wdrożyć samodzielnie:
Użyj stosu globalnego (łańcuchowego) i na początku każdej funkcji umieść nazwę funkcji i inne wartości (np. Parametry) na ten stos; przy wyjściu z funkcji wyskakuj ponownie.
Napisz funkcję, która wydrukuje zawartość stosu, gdy zostanie wywołana, i użyj jej w funkcji, w której chcesz zobaczyć stos wywołań.
Może się to wydawać dużo pracy, ale jest całkiem przydatne.
źródło
call_registror MY_SUPERSECRETNAME(__FUNCTION__);
która odkłada argument w konstruktorze i pojawia się w jego destruktorze FUNKCJA zawsze reprezentuje nazwę bieżącej funkcji.Oczywiście następne pytanie brzmi: czy to wystarczy?
Główną wadą śledzenia stosu jest to, że dlaczego wywoływana jest precyzyjna funkcja, nie masz nic innego, jak wartość jej argumentów, co jest bardzo przydatne do debugowania.
Jeśli masz dostęp do gcc i gdb, sugerowałbym użycie
assert
do sprawdzenia określonego warunku i utworzenia zrzutu pamięci, jeśli nie jest on spełniony. Oczywiście oznacza to, że proces się zatrzyma, ale zamiast zwykłego śladu stosu będziesz mieć pełny raport.Jeśli chcesz mniej uciążliwy sposób, zawsze możesz skorzystać z logowania. Istnieją bardzo wydajne urządzenia do pozyskiwania drewna, takie jak na przykład Pantheios . Co jeszcze raz może dać ci znacznie dokładniejszy obraz tego, co się dzieje.
źródło
Możesz do tego użyć Poppy . Zwykle jest używany do zbierania śladów stosu podczas awarii, ale może również wyświetlać je dla uruchomionego programu.
Teraz dobra część: może wyprowadzać rzeczywiste wartości parametrów dla każdej funkcji na stosie, a nawet zmienne lokalne, liczniki pętli itp.
źródło
Wiem, że ten wątek jest stary, ale myślę, że może być przydatny dla innych osób. Jeśli używasz gcc, możesz użyć jego funkcji instrumentu (opcja -finstrument-functions) do logowania dowolnego wywołania funkcji (wejścia i wyjścia). Spójrz na to, aby uzyskać więcej informacji: http://hacktalks.blogspot.fr/2013/08/gcc-instrument-functions.html
Możesz więc na przykład push i wrzucić wszystkie wywołania do stosu, a kiedy chcesz je wydrukować, po prostu spójrz na to, co masz w swoim stosie.
Przetestowałem to, działa doskonale i jest bardzo poręczne
AKTUALIZACJA: możesz również znaleźć informacje o opcji kompilacji -finstrument-functions w dokumencie GCC dotyczącym opcji Instrumentation: https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html
źródło
Możesz użyć bibliotek Boost, aby wydrukować bieżący stos wywołań.
Człowiek tutaj: https://www.boost.org/doc/libs/1_65_1/doc/html/stacktrace.html
źródło
cannot locate SymEnumSymbolsExW at C:\Windows\SYSTEM32\dbgeng.dll
Wystąpił błąd na Win10.Możesz użyć profilera GNU. Pokazuje również wykres połączeń! komendą jest
gprof
i musisz skompilować swój kod z jakąś opcją.źródło
Nie, nie ma, chociaż mogą istnieć rozwiązania zależne od platformy.
źródło