CIの場合、次のように記述します。
int num;
に何かを割り当てる前にnum
、の値はnum
不確定ですか?
静的変数(ファイルスコープと関数static)はゼロに初期化されます:
int x; // zero
int y = 0; // also zero
void foo() {
static int x; // also zero
}
非静的変数(ローカル変数)は不確定です。値を割り当てる前にそれらを読み取ると、未定義の動作が発生します。
void foo() {
int x;
printf("%d", x); // the compiler is free to crash here
}
実際には、最初は無意味な値が含まれる傾向があります。一部のコンパイラは、デバッガを確認するときに明確にするために特定の固定値を入力することもありますが、厳密に言えば、コンパイラはクラッシュから呼び出しまで何でも自由に実行できます。あなたの鼻の通路を通る悪魔。
単に「未定義/任意の値」ではなく未定義の動作である理由については、さまざまなタイプの表現に追加のフラグビットがあるCPUアーキテクチャがいくつかあります。現代の例は、レジスタに「NotaThing」ビットがあるItaniumです。もちろん、C標準製図者はいくつかの古いアーキテクチャを検討していました。
これらのフラグビットが設定された値を操作しようとすると、実際には失敗してはならない操作(整数の加算、別の変数への割り当てなど)でCPU例外が発生する可能性があります。また、変数を初期化しないままにしておくと、コンパイラはこれらのフラグビットが設定されたランダムなゴミを拾う可能性があります。つまり、初期化されていない変数に触れると致命的となる可能性があります。
Cは、オブジェクトの初期値について常に非常に具体的です。グローバルまたはの場合static
、それらはゼロになります。の場合auto
、値は不確定です。
これはC89以前のコンパイラの場合であり、K&RおよびDMRの元のCレポートでそのように指定されていました。
これはC89の場合でした。セクション6.5.7の初期化を参照してください。
自動保存期間を持つオブジェクトが明示的に初期化されていない場合、その値は不確定です。静的ストレージ期間を持つオブジェクトが明示的に初期化されない場合、算術型を持つすべてのメンバーに0が割り当てられ、ポインター型を持つすべてのメンバーにnullポインター定数が割り当てられるかのように暗黙的に初期化されます。
これはC99の場合でした。セクション6.7.8初期化を参照してください。
自動保存期間を持つオブジェクトが明示的に初期化されていない場合、その値は不確定です。静的ストレージ期間を持つオブジェクトが明示的に初期化されていない
場合、次のようになります。—ポインター型の場合、nullポインターに初期化されます。
—算術タイプの場合、(正または符号なし)ゼロに初期化されます。
—集合体の場合、すべてのメンバーはこれらのルールに従って(再帰的に)初期化されます。
—ユニオンの場合、最初に指定されたメンバーは、これらのルールに従って(再帰的に)初期化されます。
正確に不確定な意味については、C89についてはよくわかりません、C99は次のように述べています。
3.17.2
不確定な値
不特定の値またはトラップ表現
しかし、標準が何を言っているかに関係なく、実際には、各スタックページは実際にはゼロから始まりますが、プログラムがauto
ストレージクラスの値を調べると、それらのスタックアドレスを最後に使用したときに自分のプログラムによって残されたものがすべてわかります。多くのauto
配列を割り当てると、最終的にはゼロできれいに始まることがわかります。
不思議に思うかもしれませんが、なぜこのようになっているのですか?別のSO回答がその質問を扱っています。https ://stackoverflow.com/a/2091505/140740を参照してください。
これは、変数の保存期間によって異なります。静的ストレージ期間を持つ変数は、常にゼロで暗黙的に初期化されます。
自動(ローカル)変数の場合、初期化されていない変数の値は不定です。不確定な値は、とりわけ、その変数で「見る」可能性のある「値」が予測できないだけでなく、安定していることさえ保証されていないことを意味します。たとえば、実際には(つまり、UBを1秒間無視する)このコード
int num;
int a = num;
int b = num;
a
変数とb
が同じ値を受け取ることを保証するものではありません。興味深いことに、これはいくつかの衒学的な理論的概念ではなく、最適化の結果として実際に容易に発生します。
したがって、一般的に、「メモリ内にあるすべてのガベージで初期化される」という一般的な答えは、リモートでさえ正しくありません。初期化されていない変数の動作は、ガベージで初期化された変数の動作とは異なります。
Ubuntu 15.10、カーネル4.2.0、x86-64、GCC5.2.1の例
十分な標準、実装を見てみましょう:-)
ローカル変数
標準:未定義の動作。
実装:プログラムはスタックスペースを割り当て、そのアドレスに何も移動しないため、以前にあったものはすべて使用されます。
#include <stdio.h>
int main() {
int i;
printf("%d\n", i);
}
コンパイル:
gcc -O0 -std=c99 a.c
出力:
0
次のコマンドで逆コンパイルします。
objdump -dr a.out
に:
0000000000400536 <main>:
400536: 55 push %rbp
400537: 48 89 e5 mov %rsp,%rbp
40053a: 48 83 ec 10 sub $0x10,%rsp
40053e: 8b 45 fc mov -0x4(%rbp),%eax
400541: 89 c6 mov %eax,%esi
400543: bf e4 05 40 00 mov $0x4005e4,%edi
400548: b8 00 00 00 00 mov $0x0,%eax
40054d: e8 be fe ff ff callq 400410 <printf@plt>
400552: b8 00 00 00 00 mov $0x0,%eax
400557: c9 leaveq
400558: c3 retq
x86-64の呼び出し規約に関する知識から:
%rdi
は最初のprintf引数であるため、"%d\n"
アドレスの文字列0x4005e4
%rsi
は2番目のprintf引数であるため、i
。
-0x4(%rbp)
これは、最初の4バイトのローカル変数であるから来ています。
この時点rbp
で、スタックの最初のページにカーネルによって割り当てられているので、その値を理解するために、カーネルコードを調べて、それが何に設定されているかを調べます。
TODOカーネルは、プロセスが停止したときに他のプロセスで再利用する前に、そのメモリを何かに設定しますか?そうでない場合、新しいプロセスは他の完成したプログラムのメモリを読み取ることができ、データをリークします。参照:初期化されていない値はセキュリティリスクになりますか?
次に、独自のスタック変更を試して、次のような楽しいことを書くこともできます。
#include <assert.h>
int f() {
int i = 13;
return i;
}
int g() {
int i;
return i;
}
int main() {
f();
assert(g() == 13);
}
GCC 11は異なるアセンブリ出力を生成するようであり、上記のコードは「動作」を停止することに注意してください。結局のところ、これは未定義の動作です。gccの-O3がローカル変数を0に初期化するように見えるのに、-O0はそうではないのはなぜですか。
のローカル変数-O3
実装分析:<valueoptimized out>はgdbで何を意味しますか?
グローバル変数
標準:0
実装:.bss
セクション。
#include <stdio.h>
int i;
int main() {
printf("%d\n", i);
}
gcc -00 -std=c99 a.c
コンパイル先:
0000000000400536 <main>:
400536: 55 push %rbp
400537: 48 89 e5 mov %rsp,%rbp
40053a: 8b 05 04 0b 20 00 mov 0x200b04(%rip),%eax # 601044 <i>
400540: 89 c6 mov %eax,%esi
400542: bf e4 05 40 00 mov $0x4005e4,%edi
400547: b8 00 00 00 00 mov $0x0,%eax
40054c: e8 bf fe ff ff callq 400410 <printf@plt>
400551: b8 00 00 00 00 mov $0x0,%eax
400556: 5d pop %rbp
400557: c3 retq
400558: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1)
40055f: 00
# 601044 <i>
それi
は住所0x601044
にあり、次のように述べています。
readelf -SW a.out
含まれています:
[25] .bss NOBITS 0000000000601040 001040 000008 00 WA 0 0 4
これは、セクション0x601044
の真ん中にあり、8バイトの長さで始まります。.bss
0x601040
ELF標準.bss
は、指定されたセクションが完全にゼロで満たされることを保証します。
.bss
このセクションは、プログラムのメモリイメージに寄与する初期化されていないデータを保持します。定義上、プログラムの実行が開始されると、システムはデータをゼロで初期化します。セクションタイプで示されるように、セクションはファイルスペースを占有しませんSHT_NOBITS
。
さらに、このタイプSHT_NOBITS
は効率的で、実行可能ファイルのスペースを占有しません。
sh_size
このメンバーは、セクションのサイズをバイト単位で示します。セクションタイプが。でない限り、セクション はファイル内のバイトをSHT_NOBITS
占有します。sh_size
タイプのセクションのSHT_NOBITS
サイズはゼロ以外である可能性がありますが、ファイル内のスペースを占有していません。
次に、プログラムの起動時にプログラムをメモリにロードするときに、そのメモリ領域をゼロにするのはLinuxカーネル次第です。
場合によります。その定義がグローバル(関数の外)である場合num
、ゼロに初期化されます。ローカル(関数内)の場合、その値は不確定です。理論的には、値を読み取ろうとしても未定義の動作があります。Cは、値に寄与しないビットの可能性を考慮に入れていますが、変数の読み取りから定義された結果を取得するには、特定の方法で設定する必要があります。
基本的な答えは、はい、それは未定義です。
このために奇妙な動作が見られる場合は、宣言されている場所によって異なる可能性があります。スタック上の関数内の場合、関数が呼び出されるたびに内容が異なる可能性があります。静的スコープまたはモジュールスコープの場合、未定義ですが変更されません。
コンピューターのストレージ容量は有限であるため、自動変数は通常、他の任意の目的で以前に使用されたストレージ要素(レジスターまたはRAM)に保持されます。値が割り当てられる前にそのような変数が使用された場合、そのストレージは以前に保持していたものを保持する可能性があるため、変数の内容は予測できません。
追加の問題として、多くのコンパイラは、関連する型よりも大きい変数をレジスタに保持する場合があります。コンパイラは、変数に書き込まれて読み戻される値が切り捨てられるか、適切なサイズに符号拡張されるようにする必要がありますが、多くのコンパイラは、変数が書き込まれるときにそのような切り捨てを実行し、変数が読み取られる前に実行されます。このようなコンパイラでは、次のようになります。
uint16_t hey(uint32_t x, uint32_t mode)
{ uint16_t q;
if (mode==1) q=2;
if (mode==3) q=4;
return q; }
uint32_t wow(uint32_t mode) {
return hey(1234567, mode);
}
wow()
値1234567をそれぞれレジスタ0と1に格納し、を呼び出す結果になる可能性がありますfoo()
。x
「foo」内では必要ないため、また関数は戻り値をレジスタ0に入れることになっているため、コンパイラはレジスタ0をに割り当てることができますq
。が1または3の場合mode
、レジスタ0にはそれぞれ2または4がロードされますが、それが他の値である場合、その値が範囲内にない場合でも、関数はレジスタ0にあったもの(つまり値1234567)を返す場合があります。 uint16_tの。
初期化されていない変数がドメイン外の値を保持していないように見えるようにコンパイラーに余分な作業を要求することを回避し、不確定な動作を過度に詳細に指定する必要を回避するために、標準では、初期化されていない自動変数の使用は未定義の動作であるとしています。場合によっては、この結果は、値がそのタイプの範囲外であるよりもさらに驚くべきものになる可能性があります。たとえば、次のようになります。
void moo(int mode)
{
if (mode < 5)
launch_nukes();
hey(0, mode);
}
moo()
コンパイラーは、3より大きいモードで呼び出すと、必然的に未定義動作を呼び出すプログラムにつながるため、コンパイラーは、mode
通常は防止するコードなど、4以上の場合にのみ関連するコードを省略できると推測できます。そのような場合の核兵器の発射。標準も最新のコンパイラ哲学も、「hey」からの戻り値が無視されるという事実を気にしないことに注意してください。それを返そうとする行為は、コンパイラに任意のコードを生成する無制限のライセンスを与えます。
ストレージクラスが静的またはグローバルの場合、ロード中に、変数に最初に何らかの値が割り当てられていない限り、 BSSは変数またはメモリ位置(ML)を0に初期化します。ローカルの初期化されていない変数の場合、トラップ表現はメモリ位置に割り当てられます。したがって、重要な情報を含むレジスタのいずれかがコンパイラによって上書きされると、プログラムがクラッシュする可能性があります。
ただし、一部のコンパイラには、このような問題を回避するメカニズムがある場合があります。
char以外のデータ型の未定義の値を表すビットパターンを持つトラップ表現があることに気付いたとき、necv850シリーズで作業していました。初期化されていないcharを取得すると、トラップ表現のためにデフォルト値がゼロになりました。これは、necv850esを使用するany1に役立つ可能性があります
私が行った限り、それはほとんどコンパイラに依存しますが、一般的にほとんどの場合、値はコンパイラによって0と事前に想定されています。
TCが0として値を与えている間、私はVC++の場合にガベージ値を取得しました。私はそれを以下のように印刷します
int i;
printf('%d',i);