Naruszenie ochrony pamięci
Naruszenie ochrony pamięci – zdarzenie wykrywane przez sprzęt, polegające na korzystaniu przez program z pamięci poza zaalokowanym dla niego obszarem.
Zwykle wynika to z błędów, czasem jednak jest to świadome działanie programisty – np. program może zalokować mały stos i nie sprawdzać jego przepełnienia, za to kiedy ono nastąpi – i nastąpi naruszenie ochrony pamięci – przechwycić ten sygnał i rozszerzyć stos. Jest to o wiele bardziej efektywne od ciągłego sprawdzania przepełnienia (co musi następować ogromną liczbę razy), oraz od alokacji dużej ilości pamięci na stos (co marnuje pamięć).
Objawy błędu
[edytuj | edytuj kod]W systemie Linux w konsoli pojawia się napis „Segmentation fault” opcjonalnie z dodatkowymi informacjami, o ile sygnał SIGSEGV nie zostanie przechwycony przez aplikację.
W systemach Windows aplikacje często w tej sytuacji wyświetlają okno z błędem o treści „EAccess Violation”, „Access Violation” lub informacji o kodzie błędu 0xC0000005. Zdarzają się też przypadki zakończenia aplikacji bez wyświetlenia jakiegokolwiek komunikatu.
Przykłady
[edytuj | edytuj kod]Zapis do obszaru pamięci przeznaczonego tylko do odczytu
[edytuj | edytuj kod]Zapis do obszaru pamięci, który przeznaczony jest tylko do odczytu powoduje zgłoszenie błędu naruszenia ochrony pamięci. Błąd ten wystąpi również przy próbie zapisu do obszaru, w którym znajduje się kod wykonywalny lub dane przeznaczone tylko do odczytu (np. tablice stałych) lub biblioteki systemowe (np. kernel32.dll).
Poniżej znajduje się fragment kodu napisanego w ANSI C, który powoduje błąd naruszenia ochrony pamięci na platformach, posiadających ochronę pamięci. Pamiętaj, że modyfikowanie stałych łańcuchów tekstowych nie jest zdefiniowane standardem ANSI C, ale większość kompilatorów nie zauważy błędu podczas kompilacji i nie zgłosi błędu ani ostrzeżenia. Uruchomiony program zakończy się błędem.
int main(void)
{
char *s = "hello world";
*s = 'H';
}
Gdy program jest kompilowany stała tekstowa „Hello World” umieszczana jest w sekcji rodata pliku wykonywalnego. Rodata jest częścią segmentu danych przeznaczoną tylko do odczytu. Do zmiennej „s” przypisywany jest wskaźnik na pierwszy znak tekstu „hello world”. Próba zmiany pierwszego znaku, na który wskazuje zmienna „s” kończy się wystąpieniem wyjątku. Uruchomienie programu na systemach Linux i Unix kończy się wyświetleniem poniższego komunikatu (lub podobnego zależnie od konfiguracji systemu):
$ gcc segfault.c -g -o segfault
$ ./segfault
Segmentation fault
Śledzenie wsteczne działania programu przy użyciu gdb zwróci:
Program received signal SIGSEGV, Segmentation fault. 0x1c0005c2 in main () at segfault.c:6 6 *s = 'H';
Powyższy błąd w kodzie może być naprawiony poprzez użycie tablicy w miejsce wskaźnika do znaku (char*
). Spowoduje to za-alokowanie tablicy na stosie oraz przepisanie w ten obszar łańcucha znaków.
char s[] = "hello world";
s[0] = 'H'; // lub również poprawnie: *s = 'H';
Z uwagi, że w C++ łańcuchy znaków są typu const char*
kompilatory powinny wykrywać próbę niejawnej konwersji zmiennej const char*
do typu char*
i zgłosić uwagę podczas kompilacji.
Odwołanie do zerowego adresu pamięci
[edytuj | edytuj kod]Ponieważ dosyć częstym błędem jest próba zapisu/odczytu spod zerowego adresu pamięci (wskaźnik zainicjowany wartością NULL – oznaczającą brak obiektu), większość systemów operacyjnych nie alokuje adresu zerowego dla jakichkolwiek danych do odczytu lub zapisu. W systemach tych próba zapisu lub odczytu danych oraz wykonania instrukcji znajdujących się pod adresem zerowym kończy się zgłoszeniem błędu naruszenia ochrony pamięci.
int *ptr = NULL;
printf("%d", *ptr);
Powyższy kod tworzy zerowy wskaźnik i próbuje przeczytać dane, do których on wskazuje. Wykonanie tego kodu spowoduje naruszenie ochrony pamięci na systemach wspierających ochronę pamięci. Podobnie zakończy się próba zapisu z poniższego kodu:
int *ptr = NULL;
*ptr = 1;
Przepełnienie bufora
[edytuj | edytuj kod]Przepełnienie stosu
[edytuj | edytuj kod]Kolejnym przykładem błędu mogącego doprowadzić do błędu naruszenia ochrony pamięci jest przepełnienie stosu, które można uzyskać rekurencyjnym wywołaniem funkcji nie posiadającym warunku zakończenia:[1]
int main(void)
{
main();
return 0;
}
Powyższy kod z uwagi na włączoną optymalizację w kompilatorze może prowadzić do różnych zachowań w powyższym fragmencie: Wynikowy program wykonywalny może:
- nie zgłosić błędu i wykonywać się w sposób ciągły (usunięcie nieosiągalnego
return 0
oraz zamienienie na iterację) - zgłosić błąd naruszenia ochrony pamięci (gdy ramka stosu nie jest kontrolowana)
- zgłosić błąd przepełnienia stosu, gdy system obsługuje kontrolę ramki stosu.