Winy segmentacji występuje, gdy program próbuje zewnętrznej pamięci dostęp do przestrzeni, która została przydzielona do niego.
W tym przypadku doświadczony programista C może zobaczyć, że problem występuje na linii, w której sprintf
jest wywoływany. Ale jeśli nie możesz powiedzieć, gdzie występuje błąd segmentacji lub jeśli nie chcesz zawracać sobie głowy czytaniem kodu, aby spróbować go rozgryźć, możesz zbudować swój program za pomocą symboli debugowania (za gcc
pomocą -g
flagi to robi ), a następnie uruchom go przez debugger.
Skopiowałem twój kod źródłowy i wkleiłem go do pliku o nazwie slope.c
. Potem zbudowałem to w ten sposób:
gcc -Wall -g -o slope slope.c
(Opcja -Wall
jest opcjonalna. Chodzi o to, aby wyświetlała ostrzeżenia w większej liczbie sytuacji. Może to pomóc w ustaleniu, co może być nie tak.)
Następnie uruchomiłem program w debuggerze gdb
, najpierw uruchamiając go, gdb ./slope
aby uruchomić gdb
program, a następnie raz w debuggerze, wydając run
polecenie debuggerowi:
ek@Kip:~/source$ gdb ./slope
GNU gdb (GDB) 7.5-ubuntu
Copyright (C) 2012 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "i686-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /home/ek/source/slope...done.
(gdb) run
Starting program: /home/ek/source/slope
warning: Cannot call inferior functions, you have broken Linux kernel i386 NX (non-executable pages) support!
Program received signal SIGSEGV, Segmentation fault.
0x001a64cc in _IO_default_xsputn () from /lib/i386-linux-gnu/libc.so.6
(Nie martw się o moją you have broken Linux kernel i386 NX
... support
wiadomość; nie przeszkadza gdb
to w efektywnym użyciu do debugowania tego programu).
Ta informacja jest bardzo tajemnicza ... a jeśli nie masz zainstalowanych symboli debugowania dla libc, otrzymasz jeszcze bardziej tajemniczą wiadomość, która ma adres szesnastkowy zamiast nazwy funkcji symbolicznej _IO_default_xsputn
. Na szczęście to nie ma znaczenia, ponieważ tak naprawdę chcemy wiedzieć, gdzie w twoim programie występuje problem.
Tak więc rozwiązaniem jest spojrzenie wstecz, aby zobaczyć, jakie wywołania funkcji miały miejsce przed tym konkretnym wywołaniem funkcji w bibliotece systemowej, w której SIGSEGV
sygnał został ostatecznie wyzwolony.
gdb
(i każdy debugger) ma tę wbudowaną funkcję: nazywa się to śledzeniem stosu lub śladem wstecznym . Korzystam z bt
polecenia debugger, aby wygenerować ślad w gdb
:
(gdb) bt
#0 0x001a64cc in _IO_default_xsputn () from /lib/i386-linux-gnu/libc.so.6
#1 0x00178e04 in vfprintf () from /lib/i386-linux-gnu/libc.so.6
#2 0x0019b234 in vsprintf () from /lib/i386-linux-gnu/libc.so.6
#3 0x0017ff7b in sprintf () from /lib/i386-linux-gnu/libc.so.6
#4 0x080484cc in calc_slope (input1=1, input2=150) at slope.c:26
#5 0x08048578 in main () at slope.c:52
(gdb)
Możesz zobaczyć, że twoja main
funkcja wywołuje calc_slope
funkcję (którą zamierzałeś), a następnie calc_slope
wywołania sprintf
, które (w tym systemie) są implementowane wraz z wywołaniami kilku innych powiązanych funkcji bibliotecznych.
To, co ogólnie Cię interesuje, to wywołanie funkcji w twoim programie, która wywołuje funkcję poza twoim programem . O ile w używanej bibliotece / bibliotekach nie ma błędu (w tym przypadku standardowej biblioteki C libc
dostarczonej przez plik biblioteki libc.so.6
), błąd, który powoduje awarię, występuje w twoim programie i często będzie znajdować się w pobliżu ostatnie połączenie w twoim programie.
W tym przypadku jest to:
#4 0x080484cc in calc_slope (input1=1, input2=150) at slope.c:26
Tam właśnie wzywa twój program sprintf
. Wiemy o tym, ponieważ sprintf
jest to kolejny krok do przodu. Ale nawet bez stwierdzenia tego, wiesz o tym, ponieważ tak dzieje się w linii 26 i mówi:
... at slope.c:26
W twoim programie wiersz 26 zawiera:
sprintf(s,"%d",curr);
(Zawsze powinieneś używać edytora tekstu, który automatycznie pokazuje numery linii, przynajmniej dla linii, na której aktualnie jesteś. Jest to bardzo pomocne w interpretacji zarówno błędów kompilacji, jak i problemów w czasie wykonywania ujawnionych podczas korzystania z debuggera.)
Jak omówiono w odpowiedzi Dennisa Kaarsemakera , s
jest to tablica jednobajtowa. (Nie zero, ponieważ przypisana mu wartość ""
jest długa na jeden bajt, to znaczy jest równa { '\0' }
, w ten sam sposób, co "Hello, world!\n"
jest równa { 'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', '\n', '\0' }
.)
Dlaczego więc to może nadal działać na niektórych platformach (i najwyraźniej działa po skompilowaniu z VC9 dla Windows)?
Ludzie często mówią, że przydzielenie pamięci, a następnie próba uzyskania dostępu do pamięci poza nią, powoduje błąd. Ale to nie do końca prawda. Zgodnie ze standardami technicznymi C i C ++ tak naprawdę powoduje to nieokreślone zachowanie.
Innymi słowy, wszystko może się zdarzyć!
Niektóre rzeczy są jednak bardziej prawdopodobne niż inne. Dlaczego mała tablica na stosie w niektórych implementacjach wydaje się działać jak większa tablica na stosie?
Sprowadza się to do sposobu alokacji stosu, który może różnić się w zależności od platformy. Plik wykonywalny może przydzielić więcej pamięci do swojego stosu, niż jest faktycznie przeznaczony do użycia w danym momencie. Czasami może to pozwolić ci na zapisanie w pamięci miejsc, do których wprost nie zgłosiłeś roszczenia w kodzie. Jest bardzo prawdopodobne, że tak się dzieje, gdy budujesz swój program w VC9.
Jednak nie powinieneś polegać na tym zachowaniu nawet w VC9. Może to potencjalnie zależeć od różnych wersji bibliotek, które mogą istnieć w różnych systemach Windows. Ale jeszcze bardziej prawdopodobny jest problem, że dodatkowe miejsce na stosie jest przydzielane z zamiarem, że zostanie ono faktycznie wykorzystane, a więc może być faktycznie wykorzystane.Następnie doświadczasz pełnego koszmaru „niezdefiniowanego zachowania”, w którym w tym przypadku więcej niż jedna zmienna mogłaby zostać zapisana w tym samym miejscu, w którym zapis do jednej nadpisuje drugą ... ale nie zawsze, ponieważ czasami zapisuje do zmiennych są buforowane w rejestrach i faktycznie nie wykonywane natychmiast (lub odczyty zmiennych mogą być buforowane, lub można założyć, że zmienna jest taka sama jak wcześniej, ponieważ przydzielona do niej pamięć jest znana kompilatorowi, że nie została zapisana za pośrednictwem sama zmienna).
I to prowadzi mnie do innej prawdopodobnej możliwości, dlaczego program działał po zbudowaniu z VC9. Jest możliwe i dość prawdopodobne, że pewna tablica lub inna zmienna została faktycznie przydzielona przez twój program (co może obejmować przydzielenie przez bibliotekę, z której korzysta Twój program), aby wykorzystać przestrzeń po tablicy jednobajtowej s
. Zatem traktowanie s
jako tablicy dłuższej niż jeden bajt skutkowałoby uzyskaniem dostępu do zawartości tej / tych zmiennych / tablic, co również mogłoby być złe.
Podsumowując, jeśli masz błąd jak ten, to szczęście, aby dostać się błąd jak „winy segmentacji” lub „Ogólny błąd ochrony”. Jeśli tego nie masz, możesz nie dowiedzieć się, dopóki nie będzie za późno, aby Twój program zachował się w sposób nieokreślony.