C言語でconstなオブジェクトが書き換えられることがある話
- angel_p_57
- 4566
- 7
- 1
- 0
先に注意事項ですが、今回の話はC言語ソースのコンパイル結果、つまり「処理系での実現方法」の話であって、C言語そのものの決まりではありません。
※C言語としての話と、コンパイル後のバイナリレベルの話を混同する人が山のようにいるので念のため
なんかふと思い立ってC言語コードのコンパイル結果を見てみたんだけど、不思議に思ったことが。 const修飾したオブジェクトって実質読み取り専用だから、メモリ上に置かれるとしたら rodata 等の書き込み不可領域になるんだけど。ところが auto変数 ( 要はレキシカルな変数 ) はそうならないようだ。
2023-07-29 21:12:41例えば static const char str[] = "…"; としてstaticなconst文字列作ると、これは当に rodata に置かれて、((char *)str)[0]='?'; とか、C++で言う「constキャスト」的なポインタのキャストで文字列の内容を変更しようとすると、セグメンテーションフォルトで落ちる。
2023-07-29 21:15:27> 「constキャスト」的なポインタのキャストで文字列の内容を変更 あ、そもそもこの行為自体が「未定義」なので、もちろん何が起こっても文句言えないってことで。そもそもやっちゃいけないんだけど。
2023-07-29 21:16:32ところが関数内で const char str[] = "…"; とする、つまりauto変数としてconstな文字列を作った場合は、なんと文字列の内容が変更できてしまう。
2023-07-29 21:18:05ちなみに「(constな)文字列の内容が変更できた」と言っても、別に処理系の不具合ではない。もっと言うと、「変更しようとするとセグフォ」も、処理系がそうしなければいけない義理はない。
※「未定義動作」なので、異常終了しようが一見正常な動作をしようが、どうなろうが「無保証」であるため
つまり、非constの場合と同様にスタックに領域を確保して都度初期値を設定するような、そういうコードを生成してるっぽい。…まあなので、気になってアセンブリコードを見てみたんだけど。
2023-07-29 21:19:33果たして、最適化の具合によっても違いはあるんだけど、非constでauto変数定義→memcpyでデータコピーの場合と、constでauto変数定義 ( 初期値あり ) とは、実質ほとんど同じコードを生成することが分かった。
2023-07-29 21:21:02で、気になる点は何かと言うと、「それ const の意味薄くね?」というのと「わざわざ memcpy 相当の処理するの無駄じゃね?」という2点。
2023-07-29 21:22:12まあ「auto変数だからスタックに置く」ってのを優先するとそうなるんだけど、別に const で書き換えを行わないという想定だと、そんな拘り別に要らんよね? と。なんで rodata 等に置いて static な変数と同じ扱いにしないんだろうなと思ったと。
2023-07-29 21:24:22ちなみに実際に比較した結果を wandbox に残してみた。 wandbox.org/permlink/eaTgU…
2023-07-29 21:25:16あともう1点以外だったのは、「const属性をはぎ取って変更を行う」って「未定義」なんだから、それこそ警告に引っかかるもんだと思ってたんだけど。なんとコンパイルオプションの -Wall -Wextra でも警告に引っかからない。 ※-Wcast-qual なら引っかかる
2023-07-29 21:27:10そんな「constオブジェクトの書き換え」を緩く扱う動機ってあるの? というように気になったのだった。 ※とは言え、static な変数なら普通にセグフォで落ちることになるんだよなあ…。
2023-07-29 21:28:32ちなみに static って言ってるけど、別にいわゆるグローバル変数でも話は同じ。ついでに「文字列リテラル」も、C言語上での扱いは const で static な文字列とほぼ同じになっていて、やはり rodata に配置される模様。
2023-07-29 21:30:27ところで、こういうのの実行例を共有するような場合、個人的には ideone をよく使ってるんだけど。今回 wandbox にしたのは、「シェルスクリプト内でコンパイルしてできたexeを実行」が ideone でできなかったから。
2023-07-29 21:33:07まあ、できないようにしているのは TMPDIR の領域を noexec マウントしているからなんだけど。実は最初「そうは言っても ld-linux .so 経由で実行すればいいからザルじゃね?」と見込んでたのが、見事に外してしまった。
2023-07-29 21:35:17補足:
noexecマウント: ここでは、ファイルシステムをマウントする際に noexec オプションを指定しておくことを指している。特に /tmp のような、一般ユーザが誰でも書き込める領域では、意図しないアプリの実行で事故る ( それを利用した攻撃をしかける ) リスクがあるので、noexec にすることも多い。
ld-linux.so: ELFプログラムローダでもあるライブラリのこと。手元のUbuntu環境だと /lib64/ld-linux-x86-64.so.2 だった。ライブラリの体裁をとっているけど、exeファイルを引数に指定して実行することで、そのexeファイルを起動させることができる。
ちょっと確認してみたら、noexec って単に execve によるプログラム起動を禁止しているだけじゃなくて、mmap による PROT_EXEC でのメモリマッピング、平たく言うと、「実行ファイルの中のコード部分を実行可能なものとしてメモリにロードする」ことも禁止されるのね。
2023-07-29 21:37:44ld-linux .so を経由すれば execve禁止はすりぬけられるけど、この PROT_EXEC での mmap禁止には引っかかるから、やっぱり実行はできない、ということになる。これ全然知らなかった。 ※なので、noexec領域に置いた .so ( 動的ライブラリ ) ファイルも、同じ理由で使い物にならないんだろうな。
2023-07-29 21:40:22もちろん、単に○○スクリプト ( シェルとかrubyとかpythonとか、お好みでどうぞ ) を noexec領域において、インタプリタ経由で読み込んで実行させるのは全然問題ないので念のため。 ※ execveは禁止なので、シバンを利用した実行はダメだけど
2023-07-29 21:42:14元の話に戻って。敢えて const なデータを都度確保した領域に置くのか。その理由に思い当たる点があった。
あ。一応ひとつだけ理由になりそうなことは思いついた。それは auto変数の場合は const であっても、初期値が動的に決まる可能性がある、ということだ。
2023-07-29 22:13:18つまり、static const m = n+1; ( n は何かの変数 ) のように、static だと動的な初期値決定はできないんだけど ( これは const な変数を使った式を組んでもダメ )、これが auto変数の場合は可能になっている。
2023-07-29 22:17:10ちなみに、「動的な初期値による初期化ができない」のは別に const には関係なくて、staticな変数 ( グローバル変数含む ) の話なので注意。
なので、auto変数の場合は、たとえ const であっても領域を毎回確保して「その時に応じた初期値」を設定するようなコードで統一しておく、と。それなら筋は通っている。
2023-07-29 22:18:18