Warum erhalte ich einen Segmentierungsfehler, wenn ich in ein Zeichen schreibe, das mit einem Zeichenfolgenliteral initialisiert wurde, nicht jedoch in ein Zeichen []?

286

Der folgende Code empfängt einen Seg-Fehler in Zeile 2:

char *str = "string";
str[0] = 'z';  // could be also written as *str = 'z'
printf("%s\n", str);

Während dies perfekt funktioniert:

char str[] = "string";
str[0] = 'z';
printf("%s\n", str);

Getestet mit MSVC und GCC.

Markus
quelle
1
Es ist lustig - aber dies wird tatsächlich kompiliert und läuft perfekt, wenn Windows Compiler (cl) an einer Eingabeaufforderung für Visual Studio-Entwickler verwendet wird. Habe mich für ein paar Momente verwirrt ...
David Refaeli

Antworten:

240

Siehe die C-FAQ, Frage 1.32

F : Was ist der Unterschied zwischen diesen Initialisierungen?
char a[] = "string literal";
char *p = "string literal";
Mein Programm stürzt ab, wenn ich versuche, einen neuen Wert zuzuweisen p[i].

A : Ein String-Literal (der formale Begriff für einen String in doppelten Anführungszeichen in der C-Quelle) kann auf zwei leicht unterschiedliche Arten verwendet werden:

  1. Als Initialisierer für ein Array von char gibt er wie in der Deklaration von char a[]die Anfangswerte der Zeichen in diesem Array (und gegebenenfalls seine Größe) an.
  2. Überall sonst wird es zu einem unbenannten, statischen Array von Zeichen, und dieses unbenannte Array kann im Nur-Lese-Speicher gespeichert werden und kann daher nicht unbedingt geändert werden. In einem Ausdruckskontext wird das Array wie üblich sofort in einen Zeiger konvertiert (siehe Abschnitt 6), sodass die zweite Deklaration p so initialisiert, dass sie auf das erste Element des unbenannten Arrays verweist.

Einige Compiler verfügen über einen Schalter, der steuert, ob Zeichenfolgenliterale beschreibbar sind oder nicht (zum Kompilieren von altem Code), und einige haben möglicherweise Optionen, um zu bewirken, dass Zeichenfolgenliterale formal als Arrays von const char behandelt werden (zur besseren Fehlererkennung).

matli
quelle
7
Einige andere Punkte: (1) Der Segfault tritt wie beschrieben auf, sein Auftreten ist jedoch eine Funktion der Laufumgebung. Wenn sich derselbe Code in einem eingebetteten System befand, hat der Schreibvorgang möglicherweise keine Auswirkung, oder er ändert das s tatsächlich in ein z. (2) Da String-Literale nicht beschreibbar sind, kann der Compiler Platz sparen, indem er zwei Instanzen von "string" an derselben Stelle platziert. oder wenn Sie irgendwo anders im Code "eine andere Zeichenfolge" haben, kann ein Teil des Speichers beide Literale unterstützen. Wenn Code dann diese Bytes ändern könnte, könnten natürlich seltsame und schwierige Fehler auftreten.
Greggo
1
@greggo: Guter Punkt. Es gibt auch eine Möglichkeit, dies auf Systemen mit MMU zu tun, indem mprotectder Nur-Lese-Schutz verwendet wird (siehe hier) ).
Also erstellt char * p = "blah" tatsächlich ein temporäres Array? Seltsam.
Rahul Tyagi
1
Und nach 2 Jahren Schreiben in C ++ ... TIL
zeboidlund
@rahultyagi was meinst du?
Suraj Jain
105

Normalerweise werden String-Literale beim Ausführen des Programms im Nur-Lese-Speicher gespeichert. Dies soll verhindern, dass Sie versehentlich eine Zeichenfolgenkonstante ändern. In Ihrem ersten Beispiel "string"wird im Nur-Lese-Speicher gespeichert und *strzeigt auf das erste Zeichen. Der Segfault tritt auf, wenn Sie versuchen, das erste Zeichen in zu ändern 'z'.

Im zweiten Beispiel wird die Zeichenfolge "string"wird kopiert vom Compiler aus seiner schreibgeschützten Heimat die str[]Array. Dann ist das Ändern des ersten Zeichens zulässig. Sie können dies überprüfen, indem Sie jeweils die Adresse ausdrucken:

printf("%p", str);

Wenn Sie strim zweiten Beispiel die Größe von drucken, wird angezeigt, dass der Compiler 7 Byte zugewiesen hat:

printf("%d", sizeof(str));
Greg Hewgill
quelle
13
Wenn Sie "% p" für printf verwenden, sollten Sie den Zeiger wie in printf ("% p", (void *) str) auf void * setzen. Wenn Sie ein size_t mit printf drucken, sollten Sie "% zu" verwenden, wenn Sie den neuesten C-Standard (C99) verwenden.
Chris Young
4
Außerdem wird die Klammer mit sizeof nur benötigt, wenn die Größe eines Typs verwendet wird (das Argument sieht dann wie eine Umwandlung aus). Denken Sie daran, dass sizeof ein Operator und keine Funktion ist.
Entspannen Sie
34

Die meisten dieser Antworten sind richtig, aber nur um ein bisschen mehr Klarheit zu schaffen ...

Der "Nur-Lese-Speicher", auf den sich die Benutzer beziehen, ist das Textsegment in ASM-Begriffen. Es ist dieselbe Stelle im Speicher, an der die Anweisungen geladen werden. Dies ist aus offensichtlichen Gründen wie der Sicherheit schreibgeschützt. Wenn Sie ein Zeichen * erstellen, das für eine Zeichenfolge initialisiert wurde, werden die Zeichenfolgendaten in das Textsegment kompiliert und das Programm initialisiert den Zeiger so, dass er in das Textsegment zeigt. Also, wenn Sie versuchen, es zu ändern, Kaboom. Segfault.

Wenn der Compiler als Array geschrieben wird, platziert er die initialisierten Zeichenfolgendaten stattdessen im Datensegment. Dies ist derselbe Ort, an dem Ihre globalen Variablen und dergleichen leben. Dieser Speicher ist veränderbar, da das Datensegment keine Anweisungen enthält. Wenn der Compiler dieses Mal das Zeichenarray initialisiert (das immer noch nur ein Zeichen * ist), zeigt er auf das Datensegment und nicht auf das Textsegment, das Sie zur Laufzeit sicher ändern können.

Bob Somers
quelle
Aber stimmt es nicht, dass es Implementierungen geben kann, mit denen der "Nur-Lese-Speicher" geändert werden kann?
Pacerier
Beim Schreiben als Array platziert der Compiler die initialisierten Zeichenfolgendaten im Datensegment, wenn sie statisch oder global sind. Andernfalls (z. B. für ein normales automatisches Array) wird es auf dem Stapel im Stapelrahmen der Funktion main platziert. Richtig?
SE
26

Warum erhalte ich beim Schreiben in eine Zeichenfolge einen Segmentierungsfehler?

C99 N1256 Entwurf

Es gibt zwei verschiedene Verwendungen von Zeichenkettenliteralen:

  1. Initialisieren char[]:

    char c[] = "abc";      

    Dies ist "mehr Magie" und wird unter 6.7.8 / 14 "Initialisierung" beschrieben:

    Ein Array vom Zeichentyp kann durch ein Zeichenfolgenliteral initialisiert werden, das optional in geschweiften Klammern eingeschlossen ist. Aufeinanderfolgende Zeichen des Zeichenfolgenliteral (einschließlich des abschließenden Nullzeichens, wenn Platz vorhanden ist oder wenn das Array eine unbekannte Größe hat) initialisieren die Elemente des Arrays.

    Dies ist also nur eine Abkürzung für:

    char c[] = {'a', 'b', 'c', '\0'};

    cKann wie jedes andere reguläre Array geändert werden.

  2. Überall sonst: es erzeugt ein:

    Also, wenn Sie schreiben:

    char *c = "abc";

    Dies ist ähnlich wie:

    /* __unnamed is magic because modifying it gives UB. */
    static char __unnamed[] = "abc";
    char *c = __unnamed;

    Beachten Sie die implizite Besetzung von char[]bischar * , die immer legal ist.

    Wenn Sie dann ändern c[0], ändern Sie auch__unnamed , was UB ist.

    Dies ist unter 6.4.5 "String-Literale" dokumentiert:

    5 In der Übersetzungsphase 7 wird an jede Multibyte-Zeichenfolge, die sich aus einem String-Literal oder Literalen ergibt, ein Byte oder ein Code mit dem Wert Null angehängt. Die Multibyte-Zeichenfolge wird dann verwendet, um ein Array mit statischer Speicherdauer und -länge zu initialisieren, das gerade ausreicht, um die Folge aufzunehmen. Bei Zeichenkettenliteralen haben die Array-Elemente den Typ char und werden mit den einzelnen Bytes der Multibyte-Zeichenfolge [...] initialisiert.

    6 Es ist nicht spezifiziert, ob diese Arrays unterschiedlich sind, vorausgesetzt, ihre Elemente haben die entsprechenden Werte. Wenn das Programm versucht, ein solches Array zu ändern, ist das Verhalten undefiniert.

6.7.8 / 32 "Initialisierung" gibt ein direktes Beispiel:

BEISPIEL 8: Die Erklärung

char s[] = "abc", t[3] = "abc";

definiert "einfache" char-Array-Objekte sundt deren Elemente mit Zeichenfolge Literale initialisiert.

Diese Erklärung ist identisch mit

char s[] = { 'a', 'b', 'c', '\0' },
t[] = { 'a', 'b', 'c' };

Der Inhalt der Arrays kann geändert werden. Auf der anderen Seite die Erklärung

char *p = "abc";

definiert pmit dem Typ "Zeiger auf Zeichen" und initialisiert es so, dass es auf ein Objekt mit dem Typ "Array von Zeichen" mit der Länge 4 zeigt, dessen Elemente mit einem Zeichenfolgenliteral initialisiert werden. Wenn versucht wird, pden Inhalt des Arrays zu ändern, ist das Verhalten undefiniert.

GCC 4.8 x86-64 ELF-Implementierung

Programm:

#include <stdio.h>

int main(void) {
    char *s = "abc";
    printf("%s\n", s);
    return 0;
}

Kompilieren und dekompilieren:

gcc -ggdb -std=c99 -c main.c
objdump -Sr main.o

Die Ausgabe enthält:

 char *s = "abc";
8:  48 c7 45 f8 00 00 00    movq   $0x0,-0x8(%rbp)
f:  00 
        c: R_X86_64_32S .rodata

Fazit: GCC speichert char*es in .rodataAbschnitt, nicht in .text.

Wenn wir dasselbe tun für char[]:

 char s[] = "abc";

wir erhalten:

17:   c7 45 f0 61 62 63 00    movl   $0x636261,-0x10(%rbp)

so wird es im Stapel gespeichert (relativ zu %rbp ).

Beachten Sie jedoch , dass der Standard Linker - Skript setzt .rodataund .textim gleichen Segment, das auszuführen hat , aber keine Schreibrechte. Dies kann beobachtet werden mit:

readelf -l a.out

was beinhaltet:

 Section to Segment mapping:
  Segment Sections...
   02     .text .rodata
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
quelle
17

Im ersten Code ist "string" eine String-Konstante, und String-Konstanten sollten niemals geändert werden, da sie häufig im Nur-Lese-Speicher abgelegt werden. "str" ​​ist ein Zeiger, mit dem die Konstante geändert wird.

Im zweiten Code ist "string" ein Array-Initialisierer, eine Art Abkürzung für

char str[7] =  { 's', 't', 'r', 'i', 'n', 'g', '\0' };

"str" ​​ist ein Array, das dem Stapel zugewiesen ist und frei geändert werden kann.

Andru Luvisi
quelle
1
Auf dem Stapel oder dem Datensegment, wenn strglobal oder static.
Gauthier
12

Weil der Typ "whatever"im Kontext des 1. Beispiels istconst char * (auch wenn Sie ihn einem nicht konstanten Zeichen * zuweisen), sollten Sie nicht versuchen, darauf zu schreiben.

Der Compiler hat dies erzwungen, indem er die Zeichenfolge in einen schreibgeschützten Teil des Speichers gestellt hat. Daher wird durch das Schreiben in die Zeichenfolge ein Segfault generiert.


quelle
8

Um diesen Fehler oder dieses Problem zu verstehen, sollten Sie zuerst den Unterschied zwischen Zeiger und Array kennen. Hier habe ich Ihnen zunächst die Unterschiede zwischen ihnen erklärt

String-Array

 char strarray[] = "hello";

Das Speicherarray wird in fortlaufenden Speicherzellen gespeichert, die als [h][e][l][l][o][\0] =>[]Speicherzelle mit einer Größe von 1 Zeichenbyte gespeichert sind, und auf diese fortlaufenden Speicherzellen kann hier mit dem Namen strarray zugegriffen werden. Hier enthält das Zeichenfolgenarray strarrayselbst alle Zeichen der darauf initialisierten Zeichenfolge In diesem Fall "hello" können wir den Speicherinhalt leicht ändern, indem wir auf jedes Zeichen über seinen Indexwert zugreifen

`strarray[0]='m'` it access character at index 0 which is 'h'in strarray

und sein Wert wurde geändert, 'm'so dass sich der Strarray-Wert in änderte"mello" ;

Ein Punkt, der hier zu beachten ist, dass wir den Inhalt des String-Arrays ändern können, indem wir Zeichen für Zeichen ändern, aber andere Zeichenfolgen nicht direkt wie folgt initialisieren können strarray="new string" es ungültig ist

Zeiger

Wie wir alle wissen, zeigt der Zeiger auf den Speicherort im Speicher, der nicht initialisierte Zeiger auf den zufälligen Speicherort und nach der Initialisierung auf einen bestimmten Speicherort

char *ptr = "hello";

Hier wird der Zeiger ptr auf einen String initialisiert, bei "hello"dem es sich um einen konstanten String handelt, der im Nur-Lese-Speicher (ROM) "hello"gespeichert ist und daher nicht geändert werden kann, da er im ROM gespeichert ist

und ptr wird im Stapelabschnitt gespeichert und zeigt auf eine konstante Zeichenfolge "hello"

Daher ist ptr [0] = 'm' ungültig, da Sie nicht auf den Nur-Lese-Speicher zugreifen können

Ptr kann jedoch direkt mit einem anderen Zeichenfolgenwert initialisiert werden, da es nur ein Zeiger ist, sodass es auf eine beliebige Speicheradresse einer Variablen seines Datentyps verweisen kann

ptr="new string"; is valid

quelle
7
char *str = "string";  

Die obigen strAngaben verweisen auf den Literalwert"string" der im Binärbild des Programms fest codiert ist und im Speicher wahrscheinlich als schreibgeschützt gekennzeichnet ist.

Es str[0]=wird also versucht, in den schreibgeschützten Code der Anwendung zu schreiben. Ich würde vermuten, dass dies wahrscheinlich vom Compiler abhängig ist.

DougN
quelle
6
char *str = "string";

weist einem Zeichenfolgenliteral einen Zeiger zu, den der Compiler in einen nicht modifizierbaren Teil Ihrer ausführbaren Datei einfügt;

char str[] = "string";

weist ein lokales Array zu und initialisiert es, das geändert werden kann

Rob Walker
quelle
können wir schreiben int *b = {1,2,3) wie wir schreiben char *s = "HelloWorld"?
Suraj Jain
6

In den C-FAQs, mit denen @matli verknüpft ist, wird es erwähnt, aber noch niemand hier. Zur Verdeutlichung: Wenn ein Zeichenfolgenliteral (Zeichenfolge in doppelten Anführungszeichen in Ihrer Quelle) an einer anderen Stelle als zum Initialisieren eines Zeichenarrays verwendet wird (dh: @ Marks zweites Beispiel, das korrekt funktioniert, diese Zeichenfolge wird vom Compiler in einer speziellen statischen Zeichenfolgentabelle gespeichert , die dem Erstellen einer globalen statischen Variablen (natürlich schreibgeschützt) ähnelt, die im Wesentlichen anonym ist (keinen Variablennamen hat) "). Der schreibgeschützte Teil ist der wichtige Teil, weshalb das erste Codebeispiel von @ Mark fehlerhaft ist.

rpj
quelle
können wir schreiben int *b = {1,2,3) wie wir schreiben char *s = "HelloWorld"?
Suraj Jain
4

Das

 char *str = "string";

line definiert einen Zeiger und zeigt auf eine Literalzeichenfolge. Die Literalzeichenfolge ist nicht beschreibbar, wenn Sie Folgendes tun:

  str[0] = 'z';

Sie erhalten einen Seg-Fehler. Auf einigen Plattformen befindet sich das Literal möglicherweise im beschreibbaren Speicher, sodass kein Segfault angezeigt wird, der Code jedoch ungültig ist (was zu undefiniertem Verhalten führt).

Die Linie:

char str[] = "string";

Weist ein Array von Zeichen zu und kopiert die Literalzeichenfolge in dieses Array, das vollständig beschreibbar ist, sodass die nachfolgende Aktualisierung kein Problem darstellt.

Michael Burr
quelle
können wir schreiben int *b = {1,2,3) wie wir schreiben char *s = "HelloWorld"?
Suraj Jain
3

String-Literale wie "string" werden wahrscheinlich im Adressraum Ihrer ausführbaren Datei als schreibgeschützte Daten zugewiesen (geben oder nehmen Sie Ihren Compiler). Wenn Sie es berühren, wird Ihnen klar, dass Sie sich im Badeanzugbereich befinden, und Sie werden über einen Seg-Fehler informiert.

In Ihrem ersten Beispiel erhalten Sie einen Zeiger auf diese konstanten Daten. In Ihrem zweiten Beispiel initialisieren Sie ein Array mit 7 Zeichen mit einer Kopie der const-Daten.

Jurney
quelle
2
// create a string constant like this - will be read only
char *str_p;
str_p = "String constant";

// create an array of characters like this 
char *arr_p;
char arr[] = "String in an array";
arr_p = &arr[0];

// now we try to change a character in the array first, this will work
*arr_p = 'E';

// lets try to change the first character of the string contant
*str_p = 'G'; // this will result in a segmentation fault. Comment it out to work.


/*-----------------------------------------------------------------------------
 *  String constants can't be modified. A segmentation fault is the result,
 *  because most operating systems will not allow a write
 *  operation on read only memory.
 *-----------------------------------------------------------------------------*/

//print both strings to see if they have changed
printf("%s\n", str_p); //print the string without a variable
printf("%s\n", arr_p); //print the string, which is in an array. 
Jokeysmurf
quelle
1

In erster Linie strist ein Zeiger, der auf zeigt "string". Der Compiler darf Zeichenfolgenliterale an Stellen im Speicher ablegen, in die Sie nicht schreiben, sondern nur lesen können. (Dies hätte wirklich eine Warnung auslösen sollen, da Sie a const char *einem zuweisen char *. Hatten Sie Warnungen deaktiviert oder haben Sie sie einfach ignoriert?)

Zweitens erstellen Sie ein Array, bei dem es sich um Speicher handelt, auf den Sie vollen Zugriff haben, und initialisieren es mit "string". Sie erstellen eine char[7](sechs für die Buchstaben, eine für die abschließende '\ 0') und machen damit, was Sie wollen.

David Thornley
quelle
@Ferruccio ,? Ja constPräfix machen Variablen schreibgeschützt
EsmaeelE
In C haben String-Literale den Typ char [N]nicht const char [N], daher gibt es keine Warnung. (Sie können das in gcc zumindest durch Passieren ändern -Wwrite-strings.)
Melpomene
0

Angenommen, die Zeichenfolgen sind,

char a[] = "string literal copied to stack";
char *p  = "string literal referenced by p";

Im ersten Fall ist das Literal zu kopieren, wenn 'a' in den Geltungsbereich kommt. Hier ist 'a' ein Array, das auf einem Stapel definiert ist. Dies bedeutet, dass die Zeichenfolge auf dem Stapel erstellt wird und ihre Daten aus dem Code- (Text-) Speicher kopiert werden, der normalerweise schreibgeschützt ist (dies ist implementierungsspezifisch, ein Compiler kann diese schreibgeschützten Programmdaten auch im schreibgeschützten Speicher ablegen ).

Im zweiten Fall ist p ein Zeiger, der auf dem Stapel definiert ist (lokaler Bereich) und auf ein Zeichenfolgenliteral (Programmdaten oder Text) verweist, das an anderer Stelle gespeichert ist. Normalerweise wird das Ändern eines solchen Speichers weder empfohlen noch empfohlen.

Venki
quelle
-1

Erstens ist eine konstante Zeichenfolge, die nicht geändert werden kann. Zweitens ist ein Array mit initialisiertem Wert, so dass es geändert werden kann.

Libralhb
quelle
-2

Ein Segmentierungsfehler wird verursacht, wenn Sie versuchen, auf den Speicher zuzugreifen, auf den nicht zugegriffen werden kann.

char *str ist ein Zeiger auf eine Zeichenfolge, die nicht geändert werden kann (der Grund für den Erhalt von Segfault).

wohingegen char str[]ist ein Array und kann modifizierbar sein ..

Raghu Srikanth Reddy
quelle