¿Por qué obtengo una falla de segmentación cuando escribo en un “char *s” inicializado con un literal de cadena, pero no “char s[]”?

9 minutos de lectura

avatar de usuario
Marcos

El siguiente código recibe una falla de segmento 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.

  • Es divertido, pero en realidad se compila y se ejecuta perfectamente cuando se usa el compilador de Windows (cl) en un símbolo del sistema de desarrollador de Visual Studio. Me confundí por unos momentos…

    – Suricata inconformista

    13 de septiembre de 2016 a las 8:30

avatar de usuario
Ciro Santilli Путлер Капут 六四事

¿Por qué obtengo una falla de segmentación cuando escribo en una cadena?

Proyecto C99 N1256

Hay dos usos diferentes de los literales de cadenas 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 carácter puede inicializarse mediante una cadena de caracteres literal, opcionalmente encerrada entre llaves. Los caracteres sucesivos del literal de la 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.

    Así que esto es solo un atajo para:

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

    Como cualquier otra matriz regular, c se puede modificar

  2. En cualquier otro lugar: genera un:

    • sin nombre
    • array of char ¿Cuál es el tipo de cadenas literales en C y C++?
    • con almacenamiento estático
    • que da UB si se modifica

    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 el elenco implícito de char[] para char *que siempre es legal.

    Entonces si modificas c[0]también modificas __unnamedque es UB.

    Esto está documentado 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 de almacenamiento estático y la longitud suficiente para contener la secuencia. Para los literales de cadenas 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 una matriz de este tipo, el comportamiento no está definido.

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” s y t cuyos elementos se inicializan con literales de cadenas de caracteres.

Esta declaración es idéntica a

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

El contenido de las matrices es modificable. Por otra parte, la declaración

char *p = "abc";

define p con tipo “puntero a char” y lo inicializa para apuntar a un objeto con tipo “matriz de char” con longitud 4 cuyos elementos se inicializan con una cadena de caracteres literal. Si se intenta utilizar p para 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: tiendas GCC char* en .rodata secció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 el script del enlazador predeterminado pone .rodata y .text en el mismo segmento, que tiene permiso de ejecución pero no de escritura. Esto se puede observar con:

readelf -l a.out

que contiene:

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

  • Siempre que use “%p” en printf, debe convertir el puntero en void * como en printf(“%p”, (void *)str); Al imprimir un tamaño_t con printf, debe usar “%zu” si usa el último estándar C (C99).

    – Chris joven

    3 de octubre de 2008 a las 7:44

  • Además, los paréntesis con sizeof solo son necesarios cuando se toma el tamaño de un tipo (entonces el argumento parece un molde). Recuerda que sizeof es un operador, no una función.

    – relajarse

    25 de noviembre de 2008 a las 8:45

  • y use %zu imprimir size_t

    – phuclv

    11 de abril de 2017 a las 15:36

  • advertencia: carácter de tipo de conversión desconocido ‘z’ en formato [-Wformat=] :/

    – Juan

    26 de febrero de 2021 a las 10:02

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 refiere la gente es el segmento de texto en términos 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 que apunte al segmento de texto. Entonces, si intentas cambiarlo, kaboom. Fallo de segmento.

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 demás. 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 aún es solo un carácter *), apunta al segmento de datos en lugar del segmento de texto, que puede modificar de manera segura en tiempo de ejecución.

  • ¿Pero no es cierto que puede haber implementaciones que permitan modificar la “memoria de solo lectura”?

    – Pacerier

    21 de septiembre de 2013 a las 5:07

  • 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 principal. ¿Correcto?

    – SE

    4 de diciembre de 2019 a las 2:15

  • @SE Sí, me imagino que Bob Somers se refiere tanto a la pila, el montón como a la estática (incluidas las variables estáticas y globales) al escribir “el segmento de datos”. Y se coloca una matriz local en la pila, por lo que tiene razón allí 🙂

    – Olov

    27 de diciembre de 2020 a las 1:44

  • Lo sentimos, pero probablemente tenga razón aquí. El segmento de datos es la parte de la memoria dedicada a las variables estáticas o globales inicializadas, pero la matriz también podría colocarse en la pila si es local, como ha escrito.

    – Olov

    27 de diciembre de 2020 a las 1:53

avatar de usuario
Bence Kaulics

char *str = "string";  

Los conjuntos anteriores str para señalar el valor literal "string" que está codificado en la imagen binaria del programa, que probablemente esté marcado como de solo lectura en la memoria.

Asi que 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.

  • ¿Pero no es cierto que puede haber implementaciones que permitan modificar la “memoria de solo lectura”?

    – Pacerier

    21 de septiembre de 2013 a las 5:07

  • 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 principal. ¿Correcto?

    – SE

    4 de diciembre de 2019 a las 2:15

  • @SE Sí, me imagino que Bob Somers se refiere tanto a la pila, el montón como a la estática (incluidas las variables estáticas y globales) al escribir “el segmento de datos”. Y se coloca una matriz local en la pila, por lo que tiene razón allí 🙂

    – Olov

    27 de diciembre de 2020 a las 1:44

  • Lo sentimos, pero probablemente tenga razón aquí. El segmento de datos es la parte de la memoria dedicada a las variables estáticas o globales inicializadas, pero la matriz también podría colocarse en la pila si es local, como ha escrito.

    – Olov

    27 de diciembre de 2020 a las 1:53

memoria constante

Dado que los literales de cadena son de solo lectura por diseño, se almacenan en el parte constante de la memoria Los datos almacenados allí son inmutables, es decir, no se pueden cambiar. Por lo tanto, todos los literales de cadena definidos en el código C obtienen aquí una dirección de memoria de solo lectura.

memoria de pila

Él parte de la pila de memoria es donde residen las direcciones de las variables locales, por ejemplo, variables definidas en funciones.


Como sugiere la respuesta de @matli, hay dos formas de trabajar con cadenas de estas cadenas constantes.

1. Puntero a literal de cadena

Cuando definimos un puntero a un literal de cadena, estamos creando una variable de puntero que vive en memoria de pila. Apunta a la dirección de solo lectura donde reside el literal de cadena subyacente.

#include <stdio.h>

int main(void) {
  char *s = "hello";
  printf("%p\n", &s);  // Prints a read-only address, e.g. 0x7ffc8e224620
  return 0;
}

Si tratamos de modificar s insertando

s[0] = 'H';

obtenemos un Segmentation fault (core dumped). Estamos tratando de acceder a la memoria a la que no deberíamos acceder. Estamos intentando modificar el valor de una dirección de solo lectura, 0x7ffc8e224620.

2. Matriz de caracteres

Por el bien del ejemplo, supongamos que el literal de cadena "Hello" almacenado en la memoria constante tiene una dirección de memoria de solo lectura idéntica a la anterior, 0x7ffc8e224620.

#include <stdio.h>

int main(void) {
  // We create an array from a string literal with address 0x7ffc8e224620.
  // C initializes an array variable in the stack, let's give it address
  // 0x7ffc7a9a9db2.
  // C then copies the read-only value from 0x7ffc8e224620 into 
  // 0x7ffc7a9a9db2 to give us a local copy we can mutate.
  char a[] = "hello";

  // We can now mutate the local copy
  a[0] = 'H';

  printf("%p\n", &a);  // Prints the Stack address, e.g. 0x7ffc7a9a9db2
  printf("%s\n", a);   // Prints "Hello"

  return 0;
}

Nota: Cuando se usan punteros para cadenas de literales como en 1., la mejor práctica es usar el const palabra clave, como const *s = "hello". Esto es más legible y el compilador proporcionará una mejor ayuda cuando se viole. Luego arrojará un error como error: assignment of read-only location ‘*s’ en lugar de la falla seg. Es probable que los linters en los editores detecten el error antes de compilar manualmente el código.

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad