Cで書く場合:
int num;
num
に何かを割り当てる前に、num
の値は不定ですか?
静的変数(ファイルスコープと静的関数)はゼロに初期化されます。
int x; // zero
int y = 0; // also zero
void foo() {
static int x; // also zero
}
非静的変数(ローカル変数)はindeterminateです。値を割り当てる前にそれらを読み取ると、未定義の動作が発生します。
void foo() {
int x;
printf("%d", x); // the compiler is free to crash here
}
実際には、最初は無意味な値を持っている傾向があります-一部のコンパイラーは、デバッガーで見たときに明確にするために特定の固定値を入れることさえあります-厳密に言えば、コンパイラーはクラッシュから呼び出しまで何でも自由に行うことができます 鼻の通路を通る悪魔 。
単に「未定義/任意の値」ではなく未定義の動作である理由については、さまざまなタイプの表現に追加のフラグビットがあるCPUアーキテクチャがいくつかあります。最新の例は、 レジスタに「Not a Thing」ビットがあるItanium ;です。もちろん、C標準ドラフト作成者はいくつかの古いアーキテクチャを検討していました。
これらのフラグビットを設定して値を操作しようとすると、reallyが失敗してはならない操作(たとえば、整数の加算、または別の変数に割り当てる)。そして、変数を初期化せずに残すと、コンパイラはこれらのフラグビットが設定されたランダムなゴミを拾う可能性があります。つまり、初期化されていない変数に触れることは致命的です。
Cは常にオブジェクトの初期値について非常に具体的でした。グローバルまたはstatic
の場合、それらはゼロになります。 auto
の場合、値はindeterminateです。
これは、C89以前のコンパイラの場合であり、K&RおよびDMRの元のCレポートで指定されていました。
これはC89の場合でした。セクション6.5.7初期化を参照してください。
自動保存期間を持つオブジェクトが明示的に初期化されていない場合、その値は不定です。静的ストレージ期間を持つオブジェクトが明示的に初期化されない場合、算術型を持つすべてのメンバーに0が割り当てられ、ポインター型を持つすべてのメンバーにNULLポインター定数が割り当てられるように暗黙的に初期化されます。
これはC99の場合でした。セクション6.7.8初期化を参照してください。
自動保存期間を持つオブジェクトが明示的に初期化されていない場合、その値は不定です。静的な保存期間を持つオブジェクトが明示的に初期化されていない場合、次のようになります。
—ポインタータイプの場合、nullポインターに初期化されます。
—算術型の場合、ゼロ(正または符号なし)に初期化されます。
—集約の場合、すべてのメンバーはこれらの規則に従って(再帰的に)初期化されます。
—ユニオンの場合、最初の名前付きメンバーはこれらの規則に従って(再帰的に)初期化されます。
indeterminateの正確な意味については、C89については確信がありません。
3.17.2
不定値
未指定の値またはトラップ表現
しかし、標準が言っていることに関係なく、実際には、各スタックページは実際にはゼロから始まりますが、プログラムがauto
ストレージクラスの値を見ると、最後に自分のプログラムによって残されたものを見ることができますそれらのスタックアドレスを使用しました。多数のauto
配列を割り当てた場合、それらは最終的にゼロできれいに開始されます。
不思議に思うかもしれませんが、なぜこのようになっているのでしょうか?別のSO回答がその質問を扱っています。 https://stackoverflow.com/a/2091505/14074 を参照してください。
変数の保存期間に依存します。静的ストレージ期間を持つ変数は、常に暗黙的にゼロで初期化されます。
自動(ローカル)変数については、初期化されていない変数には不定値があります。不定値とは、とりわけ、その変数で「見る」可能性のある「値」が予測できないだけでなく、安定であることさえ保証されないことを意味します。たとえば、実際には(つまり、UBを一瞬無視する)このコード
int num;
int a = num;
int b = num;
変数a
とb
が同じ値を受け取ることを保証しません。興味深いことに、これはいくつかの理論的な概念ではなく、実際には最適化の結果として容易に起こります。
そのため、一般的に、「メモリ内にあるガベージで初期化される」という一般的な答えは、リモートでも正しくありません。 初期化されていない変数の動作は、変数の動作とは異なります初期化されたガベージあり。
Ubuntu 15.10、Kernel 4.2.0、x86-64、GCC 5.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
です。
これは、最初の4バイトのローカル変数である-0x4(%rbp)
から取得されます。
この時点で、rbp
はカーネルによって割り当てられたスタックの最初のページにあるため、その値を理解するには、カーネルコードを調べて、それが何に設定されているかを調べます。
TODOは、プロセスが終了したときに他のプロセスで再利用する前に、カーネルがそのメモリを何かに設定しますか?そうでない場合、新しいプロセスは他の終了したプログラムのメモリを読み取ることができ、データがリークします。参照: 初期化されていない値はセキュリティリスクになりますか?
その後、独自のスタック変更を行って、次のような楽しいことを書くこともできます。
#include <assert.h>
int f() {
int i = 13;
return i;
}
int g() {
int i;
return i;
}
int main() {
f();
assert(g() == 13);
}
グローバル変数
標準: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
は.bss
セクションの真ん中にあり、0x601040
で始まり8バイト長です。
ELF標準 は、.bss
という名前のセクションが完全にゼロで埋められることを保証します。
.bss
このセクションは、プログラムのメモリイメージに寄与する初期化されていないデータを保持します。定義により、プログラムの実行が開始されると、システムはデータをゼロで初期化します。セクションタイプSHT_NOBITS
で示されるように、セクションにはファイルスペースがありません。
さらに、タイプSHT_NOBITS
は効率的で、実行可能ファイルのスペースを占有しません。
sh_size
このメンバーは、セクションのサイズをバイト単位で提供します。セクションタイプがSHT_NOBITS
でない限り、セクションはファイルのsh_size
バイトを占有します。タイプSHT_NOBITS
のセクションのサイズは0以外でもかまいませんが、ファイル内のスペースを占有しません。
その後、プログラムの起動時にメモリにロードするときにそのメモリ領域をゼロにするのはLinuxカーネル次第です。
場合によります。その定義がグローバル(関数の外側)である場合、num
はゼロに初期化されます。ローカル(関数内)の場合、その値は不定です。理論的には、値を読み取ろうとしても未定義の動作があります-Cは値に寄与しないビットの可能性を許容しますが、変数の読み取りから定義された結果を得るために特定の方法で設定する必要があります。
基本的な答えは、はい、未定義です。
このために奇妙な動作が見られる場合、それが宣言されている場所に依存する可能性があります。スタック上の関数内の場合、関数が呼び出されるたびに内容が異なる可能性が高くなります。静的スコープまたはモジュールスコープの場合、未定義ですが変更されません。
ストレージクラスが静的またはグローバルの場合、ロード中にBSSが初期化されます変数またはメモリロケーション(ML)は、変数に最初に値が割り当てられていない限り、0になります。ローカルの初期化されていない変数の場合、トラップ表現はメモリ位置に割り当てられます。したがって、重要な情報を含むレジスタのいずれかがコンパイラによって上書きされると、プログラムがクラッシュする可能性があります。
ただし、一部のコンパイラには、このような問題を回避するメカニズムがあります。
Char以外のデータ型の未定義値を表すビットパターンを持つトラップ表現があることに気付いたとき、nec v850シリーズで作業していました。初期化されていない文字を使用すると、トラップ表現のためにデフォルト値がゼロになりました。これは、necv850esを使用するany1に役立つ場合があります
コンピュータの記憶容量は有限であるため、自動変数は通常、他の任意の目的で以前に使用されていた記憶要素(レジスタまたは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()
を呼び出すことになります。 「foo」内ではx
は不要であり、関数は戻り値をレジスタ0に入れることになっているため、コンパイラはq
にレジスタ0を割り当てることができます。 mode
が1または3の場合、レジスタ0にはそれぞれ2または4がロードされますが、それ以外の値の場合、その値はレジスタ0にあったもの(つまり、値1234567)を返します。 uint16_tの範囲内ではありません。
初期化されていない変数がドメイン外の値を保持しないようにするためにコンパイラーに余分な作業を行わせることを回避し、不明確な動作を過度に詳細に指定する必要を避けるために、標準では、初期化されていない自動変数の使用は未定義の動作であると述べています。場合によっては、この結果は、値がそのタイプの範囲外であるよりもさらに驚くかもしれません。たとえば、次の場合:
void moo(int mode)
{
if (mode < 5)
launch_nukes();
hey(0, mode);
}
コンパイラは、moo()
を3よりも大きいモードで呼び出すと、プログラムが未定義動作を呼び出すことになるので、コンパイラはmode
が4またはこのような場合に通常は核兵器の発射を妨げるコードなど、より優れています。標準も現代のコンパイラ哲学も、「hey」からの戻り値が無視されるという事実を気にしないことに注意してください。それを返そうとする行為は、コンパイラに任意のコードを生成する無制限のライセンスを与えます。