はじめに
下記のコードをclang 14.0.0に最適化オプション-O2
を指定してコンパイル、実行してみた結果、
#include <stdio.h>
int main(void)
{
int x; // 未初期化
printf("x = %d\n", x);
printf("x = %d\n", x);
}
x = -927211032
x = 4202506
どうしてこうなったのかという話です。
C言語の規格を確認してみる
↑を確認したところ、
6.7.8 初期化
(略)
自動記憶域期間をもつオブジェクトを明示的に初期化しない場合,その値は不定とする。
未初期化のオブジェクトは「不定(の値)」となることが明記されています。「不定の値」とは、
3.17.2 不定の値(indeterminate value) 未規定の値又はトラップ表現。
に定義があり、未規定の値(=ゴミ値)か「トラップ表現」という聞き慣れないものであるとされています。
「トラップ表現」については
6.2.6 型の表現
6.2.6.1 一般的規定
(略)
すべての型の表現は,6.2.6オブジェクト表現の中には,そのオブジェクト型の値を表現しないものがある。オブジェクトに格納した値がオブジェクト型の値を表現せず,文字型ではない左辺値式でそれを読み取る場合,その動作は未定義とする。文字型ではない左辺値式の副作用(その結果,オブジェクトのすべて又は一部が変更される。)によって,オブジェクト型の値を表現しないオブジェクト表現が生成される場合,その動作は未定義とする(41)。オブジェクト型の値を表現しないオブジェクト表現をトラップ表現(trap representation)と呼ぶ。
(41) したがって,自動変数は,未定義な動作を起こすことなく,トラップ表現で初期化することが可能である。この場合,この変数の値は,適切な値が格納されるまでは使用できない。
に説明があります。要は、未初期化のローカル変数を読み取ると未定義動作を引き起こすことがあるということですね。
折角なのでclangが生成したコードを確認してみる
折角なのでclangの生成したコードを確認してみました。
main: # @main
pushq %rax
movl $.L.str, %edi
xorl %eax, %eax
callq printf
movl $.L.str, %edi
xorl %eax, %eax
callq printf
xorl %eax, %eax
popq %rcx
retq
.L.str:
.asciz "x = %d\n"
printf()
の第2引数であるx
の値を%esi
へ設定をせずにprintf()
を呼び出しており、%esi
のたまたまの値が出力されていたことが分かります。x
が未初期化の状態で参照しようとしているコードなので、x
を参照する部分が未定義動作としてコード生成がスッパリ削除されたのだと思われます。
『プログラミング言語C』を確認してみる
もはや古典である『プログラミング言語C』(初版)を引っ張り出してきて未初期化の変数について説明のある部分を確認してみました。
41ページ
はっきりと初期化宣言しない自動変数は不定値(すなわちゴミの値)をもつ。
216ページ
自動変数とレジスタ変数は初期化されないとゴミの値でスタートとなる。
未初期化のローカル変数がゴミ値を持つという刷り込みはこの時代のC言語の仕様によるものと思います。その後のC言語の標準規格化により未定義動作も許容されるようなったということでしょう。
おわりに
おわりです。