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?
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?
Odpowiedzi:
GCC nie może tego zrobić, ale GDB ( debugger ) z pewnością może. Skompiluj swój program za pomocą -g
przełą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.
bt
jako skrótu dla backtrace
.
Możesz valgrind
także spróbować: jeśli zainstalujesz valgrind
i 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.
--leak-check=full
nie pomoże w debugowaniu segfaultów. Przydaje się tylko do debugowania wycieków pamięci.
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 -g
flagą.
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.
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=address
flagę, 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+ .
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.
ulimit -c unlimited
w pierwszej kolejności włączyć zrzutów pamięci .
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)