web-dev-qa-db-fra.com

Pourquoi ai-je un défaut de segmentation lorsque j'écris dans une chaîne initialisée avec "char * s" mais pas "char s []"?

Le code suivant reçoit l'erreur seg 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.

253
Markus

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].

A: un littéral de chaîne (le terme formel .__ pour une chaîne à double guillemet dans C source) peut être utilisé légèrement dans deux. différentes façons:

  1. En tant qu'initialiseur pour 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, de sa taille.).
  2. Partout ailleurs, il se transforme en un tableau de caractères statique et non nommé, et ce tableau sans nom peut être stocké en mémoire morte, et qui donc ne peut 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 sur le premier du tableau sans nom. élément. 

Certains compilateurs ont un commutateur contrôler si les littéraux de chaîne sont inscriptibles ou non (pour compiler l'ancien code ), et certains peuvent avoir des options pour faire en sorte que les littéraux de chaîne soient formellement traités comme des tableaux de caractères const (pour meilleure capture d'erreur).

211
matli

Normalement, les littéraux de chaîne sont stockés dans la mémoire en lecture seule lorsque le programme est exécuté. Cela vous évitera de modifier accidentellement une constante de chaîne. Dans votre premier exemple, "string" est stocké dans la mémoire en lecture seule et *str pointe vers le premier caractère. La erreur 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 depuis son emplacement en lecture seule vers le tableau str[]. Ensuite, le changement du premier caractère est autorisé. Vous pouvez le vérifier en imprimant l'adresse de chacun:

printf("%p", str);

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

printf("%d", sizeof(str));
91
Greg Hewgill

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

La "mémoire en lecture seule" à 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 telles que la sécurité. Lorsque vous créez un caractère * 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 qu'il pointe 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 les données de chaîne initialisées dans le segment de données, ce qui correspond au même emplacement 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.

31
Bob Somers

Pourquoi ai-je 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 de magie", et décrit à 6.7.8/14 "Initialisation":

    Un tableau de type caractère peut être initialisé par un littéral de chaîne de caractères, éventuellement enfermés dans des accolades. Les caractères successifs du littéral de chaîne de caractères (y compris le caractère nul final s'il reste de la place s'il y a de la place ou si le tableau est de taille inconnue) initialisent le éléments du tableau.

    Donc, ceci est juste un raccourci pour:

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

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

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

    Alors quand tu écris:

    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 correspond à UB.

    Ceci est documenté à la section 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 multi-octets séquence de caractères résultant d'un littéral de chaîne ou de littéraux. Le caractère multi-octets séquence 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 tapez char et sont initialisés avec les octets individuels du caractère multi-octets séquence [...]

    6 Il n’est pas précisé si ces tableaux sont distincts à condition que leurs éléments aient le valeurs appropriées. Si le programme tente de modifier un tel tableau, le comportement est indé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" s et t 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. D'autre part, la déclaration

char *p = "abc";

définit p avec le type "pointeur sur caractère" et l'initialise pour pointer sur un objet de type "tableau de caractère" de longueur 4 dont les éléments sont initialisés avec une chaîne de caractères. Si vous tentez d'utiliser p pour modifier le contenu du tableau, le comportement n'est pas défini.

GCC 4.8 x86-64 implémentation 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 stocke char* dans la section .rodata et non dans .text.

Si nous faisons la même chose pour char[]:

 char s[] = "abc";

on obtient:

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

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

Notez toutefois que le script de l'éditeur de liens par défaut place .rodata et .text dans le même segment, qui dispose de l'autorisation d'exécution, mais non d'une autorisation en écriture. Ceci peut être observé avec:

readelf -l a.out

qui contient:

 Section to Segment mapping:
  Segment Sections...
   02     .text .rodata

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 dans une mémoire à lecture seule. "str" ​​est un pointeur utilisé pour modifier la constante.

Dans le second code, "chaîne" est un initialiseur de tableau, une 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.

17
Andru Luvisi

Parce que le type de "whatever" dans le contexte du premier exemple est const char * (même si vous l'attribuez à un caractère non-const *), vous ne devez donc pas essayer d'écrire.

Le compilateur a imposé cela en plaçant la chaîne dans une partie de la mémoire en lecture seule; son écriture génère donc un segfault.

12
Mike F

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

tableau de chaînes

 char strarray[] = "hello";

Dans la mémoire, le tableau est stocké dans des cellules de mémoire continues, comme suit: [h][e][l][l][o][\0] =>[] est une cellule de mémoire d'une taille de 1 octet sur chaque charte. Vous pouvez accéder à ces cellules de mémoire continue par le nom strarray here.so here string array strarray contenant lui-même tous les caractères de la chaîne initialisée. dans ce cas ici "hello"so nous pouvons facilement changer le contenu de sa 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 changée en 'm' alors la valeur de strarray changée 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" est invalide

Aiguille

Comme nous le savons tous, les pointeurs pointent vers un emplacement mémoire dans la mémoire. Le pointeur non initialisé pointe vers un emplacement mémoire aléatoire de sorte que, après l’initialisation, il pointe vers un emplacement mémoire particulier.

char *ptr = "hello";

ici le pointeur ptr est initialisé à la chaîne "hello", qui est une chaîne constante stockée dans la mémoire morte (ROM), de sorte que "hello" ne peut pas être modifié car il est stocké dans la ROM

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

donc ptr [0] = 'm' est invalide 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 puisqu'il s'agit simplement d'un pointeur afin qu'il puisse pointer vers n'importe quelle adresse mémoire de la variable de son type de données.

ptr="new string"; is valid
8
user2426842
char *str = "string";  

Ce qui précède définit str pour qu'il pointe 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 dans la mémoire. 

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

7
DougN
char *str = "string";

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

char str[] = "string";

alloue et initialise un tableau local modifiable

6
Rob Walker

Le C FAQ auquel @matli est 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é n'importe où autre que à initialiser un tableau de caractères (c'est-à-dire: le deuxième exemple de @ Mark, qui fonctionne correctement), cette chaîne est stockée par le compilateur dans un tableau spécial static string , ce qui revient à créer une variable statique globale (en lecture seule, de bien sûr) qui est essentiellement anonyme (n’a pas de variable "nom"). La partie read-only est la partie la plus importante. C'est pourquoi le premier exemple de code de la marque @ Mark segfaults.

6
rpj

Le 

 char *str = "string";

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

  str[0] = 'z';

vous obtenez une faute seg. Sur certaines plates-formes, le littéral peut être dans une mémoire inscriptible afin que vous ne voyiez pas un segfault, mais c'est un code non valide (entraînant un comportement non défini).

La ligne:

char str[] = "string";

alloue un tableau de caractères et copies la chaîne littérale à ce tableau, qui est entièrement accessible en écriture, de sorte que la mise à jour ultérieure ne pose aucun problème.

4
Michael Burr

Les littéraux de chaîne tels que "chaîne" sont probablement alloués dans l'espace adresse de votre exécutable sous forme de données en lecture seule (donnez ou prenez votre compilateur). Quand vous allez le toucher, il se fait peur que vous soyez dans sa zone de maillot de bain et vous laisse savoir avec une faute distincte.

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

3
Jurney
// 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. 
2
jokeysmurf

En premier lieu, str est un pointeur qui pointe sur "string". Le compilateur est autorisé à placer des littéraux de chaîne dans des emplacements en mémoire dans lesquels vous ne pouvez pas écrire, mais vous ne pouvez que lire. (Cela aurait dû déclencher un avertissement, puisque vous attribuez un const char * à un char *. Aviez-vous des avertissements désactivés, ou les ignoriez-vous?)

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

1
David Thornley

Supposons que les chaînes sont,

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 en vigueur. 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), généralement en lecture seule (ceci est spécifique à la mise en oeuvre, un compilateur peut placer ces données de programme en lecture seule dans une mémoire en lecture seule) ).

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

0
Venki