Perché i letterali di stringhe C sono di sola lettura?


29

Quali sono i vantaggi dei letterali stringa che sono di sola lettura giustificano (-ies / -ied) il:

  1. Ancora un altro modo di spararti al piede

    char *foo = "bar";
    foo[0] = 'd'; /* SEGFAULT */
  2. Incapacità di inizializzare elegantemente una matrice di parole in lettura e scrittura in una riga:

    char *foo[] = { "bar", "baz", "running out of traditional placeholder names" };
    foo[1][2] = 'n'; /* SEGFAULT */ 
  3. Complicare la lingua stessa.

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

Salvare la memoria? Ho letto da qualche parte (non riesco a trovare l'origine ora) che molto tempo fa, quando la RAM era scarsa, i compilatori cercavano di ottimizzare l'utilizzo della memoria unendo stringhe simili.

Ad esempio, "more" e "regex" diventerebbero "moregex". È ancora vero oggi, nell'era dei film digitali di qualità blu-ray? Comprendo che i sistemi incorporati continuano a funzionare in un ambiente con risorse limitate, ma la quantità di memoria disponibile è aumentata notevolmente.

Problemi di compatibilità? Suppongo che un programma legacy che provi ad accedere alla memoria di sola lettura si bloccherebbe o continuerebbe con un bug non scoperto. Pertanto, nessun programma legacy dovrebbe tentare di accedere a stringhe letterali e pertanto consentire di scrivere su stringhe letterali non danneggerebbe programmi legacy portatili validi, non hacking .

Ci sono altri motivi? Il mio ragionamento è errato? Sarebbe ragionevole prendere in considerazione una modifica ai valori letterali stringa di lettura-scrittura nei nuovi standard C o almeno aggiungere un'opzione al compilatore? Questo è stato considerato prima o i miei "problemi" sono stati troppo lievi e insignificanti per disturbare qualcuno?


12
Presumo che tu abbia osservato come appaiono i letterali stringa nel codice compilato ?

2
Guarda l'assemblaggio che contiene il link che ho fornito. È proprio lì.

8
Il tuo esempio "moregex" non funzionerebbe a causa della terminazione nulla.
dan04,

4
Non vuoi scrivere sopra le costanti perché questo cambierà il loro valore. La prossima volta che vuoi usare la stessa costante sarebbe diverso. Il compilatore / runtime deve originare le costanti da qualche parte e dovunque non dovresti essere autorizzato a modificare.
Erik Eidt,

1
"Quindi i letterali stringa sono memorizzati nella memoria del programma, non nella RAM, e il buffer overflow comporterebbe la corruzione del programma stesso?" L'immagine del programma è anche nella RAM. Per essere precisi, i letterali stringa sono memorizzati nello stesso segmento di RAM utilizzato per memorizzare l'immagine del programma. E sì, sovrascrivere la stringa potrebbe corrompere il programma. Ai tempi di MS-DOS e CP / M non c'era protezione della memoria, si potevano fare cose del genere e di solito causava problemi terribili. I primi virus per PC userebbero trucchi del genere per modificare il programma in modo che formattasse il disco rigido quando si tentava di eseguirlo.
Charles E. Grant,

Risposte:


40

Storicamente (forse riscrivendone parti), era il contrario. Nei primissimi computer dei primi anni '70 (forse PDP-11 ) che eseguivano un prototipo C embrionale (forse BCPL ) non esistevano MMU e nessuna protezione della memoria (che esistevano sulla maggior parte dei mainframe IBM / 360 più vecchi ). Quindi ogni byte di memoria (compresi quelli che gestiscono stringhe letterali o codice macchina) potrebbe essere sovrascritto da un programma errato (immagina un programma cambiandone alcuni %in /in un printf (3) stringa di formato). Quindi, stringhe e costanti letterali erano scrivibili.

Da adolescente, nel 1975, ho codificato nel museo del Palais de la Découverte a Parigi su vecchi computer degli anni '60 senza protezione della memoria: IBM / 1620 aveva solo una memoria centrale, che era possibile inizializzare tramite la tastiera, quindi è stato necessario digitare diverse dozzine di cifre per leggere il programma iniziale su nastri punzonati; CAB / 500 aveva una memoria a tamburo magnetico; potresti disabilitare la scrittura di alcune tracce tramite interruttori meccanici vicino al tamburo.

Successivamente, i computer ottennero una qualche forma di unità di gestione della memoria (MMU) con una certa protezione della memoria. C'era un dispositivo che proibiva alla CPU di sovrascrivere un qualche tipo di memoria. Quindi alcuni segmenti di memoria, in particolare il segmento di codice (noto anche come .textsegmento), sono diventati di sola lettura (ad eccezione del sistema operativo che li ha caricati dal disco). Era naturale per il compilatore e il linker inserire le stringhe letterali in quel segmento di codice e le stringhe letterali diventavano di sola lettura. Quando il tuo programma ha cercato di sovrascriverli, è stato un comportamento indefinito . E avere un segmento di codice di sola lettura nella memoria virtuale offre un vantaggio significativo: diversi processi che eseguono lo stesso programma condividono la stessa RAM ( memoria fisicapagine) per quel segmento di codice (vedi MAP_SHAREDflag per mmap (2) su Linux).

Oggi, i microcontrollori economici hanno un po ' di memoria di sola lettura (ad esempio il loro Flash o ROM) e mantengono lì il loro codice (e le stringhe letterali e altre costanti). E i microprocessori reali (come quello sul tablet, laptop o desktop) hanno una sofisticata unità di gestione della memoria e un meccanismo di cache utilizzato per il paging e la memoria virtuale . Quindi il segmento di codice del programma eseguibile (ad es. In ELF ) è mappato in memoria come segmento di sola lettura, condivisibile ed eseguibile (da mmap (2) o esegui (2) per ottenere un segmento di codice scrivibile se lo desideri davvero) . Scrivere o abusare è generalmente un errore di segmentazione su Linux; BTW potresti dare direttive a ld.

Quindi lo standard C è barocco: legalmente (solo per ragioni storiche), le stringhe letterali non sono const char[]array, ma solo char[]array a cui è vietato sovrascrivere.

A proposito, poche lingue attuali consentono di sovrascrivere i letterali di stringa (anche Ocaml che storicamente - e malamente - aveva stringhe letterali scrivibili ha cambiato quel comportamento recentemente in 4.02, e ora ha stringhe di sola lettura).

Gli attuali compilatori C sono in grado di ottimizzare, avere "ions"e"expressions" condividere i loro ultimi 5 byte (incluso il byte null terminante).

Prova a compilare il codice C in un file foo.ccon gcc -O -fverbose-asm -S foo.ce guardare all'interno del file assembler generato foo.sda GCC

Alla fine, la semantica di C è abbastanza complessa (leggi di più su CompCert e Frama-C che stanno cercando di catturarlo) e l'aggiunta di stringhe letterali costanti scrivibili lo renderebbe ancora più arcano rendendo i programmi più deboli e persino meno sicuri (e con meno comportamento definito), quindi è molto improbabile che i futuri standard C accettino stringhe letterali scrivibili. Forse al contrario li avrebbero fatticonst char[] array come dovrebbero essere moralmente.

Si noti inoltre che, per molte ragioni, i dati mutabili sono più difficili da gestire da parte del computer (coerenza della cache), codificare, comprendere dallo sviluppatore, che dati costanti. Quindi è preferibile che la maggior parte dei tuoi dati (e in particolare le stringhe letterali) rimangano immutabili . Ulteriori informazioni sul paradigma di programmazione funzionale .

Ai vecchi tempi di Fortran77 su IBM / 7094, un bug poteva persino cambiare una costante: se tu CALL FOO(1)e se avessi FOOmodificato il suo argomento passato con riferimento a 2, l'implementazione avrebbe potuto cambiare altre occorrenze da 1 a 2, e quello era davvero bug cattivo, abbastanza difficile da trovare.


Questo serve a proteggere le stringhe come costanti? Anche se non sono definiti come conststandard ( stackoverflow.com/questions/2245664/… )?
Marius Macijauskas,

Sei sicuro che i primi computer non abbiano memoria di sola lettura? Non era considerevolmente più economico del montone? Inoltre, metterli nella memoria RO non causa UB nel tentativo di modificarli erroneamente, ma fare affidamento sul fatto che OP non lo fa e che viola questa fiducia. Vedi ad esempio i programmi Fortran in cui tutti i letterali 1si comportano all'improvviso come se fossero 2così divertenti ...
Deduplicatore

1
Da adolescente in un museo, ho codificato nel 1975 su vecchi computer IBM / 1620 e CAB500. Nessuno dei due aveva ROM: IBM / 1620 aveva una memoria core e CAB500 aveva un tamburo magnetico (alcune tracce potevano essere disabilitate per essere scrivibili da un interruttore meccanico)
Basile Starynkevitch

2
Vale anche la pena sottolineare: mettere i letterali nel segmento di codice significa che possono essere condivisi tra più copie del programma perché l'inizializzazione avviene in fase di compilazione anziché in fase di esecuzione.
Blrfl,

@Deduplicator Bene, ho visto una macchina che esegue una variante BASIC che ti ha permesso di cambiare costanti intere (non sono sicuro se avessi bisogno di indurlo a farlo, ad esempio passando argomenti "byref" o se un semplice ha let 2 = 3funzionato). Ciò ha comportato un sacco di DIVERTIMENTO (nella definizione della parola fortezza nana), ovviamente. Non ho idea di come l'interprete sia stato progettato per permetterlo, ma lo era.
Luaan,

2

I compilatori non potevano combinarsi "more"e "regex", poiché il primo ha un byte nullo dopo l'altro ementre il secondo ha un x, ma molti compilatori combinerebbero letterali di stringhe che corrispondevano perfettamente, e alcuni corrispondevano anche a letterali di stringhe che condividevano una coda comune. Il codice che modifica un valore letterale stringa può quindi modificare un valore letterale stringa diverso che viene utilizzato per scopi completamente diversi ma che contiene gli stessi caratteri.

Un problema simile sorgerebbe in FORTRAN prima dell'invenzione di C. Gli argomenti venivano sempre passati per indirizzo piuttosto che per valore. Una routine per aggiungere due numeri sarebbe quindi equivalente a:

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

Nel caso in cui si volesse passare un valore costante (es. 4.0) a sum, il compilatore creerebbe una variabile anonima e la inizializzerebbe 4.0. Se lo stesso valore fosse passato a più funzioni, il compilatore passerebbe lo stesso indirizzo a tutte. Di conseguenza, se a una funzione che modificava uno dei suoi parametri veniva passata una costante a virgola mobile, il valore di quella costante altrove nel programma poteva essere modificato di conseguenza, portando così al detto "Variabili no; costanti non sono 't".

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.