Poniższy kod odbiera błąd seg w linii 2:
char *str = "string";
str[0] = 'z'; // could be also written as *str = 'z'
printf("%s\n", str);
Chociaż działa to doskonale:
char str[] = "string";
str[0] = 'z';
printf("%s\n", str);
Testowane z MSVC i GCC.
c
segmentation-fault
c-strings
Markus
źródło
źródło
Odpowiedzi:
Zobacz C FAQ, pytanie 1.32
źródło
mprotect
ochrony falowej tylko do odczytu (patrz tutaj ).Zwykle literały łańcuchowe są przechowywane w pamięci tylko do odczytu, gdy program jest uruchomiony. Zapobiega to przypadkowej zmianie stałej ciągu. W pierwszym przykładzie
"string"
jest przechowywany w pamięci tylko do odczytu i*str
wskazuje na pierwszy znak. Segfault zdarza się, gdy próbujesz zmienić pierwszy znak na'z'
.W drugim przykładzie ciąg
"string"
jest kopiowany przez kompilator z jego domu tylko do odczytu dostr[]
tablicy. Następnie zmiana pierwszej postaci jest dozwolona. Możesz to sprawdzić, drukując adres każdego:Również wydrukowanie rozmiaru
str
w drugim przykładzie pokaże, że kompilator przydzielił dla niego 7 bajtów:źródło
%zu
do drukowaniasize_t
Większość z tych odpowiedzi jest poprawna, ale dla większej przejrzystości ...
„Pamięć tylko do odczytu”, do której odnoszą się ludzie, to segment tekstowy w kategoriach ASM. To to samo miejsce w pamięci, w którym ładowane są instrukcje. Jest to tylko do odczytu z oczywistych powodów, takich jak bezpieczeństwo. Kiedy tworzysz znak * zainicjowany na łańcuch, dane łańcucha są kompilowane do segmentu tekstowego, a program inicjuje wskaźnik, aby wskazywał na segment tekstowy. Więc jeśli spróbujesz to zmienić, kaboom. Segfault.
Kompilator zapisany jako tablica umieszcza zainicjowane dane łańcuchowe w segmencie danych, czyli w tym samym miejscu, w którym znajdują się zmienne globalne. Pamięć ta jest zmienna, ponieważ w segmencie danych nie ma instrukcji. Tym razem, gdy kompilator inicjuje tablicę znaków (która wciąż jest tylko znakiem *), wskazuje ona raczej na segment danych niż segment tekstowy, który można bezpiecznie zmienić w czasie wykonywania.
źródło
Projekt C99 N1256
Istnieją dwa różne zastosowania literałów ciągów znaków:
Zainicjuj
char[]
:Jest to „więcej magii” i opisane w 6.7.8 / 14 „Inicjalizacja”:
To tylko skrót do:
Jak każda inna zwykła tablica,
c
może być modyfikowana.Wszędzie indziej: generuje:
Więc kiedy piszesz:
Jest to podobne do:
Zwróć uwagę na niejawne przesłanie od
char[]
dochar *
, co jest zawsze legalne.Następnie, jeśli zmodyfikujesz
c[0]
, zmodyfikujesz również__unnamed
, czyli UB.Jest to udokumentowane w 6.4.5 „Literały łańcuchowe”:
6.7.8 / 32 „Inicjalizacja” daje bezpośredni przykład:
Implementacja GCC 4.8 x86-64 ELF
Program:
Kompiluj i dekompiluj:
Dane wyjściowe zawierają:
Wniosek: GCC przechowuje
char*
go w.rodata
sekcji, a nie w.text
.Jeśli zrobimy to samo dla
char[]
:otrzymujemy:
więc zostaje zapisany na stosie (względem
%rbp
).Zauważ jednak, że domyślny skrypt linkera umieszcza
.rodata
i.text
w tym samym segmencie, który wykonał, ale nie ma uprawnień do zapisu. Można to zaobserwować przy:który zawiera:
źródło
W pierwszym kodzie „ciąg” jest stałą ciąg, a stałych ciąg nigdy nie należy modyfikować, ponieważ często są one umieszczane w pamięci tylko do odczytu. „str” to wskaźnik używany do modyfikowania stałej.
W drugim kodzie „string” to inicjator tablicowy, rodzaj krótkiej ręki
„str” to tablica przydzielona na stosie i może być dowolnie modyfikowana.
źródło
str
jest globalny lubstatic
.Ponieważ typ
"whatever"
w kontekście pierwszego przykładu toconst char *
(nawet jeśli przypiszesz go do non-const char *), co oznacza, że nie powinieneś próbować do niego pisać.Kompilator wymusił to, umieszczając ciąg znaków w części tylko do odczytu, dlatego zapis do niego generuje błąd segfault.
źródło
Aby zrozumieć ten błąd lub problem, powinieneś najpierw poznać różnicę między wskaźnikiem i tablicą, więc tutaj najpierw wyjaśnię ci różnice między nimi
tablica ciągów
W tablicy pamięci jest przechowywany w ciągłych komórkach pamięci, przechowywany tak samo jak
[h][e][l][l][o][\0] =>[]
komórka pamięci o rozmiarze 1 bajta, a do tych ciągłych komórek pamięci można uzyskać dostęp pod nazwą o nazwie strarray tutaj. Więc tutajstrarray
sama tablica ciągów zawierająca wszystkie znaki łańcucha zainicjowane w tym. przypadku,"hello"
abyśmy mogli łatwo zmienić zawartość pamięci, uzyskując dostęp do każdego znaku według jego wartości indeksua jego wartość zmieniono na
'm'
tak zmienną wartość zmienną na"mello"
;należy zwrócić uwagę na to, że możemy zmienić zawartość tablicy łańcuchów, zmieniając znak po znaku, ale nie możemy zainicjować innego łańcucha bezpośrednio na nim, ponieważ
strarray="new string"
jest nieprawidłowyWskaźnik
Jak wszyscy wiemy wskaźnik wskazuje na lokalizację pamięci w pamięci, niezainicjowany wskaźnik wskazuje na losową lokalizację pamięci, a po inicjalizacji wskazuje na konkretną lokalizację pamięci
tutaj wskaźnik ptr jest inicjowany na ciąg znaków,
"hello"
który jest ciągiem stałym przechowywanym w pamięci tylko do odczytu (ROM), więc"hello"
nie można go zmienić, ponieważ jest przechowywany w pamięci ROMa ptr jest przechowywany w sekcji stosu i wskazuje na stały ciąg
"hello"
więc ptr [0] = 'm' jest nieprawidłowy, ponieważ nie można uzyskać dostępu do pamięci tylko do odczytu
Ale ptr można zainicjować bezpośrednio na inną wartość ciągu, ponieważ jest to tylko wskaźnik, więc może wskazywać dowolny adres pamięci zmiennej typu danych
źródło
Powyższe wskazuje
str
na wartość literalną"string"
która jest zakodowana na stałe w obrazie binarnym programu, który prawdopodobnie jest oznaczony jako tylko do odczytu w pamięci.Więc
str[0]=
próbuje pisać na tylko do odczytu kodu aplikacji. Sądzę jednak, że to prawdopodobnie zależy od kompilatora.źródło
przydziela wskaźnik do literału łańcuchowego, który kompilator umieszcza w niemodyfikowalnej części pliku wykonywalnego;
przydziela i inicjuje tablicę lokalną, którą można modyfikować
źródło
int *b = {1,2,3)
tak jak mychar *s = "HelloWorld"
?Często zadawane pytania dotyczące C, które @matli odsyła do tej wzmianki, ale nikt jeszcze tu nie ma, więc dla wyjaśnienia: jeśli dosłowny ciąg znaków (ciąg cudzysłowu w źródle) jest używany w dowolnym miejscu innym niż inicjalizacja tablicy znaków (tj .: @ Drugi przykład Marka, który działa poprawnie), ten ciąg jest przechowywany przez kompilator w specjalnej tabeli ciągów statycznych , która jest podobna do tworzenia globalnej zmiennej statycznej (oczywiście tylko do odczytu), która jest zasadniczo anonimowa (nie ma nazwy zmiennej) „). Część tylko do odczytu jest ważną częścią i dlatego pierwszy przykład kodu @ Mark segfaults.
źródło
int *b = {1,2,3)
tak jak mychar *s = "HelloWorld"
?The
linia definiuje wskaźnik i wskazuje dosłowny ciąg. Ciąg literalny nie jest zapisywalny, więc gdy wykonasz:
dostajesz błąd seg. Na niektórych platformach literał może znajdować się w zapisywalnej pamięci, więc nie zobaczysz segfault, ale niezależnie od tego jest to nieprawidłowy kod (skutkujący niezdefiniowanym zachowaniem).
Linia:
przydziela tablicę znaków i kopiuje literalny ciąg do tej tablicy, która jest w pełni zapisywalna, więc kolejna aktualizacja nie stanowi problemu.
źródło
int *b = {1,2,3)
tak jak mychar *s = "HelloWorld"
?Literały łańcuchowe, takie jak „łańcuch”, są prawdopodobnie przydzielane w przestrzeni adresowej twojego pliku wykonywalnego jako dane tylko do odczytu (daj lub weź kompilator). Kiedy go dotkniesz, przestraszy Cię, że jesteś w obszarze kostiumu kąpielowego i powiadomi Cię z błędem seg.
W pierwszym przykładzie otrzymujesz wskaźnik do tych stałych danych. W drugim przykładzie inicjujesz tablicę 7 znaków z kopią stałych danych.
źródło
źródło
Przede wszystkim
str
jest wskaźnikiem, który wskazuje"string"
. Kompilator może umieszczać literały łańcuchowe w miejscach w pamięci, w których nie można pisać, ale można tylko czytać. (To naprawdę powinno wywołać ostrzeżenie, ponieważ przypisujesz aconst char *
dochar *
. Czy masz wyłączone ostrzeżenia, czy po prostu je zignorowałeś?)Po drugie, tworzysz tablicę, do której masz pełny dostęp, i inicjujesz ją
"string"
. Tworzyszchar[7]
(sześć dla liter, jeden dla kończącego „\ 0”) i robisz z nim, co chcesz.źródło
const
prefiks tworzy zmienne Tylko dochar [N]
, nieconst char [N]
, więc nie ma ostrzeżenia. (Możesz to zmienić przynajmniej w gcc, przechodząc-Wwrite-strings
.)Załóżmy, że ciągi są
W pierwszym przypadku literał należy skopiować, gdy „a” wchodzi w zakres. Tutaj „a” jest tablicą zdefiniowaną na stosie. Oznacza to, że ciąg zostanie utworzony na stosie, a jego dane zostaną skopiowane z pamięci kodu (tekstu), która zwykle jest tylko do odczytu (jest to specyficzne dla implementacji, kompilator może również umieścić dane tego programu tylko do odczytu w pamięci do odczytu ).
W drugim przypadku p jest wskaźnikiem zdefiniowanym na stosie (zasięg lokalny) i odnosi się do literału łańcucha (dane programu lub tekst) przechowywanego gdzie indziej. Zwykle modyfikowanie takiej pamięci nie jest dobrą praktyką ani nie jest zalecane.
źródło
Pierwszy to ciąg stały, którego nie można modyfikować. Drugi to tablica z zainicjowaną wartością, więc można ją modyfikować.
źródło
Błąd segmentacji powstaje, gdy próbujesz uzyskać dostęp do pamięci, która jest niedostępna.
char *str
jest wskaźnikiem na ciąg, który nie jest modyfikowalny (przyczyna uzyskania segfault).podczas gdy
char str[]
jest tablicą i może być modyfikowalny ..źródło