Uszkodzona ramka stosu GDB - jak debugować?

113

Mam następujący ślad stosu. Czy można wyciągnąć z tego cokolwiek przydatnego do debugowania?

Program received signal SIGSEGV, Segmentation fault.
0x00000002 in ?? ()
(gdb) bt
#0  0x00000002 in ?? ()
#1  0x00000001 in ?? ()
#2  0xbffff284 in ?? ()
Backtrace stopped: previous frame inner to this frame (corrupt stack?)
(gdb) 

Od czego zacząć patrzeć na kod, gdy otrzymamy a Segmentation fault, a ślad stosu nie jest tak przydatny?

UWAGA: Jeśli wyślę kod, eksperci SO udzielą mi odpowiedzi. Chcę skorzystać ze wskazówek od SO i samemu znaleźć odpowiedź, więc nie umieszczam tutaj kodu. Przeprosiny.

Sangeeth Saravanaraj
źródło
Prawdopodobnie twój program wskoczył w chwasty - czy możesz odzyskać cokolwiek ze wskaźnika stosu?
Carl Norum,
1
Inną rzeczą do rozważenia jest to, czy wskaźnik ramki jest ustawiony poprawnie. Czy budujesz bez optymalizacji lub nie przekazujesz takiej flagi -fno-omit-frame-pointer? Również w przypadku uszkodzenia pamięci valgrindmoże być bardziej odpowiednim narzędziem, jeśli jest to opcja dla Ciebie.
FatalError

Odpowiedzi:

155

Te fałszywe adresy (0x00000002 i tym podobne) są w rzeczywistości wartościami PC, a nie wartościami SP. Teraz, kiedy otrzymujesz tego rodzaju SEGV, z fałszywym (bardzo małym) adresem komputera, 99% czasu jest to spowodowane wywołaniem przez fałszywy wskaźnik funkcji. Zwróć uwagę, że wywołania wirtualne w C ++ są implementowane za pomocą wskaźników funkcji, więc każdy problem z wywołaniem wirtualnym może objawiać się w ten sam sposób.

Pośrednią instrukcja wywołanie tylko pcha komputera po wywołaniu na stosie, a następnie ustawia komputer do wartości docelowej (podrobiony w tym przypadku), więc jeśli to jest to, co się stało, można łatwo cofnąć ręcznie pojawiały komputer ze stosu . W 32-bitowym kodzie x86 po prostu robisz:

(gdb) set $pc = *(void **)$esp
(gdb) set $esp = $esp + 4

Z 64-bitowym kodem x86 potrzebujesz

(gdb) set $pc = *(void **)$rsp
(gdb) set $rsp = $rsp + 8

Następnie powinieneś być w stanie zrobić a bti dowiedzieć się, gdzie naprawdę jest kod.

W pozostałych 1% przypadków błąd będzie spowodowany nadpisaniem stosu, zwykle przez przepełnienie tablicy przechowywanej na stosie. W takim przypadku możesz uzyskać większą jasność w sytuacji, używając narzędzia takiego jak valgrind

Chris Dodd
źródło
5
@George: gdb executable corefileotworzy gdb z plikiem wykonywalnym i podstawowym, w którym to momencie możesz wykonać bt(lub powyższe polecenia, a następnie bt) ...
Chris Dodd
2
@mk .. ARM nie używa stosu dla adresów zwrotnych - zamiast tego używa rejestru linków. Więc generalnie nie ma tego problemu, a jeśli tak, to zwykle jest to spowodowane innym uszkodzeniem stosu.
Chris Dodd
2
Myślę, że nawet w ARM wszystkie rejestry ogólnego przeznaczenia i LR są przechowywane na stosie, zanim wywoływana funkcja zacznie się wykonywać. Po zakończeniu funkcji wartość LR jest wprowadzana do komputera i dlatego funkcja zwraca. Więc jeśli stos jest uszkodzony, widzimy złą wartość, czy PC, prawda? W tym przypadku dostosowanie wskaźnika stosu może doprowadzić do odpowiedniego stosu i pomóc w rozwiązaniu problemu. Co myślisz? pls daj mi znać swoje myśli. Dziękuję Ci.
mk ..
1
Co to znaczy fałszywe?
Danny Lo
5
ARM nie jest x86 - jego wskaźnik stosu jest wywoływany sp, nie esplub rsp, a jego instrukcja wywołania przechowuje adres powrotu w lrrejestrze, a nie na stosie. W przypadku ARM wszystko, czego naprawdę potrzebujesz, aby cofnąć połączenie, to set $pc = $lr. Jeśli $lrjest nieprawidłowy, masz znacznie trudniejszy problem z odprężeniem.
Chris Dodd
44

Jeśli sytuacja jest dość prosta, odpowiedź Chrisa Dodda jest najlepsza. Wygląda na to, że przeskoczył przez wskaźnik NULL.

Jednak możliwe jest, że program strzelił sobie w stopę, kolano, szyję i oko przed awarią - nadpisał stos, zepsuł wskaźnik ramki i inne zło. Jeśli tak, to rozwikłanie haszyszu prawdopodobnie nie pokaże ci ziemniaków i mięsa.

Bardziej wydajnym rozwiązaniem będzie uruchomienie programu w debugerze i przechodzenie przez funkcje aż do awarii programu. Po zidentyfikowaniu funkcji powodującej awarię uruchom ponownie, przejdź do tej funkcji i określ, która funkcja wywołuje awarię. Powtarzaj, aż znajdziesz jeden nieprawidłowy wiersz kodu. W 75% przypadków poprawka będzie wtedy oczywista.

W pozostałych 25% sytuacji tak zwana niewłaściwa linia kodu to czerwony śledź. Będzie reagować na (nieprawidłowe) warunki ustawione wcześniej wiele linii - może tysiące linii wcześniej. W takim przypadku wybór najlepszego kursu zależy od wielu czynników: głównie od zrozumienia kodu i doświadczenia z nim:

  • Być może ustawienie punktu obserwacyjnego debuggera lub wstawienie diagnostyki printfdo krytycznych zmiennych doprowadzi do niezbędnego A ha!
  • Może zmiana warunków testowych z różnymi danymi wejściowymi zapewni lepszy wgląd niż debugowanie.
  • Może druga para oczu zmusi cię do sprawdzenia swoich przypuszczeń lub zebrania przeoczonych dowodów.
  • Czasami wystarczy pójść na obiad i pomyśleć o zebranych dowodach.

Powodzenia!

wallyk
źródło
13
Jeśli druga para oczu nie jest dostępna, sprawdzoną alternatywą są gumowe kaczki.
Matt
2
Wypisanie końca bufora też może to zrobić. Może się nie zawiesić, gdy wypiszesz koniec bufora, ale kiedy wyjdziesz z funkcji, umiera.
phyatt
Może być przydatny: GDB: Automatyczne „Dalej”
user202729
28

Zakładając, że wskaźnik stosu jest prawidłowy ...

Dokładne określenie miejsca wystąpienia SEGV na podstawie śledzenia wstecznego może być niemożliwe - myślę, że pierwsze dwie ramki stosu są całkowicie nadpisane. 0xbffff284 wydaje się być prawidłowym adresem, ale dwa następne nie. Aby dokładniej przyjrzeć się stosowi, możesz spróbować następujących rozwiązań:

gdb $ x / 32ga $ rsp

lub wariant (zamień 32 na inny numer). To wypisze pewną liczbę słów (32) zaczynając od wskaźnika stosu o gigantycznym rozmiarze (g), sformatowanych jako adresy (a). Wpisz „help x”, aby uzyskać więcej informacji na temat formatu.

W tym przypadku oprzyrządowanie twojego kodu za pomocą pewnych wartowniczych 'printf' może nie być złym pomysłem.

manabear
źródło
Niezwykle pomocne, dziękuję - miałem stos, który cofnął się tylko o trzy klatki, a następnie kliknąłem „Wstecz zatrzymany: poprzednia klatka identyczna z tą (uszkodzony stos?)”; Zrobiłem coś dokładnie takiego w kodzie w module obsługi wyjątków procesora, ale nie pamiętałem innego niż info symboljak to zrobić w gdb.
leander
23
FWIW na 32-bitowych urządzeniach ARM: x/256wa $sp =)
leander
2
@leander Czy możesz mi powiedzieć, co to jest X / 256wa? Potrzebuję tego do 64-bitowego ARM. Ogólnie przydatne będzie wyjaśnienie, co to jest.
mk ..
5
Zgodnie z odpowiedzią „x” = zbadać lokalizację pamięci; wypisuje liczbę „w” = słów (w tym przypadku 256) i interpretuje je jako „a” = adresy. Więcej informacji można znaleźć w podręczniku GDB na sourceware.org/gdb/current/onlinedocs/gdb/Memory.html#Memory .
leander
7

Spójrz na inne swoje rejestry, aby zobaczyć, czy jeden z nich ma buforowany wskaźnik stosu. Stamtąd możesz odzyskać stos. Ponadto, jeśli jest to osadzone, dość często stos jest definiowany pod bardzo konkretnym adresem. Używając tego, możesz czasami uzyskać przyzwoity stack. Wszystko to zakłada, że ​​kiedy skoczyłeś w nadprzestrzeń, twój program nie rzygał całą pamięcią po drodze ...

Michael Dorgan
źródło
3

Jeśli jest to nadpisanie stosu, wartości mogą równie dobrze odpowiadać czemuś rozpoznawalnemu w programie.

Na przykład po prostu patrzyłem na stos

(gdb) bt
#0  0x0000000000000000 in ?? ()
#1  0x000000000000342d in ?? ()
#2  0x0000000000000000 in ?? ()

i 0x342dto 13357, który okazał się być identyfikatorem węzła, kiedy grepowałem dla niego dzienniki aplikacji. To natychmiast pomogło zawęzić potencjalne witryny, w których mogło nastąpić nadpisanie stosu.

Craig Ringer
źródło