¿Por qué los literales de cadena C son de solo lectura?

29

¿Qué ventaja (s) de los literales de cadena de solo lectura justifican (-ies / -ied) el:

  1. Otra forma de dispararte en el pie

    char *foo = "bar";
    foo[0] = 'd'; /* SEGFAULT */
  2. Incapacidad para inicializar elegantemente un conjunto de palabras de lectura-escritura en una línea:

    char *foo[] = { "bar", "baz", "running out of traditional placeholder names" };
    foo[1][2] = 'n'; /* SEGFAULT */ 
  3. Complicando el lenguaje en sí.

    char *foo = "bar";
    char var[] = "baz";
    some_func(foo); /* VERY DANGEROUS! */
    some_func(var); /* LESS DANGEROUS! */

¿Ahorrando memoria? Leí en alguna parte (no pude encontrar la fuente ahora) hace mucho tiempo, cuando la RAM era escasa, los compiladores intentaron optimizar el uso de la memoria fusionando cadenas similares.

Por ejemplo, "more" y "regex" se convertirían en "moregex". ¿Sigue siendo cierto hoy, en la era de las películas digitales con calidad de blu-ray? Entiendo que los sistemas integrados todavía funcionan en un entorno de recursos restringidos, pero aún así, la cantidad de memoria disponible ha aumentado dramáticamente.

Problemas de compatibilidad? Supongo que un programa heredado que intente acceder a la memoria de solo lectura se bloqueará o continuará con un error no descubierto. Por lo tanto, ningún programa heredado debería intentar acceder al literal de cadena y, por lo tanto, permitir escribir en el literal de cadena no dañaría los programas heredados válidos, no hackeados y portátiles .

¿Hay alguna otra razón? ¿Es incorrecto mi razonamiento? ¿Sería razonable considerar un cambio en los literales de cadena de lectura-escritura en los nuevos estándares C o al menos agregar una opción al compilador? ¿Se consideró esto antes o mis "problemas" son demasiado pequeños e insignificantes para molestar a alguien?

Marius Macijauskas
fuente
12
¿Supongo que has visto cómo se ven los literales de cadena en el código compilado ?
2
Mire el ensamblaje que contiene el enlace que proporcioné. Está justo ahí.
8
Su ejemplo "moregex" no funcionaría debido a la terminación nula.
dan04
44
No desea escribir sobre constantes porque eso cambiará su valor. La próxima vez que quiera usar la misma constante, sería diferente. El compilador / tiempo de ejecución tiene que obtener las constantes desde algún lugar, y donde sea que esté no se le debe permitir modificar.
Erik Eidt
1
'Entonces, ¿los literales de cadena se almacenan en la memoria del programa, no en la RAM, y el desbordamiento del búfer podría dañar el programa?' La imagen del programa también está en la RAM. Para ser precisos, los literales de cadena se almacenan en el mismo segmento de RAM utilizado para almacenar la imagen del programa. Y sí, sobrescribir la cadena podría dañar el programa. En los días de MS-DOS y CP / M no había protección de memoria, podía hacer cosas como esta, y generalmente causaba problemas terribles. Los primeros virus de PC usarían trucos como ese para modificar su programa para que formateara su disco duro cuando intentara ejecutarlo.
Charles E. Grant

Respuestas:

40

Históricamente (tal vez reescribiendo partes de él), fue todo lo contrario. En las primeras computadoras de principios de la década de 1970 (quizás PDP-11 ) que ejecutaban un C embrionario prototípico (quizás BCPL ) no había MMU ni protección de memoria (que existía en la mayoría de los mainframes IBM / 360 más antiguos ). Por lo tanto, cada byte de memoria (incluidos los que manejan cadenas literales o código de máquina) podría ser sobrescrito por un programa erróneo (imagine un programa cambiando algunos %a /una cadena de formato printf (3) ). Por lo tanto, las cadenas literales y las constantes se podían escribir.

Cuando era adolescente en 1975, codifiqué en el museo Palais de la Découverte en París en computadoras antiguas de la década de 1960 sin protección de memoria: IBM / 1620 tenía solo una memoria central, que se podía inicializar a través del teclado, por lo que tenía que escribir varias docenas de dígitos para leer el programa inicial en cintas perforadas; CAB / 500 tenía una memoria de tambor magnético; puede deshabilitar la escritura de algunas pistas a través de interruptores mecánicos cerca del tambor.

Más tarde, las computadoras obtuvieron algún tipo de unidad de administración de memoria (MMU) con cierta protección de memoria. Había un dispositivo que prohibía a la CPU sobrescribir algún tipo de memoria. Entonces, algunos segmentos de memoria, en particular el segmento de código (también conocido como .textsegmento) se convirtió en solo lectura (excepto por el sistema operativo que los cargó desde el disco). Era natural que el compilador y el enlazador pusieran las cadenas literales en ese segmento de código, y las cadenas literales se volvieron de solo lectura. Cuando su programa intentó sobrescribirlos, fue malo, un comportamiento indefinido . Y tener un segmento de código de solo lectura en la memoria virtual ofrece una ventaja significativa: varios procesos que ejecutan el mismo programa comparten la misma RAM ( memoria físicapáginas) para ese segmento de código (vea el MAP_SHAREDindicador para mmap (2) en Linux).

Hoy en día, los microcontroladores baratos tienen algo de memoria de solo lectura (por ejemplo, su Flash o ROM), y mantienen allí su código (y las cadenas literales y otras constantes). Y los microprocesadores reales (como el de su tableta, computadora portátil o computadora de escritorio) tienen una unidad de administración de memoria sofisticada y maquinaria de caché utilizada para memoria virtual y paginación . Por lo tanto, el segmento de código del programa ejecutable (por ejemplo, en ELF ) está mapeado en memoria como un segmento de solo lectura, compartible y ejecutable (por mmap (2) o execve (2) en Linux; por cierto, podría dar instrucciones a ldpara obtener un segmento de código grabable si realmente lo desea). Escribirlo o abusarlo generalmente es una falla de segmentación .

Por lo tanto, el estándar C es barroco: legalmente (solo por razones históricas), las cadenas literales no son const char[]matrices, sino solo char[]matrices que tienen prohibido sobrescribirse.

Por cierto, pocos idiomas actuales permiten que se sobrescriban los literales de cadena (incluso Ocaml, que históricamente -y mal- tenía cadenas literales de escritura ha cambiado ese comportamiento recientemente en 4.02, y ahora tiene cadenas de solo lectura).

Los compiladores actuales de C pueden optimizar y tener "ions"y "expressions"compartir sus últimos 5 bytes (incluido el byte nulo de terminación).

Tratar de compilar el código C en el archivo foo.ccon gcc -O -fverbose-asm -S foo.cy mirada dentro del archivo de ensamblador generado foo.spor GCC

Por último, la semántica de C es lo suficientemente compleja (lea más sobre CompCert y Frama-C, que están tratando de capturarlo) y agregar cadenas literales constantes y grabables lo haría aún más arcano mientras hace que los programas sean más débiles y menos seguros (y con menos comportamiento definido), por lo que es muy poco probable que los futuros estándares C acepten cadenas literales grabables. Quizás, por el contrario, los convertirían en const char[]matrices como deberían ser moralmente.

Observe también que, por muchas razones, los datos mutables son más difíciles de manejar por la computadora (coherencia de caché), de codificar, de entender por el desarrollador, que los datos constantes. Por lo tanto, es preferible que la mayoría de sus datos (y especialmente las cadenas literales) permanezcan inmutables . Lea más sobre el paradigma de programación funcional .

En los viejos días de Fortran77 en IBM / 7094, un error incluso podía cambiar una constante: si usted CALL FOO(1)y si FOOmodificara su argumento pasado por referencia a 2, la implementación podría haber cambiado otras ocurrencias de 1 a 2, y eso fue realmente bicho travieso, bastante difícil de encontrar.

Basile Starynkevitch
fuente
¿Es esto para proteger las cadenas como constantes? ¿Aunque no están definidos como constestándar ( stackoverflow.com/questions/2245664/… )?
Marius Macijauskas
¿Estás seguro de que las primeras computadoras no tenían memoria de solo lectura? ¿No era eso considerablemente más barato que el carnero? Además, ponerlos en la memoria RO no causa que UB intente modificarlos erróneamente, sino que confíe en que el OP no lo haga y que viole esa confianza. Vea, por ejemplo, los programas de Fortran donde todos los 1s literales se comportan repentinamente como 2s y tan divertidos ...
Deduplicator
1
Cuando era adolescente en un museo, codifiqué en 1975 en viejas computadoras IBM / 1620 y CAB500. Tampoco tenía ninguna ROM: IBM / 1620 tenía memoria central, y CAB500 tenía un tambor magnético (algunas pistas podían desactivarse para que se pudieran escribir con un interruptor mecánico)
Basile Starynkevitch
2
También vale la pena señalar: poner literales en el segmento de código significa que se pueden compartir entre múltiples copias del programa porque la inicialización ocurre en tiempo de compilación en lugar de tiempo de ejecución.
Blrfl
@Dupuplicator Bueno, he visto una máquina que ejecuta una variante BÁSICA que le permite cambiar constantes enteras (no estoy seguro de si necesita engañarlo para que lo haga, por ejemplo, pasar argumentos "byref" o si un simple let 2 = 3funcionó). Esto resultó en mucha DIVERSIÓN (en la definición de la palabra Fortaleza Enana), por supuesto. No tengo idea de cómo fue diseñado el intérprete que permitió esto, pero lo fue.
Luaan
2

Los compiladores no podían combinar "more"y "regex", ya que el primero tiene un byte nulo después de que el emientras que el segundo tiene una x, pero muchos compiladores combinarían literales de cadena que han concordado perfectamente, y algunos también se correspondería con literales de cadena que compartían una cola común. El código que cambia un literal de cadena puede cambiar un literal de cadena diferente que se utiliza para un propósito completamente diferente pero que contiene los mismos caracteres.

Surgiría un problema similar en FORTRAN antes de la invención de C. Los argumentos siempre se pasaban por dirección en lugar de por valor. Por lo tanto, una rutina para agregar dos números sería equivalente a:

float sum(float *f1, float *f2) { return *f1 + *f2; }

En el caso de que uno quisiera pasar un valor constante (p. Ej., 4.0) sum, el compilador crearía una variable anónima y la inicializaría 4.0. Si se pasara el mismo valor a múltiples funciones, el compilador pasaría la misma dirección a todas ellas. Como consecuencia, si una función que modificó uno de sus parámetros pasó una constante de punto flotante, el valor de esa constante en otra parte del programa podría cambiar como resultado, lo que llevaría al dicho "Las variables no; las constantes no son 't ".

Super gato
fuente