¿Por qué aparece un error de segmentación cuando escribo en un "char * s" inicializado con un literal de cadena, pero no en "char s []"?

287

El siguiente código recibe una falla seg en la línea 2:

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

Si bien esto funciona perfectamente bien:

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

Probado con MSVC y GCC.

Markus
fuente
1
Es divertido, pero esto realmente se compila y se ejecuta perfectamente cuando se usa el compilador de Windows (cl) en un símbolo del sistema del desarrollador de Visual Studio. Me confundió por unos momentos ...
David Refaeli

Respuestas:

241

Consulte las preguntas frecuentes de C, pregunta 1.32

P : ¿Cuál es la diferencia entre estas inicializaciones?
char a[] = "string literal";
char *p = "string literal";
Mi programa se bloquea si intento asignar un nuevo valor a p[i].

R : Un literal de cadena (el término formal para una cadena entre comillas dobles en fuente C) se puede usar de dos maneras ligeramente diferentes:

  1. Como inicializador de una matriz de caracteres, como en la declaración de char a[], especifica los valores iniciales de los caracteres en esa matriz (y, si es necesario, su tamaño).
  2. En cualquier otro lugar, se convierte en una matriz estática de caracteres sin nombre, y esta matriz sin nombre se puede almacenar en la memoria de solo lectura y, por lo tanto, no se puede modificar necesariamente. En un contexto de expresión, la matriz se convierte de inmediato en un puntero, como de costumbre (consulte la sección 6), por lo que la segunda declaración inicializa p para señalar el primer elemento de la matriz sin nombre.

Algunos compiladores tienen un interruptor que controla si los literales de cadena se pueden escribir o no (para compilar código antiguo), y algunos pueden tener opciones para hacer que los literales de cadena se traten formalmente como matrices de caracteres constantes (para una mejor captura de errores).

matli
fuente
77
Un par de otros puntos: (1) la falla predeterminada ocurre como se describe, pero su ocurrencia es una función del entorno de ejecución; Si el mismo código estaba en un sistema embebido, la escritura puede no tener efecto, o puede cambiar la sa la z. (2) Debido a que los literales de cadena no se pueden escribir, el compilador puede ahorrar espacio al colocar dos instancias de "cadena" en el mismo lugar; o, si en otro lugar del código tiene "otra cadena", entonces un trozo de memoria podría admitir ambos literales. Claramente, si se permitiera que el código cambiara esos bytes, podrían ocurrir errores extraños y difíciles.
greggo
1
@greggo: Buen punto. También hay una manera de hacer esto en sistemas con MMU mediante el uso mprotectde protección de solo lectura (ver aquí ).
Entonces char * p = "blah" en realidad crea una matriz temporal? Raro.
rahul tyagi
1
Y después de 2 años de escribir en C ++ ... TIL
zeboidlund
@rahultyagi, ¿qué quieres decir?
Suraj Jain
105

Normalmente, los literales de cadena se almacenan en la memoria de solo lectura cuando se ejecuta el programa. Esto es para evitar que cambie accidentalmente una constante de cadena. En su primer ejemplo, "string"se almacena en la memoria de solo lectura y *strapunta al primer carácter. El segfault ocurre cuando intenta cambiar el primer carácter a 'z'.

En el segundo ejemplo, la cadena "string"está copiado por el compilador de su hogar de sólo lectura a la str[]matriz. Luego se permite cambiar el primer carácter. Puede verificar esto imprimiendo la dirección de cada uno:

printf("%p", str);

Además, imprimir el tamaño de stren el segundo ejemplo le mostrará que el compilador le ha asignado 7 bytes:

printf("%d", sizeof(str));
Greg Hewgill
fuente
13
Siempre que use "% p" en printf, debe lanzar el puntero a void * como en printf ("% p", (void *) str); Al imprimir un size_t con printf, debe usar "% zu" si usa el último estándar C (C99).
Chris Young
44
Además, los paréntesis con sizeof solo son necesarios cuando se toma el tamaño de un tipo (el argumento parece un molde). Recuerde que sizeof es un operador, no una función.
Descanse el
34

La mayoría de estas respuestas son correctas, pero solo para agregar un poco más de claridad ...

La "memoria de solo lectura" a la que se refieren las personas es el segmento de texto en términos de ASM. Es el mismo lugar en la memoria donde se cargan las instrucciones. Esto es de solo lectura por razones obvias como la seguridad. Cuando crea un char * inicializado en una cadena, los datos de la cadena se compilan en el segmento de texto y el programa inicializa el puntero para apuntar al segmento de texto. Entonces, si intentas cambiarlo, kaboom. Segfault.

Cuando se escribe como una matriz, el compilador coloca los datos de cadena inicializados en el segmento de datos, que es el mismo lugar donde viven sus variables globales y tales. Esta memoria es mutable, ya que no hay instrucciones en el segmento de datos. Esta vez, cuando el compilador inicializa la matriz de caracteres (que todavía es solo un carácter *), apunta al segmento de datos en lugar del segmento de texto, que puede modificar de forma segura en tiempo de ejecución.

Bob Somers
fuente
Pero, ¿no es cierto que puede haber implementaciones que permitan modificar la "memoria de solo lectura"?
Pacerier
Cuando se escribe como una matriz, el compilador coloca los datos de cadena inicializados en el segmento de datos si son estáticos o globales. De lo contrario (por ejemplo, para una matriz automática normal), se coloca en la pila, en el marco de la pila de la función main. ¿Correcto?
SE
26

¿Por qué aparece un error de segmentación al escribir en una cadena?

C99 N1256 draft

Hay dos usos diferentes de los literales de cadena de caracteres:

  1. Inicializar char[]:

    char c[] = "abc";      

    Esto es "más mágico", y se describe en 6.7.8 / 14 "Inicialización":

    Una matriz de tipo de caracteres puede ser inicializada por una cadena de caracteres literal, opcionalmente encerrada entre llaves. Los caracteres sucesivos del literal de cadena de caracteres (incluido el carácter nulo de terminación si hay espacio o si la matriz tiene un tamaño desconocido) inicializan los elementos de la matriz.

    Entonces esto es solo un atajo para:

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

    Al igual que cualquier otra matriz regular, cse puede modificar.

  2. En todas partes: genera un:

    Entonces cuando escribes:

    char *c = "abc";

    Esto es similar a:

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

    Tenga en cuenta la conversión implícita de char[]a char *, que siempre es legal.

    Luego, si modifica c[0], también modifica __unnamed, que es UB.

    Esto se documenta en 6.4.5 "Literales de cadena":

    5 En la fase de traducción 7, se agrega un byte o código de valor cero a cada secuencia de caracteres multibyte que resulta de una cadena literal o literales. La secuencia de caracteres multibyte se usa luego para inicializar una matriz de duración y longitud de almacenamiento estático solo suficiente para contener la secuencia. Para los literales de cadena de caracteres, los elementos de la matriz tienen el tipo char y se inicializan con los bytes individuales de la secuencia de caracteres multibyte [...]

    6 No se especifica si estas matrices son distintas siempre que sus elementos tengan los valores apropiados. Si el programa intenta modificar dicha matriz, el comportamiento es indefinido.

6.7.8 / 32 "Inicialización" da un ejemplo directo:

EJEMPLO 8: La declaración

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

define objetos de matriz de caracteres "simples" sy tcuyos elementos se inicializan con literales de cadena de caracteres.

Esta declaración es idéntica a

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

Los contenidos de las matrices son modificables. Por otro lado, la declaración

char *p = "abc";

define pcon el tipo "puntero a char" y lo inicializa para apuntar a un objeto con el tipo "matriz de char" con longitud 4 cuyos elementos se inicializan con una cadena de caracteres literal. Si se intenta utilizar ppara modificar el contenido de la matriz, el comportamiento no está definido.

Implementación de GCC 4.8 x86-64 ELF

Programa:

#include <stdio.h>

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

Compilar y descompilar:

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

La salida contiene:

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

Conclusión: GCC lo almacena char*en .rodatasección, no en .text.

Si hacemos lo mismo para char[]:

 char s[] = "abc";

obtenemos:

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

por lo que se almacena en la pila (en relación con %rbp).

Sin embargo , tenga en cuenta que la secuencia de comandos del enlazador predeterminado coloca .rodatay .texten el mismo segmento, que tiene permiso de ejecución pero no escritura. Esto se puede observar con:

readelf -l a.out

que contiene:

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

En el primer código, "cadena" es una constante de cadena, y las constantes de cadena nunca deben modificarse porque a menudo se colocan en la memoria de solo lectura. "str" ​​es un puntero que se utiliza para modificar la constante.

En el segundo código, "cadena" es un inicializador de matriz, una especie de abreviatura para

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

"str" ​​es una matriz asignada en la pila y se puede modificar libremente.

Andru Luvisi
fuente
1
En la pila, o el segmento de datos si stres global o static.
Gauthier
12

Porque el tipo de "whatever"en el contexto del primer ejemplo es const char *(incluso si lo asigna a un carácter no constante *), lo que significa que no debe intentar escribir en él.

El compilador ha forzado esto colocando la cadena en una parte de solo lectura de la memoria, por lo tanto, escribir en ella genera un defecto de seguridad.


fuente
8

Para comprender este error o problema, primero debe conocer la diferencia b / w del puntero y la matriz, por lo que aquí, primero, debo explicarle las diferencias b / w

conjunto de cadenas

 char strarray[] = "hello";

En la matriz de memoria se almacena en celdas de memoria continua, almacenada como [h][e][l][l][o][\0] =>[]una celda de memoria de 1 byte de tamaño, y a estas celdas de memoria continua se puede acceder por nombre llamado strarray aquí. Así que aquí la matriz de cadenas strarraycontiene todos los caracteres de cadena inicializados en ella. caso aquí "hello" para que podamos cambiar fácilmente su contenido de memoria accediendo a cada carácter por su valor de índice

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

y su valor cambió a 'm'tan strarray valor cambiado a "mello";

Un punto a tener en cuenta aquí es que podemos cambiar el contenido de la matriz de cadenas cambiando carácter por carácter, pero no podemos inicializar otra cadena directamente como si strarray="new string"no fuera válido.

Puntero

Como todos sabemos, el puntero apunta a la ubicación de la memoria en la memoria, el puntero no inicializado apunta a una ubicación de memoria aleatoria, y después de la inicialización apunta a una ubicación de memoria particular

char *ptr = "hello";

aquí el puntero ptr se inicializa en una cadena "hello"que es una cadena constante almacenada en la memoria de solo lectura (ROM), por "hello"lo que no se puede cambiar ya que está almacenada en la ROM

y ptr se almacena en la sección de pila y apunta a una cadena constante "hello"

entonces ptr [0] = 'm' no es válido ya que no puede acceder a la memoria de solo lectura

Pero ptr se puede inicializar a otro valor de cadena directamente ya que es solo un puntero, por lo que puede apuntar a cualquier dirección de memoria de variable de su tipo de datos

ptr="new string"; is valid
Comunidad
fuente
7
char *str = "string";  

Lo anterior establece strque apunta al valor literal "string"que está codificado en la imagen binaria del programa, que probablemente está marcado como de solo lectura en la memoria.

Entonces str[0]=está intentando escribir en el código de solo lectura de la aplicación. Sin embargo, supongo que esto probablemente depende del compilador.

DougN
fuente
6
char *str = "string";

asigna un puntero a un literal de cadena, que el compilador está colocando en una parte no modificable de su ejecutable;

char str[] = "string";

asigna e inicializa una matriz local que es modificable

Rob Walker
fuente
podemos escribir int *b = {1,2,3) como escribimos char *s = "HelloWorld"?
Suraj Jain
6

Las preguntas frecuentes de C a las que @matli lo vinculó lo mencionan, pero nadie más lo ha hecho todavía, así que para aclaración: si una cadena literal (cadena de comillas dobles en su fuente) se usa en cualquier otro lugar que no sea para inicializar una matriz de caracteres (es decir: @ El segundo ejemplo de Mark, que funciona correctamente), esa cadena es almacenada por el compilador en una tabla especial de cadenas estáticas , que es similar a crear una variable estática global (solo lectura, por supuesto) que es esencialmente anónima (no tiene nombre de variable) "). La parte de solo lectura es la parte importante, y es la razón por la cual el primer ejemplo de código de @ Mark es predeterminado.

rpj
fuente
podemos escribir int *b = {1,2,3) como escribimos char *s = "HelloWorld"?
Suraj Jain
4

los

 char *str = "string";

La línea define un puntero y lo apunta a una cadena literal. La cadena literal no se puede escribir, así que cuando lo haga:

  str[0] = 'z';

Tienes una falla seg. En algunas plataformas, el literal puede estar en la memoria de escritura, por lo que no verá un segfault, pero es un código no válido (que resulta en un comportamiento indefinido) independientemente.

La línea:

char str[] = "string";

asigna una matriz de caracteres y copia la cadena literal en esa matriz, que es completamente grabable, por lo que la actualización posterior no es un problema.

Michael Burr
fuente
podemos escribir int *b = {1,2,3) como escribimos char *s = "HelloWorld"?
Suraj Jain
3

Los literales de cadena como "cadena" probablemente se asignan en el espacio de direcciones de su ejecutable como datos de solo lectura (más o menos su compilador). Cuando vas a tocarlo, se asusta de que estés en el área de su traje de baño y te avisa con una falla seg.

En su primer ejemplo, obtendrá un puntero a esos datos constantes. En su segundo ejemplo, está inicializando una matriz de 7 caracteres con una copia de los datos constantes.

Jurney
fuente
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. 
broma
fuente
1

En primer lugar, stres un puntero que apunta a "string". El compilador puede colocar literales de cadena en lugares en la memoria en los que no puede escribir, pero solo puede leer. (Esto realmente debería haber activado una advertencia, ya que está asignando una const char *a unachar * . ¿Tenía las advertencias desactivadas o simplemente las ignoró?)

En segundo lugar, está creando una matriz, que es la memoria a la que tiene acceso completo, e inicializándola "string". Estás creando un char[7](seis para las letras, uno para la terminación '\ 0'), y haces lo que quieras con él.

David Thornley
fuente
@Ferruccio,? Sí constprefijo make variables Solo
lectura
En C los literales de cadena tienen tipo char [N], no const char [N], por lo que no hay advertencia. (Puede cambiar eso en gcc al menos pasando -Wwrite-strings).
melpomene
0

Supongamos que las cuerdas son,

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

En el primer caso, el literal debe copiarse cuando 'a' entra en el alcance. Aquí 'a' es una matriz definida en la pila. Significa que la cadena se creará en la pila y sus datos se copian de la memoria de código (texto), que generalmente es de solo lectura (esto es específico de la implementación, un compilador puede colocar estos datos del programa de solo lectura en la memoria de lectura y escritura también )

En el segundo caso, p es un puntero definido en la pila (ámbito local) y que hace referencia a un literal de cadena (datos de programa o texto) almacenado en otro lugar donde. Por lo general, modificar dicha memoria no es una buena práctica ni se recomienda.

Venki
fuente
-1

Primero hay una cadena constante que no se puede modificar. El segundo es una matriz con valor inicializado, por lo que se puede modificar.

libralhb
fuente
-2

La falla de segmentación se produce cuando intenta acceder a la memoria que es inaccesible.

char *str es un puntero a una cadena que no se puede modificar (la razón para obtener la segfault).

mientras que char str[]es una matriz y puede ser modificable.

Raghu Srikanth Reddy
fuente