it-swarm.com.de

Warum sind C-String-Literale schreibgeschützt?

Welche Vorteile von schreibgeschützten String-Literalen rechtfertigen (-ies/-ied):

  1. Noch eine andere Möglichkeit, 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 vor langer Zeit irgendwo gelesen (konnte die Quelle jetzt nicht finden), als RAM knapp war) Compiler versuchten, die Speichernutzung durch zu optimieren Zusammenführen ähnlicher Zeichenfolgen.

Zum Beispiel würden "more" und "regex" zu "moregex". Gilt dies auch heute noch im Zeitalter digitaler Filme in Blu-ray-Qualität? Ich verstehe, dass eingebettete Systeme immer noch in Umgebungen mit eingeschränkten Ressourcen arbeiten, aber die verfügbare Speichermenge 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 ein String-Literal zuzugreifen, und daher würde das Zulassen des Schreibens in ein String-Literal gültig, nicht hackisch, portabel Legacy-Programme nicht schädigen.

Gibt es noch andere Gründe? Ist meine Argumentation falsch? Wäre es sinnvoll, eine Änderung der Lese-/Schreibzeichenfolgenliterale 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?

28

Historisch (vielleicht durch Umschreiben von Teilen davon) war es das Gegenteil. Auf den allerersten Computern der frühen 1970er Jahre (vielleicht PDP-11 ) lief ein prototypisches embryonales C (vielleicht BCPL ) no MMU und no Speicherschutz (das auf den meisten älteren IBM/36 Mainframes vorhanden war). So könnte jedes Speicherbyte (einschließlich derjenigen, die Literalzeichenfolgen oder Maschinencode verarbeiten) von einem fehlerhaften Programm überschrieben werden (stellen Sie sich ein Programm vor, das einige % In / In einem printf (3) ändert ) Formatzeichenfolge). Daher waren wörtliche Zeichenfolgen und Konstanten beschreibbar.

Als Teenager habe ich 1975 im Palais de la Découverte in Paris auf Computern aus den 1960er Jahren ohne Speicherschutz codiert: IBM/162 hatte nur einen Kernspeicher, den Sie über die Tastatur initialisieren konnten. Sie mussten also mehrere Dutzend Ziffern eingeben, um das ursprüngliche Programm auf Lochbändern zu lesen. CAB/5 hatte einen magnetischen Trommelspeicher; Sie können das Schreiben einiger Spuren über mechanische Schalter in der Nähe der Trommel deaktivieren.

Später erhielten Computer eine Art Speicherverwaltungseinheit (Memory Management Unit, MMU) mit einem gewissen Speicherschutz. Es gab ein Gerät, das der CPU verbot, irgendeine Art von Speicher zu überschreiben. Einige Speichersegmente, insbesondere das Segment Codesegment (a.k.a. .text), Wurden schreibgeschützt (außer vom Betriebssystem, 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 es schlecht, ein ndefiniertes Verhalten . Ein schreibgeschütztes Codesegment im virtuellen Speicher bietet einen erheblichen Vorteil: Mehrere Prozesse , die dasselbe Programm ausführen, teilen sich das gleiche RAM ( physischer Speicher Seiten) für dieses Codesegment (siehe MAP_SHARED Flag für mmap (2) unter Linux).

Heutzutage haben billige Mikrocontroller einige Nur-Lese-Speicher (z. B. ihren Flash oder ROM) und behalten ihren Code (und die Literalzeichenfolgen und andere Konstanten) dort. Und echte Mikroprozessoren (wie der in Ihrem Tablet, Laptop oder Desktop) verfügen über eine ausgeklügelte Speicherverwaltungseinheit und Cache Maschinen, die für virtueller Speicher & Paging verwendet werden =. Das Codesegment des Programms ausführbare Datei (z. B. in ELF ) wird also als schreibgeschützt, gemeinsam nutzbar und ausführbar zugeordnet segment (by mmap (2) oder execve (2) unter Linux; Übrigens könnten Sie ld Anweisungen geben, um ein beschreibbares Codesegment zu erhalten, wenn du wolltest wirklich). Schreiben oder Missbrauch ist im Allgemeinen ein Segmentierungsfehler .

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

Übrigens erlauben nur wenige aktuelle Sprachen das Überschreiben von String-Literalen (selbst Ocaml, das historisch - und schlecht - beschreibbare Literal-Strings hatte, hat dieses Verhalten kürzlich in 4.02 geändert und verfügt nun über schreibgeschützte Strings).

Aktuelle C-Compiler können optimieren und "ions" Und "expressions" Ihre letzten 5 Bytes (einschließlich des abschließenden Null-Bytes) gemeinsam nutzen lassen.

Versuchen Sie, Ihren C-Code in der Datei foo.c Mit gcc -O -fverbose-asm -S foo.c Zu kompilieren und schauen Sie in die generierte Assembler-Datei foo.s Von GCC

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

Beachten Sie auch, dass veränderbare Daten aus vielen Gründen vom Computer schwerer zu verarbeiten sind (Cache-Kohärenz), codiert werden und vom Entwickler verstanden werden können als konstante Daten. Daher ist es vorzuziehen, dass die meisten Ihrer Daten (und insbesondere wörtliche Zeichenfolgen) nveränderlich bleiben. Lesen Sie mehr über funktionale ProgrammierungParadigma .

In den alten Fortran77-Tagen unter IBM/7094 konnte ein Fehler sogar eine Konstante ändern: Wenn Sie CALL FOO(1) und FOO zufällig das Argument ändern, das unter Bezugnahme auf 2 übergeben wurde, hat die Implementierung möglicherweise hat andere Vorkommen von 1 in 2 geändert, und das war ein wirklich ungezogener Fehler, der ziemlich schwer zu finden war.

40

Compiler konnten "more" Und "regex" Nicht kombinieren, da der erstere nach dem e ein Null-Byte hat, während der letztere ein x hat, aber viele Compiler würden dies tun Kombinieren Sie String-Literale, die perfekt zusammenpassen, und einige stimmen auch mit String-Literalen überein, die einen gemeinsamen Schwanz haben. Code, der ein Zeichenfolgenliteral ändert, kann daher ein anderes Zeichenfolgenliteral ändern, das für einen völlig anderen Zweck verwendet wird, aber zufällig dieselben Zeichen enthält.

Ein ähnliches Problem würde bei FORTRAN vor der Erfindung von C auftreten. Argumente wurden immer eher nach Adresse als 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 ein konstanter Wert (z. B. 4.0) an sum übergeben werden soll, erstellt der Compiler eine anonyme Variable und initialisiert sie mit 4.0. Wenn der gleiche 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 der Wert dieser Konstante an anderer Stelle im Programm ändern, was zu dem Sprichwort "Variablen werden nicht; Konstanten sind nicht" führt 't ".

2
supercat

Betrachten Sie ein Programm wie grep. Viele der darin enthaltenen Literalkonstanten sind Fehlermeldungen. Wenn ein Mehrbenutzer-Computer sie als unveränderlich und Teil des Codesegments betrachtet, kann er das Codesegment dasselbe in RAM für Dutzende von Benutzer, die alle gleichzeitig grep ausführen, sogar asynchron.

0
Ross Presser