Warum sind C-String-Literale schreibgeschützt?

29

Welche Vorteile haben schreibgeschützte String-Literale, die Folgendes rechtfertigen (-ies / -ied):

  1. Noch eine andere Art, sich in den Fuß zu schießen

    char *foo = "bar";
    foo[0] = 'd'; /* SEGFAULT */
  2. Unfähigkeit, ein Lese-Schreib-Array von Wörtern in einer Zeile elegant zu initialisieren:

    char *foo[] = { "bar", "baz", "running out of traditional placeholder names" };
    foo[1][2] = 'n'; /* SEGFAULT */ 
  3. Die Sprache selbst komplizieren.

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

Speicher sparen? Ich habe irgendwo gelesen (konnte den Quellcode jetzt nicht finden), dass Compiler vor langer Zeit, als RAM knapp war, versuchten, die Speichernutzung durch Zusammenführen ähnlicher Zeichenfolgen zu optimieren.

Beispielsweise würden "more" und "regex" zu "moregex". Trifft dies auch heute noch zu, im Zeitalter digitaler Filme in Blu-ray-Qualität? Ich verstehe, dass eingebettete Systeme immer noch in Umgebungen mit eingeschränkten Ressourcen arbeiten, aber der verfügbare Speicher hat sich dramatisch erhöht.

Kompatibilitätsprobleme? Ich gehe davon aus, dass ein Legacy-Programm, das versucht, auf den Nur-Lese-Speicher zuzugreifen, entweder abstürzt oder mit einem unentdeckten Fehler fortfährt. Daher sollte kein Legacy-Programm versuchen, auf String-Literal zuzugreifen, und daher würde das Schreiben in String-Literal keinen Schaden für gültige, nicht-hackische, portable Legacy-Programme anrichten .

Gibt es noch andere Gründe? Ist meine Argumentation falsch? Wäre es sinnvoll, eine Änderung der Lese- / Schreib-Zeichenfolgenliterale in neuen C-Standards in Betracht zu ziehen oder dem Compiler zumindest eine Option hinzuzufügen? Wurde dies vorher in Betracht gezogen oder sind meine "Probleme" zu gering und unbedeutend, um jemanden zu stören?

Marius Macijauskas
quelle
12
Ich nehme an, Sie haben sich angesehen, wie String-Literale in kompiliertem Code aussehen .
2
Schauen Sie sich die Assembly an, die der von mir bereitgestellte Link enthält. Es ist genau da.
8
Ihr "moregex" -Beispiel würde aufgrund einer Nullterminierung nicht funktionieren.
Dan04
4
Sie möchten keine Konstanten überschreiben, da sich dadurch ihr Wert ändert. Wenn Sie das nächste Mal dieselbe Konstante verwenden möchten, ist dies anders. Der Compiler / die Laufzeit muss die Konstanten von irgendwoher beziehen, und wo immer das ist, sollten Sie nicht die Erlaubnis haben, Änderungen vorzunehmen.
Erik Eidt
1
"Also werden String-Literale im Programmspeicher und nicht im RAM gespeichert, und ein Pufferüberlauf würde zur Beschädigung des Programms selbst führen?" Das Programm-Image befindet sich ebenfalls im RAM. Um genau zu sein, werden die Zeichenkettenliterale in demselben RAM-Segment gespeichert, das zum Speichern des Programmabbilds verwendet wird. Und ja, das Überschreiben des Strings könnte das Programm beschädigen. In den Tagen von MS-DOS und CP / M gab es keinen Speicherschutz, Sie konnten solche Dinge tun, und es verursachte normalerweise schreckliche Probleme. Die ersten PC-Viren verwendeten solche Tricks, um Ihr Programm so zu ändern, dass es Ihre Festplatte formatierte, als Sie versuchten, es auszuführen.
Charles E. Grant

Antworten:

40

Historisch (vielleicht durch Umschreiben von Teilen) war es das Gegenteil. Auf den allerersten Computern der frühen 1970er Jahre (möglicherweise PDP-11 ), auf denen ein prototypisches embryonales C (möglicherweise BCPL ) ausgeführt wurde, gab es keine MMU und keinen Speicherschutz (der auf den meisten älteren IBM / 360- Mainframes vorhanden war). Also jeder Byte Speicher (einschließlich der Handhabung Literalzeichenfolgen oder Maschinencode) könnte durch ein fehlerhaftes Programm überschrieben werden (man stelle mir ein Programm einige Wechsel %zu /in einem printf (3) Format - String). Daher waren wörtliche Zeichenfolgen und Konstanten beschreibbar.

Als Teenager habe ich 1975 im Museum Palais de la Découverte in Paris auf Computern ohne Speicherschutz aus den alten 1960er Jahren codiert: IBM / 1620 verfügte nur über einen Kernspeicher, den Sie über die Tastatur initialisieren konnten, sodass Sie mehrere Dutzend eingeben mussten von Ziffern zum Lesen des Anfangsprogramms auf Lochbändern; CAB / 500 hatte einen magnetischen Trommelspeicher; Sie könnten das Schreiben einiger Spuren durch mechanische Schalter in der Nähe der Trommel deaktivieren.

Später erhielten Computer eine Speicherverwaltungseinheit (Memory Management Unit, MMU) mit einem gewissen Speicherschutz. Es gab ein Gerät, das der CPU verbot, eine Art Speicher zu überschreiben. Einige Speichersegmente, insbesondere das Codesegment (auch bekannt als .textSegment), waren schreibgeschützt (mit Ausnahme des Betriebssystems, das sie von der Festplatte geladen hat). Es war für den Compiler und den Linker selbstverständlich, die Literalzeichenfolgen in dieses Codesegment einzufügen, und Literalzeichenfolgen wurden schreibgeschützt. Als Ihr Programm versuchte, sie zu überschreiben, war dies ein undefiniertes Verhalten . Ein schreibgeschütztes Codesegment im virtuellen Speicher bietet einen erheblichen Vorteil: Mehrere Prozesse, auf denen dasselbe Programm ausgeführt wird, teilen sich denselben RAM ( physischen Speicher)Seiten) für dieses Codesegment (siehe MAP_SHAREDFlag für mmap (2) unter Linux).

Heutzutage haben billige Mikrocontroller einen Nur-Lese-Speicher (z. B. Flash oder ROM) und behalten dort ihren Code (und die wörtlichen Zeichenfolgen und anderen Konstanten). Reale Mikroprozessoren (wie der in Ihrem Tablet, Laptop oder Desktop) verfügen über eine ausgeklügelte Speicherverwaltungseinheit und Cache- Maschinen, die für virtuellen Speicher und Paging verwendet werden . Das Codesegment des ausführbaren Programms (z. B. in ELF ) ist also ein Speicher, der als schreibgeschütztes, gemeinsam nutzbares und ausführbares Segment abgebildet wird (gemäß mmap (2) oder execve (2) unter Linux; übrigens können Sie ld Anweisungen gebenum ein beschreibbares Codesegment zu erhalten, wenn Sie es wirklich wollten). Das Schreiben oder Missbrauchen ist im Allgemeinen ein Segmentierungsfehler .

Der C-Standard ist also barock: Literal-Strings sind legal (nur aus historischen Gründen) keine const char[]Arrays, sondern nur char[]Arrays, deren Überschreiben verboten ist.

Übrigens erlauben nur wenige aktuelle Sprachen das Überschreiben von Zeichenfolgenliteralen (selbst Ocaml, das historisch - und schlecht - beschreibbare Zeichenfolgen hatte, hat dieses Verhalten kürzlich in 4.02 geändert und enthält jetzt schreibgeschützte Zeichenfolgen).

Aktuelle C - Compiler sind in der Lage zu optimieren und haben "ions"und "expressions"ihre letzten 5 Bytes (einschließlich des abschließenden Null - Byte) teilen.

Versuchen Sie, Ihren C-Code in der Datei foo.cmit zu kompilieren gcc -O -fverbose-asm -S foo.cund in der foo.svon GCC generierten Assembler-Datei nachzuschauen

Schließlich ist die Semantik von C komplex genug (lesen Sie mehr über CompCert & Frama-C, die versuchen, sie zu erfassen), und das Hinzufügen von beschreibbaren Konstanten-Literal-Strings würde sie noch arkaner machen, während Programme schwächer und noch weniger sicher (und mit weniger Sicherheit) werden Daher ist es sehr unwahrscheinlich, dass zukünftige C-Standards beschreibbare wörtliche Zeichenfolgen akzeptieren. Vielleicht würden sie sie im Gegenteil zu const char[]Arrays machen, wie sie moralisch sein sollten.

Beachten Sie auch, dass es aus vielen Gründen für den Computer schwieriger ist, veränderbare Daten zu handhaben (Cache-Kohärenz), für die der Entwickler Code schreiben kann, als für konstante Daten. Daher ist es vorzuziehen, dass die meisten Ihrer Daten (und insbesondere die wörtlichen Zeichenfolgen) unveränderlich bleiben . Lesen Sie mehr über funktionale Programmierung Paradigma .

In den alten Fortran77 Tage auf IBM / 7094 könnte ein Fehler sogar eine Konstante ändern: Wenn Sie CALL FOO(1)und wenn FOOpassiert sein Argument durch Bezugnahme auf 2 geführt zu ändern, haben die Durchführung andere Vorkommen von 1 in 2 geändert könnten, und das war ein wirklich ungezogener Bug, ziemlich schwer zu finden.

Basile Starynkevitch
quelle
Dient dies zum Schutz von Zeichenfolgen als Konstanten? Auch wenn sie nicht wie constim Standard definiert sind ( stackoverflow.com/questions/2245664/… )?
Marius Macijauskas
Sind Sie sicher, dass die ersten Computer keinen Nur-Lese-Speicher hatten? War das nicht wesentlich billiger als RAM? Außerdem führt das Speichern in den RO-Speicher nicht dazu, dass UB versucht, sie fälschlicherweise zu ändern, sondern sich darauf verlässt, dass das OP dies nicht tut und dass er dieses Vertrauen verletzt. Siehe zum Beispiel Fortran-Programme, in denen sich alle wörtlichen 1s plötzlich wie 2s verhalten und so viel Spaß machen ...
Deduplizierer
1
Als Teenager in einem Museum habe ich 1975 auf alten IBM / 1620- und CAB500-Computern codiert. Weder hatte keine ROM: IBM / 1620 hatte Kernspeicher, und CAB500 hatte eine Magnettrommel (einige Spuren konnten deaktiviert werden, um durch einen mechanischen Schalter beschreibbar zu sein)
Basile Starynkevitch
2
Erwähnenswert ist auch, dass durch das Einfügen von Literalen in das Codesegment diese für mehrere Kopien des Programms freigegeben werden können, da die Initialisierung nicht zur Laufzeit, sondern zur Kompilierungszeit erfolgt.
Freitag,
@Deduplicator Nun, ich habe eine Maschine gesehen, auf der eine BASIC-Variante ausgeführt wird, mit der Sie ganzzahlige Konstanten ändern können (ich bin mir nicht sicher, ob Sie sie dazu überlisten müssen, z let 2 = 3. Dies führte natürlich zu viel SPASS (in der Zwergenfestungsdefinition des Wortes). Ich habe keine Ahnung, wie der Dolmetscher dafür ausgelegt war, aber es war so.
Luaan,
2

Compiler konnte nicht kombinieren "more"und "regex", weil das erstere ein Null - Byte hat , nachdem die ewährend letztere eine hat x, aber viele Compiler würde Stringliterale , die perfekt aufeinander abgestimmt kombinieren, und einige würden auch Stringliterale übereinstimmen , die einen gemeinsamen Schwanz geteilt. Code, der ein Zeichenfolgenliteral ändert, kann daher ein anderes Zeichenfolgenliteral ändern, das für einen ganz anderen Zweck verwendet wird, aber zufällig dieselben Zeichen enthält.

Ein ähnliches Problem trat in FORTRAN vor der Erfindung von C auf. Argumente wurden immer nach Adresse und nicht nach Wert übergeben. Eine Routine zum Hinzufügen von zwei Zahlen wäre also äquivalent zu:

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

Für den Fall, dass man einen konstanten Wert (zB 4.0) übergeben summöchte, erstellt der Compiler eine anonyme Variable und initialisiert diese auf 4.0. Wenn derselbe Wert an mehrere Funktionen übergeben würde, würde der Compiler allen dieselbe Adresse übergeben. Wenn einer Funktion, die einen ihrer Parameter geändert hat, eine Gleitkommakonstante übergeben wird, kann sich folglich der Wert dieser Konstante an einer anderen Stelle im Programm ändern. Dies führt zu der Meldung "Variablen werden nicht; Konstanten sind nicht vorhanden" 't ".

Superkatze
quelle