web-dev-qa-db-ja.com

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

文字列リテラルが読み取り専用であることの利点は何ですか(-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標準で読み書き可能な文字列リテラルの変更を検討するか、少なくともコンパイラにオプションを追加することは妥当でしょうか?これは以前に考慮されたのですか、それとも私の「問題」はあまりにもマイナーであまり重要ではないのですか?

28

歴史的に(おそらくその一部を書き換えることによって)、それは正反対でした。 1970年代初頭の非常に最初のコンピューター(おそらく PDP-11 )で、典型的な初期Cを実行しています(おそらく [〜#〜] bcpl [〜# 〜][〜#〜] mmu [〜#〜] がなく、 メモリ保護がなかった (これは、ほとんどの古い IBM/360 メインフレームに存在していました)。したがって、メモリのすべてのバイト(リテラル文字列またはマシンコードを処理するものを含む)は、エラーのあるプログラムによって上書きされる可能性があります( printf(3) 形式の%/に変更するプログラムを想像してください)ストリング)。したがって、リテラル文字列と定数は書き込み可能でした。

私は1975年のティーンエイジャーとして、メモリ保護のない1960年代の古いコンピューターでパリのパレドゥラデクヴェール美術館にコーディングしました: IBM/1620 にはコアメモリしかありませんでした。キーボードを介して初期化するため、パンチされたテープで初期プログラムを読み取るには数十桁を入力する必要がありました。 CAB/500 には磁気ドラムメモリがありました。ドラムの近くのメカニカルスイッチを介して一部のトラックの書き込みを無効にすることができます。

その後、コンピュータは、何らかのメモリ保護を備えた何らかの形のメモリ管理ユニット(MMU)を取得しました。 CPUがある種のメモリを上書きすることを禁止するデバイスがありました。そのため、一部のメモリセグメント、特に コードセグメント (別名.textセグメント)は読み取り専用になりました(ディスクからロードしたオペレーティングシステムを除く)。コンパイラとリンカがリテラル文字列をそのコードセグメントに入れるのは自然なことで、リテラル文字列は読み取り専用になりました。あなたのプログラムがそれらを上書きしようとしたとき、それは悪いことでした、 未定義の動作 。また、仮想メモリに読み取り専用のコードセグメントがあると、大きな利点が得られます。同じプログラムを実行するいくつかの プロセス が同じ を共有します[〜#〜] ram [〜#〜]physical memory pages)for the code segment(see MAP_SHARED flag for mmap(2) Linuxの場合)。

今日、安価な マイクロコントローラー には、いくつかの 読み取り専用メモリー (例:フラッシュまたはROM)があり、コード(およびリテラル文字列およびその他の定数)があります。また、実際のマイクロプロセッサ(タブレット、ラップトップ、デスクトップのマイクロプロセッサなど)には、洗練されたメモリ管理ユニットと キャッシュ 機構が 仮想メモリpaging 。したがって、 実行可能 プログラムのコードセグメント(たとえば [〜#〜] elf [〜#〜] 内)はメモリマップされますLinuxでは読み取り専用、共有可能、実行可能なセグメントとして( mmap(2) または execve(2) によって); BTW ld にディレクティブを与えて、本当に望めば、書き込み可能なコードセグメントを取得できます。書き込みまたは乱用は、通常 セグメンテーション違反 です。

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

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

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

foo.cを使用してファイルgcc -O -fverbose-asm -S foo.cのCコードをコンパイルし、生成されたアセンブラーファイルfoo.s[〜#〜] gcc [〜#〜] で調べてみてください。

最後に、Cの セマンティクス は十分に複雑です( CompCert の詳細を読む)& Frama-C キャプチャしようとしている)および書き込み可能な定数リテラル文字列を追加すると、プログラムがより弱くなり、安全性が低下します(動作が定義されなくなります)。書き込み可能なリテラル文字列を受け入れます。おそらくそれとは逆に、道徳的にそうであるように、それらはそれらをconst char[]配列にするでしょう。

また、多くの理由により、変更可能なデータは、定数データよりも、コンピューターによる処理(キャッシュコヒーレンシー)、コーディング、開発者による理解が難しいことに注意してください。したがって、ほとんどのデータ(特にリテラル文字列)を immutable のままにしておくことをお勧めします。 関数型プログラミングparadigm の詳細をご覧ください。

IBM/7094の古いFortran77日では、バグが定数を変更することさえありました。CALL FOO(1)で、FOOが2への参照によって渡された引数を変更した場合、実装は他の1の出現を2に変更しましたが、それは本当にわずらわしいバグであり、見つけるのは非常に困難でした。

40

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

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

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

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

2
supercat

Grepのようなプログラムを考えてみましょう。その中のリテラル定数の多くはエラーメッセージです。それらが不変でコードセグメントの一部であると見なすことにより、マルチユーザーマシンはsameコードセグメントをRAM for dozens ofすべてのユーザーがgrepを同時に、非同期でも実行しています。

0
Ross Presser