Pourquoi est-ce que j'obtiens une erreur de segmentation lors de l'écriture dans un "char * s" initialisé avec un littéral de chaîne, mais pas dans "char s []"?

287

Le code suivant reçoit une erreur de segmentation sur la ligne 2:

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

Bien que cela fonctionne parfaitement bien:

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

Testé avec MSVC et GCC.

Markus
la source
1
C'est drôle - mais cela se compile et fonctionne parfaitement lorsque vous utilisez le compilateur Windows (cl) sur une invite de commande de développeur Visual Studio. Ça m'a dérouté quelques instants ...
David Refaeli

Réponses:

241

Voir la FAQ C, question 1.32

Q : Quelle est la différence entre ces initialisations?
char a[] = "string literal";
char *p = "string literal";
Mon programme se bloque si j'essaie d'attribuer une nouvelle valeur à p[i].

R : Un littéral de chaîne (le terme formel pour une chaîne entre guillemets doubles dans la source C) peut être utilisé de deux manières légèrement différentes:

  1. En tant qu'initialiseur d'un tableau de caractères, comme dans la déclaration de char a[], il spécifie les valeurs initiales des caractères de ce tableau (et, si nécessaire, sa taille).
  2. Partout ailleurs, il se transforme en un tableau de caractères statique sans nom, et ce tableau sans nom peut être stocké dans une mémoire en lecture seule, et qui ne peut donc pas nécessairement être modifié. Dans un contexte d'expression, le tableau est converti immédiatement en un pointeur, comme d'habitude (voir section 6), donc la deuxième déclaration initialise p pour pointer vers le premier élément du tableau sans nom.

Certains compilateurs ont un commutateur contrôlant si les littéraux de chaîne sont inscriptibles ou non (pour la compilation de l'ancien code), et certains peuvent avoir des options pour que les littéraux de chaîne soient formellement traités comme des tableaux de const char (pour une meilleure capture des erreurs).

matli
la source
7
Quelques autres points: (1) le défaut de segmentation se produit comme décrit, mais son occurrence est fonction de l'environnement d'exécution; si le même code était dans un système embarqué, l'écriture peut n'avoir aucun effet ou elle peut en fait changer le s en z. (2) Comme les littéraux de chaîne ne sont pas accessibles en écriture, le compilateur peut économiser de l'espace en plaçant deux instances de "chaîne" au même endroit; ou, si ailleurs dans le code vous avez "une autre chaîne", alors un morceau de mémoire pourrait supporter les deux littéraux. De toute évidence, si le code était alors autorisé à modifier ces octets, des bogues étranges et difficiles pourraient se produire.
greggo
1
@greggo: Bon point. Il existe également un moyen de le faire sur les systèmes avec MMU en utilisant mprotectpour onduler la protection en lecture seule (voir ici ).
Donc char * p = "blah" crée en fait un tableau temporaire? Bizarre.
rahul tyagi
1
Et après 2 ans d'écriture en C ++ ... TIL
zeboidlund
@rahultyagi que voulez-vous dire?
Suraj Jain
105

Normalement, les littéraux de chaîne sont stockés en mémoire morte lorsque le programme est exécuté. Cela vous empêche de modifier accidentellement une constante de chaîne. Dans votre premier exemple, "string"est stocké en mémoire morte et *strpointe vers le premier caractère. Le défaut de segmentation se produit lorsque vous essayez de remplacer le premier caractère par 'z'.

Dans le deuxième exemple, la chaîne "string"est copiée par le compilateur de son répertoire d'origine en lecture seule vers le str[]tableau. La modification du premier caractère est alors autorisée. Vous pouvez le vérifier en imprimant l'adresse de chacun:

printf("%p", str);

De plus, l'impression de la taille de strdans le deuxième exemple vous montrera que le compilateur lui a alloué 7 octets:

printf("%d", sizeof(str));
Greg Hewgill
la source
13
Chaque fois que vous utilisez "% p" sur printf, vous devez convertir le pointeur en void * comme dans printf ("% p", (void *) str); Lors de l'impression d'un size_t avec printf, vous devez utiliser "% zu" si vous utilisez la dernière norme C (C99).
Chris Young
4
De plus, les parenthèses avec sizeof ne sont nécessaires que lors de la prise de la taille d'un type (l'argument ressemble alors à un transtypage). N'oubliez pas que sizeof est un opérateur, pas une fonction.
détendez-vous
34

La plupart de ces réponses sont correctes, mais juste pour ajouter un peu plus de clarté ...

La "mémoire morte" à laquelle les gens font référence est le segment de texte en termes ASM. C'est le même endroit en mémoire où les instructions sont chargées. Ceci est en lecture seule pour des raisons évidentes comme la sécurité. Lorsque vous créez un char * initialisé à une chaîne, les données de la chaîne sont compilées dans le segment de texte et le programme initialise le pointeur pour pointer dans le segment de texte. Donc, si vous essayez de le changer, kaboom. Segfault.

Lorsqu'il est écrit sous forme de tableau, le compilateur place à la place les données de chaîne initialisées dans le segment de données, qui est le même endroit que vos variables globales et autres. Cette mémoire est modifiable, car il n'y a pas d'instructions dans le segment de données. Cette fois, lorsque le compilateur initialise le tableau de caractères (qui n'est encore qu'un caractère *), il pointe vers le segment de données plutôt que vers le segment de texte, que vous pouvez modifier en toute sécurité au moment de l'exécution.

Bob Somers
la source
Mais n'est-il pas vrai qu'il peut y avoir des implémentations qui permettent de modifier la "mémoire morte"?
Pacerier
Lorsqu'il est écrit sous forme de tableau, le compilateur place les données de chaîne initialisées dans le segment de données si elles sont statiques ou globales. Sinon (par exemple pour un tableau automatique normal), il place sur la pile, dans le cadre de pile de la fonction main. Correct?
SE
26

Pourquoi est-ce que j'obtiens une erreur de segmentation lors de l'écriture dans une chaîne?

C99 N1256 draft

Il existe deux utilisations différentes des littéraux de chaîne de caractères:

  1. Initialiser char[]:

    char c[] = "abc";      

    C'est "plus magique", et décrit au 6.7.8 / 14 "Initialisation":

    Un tableau de type caractère peut être initialisé par une chaîne de caractères littérale, éventuellement entourée d'accolades. Les caractères successifs du littéral de chaîne de caractères (y compris le caractère nul de fin s'il y a de la place ou si le tableau est de taille inconnue) initialisent les éléments du tableau.

    Ce n'est donc qu'un raccourci pour:

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

    Comme tout autre tableau régulier, cpeut être modifié.

  2. Partout ailleurs: il génère un:

    Donc, quand vous écrivez:

    char *c = "abc";

    Ceci est similaire à:

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

    Notez la conversion implicite de char[]àchar * , qui est toujours légale.

    Ensuite, si vous modifiez c[0], vous modifiez également__unnamed , qui est UB.

    Ceci est documenté en 6.4.5 "Littéraux de chaîne":

    5 Dans la phase de traduction 7, un octet ou un code de valeur zéro est ajouté à chaque séquence de caractères multi-octets résultant d'un ou plusieurs littéraux de chaîne. La séquence de caractères multi-octets est ensuite utilisée pour initialiser un tableau de durée et de longueur de stockage statique juste suffisant pour contenir la séquence. Pour les littéraux de chaîne de caractères, les éléments du tableau ont le type char et sont initialisés avec les octets individuels de la séquence de caractères multi-octets [...]

    6 Il n'est pas précisé si ces tableaux sont distincts à condition que leurs éléments aient les valeurs appropriées. Si le programme tente de modifier un tel tableau, le comportement n'est pas défini.

6.7.8 / 32 "Initialisation" donne un exemple direct:

EXEMPLE 8: La déclaration

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

définit les objets de tableau de caractères "simples" sett dont les éléments sont initialisés avec des littéraux de chaîne de caractères.

Cette déclaration est identique à

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

Le contenu des tableaux est modifiable. En revanche, la déclaration

char *p = "abc";

définit pavec le type "pointeur sur char" et l'initialise pour pointer vers un objet de type "tableau de char" de longueur 4 dont les éléments sont initialisés avec une chaîne de caractères littérale. Si une tentative est faite pour pmodifier le contenu du tableau, le comportement n'est pas défini.

Mise en œuvre de GCC 4.8 x86-64 ELF

Programme:

#include <stdio.h>

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

Compiler et décompiler:

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

La sortie contient:

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

Conclusion: GCC le stocke char*dans.rodata section, pas dans.text .

Si nous faisons de même pour char[]:

 char s[] = "abc";

on obtient:

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

donc il est stocké dans la pile (par rapport à %rbp ).

Notez cependant que le script de l'éditeur de liens par défaut place .rodataet .textdans le même segment, qui a une autorisation d'exécution mais pas d'écriture. Cela peut être observé avec:

readelf -l a.out

qui contient:

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

Dans le premier code, "chaîne" est une constante de chaîne et les constantes de chaîne ne doivent jamais être modifiées car elles sont souvent placées en mémoire morte. "str" ​​est un pointeur utilisé pour modifier la constante.

Dans le deuxième code, "chaîne" est un initialiseur de tableau, sorte de raccourci pour

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

"str" ​​est un tableau alloué sur la pile et peut être modifié librement.

Andru Luvisi
la source
1
Sur la pile ou le segment de données si strest global ou static.
Gauthier
12

Parce que le type de "whatever"dans le contexte du 1er exemple estconst char * (même si vous l'assignez à un caractère non const *), ce qui signifie que vous ne devriez pas essayer d'écrire dessus.

Le compilateur a appliqué cela en plaçant la chaîne dans une partie en lecture seule de la mémoire, d'où l'écriture génère une erreur de segmentation.


la source
8

Pour comprendre cette erreur ou ce problème, vous devez d'abord connaître la différence b / w le pointeur et le tableau alors voici d'abord je vous explique les différences b / w les

tableau de chaînes

 char strarray[] = "hello";

Dans le tableau mémoire est stocké dans les cellules de mémoire continue, stockée sous forme [h][e][l][l][o][\0] =>[]est une cellule de mémoire char taille d'octets, et cette cellules de mémoire continue peut être accès par nom de nom strArray here.so ici tableau de chaînes strarraycontenant lui - même tous les caractères de chaîne initialisée à it.In cette cas ici "hello" afin que nous puissions facilement changer son contenu en mémoire en accédant à chaque caractère par sa valeur d'index

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

et sa valeur a changé en valeur 'm'si strarray changé en"mello" ;

un point à noter ici que nous pouvons changer le contenu du tableau de chaînes en changeant caractère par caractère mais ne pouvons pas initialiser directement une autre chaîne comme strarray="new string" n'est pas valide

Aiguille

Comme nous le savons tous, le pointeur pointe vers l'emplacement de la mémoire en mémoire, le pointeur non initialisé pointe vers un emplacement de mémoire aléatoire, et après l'initialisation pointe vers un emplacement de mémoire particulier

char *ptr = "hello";

ici le pointeur ptr est initialisé en chaîne "hello"qui est une chaîne constante stockée dans la mémoire morte (ROM) afin"hello" ne peut pas être modifiée car elle est stockée dans la ROM

et ptr est stocké dans la section pile et pointant vers une chaîne constante "hello"

donc ptr [0] = 'm' n'est pas valide car vous ne pouvez pas accéder à la mémoire en lecture seule

Mais ptr peut être initialisé directement à une autre valeur de chaîne car il ne s'agit que d'un pointeur afin qu'il puisse pointer vers n'importe quelle adresse mémoire de variable de son type de données

ptr="new string"; is valid
Communauté
la source
7
char *str = "string";  

Les ensembles ci-dessus strpointent vers la valeur littérale"string" qui est codée en dur dans l'image binaire du programme, qui est probablement marquée en lecture seule en mémoire.

Tente donc str[0]=d'écrire dans le code en lecture seule de l'application. Je suppose que cela dépend probablement du compilateur.

DougN
la source
6
char *str = "string";

alloue un pointeur sur un littéral de chaîne, que le compilateur place dans une partie non modifiable de votre exécutable;

char str[] = "string";

alloue et initialise un tableau local qui est modifiable

Rob Walker
la source
pouvons-nous écrire int *b = {1,2,3) comme nous écrivons char *s = "HelloWorld"?
Suraj Jain
6

La FAQ C à laquelle @matli a lié le mentionne, mais personne d'autre ici ne l'a encore fait, donc pour clarification: si un littéral de chaîne (chaîne entre guillemets doubles dans votre source) est utilisé ailleurs que pour initialiser un tableau de caractères (c'est-à-dire: @ Deuxième exemple de Mark, qui fonctionne correctement), cette chaîne est stockée par le compilateur dans une table de chaînes statique spéciale , ce qui s'apparente à la création d'une variable statique globale (en lecture seule, bien sûr) qui est essentiellement anonyme (n'a pas de variable "nom "). La partie en lecture seule est la partie importante, et c'est pourquoi le premier exemple de code de @ Mark segfaults.

rpj
la source
pouvons-nous écrire int *b = {1,2,3) comme nous écrivons char *s = "HelloWorld"?
Suraj Jain
4

le

 char *str = "string";

ligne définit un pointeur et le pointe vers une chaîne littérale. La chaîne littérale n'est pas accessible en écriture donc quand vous faites:

  str[0] = 'z';

vous obtenez une faute de segmentation. Sur certaines plates-formes, le littéral peut être en mémoire inscriptible, vous ne verrez donc pas de défaut de segmentation, mais c'est un code non valide (entraînant un comportement indéfini) malgré tout.

La ligne:

char str[] = "string";

alloue un tableau de caractères et copie la chaîne littérale dans ce tableau, qui est entièrement accessible en écriture, donc la mise à jour suivante ne pose aucun problème.

Michael Burr
la source
pouvons-nous écrire int *b = {1,2,3) comme nous écrivons char *s = "HelloWorld"?
Suraj Jain
3

Les littéraux de chaîne tels que "chaîne" sont probablement alloués dans l'espace d'adressage de votre exécutable en tant que données en lecture seule (donnez ou prenez votre compilateur). Lorsque vous allez le toucher, cela vous fait paniquer que vous êtes dans sa zone de maillot de bain et vous le signale avec une faute de seg.

Dans votre premier exemple, vous obtenez un pointeur vers ces données const. Dans votre deuxième exemple, vous initialisez un tableau de 7 caractères avec une copie des données const.

Jurney
la source
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
la source
1

En premier lieu, strest un pointeur qui pointe vers "string". Le compilateur est autorisé à placer des littéraux de chaîne dans des emplacements en mémoire où vous ne pouvez pas écrire, mais seulement lire. (Cela aurait dû déclencher un avertissement, car vous affectez un const char *à unchar * . Avez-vous désactivé les avertissements ou les avez-vous simplement ignorés?)

En second lieu, vous créez un tableau, c'est-à-dire la mémoire à laquelle vous avez un accès complet, et l'initialisez avec "string". Vous créez un char[7](six pour les lettres, un pour la terminaison '\ 0'), et vous faites ce que vous voulez avec.

David Thornley
la source
@Ferruccio,? Oui le constpréfixe
crée des
En C, les littéraux de chaîne ont du type char [N], non const char [N], donc il n'y a pas d'avertissement. (Vous pouvez changer cela dans gcc au moins en passant -Wwrite-strings.)
melpomene
0

Supposons que les chaînes soient,

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

Dans le premier cas, le littéral doit être copié lorsque «a» entre dans le champ d'application. Ici, «a» est un tableau défini sur la pile. Cela signifie que la chaîne sera créée sur la pile et que ses données sont copiées à partir de la mémoire de code (texte), qui est généralement en lecture seule (ceci est spécifique à l'implémentation, un compilateur peut également placer ces données de programme en lecture seule dans une mémoire accessible en écriture. ).

Dans le second cas, p est un pointeur défini sur la pile (portée locale) et faisant référence à un littéral de chaîne (données de programme ou texte) stocké ailleurs. Modifier une telle mémoire n'est généralement pas une bonne pratique ni encouragé.

Venki
la source
-1

La première est une chaîne constante qui ne peut pas être modifiée. Le second est un tableau avec une valeur initialisée, il peut donc être modifié.

libralhb
la source
-2

Une erreur de segmentation est causée lorsque vous essayez d'accéder à la mémoire qui est inaccessible.

char *str est un pointeur sur une chaîne non modifiable (la raison pour laquelle segfault).

alors que char str[]est un tableau et peut être modifiable ..

Raghu Srikanth Reddy
la source