Jak można pobrać ślad stosu w C?

83

Wiem, że nie ma do tego standardowej funkcji w C. Zastanawiałem się, jakie są techniki, aby to zrobić w systemie Windows i * nix? (Windows XP jest moim najważniejszym systemem operacyjnym, w którym teraz mogę to robić).

Kevin
źródło
Szczegółowo przetestowałem kilka metod pod adresem: stackoverflow.com/questions/3899870/print-call-stack-in-c-or-c/…
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

Odpowiedzi:

81

glibc udostępnia funkcję backtrace ().

http://www.gnu.org/software/libc/manual/html_node/Backtraces.html

sanxiyn
źródło
7
glibc FTW ... ponownie. (To kolejny powód, dla którego uważam glibc za absolutny złoty standard, jeśli chodzi o programowanie w C (to i kompilator, który się z tym wiąże).)
Trevor Boyd Smith
4
Ale czekaj, jest więcej! Funkcja backtrace () zapewnia jedynie tablicę wskaźników void * reprezentujących funkcje stosu wywołań. - To nie jest zbyt przydatne. Arg. Nie bój się! glibc udostępnia funkcję, która konwertuje wszystkie adresy void * (adresy funkcji callstack) na czytelne dla człowieka symbole łańcuchowe. char ** backtrace_symbols (void *const *buffer, int size)
Trevor Boyd Smith
1
Uwaga: myślę, że działa tylko dla funkcji C. @Trevor: Po prostu wyszukuje symbole po adresie w tabeli ELF.
Conrad Meyer,
2
Istnieje również, na przykład, void backtrace_symbols_fd(void *const *buffer, int size, int fd)który może wysłać dane wyjściowe bezpośrednio do stdout / err.
wkz
2
backtrace_symbols()jest do bani. Wymaga eksportowania wszystkich symboli i nie obsługuje symboli DWARF (debugowania). libbacktrace jest znacznie lepszą opcją w wielu (większości) przypadkach.
Erwan Legrand,
29

Istnieje backtrace () i backtrace_symbols ():

Ze strony podręcznika:

Jednym ze sposobów użycia tego w wygodniejszy sposób / OOP jest zapisanie wyniku operacji backtrace_symbols () w konstruktorze klasy wyjątków. W związku z tym za każdym razem, gdy rzucasz wyjątek tego typu, masz ślad stosu. Następnie wystarczy udostępnić funkcję drukowania. Na przykład:

class MyException : public std::exception {

    char ** strs;
    MyException( const std::string & message ) {
         int i, frames = backtrace(callstack, 128);
         strs = backtrace_symbols(callstack, frames);
    }

    void printStackTrace() {
        for (i = 0; i < frames; ++i) {
            printf("%s\n", strs[i]);
        }
        free(strs);
    }
};

...

Ta da!

Uwaga: włączenie flag optymalizacji może spowodować, że wynikowy ślad stosu będzie niedokładny. W idealnym przypadku można by użyć tej możliwości z włączonymi flagami debugowania i wyłączonymi flagami optymalizacji.

Tomek
źródło
@shuckc tylko do konwersji adresu na ciąg symboli, co w razie potrzeby można wykonać zewnętrznie za pomocą innych narzędzi.
Woodrow Barlow
22

W przypadku systemu Windows sprawdź interfejs API StackWalk64 () (również w 32-bitowym systemie Windows). W przypadku UNIX powinieneś użyć natywnego sposobu systemu operacyjnego, aby to zrobić, lub wrócić do backtrace () biblioteki glibc, jeśli jest dostępna.

Pamiętaj jednak, że zrobienie Stacktrace w kodzie natywnym rzadko jest dobrym pomysłem - nie dlatego, że nie jest to możliwe, ale dlatego, że zwykle próbujesz osiągnąć coś złego.

W większości przypadków ludzie próbują uzyskać ślad stosu, powiedzmy, w wyjątkowych okolicznościach, takich jak przechwycenie wyjątku, niepowodzenie asercji lub - co najgorsze i najgorsze ze wszystkich - kiedy otrzymujesz fatalny „wyjątek” lub sygnał podobny do naruszenie segmentacji.

Biorąc pod uwagę ten ostatni problem, większość interfejsów API będzie wymagać jawnego przydzielenia pamięci lub może to zrobić wewnętrznie. Robienie tego w kruchym stanie, w jakim obecnie znajduje się program, może poważnie pogorszyć sytuację. Na przykład raport o awarii (lub coredump) nie będzie odzwierciedlał faktycznej przyczyny problemu, ale nieudana próba jego rozwiązania).

Zakładam, że próbujesz osiągnąć tę krytyczną obsługę błędów, ponieważ większość ludzi wydaje się tego próbować, jeśli chodzi o uzyskanie śledzenia stosu. Jeśli tak, oparłbym się na debugerze (podczas rozwoju) i pozwoliłbym procesowi coredumpować w produkcji (lub mini-dump w Windows). Razem z odpowiednim zarządzaniem symbolami, nie powinieneś mieć problemów z ustaleniem przyczyny instrukcji po śmierci.

Christian. K.
źródło
2
Masz rację co do tego, że próba alokacji pamięci w module obsługi sygnału lub wyjątku jest krucha. Jednym z możliwych rozwiązań jest przydzielenie stałej ilości „awaryjnej” przestrzeni na początku programu lub użycie bufora statycznego.
j_random_hacker,
Innym wyjściem jest stworzenie usługi
coredump
5

Powinieneś używać rozwijanej biblioteki .

Twoje podejście również działałoby dobrze, chyba że wykonasz połączenie z udostępnionej biblioteki.

Możesz użyć addr2linepolecenia w systemie Linux, aby uzyskać numer funkcji źródłowej / numer linii odpowiedniego komputera.

user262802
źródło
„funkcja źródłowa / numer linii”? Co się stanie, jeśli linkowanie jest zoptymalizowane pod kątem mniejszego rozmiaru kodu? Powiem jednak, że wygląda to na przydatny projekt. Szkoda, że ​​nie ma sposobu na zdobycie rejestrów. Na pewno się temu przyjrzę. Czy wiesz, że jest to całkowicie niezależne od procesora? Po prostu działa na czymkolwiek, co ma kompilator C?
Mawg mówi, że przywróć Monikę
1
Ok, ten komentarz był tego wart, choćby ze względu na wzmiankę o pomocnym poleceniu addr2line!
Ogre Psalm33
addr2line nie działa dla relokowalnego kodu w systemach z ASLR (tj. większość tego, czego ludzie używali w ciągu ostatniej dekady).
Erwan Legrand
4

Nie ma niezależnego od platformy sposobu, aby to zrobić.

Najbliższą rzeczą, jaką możesz zrobić, jest uruchomienie kodu bez optymalizacji. W ten sposób możesz dołączyć do procesu (używając wizualnego debuggera C ++ lub GDB) i uzyskać użyteczny ślad stosu.

Nils Pipenbrinck
źródło
To mi nie pomaga, gdy dochodzi do awarii na wbudowanym komputerze w terenie. :(
Kevin,
@Kevin: Nawet na maszynach osadzonych zwykle istnieje sposób na uzyskanie kodu pośredniczącego zdalnego debuggera lub przynajmniej zrzutu pamięci. Może nie raz, kiedy zostanie rozlokowany w terenie, chociaż ...
ephemient
jeśli uruchomisz używając gcc-glibc na wybranej platformie windows / linux / mac ... wtedy backtrace () i backtrace_symbols () będą działać na wszystkich trzech platformach. Biorąc pod uwagę to stwierdzenie, użyłbym słów „nie ma [przenośnego] sposobu, aby to zrobić”.
Trevor Boyd Smith
4

W przypadku systemu Windows CaptureStackBackTrace()jest to również opcja, która wymaga mniej przygotowanego kodu po stronie użytkownika niż StackWalk64()to. (Poza tym w przypadku podobnego scenariusza, który miałem, CaptureStackBackTrace()działało lepiej (bardziej niezawodnie) niż StackWalk64().)

Quasidart
źródło
2

Solaris ma polecenie pstack , które również zostało skopiowane do Linuksa.

mądry
źródło
1
Przydatne, ale niezupełnie C (to zewnętrzne narzędzie).
ephemient
1
również z opisu (sekcja: ograniczenia) ": pstack obecnie działa tylko na Linuksie, tylko na maszynie x86 z 32-bitowymi plikami binarnymi ELF (64-bitowe nieobsługiwane)"
Ciro Costa
0

Możesz to zrobić, cofając stos. W rzeczywistości jednak często łatwiej jest dodać identyfikator do stosu wywołań na początku każdej funkcji i wstawić go na końcu, a następnie po prostu przejść przez drukowanie zawartości. To trochę PITA, ale działa dobrze i ostatecznie pozwoli Ci zaoszczędzić czas.

Serafina Brocious
źródło
1
Czy mógłbyś dokładniej wyjaśnić „przechodzenie stosu do tyłu”?
Spidey
@Spidey W systemach wbudowanych czasami to wszystko, co masz - myślę, że zostało to odrzucone, ponieważ platformą jest WinXP. Ale bez biblioteki libc, która obsługuje chodzenie po stosie, w zasadzie musisz „chodzić” po stosie. Zaczynasz od bieżącego wskaźnika podstawowego (na x86 jest to zawartość rejestru RBP). To prowadzi do wskazania na stosie z 1. zapisanym poprzednim RBP (w ten sposób kontynuuje chodzenie po stosie) i 2. adresem zwrotnym wywołania / gałęzi (zapisany reg RIP funkcji wywołującej), który informuje, jaka była funkcja. Następnie, jeśli masz dostęp do tablicy symboli, możesz sprawdzić adres funkcji.
Ted Middleton
0

Od kilku lat korzystam z libbacktrace Iana Lance'a Taylora. Jest znacznie czystszy niż funkcje w bibliotece GNU C, które wymagają wyeksportowania wszystkich symboli. Zapewnia więcej narzędzi do generowania śladów wstecznych niż libunwind. I wreszcie, nie jest pokonany przez ASLR, podobnie jak podejścia wymagające narzędzi zewnętrznych, takich jak addr2line.

Libbacktrace był początkowo częścią dystrybucji GCC, ale teraz jest udostępniany przez autora jako samodzielna biblioteka na licencji BSD:

https://github.com/ianlancetaylor/libbacktrace

W chwili pisania tego tekstu nie używałbym niczego innego, chyba że potrzebowałbym generowania śladów śledzenia na platformie, która nie jest obsługiwana przez libbacktrace.

Erwan Legrand
źródło