私は最近、Cを学び始め、Cをテーマにしたクラスを取っています。私は現在、ループで遊んでいて、説明する方法がわからない奇妙な動作に直面しています。
#include <stdio.h>
int main()
{
int array[10],i;
for (i = 0; i <=10 ; i++)
{
array[i]=0; /*code should never terminate*/
printf("test \n");
}
printf("%d \n", sizeof(array)/sizeof(int));
return 0;
}
Ubuntu 14.04を実行しているラップトップでは、このコードは壊れません。完了するまで実行されます。 CentOS 6.6を実行している私の学校のコンピューターでも正常に動作します。 Windows 8.1では、ループは終了しません。
さらに奇妙なのは、for
ループの条件をi <= 11
に編集すると、Ubuntuを実行しているラップトップでのみコードが終了することです。 CentOSおよびWindowsでは終了しません。
誰もがメモリで何が起こっているのか、同じコードを実行している異なるOSが異なる結果を与える理由を説明できますか?
編集:forループが範囲外になることを知っています。意図的にやっています。 OSやコンピューターによって動作がどのように異なるかはわかりません。
Ubuntu 14.04を実行している私のラップトップでは、このコードは実行を中断しません。 CentOS 6.6を実行している私の学校のコンピューターでも正常に動作します。 Windows 8.1では、ループは終了しません。
もっと奇妙なのは、
for
ループの条件を編集してi <= 11
にすると、コードはUbuntuを実行しているラップトップでのみ終了します。 CentOSとWindowsは終了しません。
記憶の踏みつけを発見しました。詳細については、こちらをご覧ください。 「メモリストンプ」とは?
int array[10],i;
を割り当てると、これらの変数はメモリに格納されます(具体的には、関数に関連付けられたメモリのブロックであるスタックに割り当てられます)。 array[]
とi
は、おそらくメモリ内で互いに隣接しています。 Windows 8.1では、i
はarray[10]
にあるようです。 CentOSでは、i
はarray[11]
にあります。また、Ubuntuでは、どちらにもありません(おそらくarray[-1]
?にあります)。
これらのデバッグステートメントをコードに追加してみてください。繰り返し10または11で、array[i]
がi
を指していることに注意してください。
#include <stdio.h>
int main()
{
int array[10],i;
printf ("array: %p, &i: %p\n", array, &i);
printf ("i is offset %d from array\n", &i - array);
for (i = 0; i <=11 ; i++)
{
printf ("%d: Writing 0 to address %p\n", i, &array[i]);
array[i]=0; /*code should never terminate*/
}
return 0;
}
バグは次のコードの間にあります。
int array[10],i;
for (i = 0; i <=10 ; i++)
array[i]=0;
array
には10個の要素しかないため、最後の反復ではarray[10] = 0;
はバッファーオーバーフローです。バッファオーバーフローはNDEFINED BEHAVIORです。つまり、ハードドライブがフォーマットされたり、悪魔が鼻から飛び出したりする可能性があります。
すべてのスタック変数が互いに隣接して配置されることはかなり一般的です。 i
がarray[10]
が書き込む場所にある場合、UBはi
を0
にリセットするため、ループは終了しません。
修正するには、ループ条件をi < 10
に変更します。
ループの最後の実行である必要がある場合、array[10]
に書き込みますが、配列には0〜9の番号が付けられた10個の要素しかありません。C言語仕様では、これは「未定義の動作」です。これが実際に意味することは、プログラムが、メモリ内のint
の直後にあるarray
サイズのメモリに書き込みを試みることです。何が起こるかは、実際にそこにあるものに依存します。これは、オペレーティングシステムだけでなく、コンパイラ、コンパイラオプション(最適化設定など)、プロセッサアーキテクチャ、周囲のコードにも依存します。など、実行ごとに異なる場合もあります。 アドレス空間のランダム化 (おそらくこのおもちゃの例ではありませんが、実際には発生します)が原因です。いくつかの可能性が含まれます:
i
が含まれます。 i
は0から再開するため、ループは終了しません。array
は仮想メモリページの最後にあり、次のページはマップされていないためです。Windowsで観察したことは、コンパイラが変数i
をメモリ内の配列の直後に配置することを決定したため、array[10] = 0
がi
に割り当てられることになりました。 UbuntuおよびCentOSでは、コンパイラはi
をそこに配置しませんでした。ほとんどすべてのC実装は、メモリ内のローカル変数を メモリスタック でグループ化しますが、1つの大きな例外があります。一部のローカル変数は レジスタ に完全に配置できます。変数がスタック上にある場合でも、変数の順序はコンパイラーによって決定され、ソースファイル内の順序だけでなく、それらのタイプにも依存する場合があります(メモリを無駄にしてアライメントの制約が穴を残さないようにするため) 、それらの名前、コンパイラの内部データ構造で使用されるハッシュ値など。
コンパイラが実行することを決定した場合、アセンブラコードを表示するようにコンパイラに指示できます。ああ、アセンブラーを解読する方法を学んでください(書くよりも簡単です)。 GCC(および他のいくつかのコンパイラ、特にUnixの世界)では、オプション-S
を渡して、バイナリではなくアセンブラコードを生成します。たとえば、最適化オプション-O0
(最適化なし)を使用してAMD64でGCCでコンパイルしたループのアセンブラスニペットと、コメントを手動で追加したものを次に示します。
.L3:
movl -52(%rbp), %eax ; load i to register eax
cltq
movl $0, -48(%rbp,%rax,4) ; set array[i] to 0
movl $.LC0, %edi
call puts ; printf of a constant string was optimized to puts
addl $1, -52(%rbp) ; add 1 to i
.L2:
cmpl $10, -52(%rbp) ; compare i to 10
jle .L3
ここで、変数i
はスタックの最上部から52バイト下にあり、配列はスタックの最上部から48バイト下に始まります。したがって、このコンパイラはたまたまi
を配列の直前に配置しました。たまたまarray[-1]
に書き込む場合は、i
を上書きします。 array[i]=0
をarray[9-i]=0
に変更すると、これらの特定のコンパイラオプションを使用して、この特定のプラットフォームで無限ループが発生します。
では、gcc -O1
を使用してプログラムをコンパイルしましょう。
movl $11, %ebx
.L3:
movl $.LC0, %edi
call puts
subl $1, %ebx
jne .L3
短いです!コンパイラーは、i
のスタック位置の割り当てを拒否しただけでなく、レジスターebx
にのみ格納されます—しかし、array
にメモリーを割り当てることも、要素を設定するコードを生成することもありません。の要素が使用されています。
この例をわかりやすくするために、最適化できないものをコンパイラーに提供することで、配列の割り当てが実行されるようにします。これを行う簡単な方法は、別のファイルの配列を使用することです。別のコンパイルのため、コンパイラは別のファイルで何が起こるかを知りません(リンク時に最適化しない限り、gcc -O0
またはgcc -O1
はわかりません)。を含むソースファイルuse_array.c
を作成します
void use_array(int *array) {}
そしてソースコードを
#include <stdio.h>
void use_array(int *array);
int main()
{
int array[10],i;
for (i = 0; i <=10 ; i++)
{
array[i]=0; /*code should never terminate*/
printf("test \n");
}
printf("%zd \n", sizeof(array)/sizeof(int));
use_array(array);
return 0;
}
コンパイルする
gcc -c use_array.c
gcc -O1 -S -o with_use_array1.c with_use_array.c use_array.o
今回は、アセンブラコードは次のようになります。
movq %rsp, %rbx
leaq 44(%rsp), %rbp
.L3:
movl $0, (%rbx)
movl $.LC0, %edi
call puts
addq $4, %rbx
cmpq %rbp, %rbx
jne .L3
これで、配列は上から44バイトのスタック上にあります。 i
はどうですか?どこにも表示されません!ただし、ループカウンタはレジスタrbx
に保持されます。正確にはi
ではなく、array[i]
のアドレスです。コンパイラは、i
の値が直接使用されることはなかったため、ループの各実行中に0を格納する場所を計算する算術を実行する意味はないと判断しました。その代わりに、そのアドレスはループ変数であり、境界を決定するための計算は、コンパイル時に部分的に実行され(配列要素ごとに4バイトで11の繰り返しを乗算して44を取得)、実行時に部分的に実行されますが、ループが開始される前に一度だけ(減算を実行して初期値を取得します)。
この非常に単純な例でさえ、コンパイラー・オプションの変更(最適化をオンにする)またはマイナーな変更(array[i]
からarray[9-i]
)、または明らかに関係のない変更(use_array
への呼び出しの追加)が、コンパイラによって生成された実行可能プログラムは実行します。 コンパイラの最適化は、未定義の動作を呼び出すプログラムでは直感的でないように見えることがあります。そのため、未定義の動作は完全に未定義のままです。実際のプログラムでは、トラックから少しでも逸脱すると、経験豊富なプログラマーであっても、コードが実行することと実行すべきことの関係を理解するのが非常に難しくなります。
Javaとは異なり、Cは配列境界チェックを行いません。つまり、ArrayIndexOutOfBoundsException
はありません。配列インデックスが有効であることを確認する作業はプログラマに任されています。これを意図的に行うと、未定義の動作が発生し、何が起こるかわかりません。
配列の場合:
int array[10]
インデックスは0
から9
の範囲でのみ有効です。しかし、あなたはしようとしている:
for (i = 0; i <=10 ; i++)
ここでarray[10]
にアクセスし、条件をi < 10
に変更します
境界違反があり、終了していないプラットフォームでは、ループの終わりで誤ってi
をゼロに設定しているため、最初からやり直すことになります。
array[10]
は無効です。 array[0]
からarray[9]
までの10個の要素が含まれ、array[10]
は11番目です。ループは、次のようにbefore10
を停止するように記述する必要があります。
for (i = 0; i < 10; i++)
array[10]
landsは実装定義であり、2つのプラットフォームでは面白いことにi
にあります。これらのプラットフォームはarray
の直後に配置されているようです。 i
はゼロに設定され、ループは永遠に続きます。他のプラットフォームでは、i
がarray
の前にあるか、array
の後にパディングがある場合があります。
int array[10]
を宣言すると、array
には0
から9
へのインデックスがあります(保持できる合計10
整数要素)。しかし、次のループ、
for (i = 0; i <=10 ; i++)
0
を10
にループすると、11
時間になります。したがって、i = 10
がバッファをオーバーフローさせ、 未定義の動作 が発生します。
だからこれを試してください:
for (i = 0; i < 10 ; i++)
または、
for (i = 0; i <= 9 ; i++)
array[10]
では未定義であり、前述のように未定義の動作を提供します。次のように考えてください。
食料品カートに10個のアイテムがあります。彼らです:
0:シリアルの箱
1:パン
2:ミルク
3:パイ
4:卵
5:ケーキ
6:2リットルのソーダ
7:サラダ
8:ハンバーガー
9:アイスクリーム
cart[10]
は未定義であり、一部のコンパイラでは範囲外の例外が発生する場合があります。しかし、多くはそうではないようです。明らかな11番目のアイテムは、実際にはカートに入っています。 11番目のアイテムは、「ポルターガイストアイテム」と呼んでいます。決して存在しませんでしたが、そこにありました。
一部のコンパイラがi
にarray[10]
またはarray[11]
のインデックス、さらにはarray[-1]
のインデックスを与える理由は、初期化/宣言ステートメントのためです。一部のコンパイラはこれを次のように解釈します。
array[10]
と別のint
ブロックにint
sのブロックを10個割り当てます。簡単にするためにすぐ隣に配置します。」array[10]
がi
を指さないようにします。i
をarray[-1]
に割り当てます(配列のインデックスは負にできない、またはすべきではないため)、またはOSが処理できるため、まったく異なる場所に割り当てます。 より安全コンパイラーの中には、もっと速くしたいものもあれば、安全性を好むコンパイラーもいます。コンテキストがすべてです。たとえば、古代のBREW OS(基本的な携帯電話のOS)向けのアプリを開発していた場合、安全性は気にしません。私がiPhone 6向けに開発していた場合、何があっても高速で実行できるため、安全性を重視する必要があります。 (真剣に、AppleのApp Storeガイドラインを読んだり、SwiftおよびSwift 2.0の開発について読んだりしましたか?)
サイズ10の配列を作成したため、forループ条件は次のようになります。
int array[10],i;
for (i = 0; i <10 ; i++)
{
現在、array[10]
を使用してメモリから未割り当ての場所にアクセスしようとしており、ndefined behaviorが発生しています。未定義の動作とは、プログラムが未決定の方法で動作することを意味するため、実行ごとに異なる出力を提供できます。
まあ、Cコンパイラは伝統的に境界をチェックしません。プロセスに「属していない」場所を参照した場合、セグメンテーション違反が発生する可能性があります。ただし、ローカル変数はスタックに割り当てられ、メモリの割り当て方法によっては、配列(array[10]
)のすぐ上の領域がプロセスのメモリセグメントに属する場合があります。したがって、セグメンテーションフォールトトラップはスローされず、これが発生するようです。他の人が指摘したように、これはCでの未定義の動作であり、コードは不安定であると見なされる場合があります。 Cを学習しているので、コードの境界をチェックする習慣を身につけたほうがよいでしょう。
a[10]
への書き込み試行が実際にi
を上書きするようにメモリがレイアウトされる可能性を超えて、最適化コンパイラーは、コードがなければi
の値が10より大きいループテストに到達できないと判断する可能性もあります最初に存在しない配列要素a[10]
にアクセスしました。
その要素にアクセスしようとすると未定義の動作になるため、コンパイラはその時点以降にプログラムが何をするかに関して何の義務も負いません。より具体的には、コンパイラはループインデックスをチェックするためのコードを生成する義務を負わないため、ループインデックスが10を超える場合は、ループインデックスをチェックするためのコードを生成する義務はまったくありません。代わりに、<=10
テストが常にtrueを生成すると想定できます。これは、コードがa[10]
を書き込むのではなく読み取る場合でも当てはまることに注意してください。
i==9
を超えて反復する場合、実際に配置されている「配列項目」にゼロを割り当てます配列の過去なので、他のデータを上書きします。ほとんどの場合、a[]
の後にあるi
変数を上書きします。そうすれば、単純にi
変数をゼロにリセットし、ループを再開できます。
ループ内でi
を出力すると、あなた自身でそれを発見できます。
printf("test i=%d\n", i);
ただの代わりに
printf("test \n");
もちろん、その結果は、コンパイラーとその設定に依存する変数のメモリ割り当てに強く依存するため、一般的には未定義の動作です。そのため、異なるマシンまたは異なるオペレーティングシステムまたは異なるコンパイラでは異なる場合があります。
上記で見つけたものを提案します。
Array [i] = 20を割り当ててみてください。
私はこれがどこでもコードを終了するはずだと思う..(i <= 10またはllを維持すると)
これが実行された場合、ここで指定された回答がすでに正しいことをしっかりと決定できます[exのメモリストンプに関連する回答]。
エラーは部分array [10]にありますw/cはiのアドレスでもあります(int array [10]、i;)。 array [10]が0に設定されている場合、iは0になり、w/cはループ全体をリセットし、無限ループを引き起こします。 array [10]が0〜10の場合、無限ループが発生します。正しいループはfor(i = 0; i <10; i ++){...} int array [10]、i; for(i = 0; i <= 10; i ++)array [i] = 0;