Określić wiersz kodu, który powoduje błąd segmentacji?

151

W jaki sposób można określić, gdzie w kodzie znajduje się błąd, który powoduje błąd segmentacji ?

Czy mój kompilator ( gcc) może pokazać lokalizację błędu w programie?

Trilarion
źródło
5
Żaden gcc / gdb nie może. Możesz dowiedzieć się, gdzie wystąpił błąd segfault, ale rzeczywisty błąd może znajdować się w zupełnie innej lokalizacji.

Odpowiedzi:

218

GCC nie może tego zrobić, ale GDB ( debugger ) z pewnością może. Skompiluj swój program za pomocą -gprzełącznika, na przykład:

gcc program.c -g

Następnie użyj gdb:

$ gdb ./a.out
(gdb) run
<segfault happens here>
(gdb) backtrace
<offending code is shown here>

Oto fajny samouczek ułatwiający rozpoczęcie pracy z GDB.

Miejsce, w którym dochodzi do awarii, jest zazwyczaj tylko wskazówką, gdzie w kodzie znajduje się „błąd, który ją powoduje”. Podana lokalizacja niekoniecznie jest miejscem, w którym występuje problem.

nc3b
źródło
28
Zauważ, że miejsce, w którym występuje segfault, jest na ogół wskazówką, gdzie w kodzie znajduje się „błąd, który go powoduje”. Ważna wskazówka, ale niekoniecznie chodzi o to, gdzie tkwi problem.
mpez0
9
Możesz również użyć (bt full), aby uzyskać więcej informacji.
ant2009
1
Uważam to za przydatne: gnu.org/software/gcc/bugs/segfault.html
Loves Probability
2
Użyj btjako skrótu dla backtrace.
rustyx
43

Możesz valgrindtakże spróbować: jeśli zainstalujesz valgrindi uruchomisz

valgrind --leak-check=full <program>

następnie uruchomi twój program i wyświetli ślady stosu dla wszelkich błędów segfault, a także wszelkich nieprawidłowych odczytów lub zapisów pamięci i wycieków pamięci. To naprawdę bardzo przydatne.

jwkpiano1
źródło
2
+1, Valgrind jest o wiele szybszy / łatwiejszy w użyciu do wykrywania błędów pamięci. W niezoptymalizowanych kompilacjach z symbolami debugowania informuje dokładnie, gdzie doszło do segfault i dlaczego.
Tim Post
1
Niestety mój segfault znika podczas kompilacji z -g -O0 i połączeniu z valgrind.
JohnMudd
2
--leak-check=fullnie pomoże w debugowaniu segfaultów. Przydaje się tylko do debugowania wycieków pamięci.
ks1322
@JohnMudd Mam segfault, pojawia się tylko około 1% testowanych plików wejściowych, jeśli powtórzysz nieudane wejście, nie zawiedzie. Mój problem był spowodowany wielowątkowością. Jak dotąd nie odkryłem linii kodu powodującej ten problem. Na razie używam ponownej próby, aby ukryć ten problem. Jeśli użyjesz opcji -g, błąd zniknie!
Kemin Zhou
18

Możesz również użyć zrzutu pamięci, a następnie zbadać go za pomocą gdb. Aby uzyskać przydatne informacje, musisz również skompilować się z -gflagą.

Za każdym razem, gdy otrzymasz wiadomość:

 Segmentation fault (core dumped)

plik core jest zapisywany w twoim bieżącym katalogu. Możesz to zbadać za pomocą polecenia

 gdb your_program core_file

Plik zawiera stan pamięci w momencie awarii programu. Zrzut pamięci może być przydatny podczas wdrażania oprogramowania.

Upewnij się, że Twój system nie ustawia rozmiaru pliku zrzutu pamięci na zero. Możesz ustawić go na nieograniczony za pomocą:

ulimit -c unlimited

Ostrożnie! te podstawowe zrzuty mogą stać się ogromne.

Lucas
źródło
Niedawno przesiadłem się na arch-linux. Mój bieżący katalog nie zawiera pliku zrzutu pamięci. Jak mogę to wygenerować?
Abhinav
Nie generujesz tego; Linux tak. Zrzuty rdzenia są przechowywane w różnych lokalizacjach na różnych liniach - Google dookoła. W przypadku Arch Linux przeczytaj to wiki.archlinux.org/index.php/Core_dump
Mawg mówi, że przywróć Monikę
7

Dostępnych jest wiele narzędzi, które pomagają debugować błędy segmentacji i chciałbym dodać do listy moje ulubione narzędzie: Odkażacze adresów (często w skrócie ASAN) .

Kompilatory Modern¹ mają poręczną -fsanitize=addressflagę, która dodaje trochę czasu kompilacji i narzutu czasu wykonywania, co zapewnia więcej sprawdzania błędów.

Zgodnie z dokumentacją, kontrole te domyślnie obejmują wychwytywanie błędów segmentacji. Zaletą jest to, że otrzymujesz ślad stosu podobny do danych wyjściowych gdb, ale bez uruchamiania programu w debugerze. Przykład:

int main() {
  volatile int *ptr = (int*)0;
  *ptr = 0;
}
$ gcc -g -fsanitize=address main.c
$ ./a.out
AddressSanitizer:DEADLYSIGNAL
=================================================================
==4848==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc 0x5654348db1a0 bp 0x7ffc05e39240 sp 0x7ffc05e39230 T0)
==4848==The signal is caused by a WRITE memory access.
==4848==Hint: address points to the zero page.
    #0 0x5654348db19f in main /tmp/tmp.s3gwjqb8zT/main.c:3
    #1 0x7f0e5a052b6a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x26b6a)
    #2 0x5654348db099 in _start (/tmp/tmp.s3gwjqb8zT/a.out+0x1099)

AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV /tmp/tmp.s3gwjqb8zT/main.c:3 in main
==4848==ABORTING

Wynik jest nieco bardziej skomplikowany niż to, co wygenerowałby gdb, ale są też plusy:

  • Nie ma potrzeby odtwarzania problemu, aby otrzymać ślad stosu. Wystarczy włączyć flagę podczas programowania.

  • ASAN wychwytują znacznie więcej niż tylko błędy segmentacji. Wiele wejść poza granice zostanie przechwyconych, nawet jeśli ten obszar pamięci był dostępny dla procesu.


¹ To jest Clang 3.1+ i GCC 4.8+ .

asynts
źródło
To jest dla mnie bardzo pomocne. Mam bardzo subtelny błąd, który zdarza się losowo z częstotliwością około 1%. Przetwarzam dużą liczbę plików wejściowych (16 głównych kroków; każdy jest wykonywany przez inny plik binarny C lub C ++). Jeden późniejszy krok wywoła błąd segmentacji tylko losowo z powodu wielowątkowości. Trudno jest debugować. Ta opcja wyzwoliła wyjście informacji debugowania, co najmniej dało mi punkt wyjścia do przeglądu kodu w celu znalezienia lokalizacji błędu.
Kemin Zhou
2

Odpowiedź Lucasa na temat zrzutów rdzeni jest dobra. W moim .cshrc mam:

alias core 'ls -lt core; echo where | gdb -core=core -silent; echo "\n"'

aby wyświetlić ślad, wpisując „rdzeń”. I datownik, żeby mieć pewność, że patrzę na właściwy plik :(.

Dodano : Jeśli wystąpi błąd powodujący uszkodzenie stosu , ślad zastosowany do zrzutu pamięci jest często śmieciami. W takim przypadku uruchomienie programu w gdb może dać lepsze wyniki, zgodnie z przyjętą odpowiedzią (przy założeniu, że błąd można łatwo odtworzyć). A także uważaj na wiele procesów jednocześnie zrzucających rdzeń; niektóre systemy operacyjne dodają PID do nazwy pliku podstawowego.

Joseph Quinsey
źródło
4
i nie zapomnij ulimit -c unlimitedw pierwszej kolejności włączyć zrzutów pamięci .
James Morris
@James: Zgadza się. Lucas już o tym wspomniał. A dla tych z nas, którzy wciąż tkwią w csh, użyj „limit”. I nigdy nie byłem w stanie przeczytać stackdumpów CYGWIN (ale nie próbowałem ich przez 2 lub 3 lata).
Joseph Quinsey
2

Wszystkie powyższe odpowiedzi są poprawne i zalecane; ta odpowiedź jest pomyślana tylko jako ostateczność, jeśli żadne z wyżej wymienionych podejść nie może być użyte.

Jeśli wszystko inne zawiedzie, zawsze możesz przekompilować swój program za pomocą różnych tymczasowych instrukcji debug-print (np. fprintf(stderr, "CHECKPOINT REACHED @ %s:%i\n", __FILE__, __LINE__);) Rozsianych po tym, co uważasz za odpowiednie części twojego kodu. Następnie uruchom program i obserwuj, jaki był ostatni wydruk debugowania wydrukowany tuż przed awarią - wiesz, że twój program zaszedł tak daleko, więc awaria musiała nastąpić po tym momencie. Dodaj lub usuń wydruki debugowania, skompiluj ponownie i uruchom test ponownie, aż zawęzisz go do pojedynczej linii kodu. W tym momencie możesz naprawić błąd i usunąć wszystkie tymczasowe wydruki debugowania.

Jest to dość żmudne, ale ma tę zaletę, że działa prawie wszędzie - jedyne sytuacje, w których może się to nie udać, to brak dostępu do stdout lub stderr z jakiegoś powodu lub jeśli błąd, który próbujesz naprawić, to wyścig -warunek, którego zachowanie zmienia się, gdy zmienia się czas działania programu (ponieważ debug-print spowolni program i zmieni jego czas)

Jeremy Friesner
źródło