C文字列リテラルが読み取り専用なのはなぜですか?


29

文字列リテラルが読み取り専用であることの利点は、以下を正当化(-ies / -ied)します:

  1. 足で自分を撃つ別の方法

    char *foo = "bar";
    foo[0] = 'd'; /* SEGFAULT */
    
  2. 1行で単語の読み書き配列をエレガントに初期化できない:

    char *foo[] = { "bar", "baz", "running out of traditional placeholder names" };
    foo[1][2] = 'n'; /* SEGFAULT */ 
    
  3. 言語自体を複雑にします。

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

メモリを節約しますか? RAMが不足していた昔、コンパイラは類似の文字列をマージすることでメモリ使用量を最適化しようとしていました(どこかでソースを見つけることができませんでした)。

たとえば、「more」と「regex」は「moregex」になります。デジタルブルーレイ品質の映画の時代において、これは今日でも真実ですか?組み込みシステムはリソースが制限された環境で動作することを理解していますが、それでも利用可能なメモリの量は劇的に増加しています。

互換性の問題? 読み取り専用メモリにアクセスしようとするレガシプログラムがクラッシュするか、未発見のバグが続くと想定しています。したがって、レガシープログラムは文字列リテラルにアクセスしようとしないでください。そのため、文字列リテラルへの書き込みを許可しても、有効でハッキングされていない、移植性のあるレガシープログラムを害することはありません。

他の理由はありますか?私の推論は間違っていますか?新しいC標準の読み取り/書き込み文字列リテラルの変更を検討するか、少なくともコンパイラにオプションを追加するのは合理的でしょうか?これは以前に検討されたのですか、それとも私の「問題」はあまりにも軽微であり、取るに足らないほど重要なものですか?



2
私が提供したリンクに含まれるアセンブリを見てください。その右。

8
「moregex」の例は、null終了のため機能しません。
dan04

4
定数を上書きしたくないのは、その値が変わるからです。次回同じ定数を使用する場合は、異なるものになります。コンパイラ/ランタイムは、どこかから定数を取得する必要があり、それがどこであっても変更を許可しないでください。
エリックエイド

1
「それでは、文字列リテラルはRAMではなくプログラムメモリに保存され、バッファオーバーフローはプログラム自体の破損を引き起こすでしょうか?」プログラムイメージもRAMにあります。正確には、文字列リテラルは、プログラムイメージの保存に使用されるRAMの同じセグメントに保存されます。はい、文字列を上書きするとプログラムが破損する可能性があります。MS-DOSとCP / Mの時代には、メモリ保護はありませんでした。このようなことができ、通常はひどい問題を引き起こしていました。最初のPCウイルスは、そのようなトリックを使用してプログラムを変更し、実行しようとしたときにハードドライブをフォーマットします。
チャールズE.グラント

回答:


40

歴史的に(おそらくその一部を書き換えることによって)、それは反対でした。プロトタイプの初期C(おそらくBCPL)を実行している1970年代初期の最初のコンピューター(おそらくPDP-11)には、MMUメモリー保護もありません(ほとんどの古いIBM / 360メインフレームに存在していました)。(リテラル文字列またはマシンコードを扱うものを含む)メモリのすべてのバイトが誤ったプログラムによって上書きされる可能性がありますので、(一部を変更するプログラムを想像する中のprintf(3)フォーマット文字列を)。したがって、リテラル文字列と定数は書き込み可能です。%/

1975年にティーンエイジャーとして、私は、メモリ保護せずに、古い1960年代の時代のコンピュータでは、パリのパレ・デ・ラ・DECOUVERTE博物館でコーディング:/ 1620 IBMは、あなたが数十を入力しなければならなかったので、あなたはキーボードを通して初期化することができ-whichのみコアメモリを持っていましたパンチテープの初期プログラムを読み取るための数字; CAB / 500には磁気ドラムメモリがありました。ドラムの近くの機械的なスイッチを介していくつかのトラックの書き込みを無効にすることができます。

後に、コンピューターは何らかのメモリ保護を備えた何らかの形式のメモリ管理ユニット(MMU)を取得しました。CPUが何らかのメモリを上書きすることを禁止するデバイスがありました。そのため、いくつかのメモリセグメント、特にコードセグメント.textセグメント)は読み取り専用になりました(ディスクからそれらをロードしたオペレーティングシステムを除く)。コンパイラとリンカがそのコードセグメントにリテラル文字列を置くのは自然であり、リテラル文字列は読み取り専用になりました。あなたのプログラムがそれらを上書きしようとしたとき、それは悪い、未定義の振る舞いでした。また、仮想メモリに読み取り専用コードセグメントがあると、大きな利点が得られます。同じプログラムを実行する複数のプロセスが同じRAM物理メモリを共有します。ページ)そのコードセグメント(Linuxのmmap(2)のMAP_SHAREDフラグを参照)。

現在、安価なマイクロコントローラには、読み取り専用メモリ(フラッシュやROMなど)があり、コード(およびリテラル文字列やその他の定数)がそこに保持されています。また、実際のマイクロプロセッサ(タブレット、ラップトップ、デスクトップなど)には、仮想メモリページングに使用される洗練されたメモリ管理ユニットとキャッシュ機構があります。コードセグメントだから、実行可能なプログラム(例えば中ELFが)によって、読み取り専用としてマップされたメモリ、共有可能、および実行可能なセグメント(あるのmmap(2)またははexecve(2) Linux上;ところで、あなたはに指示を与えることができるLDあなたが本当にしたい場合は、書き込み可能なコードセグメントを取得します)。それを書くか乱用するのは一般にセグメンテーション違反です。

したがって、C標準はバロックです。法的に(歴史的な理由のみ)、リテラル文字列はconst char[]配列ではなく、char[]上書きが禁止されている配列のみです。

ところで、文字列リテラルの上書きを許可する現在の言語はほとんどありません(歴史的に-ひどく-書き込み可能なリテラル文字列を持っていたOcamlでさえ、最近4.02でその動作を変更し、現在は読み取り専用の文字列を持っています)。

現在のCコンパイラは、最後の5バイト(終端のnullバイトを含む)を最適化し"ions""expressions"共有することができます。

ファイル内のCコードをコンパイルしようfoo.cgcc -O -fverbose-asm -S foo.c生成されたアセンブラファイル内のルックfoo.sGCC

最後に、意味論 Cのは複雑で十分です(詳細読みCompCertFRAMA-C 、それを捕獲しようとしている)及び(およびより少ないと、プログラムが弱く、さらには安全性の低いながら書き込み可能な定数リテラル文字列は、それがさらに難解になるだろう追加します定義された動作)、したがって、将来のC標準が書き込み可能なリテラル文字列を受け入れることはほとんどありません。おそらく反対に、彼らはconst char[]道徳的にすべきであるようにそれらを配列にするでしょう。

また、多くの理由で、可変データは、定数データよりもコンピューターによる処理(キャッシュの一貫性)、開発者によるコード化、理解が難しいことに注意してください。したがって、ほとんどのデータ(特にリテラル文字列)を不変に保つことが望ましいです。関数型プログラミングの パラダイムについて詳しく読んでください。

IBM / 7094の古いFortran77の時代には、バグが定数を変更することさえありました。もしあなたがたまたま引数を2に渡して参照を変更したCALL FOO(1)場合FOO、実装は1の他の出現を2に変更したかもしれません。いたずらなバグ、見つけるのはかなり難しい。


これは文字列を定数として保護するためですか?const標準として定義されていなくても(stackoverflow.com/questions/2245664/…)?
マリウスマチャウスカス

あなたは確かに最初のコンピュータがいたあるなし読み出し専用メモリ?それはラムよりもかなり安くなかったのですか?また、それらをROメモリに配置しても、UBが誤ってそれらを変更しようとすることはありませんが、OPがそれを行わず、信頼に違反していることに依存します。たとえば、すべてのリテラルが1sのように突然動作するFortranプログラムなどを参照してください2...
Deduplicator

1
博物館のティーンエイジャーとして、1975年に古いIBM / 1620およびCAB500コンピューターでコーディングしました。どちらのROMもありませんでした:IBM / 1620にはコアメモリがあり、CAB500には磁気ドラムがありました(一部のトラックは機械的なスイッチで書き込み可能にできなかった)
Basile Starynkevitch

2
また、指摘する価値があります。コードセグメントにリテラルを置くことは、実行時ではなくコンパイル時に初期化が行われるため、プログラムの複数のコピー間でリテラルを共有できることを意味します。
Blrfl

@Deduplicatorさて、整数定数を変更できるBASICバリアントを実行しているマシンを見ました(たとえば、「byref」引数を渡すか、単純な関数が機能するかを確認する必要があるかどうかはわかりませんlet 2 = 3)。これはもちろん、ドワーフ要塞の定義によると、多くの楽しみをもたらしました。インタープリターがどのように設計されているのかはわかりませんが、これは可能です。
ルアーン

2

コンパイラは、"more"とを組み合わせることはできませんでし"regex"た。前者の後にnullバイトがeあり、後者の後にがありますxが、多くのコンパイラは完全に一致する文字列リテラルを結合し、一部は共通のテールを共有する文字列リテラルにも一致します。したがって、文字列リテラルを変更するコードは、まったく異なる目的に使用されるが、偶然同じ文字を含む別の文字列リテラルを変更する場合があります。

Cの発明以前のFORTRANでも同様の問題が発生していました。引数は常に値ではなくアドレスで渡されていました。したがって、2つの数値を追加するルーチンは、次と同等です。

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

定数値(4.0など)を渡したいsum場合、コンパイラは匿名変数を作成し、それをに初期化し4.0ます。同じ値が複数の関数に渡された場合、コンパイラはすべての関数に同じアドレスを渡します。その結果、パラメータの1つを変更した関数に浮動小数点定数が渡されると、結果としてプログラムの他の場所でその定数の値が変更される可能性があり、「変数はありません。定数は「t」。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.