web-dev-qa-db-ja.com

バインドされた未定義の動作の外側でグローバル配列にアクセスしていますか?

今日、クラスで試験を受けました--- Cコードと入力を読んで、必要な答えは、プログラムが実際に実行された場合に画面に表示されるものでした。 _a[4][4]_をグローバル変数として宣言した質問の1つで、そのプログラムのある時点で_a[27][27]_にアクセスしようとしているので、「 境界外の配列にアクセスするのは未定義の振る舞い "しかし、先生は_a[27][27]_の値は_0_になると言いました。

その後、 some code を試して、「初期化されていないすべてのゴルバル変数が_0_に設定されている」がtrueかどうかを確認しました。まあ、それは本当のようです。

だから今私の質問:

  • コードを実行するために、余分なメモリがクリアされ、予約されているようです。どのくらいのメモリが予約されていますか?コンパイラが必要以上のメモリを予約するのはなぜですか?また、その目的は何ですか?
  • _a[27][27]_はすべての環境で_0_になりますか?

編集:

そのコードでは、_a[4][4]_は宣言されたonlyグローバル変数であり、main()にはさらにいくつかのローカル変数があります。

私はDevC++で そのコード をもう一度試しました。それらはすべて_0_です。しかし、これはVSEには当てはまりません。ほとんどの値は_0_ですが、Vyktorが指摘しているように、ランダムな値を持つものもあります。

60

あなたは正しかった:それは未定義の振る舞いであり、常に0を生成することを数えることはできない。

この場合にゼロが表示される理由については、最新のオペレーティングシステムは、個々の変数(x86では少なくとも4KB)よりもはるかに大きいページと呼ばれる比較的粗いチャンクのプロセスにメモリを割り当てます。グローバル変数が1つしかない場合は、ページのどこかに配置されます。 aのタイプがint[][]であり、intsがシステム上で4バイトであるとすると、a[27][27]aの先頭から約500バイトに配置されます。 。 aがページの先頭近くにある限り、a[27][27]へのアクセスは実際のメモリによってバックアップされ、それを読み取ってもページフォールト/アクセス違反は発生しません。

もちろん、これを当てにすることはできません。たとえば、aの前に4KB近くの他のグローバル変数がある場合、a[27][27]はメモリにバックアップされず、読み取ろうとするとプロセスがクラッシュします。

プロセスがクラッシュしなくても、値0の取得を期待することはできません。最新のマルチユーザーオペレーティングシステムで、この変数を割り当ててその値を出力するだけの非常に単純なプログラムがある場合は、おそらく0が表示されます。オペレーティングシステムは、メモリをプロセスに渡すときにメモリの内容を良性の値(通常はすべてゼロ)に設定して、あるプロセスまたはユーザーからの機密データが別のプロセスまたはユーザーに漏洩しないようにします。

ただし、読み取る任意のメモリがゼロになるという一般的な保証はありません。割り当て時にメモリが初期化されていないプラットフォームでプログラムを実行すると、最後に使用してからたまたまそこにあった値を確認できます。

また、aの後にゼロ以外の値に初期化される十分な他のグローバル変数が続く場合、a[27][27]にアクセスすると、そこにある値が何であれ表示されます。

50
Andrew Medico

範囲外の配列へのアクセスは未定義の動作です。つまり、結果は予測できないため、a[27][27]0であるというこの結果はまったく信頼できません。

clang-fsanitize=undefinedを使用すると、これが非常に明確になります。

runtime error: index 27 out of bounds for type 'int [4][4]'

未定義の動作が発生すると、コンパイラは実際に何でも実行できます。未定義の動作に関する最適化に基づいて、gcc有限ループを無限ループに変換 する例もあります。 clanggccの両方が、状況によっては、未定義の動作を検出した場合、 未定義の命令オペコードを生成 することができます。

なぜそれが未定義の振る舞いなのか なぜ範囲外のポインタ演算が未定義の振る舞いなのか? は理由の良い要約を提供します。たとえば、結果のポインタが有効なアドレスではない可能性があり、ポインタが割り当てられたメモリページの外側を指す可能性があり、RAMなどの代わりにメモリマップされたハードウェアで作業している可能性があります。

静的変数が格納されているセグメントは、割り当てている配列または踏み込んでいるセグメントよりもはるかに大きい可能性がありますが、たまたまゼロになっているため、この場合は幸運ですが、動作は完全に信頼できません。ほとんどの場合、 ページサイズは4k であり、a[27][27]のアクセスはその範囲内であるため、セグメンテーション違反が発生していない可能性があります。

規格の内容

ドラフトC99標準 これはセクション6.5.6加算演算子の未定義の動作であると教えてください。アレイアクセスの目的。それは言う:

整数型の式がポインターに加算またはポインターから減算されると、結果はポインターオペランドの型になります。ポインタオペランドが配列オブジェクトの要素を指し、配列が十分に大きい場合、結果は元の要素からオフセットされた要素を指し、結果の配列要素と元の配列要素の添え字の差が整数式に等しくなります。

[...]

ポインタオペランドと結果の両方が同じ配列オブジェクトの要素を指している場合、または配列オブジェクトの最後の要素を1つ過ぎている場合、評価によってオーバーフローが発生することはありません。それ以外の場合、動作は定義されていません。結果が配列オブジェクトの最後の要素の1つ過ぎを指している場合、評価される単項*演算子のオペランドとして使用されないものとします。

未定義の動作の標準定義は、標準が動作に要件を課していないことを示しており、可能な動作は予測できないことに注意しています。

この国際規格が要件を課していない、移植不可能または誤ったプログラム構成または誤ったデータの使用時の動作

注考えられる未定義の動作は、状況を完全に無視して予測できない結果をもたらすことまでさまざまです。[...]

28
Shafik Yaghmour

これは、未定義の動作を指定する標準からの引用です。

J.2未定義の振る舞い

  • 配列の添え字は、オブジェクトが指定された添え字で明らかにアクセス可能であっても、範囲外です(宣言int a [4] [5]が与えられた左辺式a [1] [7]のように)(6.5.6)。

  • 配列オブジェクトおよび整数型への、またはそのすぐ先へのポインターの加算または減算は、配列オブジェクトのすぐ先を指す結果を生成し、評価される単項*演算子のオペランドとして使用されます(6.5.6)。

あなたの場合、配列の添え字は完全に配列の外側にあります。値がゼロになることを依存することは完全に信頼できません。

さらに、プログラム全体の動作が問題になっています。

11
2501

Visual Studio 2012からコードを実行し、次のような結果が得られた場合(実行ごとに異なります):

Address of a: 00FB8130
Address of a[4][4]: 00FB8180
Address of a[27][27]: 00FB834C
Value of a[27][27]: 0
Address of a[1000][1000]: 00FBCF50
Value of a[1000][1000]: <<< Unhandled exception at 0x00FB3D8F in GlobalArray.exe:
                            0xC0000005: Access violation reading location 0x00FBCF50.

Modulesウィンドウを見ると、アプリケーションモジュールのメモリ範囲が00FA0000-00FBC000であることがわかります。そして、 CRTチェック がオンになっていない限り何もはあなたの記憶の中で何をするかを制御しませんメモリ保護 に違反しない限り)。

つまり、0a[27][27]を取得したのは偶然です。位置00FB8130a)からメモリビューを開くと、おそらく次のようなものが表示されます。

0x00FB8130  08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0x00FB8140  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0x00FB8150  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0x00FB8160  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0x00FB8170  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0x00FB8180  01 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00  ................
0x00FB8190  c0 90 45 00 b0 e9 45 00 00 00 00 00 00 00 00 00  À.E.°éE.........
0x00FB81A0  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0x00FB81B0  00 00 00 00 80 5c af 0f 00 00 00 00 00 00 00 00  ....€\¯.........
0x00FB81C0  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
.......... 
0x00FB8330  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0x00FB8340  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................ <<<<
0x00FB8350  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
..........                                      ^^ ^^ ^^ ^^

コンパイラでは、メモリの使用方法が原因で、そのコードに対して常に0を取得する可能性がありますが、わずか数バイト離れたところに別の変数を見つけることができます。

たとえば、上記のメモリでは、a[6][0]0x00FB8190の整数値を含むアドレス4559040を指します。

8
Vyktor

次に、先生にこれを説明してもらいます。

これがお使いのシステムで機能するかどうかはわかりませんが、ゼロ以外のバイトを含む配列aの後でメモリをブラッティングしてみると、a[27][27]の結果が異なります。

私のシステムでは、a[27][27]の内容を印刷したときは0xFFFFFFFFでした。つまり、-1が符号なしに変換されると、すべてのビットが2の補数に設定されます。

#include <stdio.h>
#include <string.h>

#define printer(expr) { printf(#expr" = %u\n", expr); }

   unsigned int d[8096];
   int a[4][4];  /* assuming an int is 4 bytes, next 4 x 4 x 4 bytes will be initialised to zero */
   unsigned int b[8096];
   unsigned int c[8096];


int main() {

   /* make sure next bytes do not contain zero'd bytes */
   memset(b, -1, 8096*4);
   memset(c, -1, 8096*4);
   memset(d, -1, 8096*4);

   /* lets check normal access */
   printer(a[0][0]);
   printer(a[3][3]);

   /* Now we disrepect the machine - undefined behaviour shall result */
   printer(a[27][27]);

   return 0;
}

これは私の出力です:

a[0][0] = 0
a[3][3] = 0
a[27][27] = 4294967295

VisualStudioでメモリを表示することについてのコメントで見ました。最も簡単な方法は、コードのどこかにブレークポイントを追加して(実行を停止するため)、[デバッグ...ウィンドウ...] [メモリ]メニューに移動し、[メモリ1]などを選択します。次に、配列のメモリアドレスを見つけますa。私の場合、住所は0x0130EFC0でした。したがって、アドレスfiendに0x0130EFC0と入力し、Enterキーを押します。これは、その場所のメモリを示しています。

たとえば私の場合。

0x0130EFC0  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ..................................
0x0130EFE2  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ff ff ff ff  ..............................ÿÿÿÿ
0x0130F004  ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff  ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ 
0x0130F026  ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff  ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ
0x0130F048  ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff  ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ

もちろん、ゼロは配列aであり、バイトサイズは4 x 4 x sizeof int(私の場合は4)= 64バイトです。アドレス0x0130EFC0からのバイトはそれぞれ0xFFです(b、c、またはdの内容から)。

ご了承ください:

0x130EFC0 + 64 = 0x130EFC0 + 0x40 = 130F000

これは、表示されるすべてのffバイトの始まりです。おそらく配列b

6
Angus Comber

一般的なコンパイラの場合、範囲を超えて配列にアクセスすると、非常に特殊な場合にのみ予測可能な結果が得られるため、それに依存しないでください。例:

int a[4][4];
int b[4][4];

位置合わせの問題がなく、積極的な最適化もサニタイズチェックも要求しない場合は、a[6][1]は実際にはb[2][1]。ただし、本番コードでは絶対に行わないでください。

5
Serge Ballesta

特定のシステムでは、先生は正しいかもしれません-それはあなたの特定のコンパイラとオペレーティングシステムがどのように振る舞うかであるかもしれません。

genericシステム(つまり、「インサイダー」の知識がない場合)では、答えは正しいです。これはUBです。

5
Mehrdad

まず第一に、C言語には境界チェックがありません。事実上、ほとんどすべてをチェックしていません。これがCの喜びと運命です。

ここで問題に戻りますが、メモリがオーバーフローしても、セグメンテーション違反がトリガーされるわけではありません。それがどのように機能するかを詳しく見てみましょう。

プログラムを開始するとき、またはサブルーチンに入るとき、プロセッサは、関数が終了したときに戻るアドレスをスタックに保存します。

スタックは、プロセスメモリの割り当て中にOSから初期化され、リターンアドレスを格納するだけでなく、好きなように読み取りまたは書き込みできる一連の有効なメモリを取得しました。

コンパイラーがローカル(自動)変数を作成するために使用する一般的な方法は、スタックにスペースを予約し、そのスペースを変数に使用することです。プロローグという名前のよく知られた32ビットアセンブラシーケンスに従ってください。これは、次のように入力する関数にあります。

Push ebp      ;save register on the stack
mov ebp,esp   ;get actual stack address
sub esp,4     ;displace the stack of 4 bytes that will be used to store a 4 chars array

スタックがデータの逆方向に大きくなることを考慮すると、メモリのレイアウトは次のようになります。

0x0.....1C   [Parameters (if any)]    ;former function
0x0.....18   [Return Address]
0x0.....14   EBP
0x0.....10   0x0......x               ;Local DWORD parameter
0x0.....0C   [Parameters (if any)]    ;our function
0x0.....08   [Return Address]
0x0.....04   EBP
0x0.....00   0, 'c', 'b', 'a'    ;our string of 3 chars plus final nul

これはスタックフレームとして知られています。

ここで、0x0 .... 0で始まり0x .... 3で終わる4バイトの文字列について考えてみます。配列に3つ以上の文字を書き込む場合は、EBPの保存されたコピー、戻りアドレス、パラメーター、前の関数のローカル変数、次にそのEBP、戻りアドレスなどを順番に置き換えます。

私たちが得る最も風光明媚な効果は、関数が戻ったときに、CPUが間違ったアドレスにジャンプして戻ってsegfaultを生成しようとすることです。ローカル変数の1つがポインターである場合も同じ動作を実現できます。この場合、間違った場所への読み取りまたは書き込みを試みて、セグメンテーション違反を再度トリガーします。

セグメンテーション違反が発生しなかった場合:肥大化した変数がスタックにない場合、またはローカル変数が多すぎてリターンアドレスに触れずに上書きする場合(ポインターではない場合)。もう1つのケースは、プロセッサがローカル変数とリターンアドレスの間にガードスペースを予約する場合です。この場合、バッファオーバーフローはアドレスに到達しません。別の可能性は、配列要素にランダムにアクセスすることです。この場合、特大の配列はスタックスペースを超えて他のデータでオーバーフローする可能性がありますが、幸いなことに、リターンアドレスが保存されている場所にマップされている要素には触れません(すべてが発生する可能性があります...) 。

スタック上にないセグメンテーション違反の肥大化変数が発生する可能性がある場合配列のバインドまたはポインターがオーバーフローした場合。

これらが役立つ情報であることを願っています...

1
Frankie_C