一部のプラットフォームでこのforループが終了し、他のプラットフォームでは終了しないのはなぜですか?


240

最近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やコンピューターによって動作がどのように異なるのかがわかりません。


147
配列をオーバーランしているため、未定義の動作が発生します。未定義の動作とは、動作しているように見えても、何かが起こる可能性があることを意味します。したがって、「コードは決して終了してはならない」は有効な期待ではありません。
kaylum 2015年

37
正確には、Cに歓迎あなたの配列は、10個の要素を持っている- 0から9までの番号
Yetti99

14
@JonCavコードを壊しました。壊れたコードである未定義の動作を取得しています。
kaylum 2015年

50
まあ、全体の要点は、未定義の動作がまさにそれであるということです。あなたはそれを確実にテストして、定義された何かが起こることを証明することはできません。おそらくあなたのWindowsマシンで起こっていることは、変数iがの終わりの直後に格納されarray、そしてそれをで上書きしていることarray[10]=0;です。これは、同じプラットフォーム上で最適化されたビルドでは当てはまらない可能性があります。これはi、レジスタに格納され、メモリ内でそれをまったく参照しない場合があります。
paddy

46
予測不可能性は未定義の動作の基本的な特性だからです。あなたはこれを理解する必要があります...絶対にすべての賭けはオフです。
水田

回答:


356

Ubuntu 14.04を実行している私のラップトップでは、このコードは実行が完了するまで中断しません。CentOS 6.6を実行している私の学校のコンピューターでも、問題なく動作します。Windows 8.1では、ループが終了することはありません。

さらに奇妙なのは、forループの条件を編集すると、i <= 11Ubuntuを実行しているラップトップでのみコードが終了することです。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; 
} 

6
やあ、ありがとう!それは本当にかなり説明しました。Windowsでは、配列からのオフセットが10の場合、iと記載されていますが、CentOSとUbuntuの両方では、-1です。奇妙なのは、デバッガコードをコメントアウトすると、CentOSはコードを実行できない(ハングする)が、デバッグコードでは実行できるということです。これまでのところ、Cは非常に言語のようですX_x
JonCav

12
たとえば、書き込みがarray[10]スタックフレームを破壊する場合、@ JonCavが「ハングする」可能性があります。デバッグ出力の有無にかかわらず、コードにどのような違いがありますか?のアドレスがi必要ない場合は、コンパイラー最適化しiてしまう可能性があります。レジスタに入れて、スタック上のメモリレイアウトを変更します...
Hagen von Eitzen

2
ハングしているとは思いません。ループカウンターをメモリからリロードしているため、無限ループにあると思います(これにより、ゼロによってゼロになりましたarray[10]=0。最適化をオンにしてコードをコンパイルした場合、これはおそらく発生しません(Cがどの種類のメモリアクセスを制限するエイリアシングルールは、他のメモリとオーバーラップする可能性があると想定する必要があります。アドレスを取得しないローカル変数として、コンパイラは何もエイリアスしないと想定できるはずです。とにかく、最後に配列の動作は未定義です。常にそれに依存しないようにしてください
Peter Cordes

4
別の選択肢は、(質問の元のコードでは)観察可能な効果がないため、最適化コンパイラーが配列を完全に削除することです。したがって、結果のコードはその定数文字列を11回出力し、その後に定数サイズを出力するため、オーバーフローがまったく気付かなくなります。
Holger

9
@JonCav一般に、メモリ管理について詳しく知る必要はなく、代わりに未定義のコードを記述しないこと、具体的には、配列の最後を超えて記述しないでください...
T. Kiley

98

バグはこれらのコード間にあります。

int array[10],i;

for (i = 0; i <=10 ; i++)

array[i]=0;

array要素は10個しかないため、最後の反復でarray[10] = 0;はバッファオーバーフローが発生します。バッファオーバーフローは未定義の動作です。つまり、ハードドライブをフォーマットしたり、悪魔を鼻から飛ばしたりする可能性があります。

すべてのスタック変数が互いに隣接して配置されることはかなり一般的です。iarray[10]書き込み先に配置されている場合、UBはにリセットさi0、ループが終了しなくなります。

修正するには、ループ条件をに変更しますi < 10


6
Nitpick:root(または同等のもの)として実行している場合を除き、市場に出回っている健全なOSのハードドライブを実際にフォーマットすることはできません。
ケビン

26
@Kevinは、UBを呼び出すと、正気の主張を放棄します。
o11c

7
コードが正しいかどうかは関係ありません。OSはあなたにそれをさせません。
ケビン

2
@Kevinあなたがハードドライブをフォーマットする例は、それが事実であるずっと前に始まりました。(Cが生まれた)当時のUNIXでさえ、あなたがそのようなことをすることを許可することに非常に満足していました-そして今日でも、多くのディストリビューションはrm -rf /、あなたがrootではなくても、幸いにもすべてを削除し始めることができますもちろんドライブ全体を「フォーマット」しますが、それでもすべてのデータを破壊します。痛い。
Luaan

5
@Kevinですが未定義の動作はOSの脆弱性を悪用し、昇格して新しいハードディスクドライバをインストールし、ドライブのスクラブを開始します。
ラチェットフリーク

38

ループの最後の実行として、に書き込みarray[10]ますが、配列には10から9までの番号が付けられた10個の要素しかありません。C言語仕様では、これは「未定義の動作」であると記載されています。これが実際に意味することは、プログラムがintメモリ内の直後にあるサイズのメモリに書き込もうとすることarrayです。次に何が起こるかは、実際にはそこにあるものに依存し、これはオペレーティングシステムだけでなく、コンパイラ、コンパイラオプション(最適化設定など)、プロセッサアーキテクチャ、周囲のコードにも依存します。など、実行ごとに異なる可能性もあります。たとえば、アドレス空間のランダム化が原因です(おそらくこのおもちゃの例ではありませんが、実際には起こります)。いくつかの可能性が含まれます:

  • 場所は使用されませんでした。ループは正常に終了します。
  • ロケーションは、たまたま値0を持つ何かに使用されました。ループは正常に終了します。
  • 場所には、関数の戻りアドレスが含まれていました。ループは正常に終了しますが、プログラムはアドレス0にジャンプしようとするためクラッシュします。
  • 場所には変数が含まれますi。ループはi0 から再開するため、終了することはありません。
  • 場所には他の変数が含まれています。ループは正常に終了しますが、「興味深い」ことが起こります。
  • 場所が無効なメモリアドレスです。たとえばarray、仮想メモリページの最後にあり、次のページがマップされていないためです。
  • 悪魔はあなたの鼻から飛び出します。幸い、ほとんどのコンピューターには必要なハードウェアがありません。

Windowsで観察したのは、コンパイラが変数iをメモリ内の配列の直後に配置することを決定したため、array[10] = 0最終的にに割り当てられたことiです。UbuntuとCentOSでは、コンパイラはiそこに配置されませんでした。ほとんどすべてのCの実装は、ローカル変数をメモリ内のメモリスタックにグループ化します。ただし、1つの大きな例外があります。一部のローカル変数は完全にレジスタに配置できます。変数がスタック上にある場合でも、変数の順序はコンパイラーによって決定され、ソースファイル内の順序だけでなくそれらの型にも依存する可能性があります(メモリを無駄に配置して制約の制約に穴を開けないようにするため)。 、名前、コンパイラの内部データ構造で使用されるハッシュ値など

コンパイラが決定したことを知りたい場合は、アセンブラコードを表示するように指示できます。ああ、アセンブラを解読する方法を学びましょう(それを書くよりも簡単です)。GCC(および他の一部のコンパイラ、特にUnixの世界)では-S、バイナリの代わりにアセンブラコードを生成するオプションを渡します。たとえば、amd64でGCCを使用してコンパイルし、最適化オプション-O0(最適化なし)を使用して、手動でコメントを追加したループのアセンブラースニペットを次に示します。

.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配列の直前に配置されています。iに書き込みを行った場合は上書きしますarray[-1]。に変更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 -O0gcc -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を格納する場所を計算する算術を実行しても意味がないと判断しました。代わりに、そのアドレスはループ変数であり、境界を決定するための計算は、部分的にコンパイル時に実行され(11回の反復に配列要素ごとに4バイトを乗算して44を取得)、部分的に実行時に実行されますが、ループが開始する前にすべて(減算を実行して初期値を取得します)。

この非常に単純な例でも、コンパイラオプションを変更する(最適化をオンにarray[i]するarray[9-i])か、マイナーなものを変更する(to )か、明らかに無関係なものを変更する(への呼び出しを追加するuse_array)だけで、実行可能プログラムが生成するものに大きな違いをもたらすことがわかりましたコンパイラが行います。コンパイラの最適化は、未定義の動作を呼び出すプログラムでは直感的に見えない可能性がある多くのことを実行できます。そのため、未定義の動作は完全に未定義のままになります。実際のプログラムでは、トラックから少しでも逸脱すると、経験豊富なプログラマーであっても、コードの動作と実行すべき動作の関係を理解するのが非常に困難になる可能性があります。


25

Javaとは異なり、Cは配列の境界チェックを行いません。つまり、はありませんArrayIndexOutOfBoundsException。配列のインデックスが有効であることを確認する作業はプログラマーに任されています。故意にこれを行うと、未定義の動作につながり、何が起こる可能性があります。


配列の場合:

int array[10]

インデックスは0からの範囲でのみ有効です9。ただし、次のことを試みています。

for (i = 0; i <=10 ; i++)

array[10]ここにアクセスし、条件をi < 10


6
意図せずにそれを行うと、未定義の動作につながります-コンパイラーは判別できません!;-)
Toby Speight 2015年

1
マクロを使用して、エラーを警告としてキャストします。#define UNINTENDED_MISTAKE(EXP)printf( "Warning:" #EXP "mis \ n");
lkraider 2015年

1
あなたはわざと間違いをしている場合、私は意味、あなたにもそのように特定し、未定義の動作を避けるために、それが安全になるかもしれない; D
lkraider

19

境界違反があり、非終了プラットフォームでiは、ループの終わりで誤ってゼロに設定しているため、最初からやり直すと思います。

array[10]無効です; それは、10個の要素が含まれていarray[0]array[9]、そしてarray[10]11日です。次のよう 10、ループはbeforeで停止するように作成する必要があります。

for (i = 0; i < 10; i++)

どこarray[10]の土地は、実装定義で、面白いこと、お使いのプラットフォームの2に、それは上の土地iこれらのプラットフォームは明らかにした後、直接にレイアウトされ、arrayiはゼロに設定され、ループは永遠に続きます。他のプラットフォームでiは、の前arrayにあるか、array後にパディングがある場合があります。


valgrindはまだ有効な場所なので、これをキャッチできないと思いますが、ASANはキャッチできます。
o11c 2015年

13

あなたは宣言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++)

7

これは、で定義されていない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]あるため、あなたの初期化/宣言文です。一部のコンパイラはこれを次のように解釈します。

  • 「に10ブロックintのとarray[10]をもう1つ割り当てintます。簡単にするために、それらを隣同士に配置します。」
  • 以前と同じですが、1〜2スペース移動して、をarray[10]指さないようにしiます。
  • 以前と同じことを行いますが、iatに割り当てますarray[-1](配列のインデックスは負にならないか、負にしてはいけないため)、またはOSが処理できるため、完全に別の場所に割り当て、より安全です。

一部のコンパイラーは、処理をより速くしたい場合もあれば、安全性を優先する場合もあります。それはすべてコンテキストに関するものです。たとえば、古代のBREW OS(基本的な電話のOS)用のアプリを開発している場合、安全性は気になりません。iPhone 6向けに開発している場合は、どうしても高速に動作する可能性があるため、安全性に重点を置く必要があります。(まじめに、AppleのApp Storeガイドラインを読んだり、SwiftやSwift 2.0の開発について読んだりしていますか?)


注:「0、1、2、3、4、5、6、7、8、9」になるようにリストを入力しましたが、SOのマークアップ言語により、順序付けされたリストの位置が修正されました。
DDPWNAGE 2015

6

サイズ10の配列を作成したので、forループ条件は次のようになります。

int array[10],i;

for (i = 0; i <10 ; i++)
{

現在、メモリを使用array[10]して未割り当ての場所にアクセスしようとしていますが、未定義の動作が発生しています。未定義の動作とは、プログラムの動作が不定になるため、実行ごとに異なる出力が得られる可能性があることを意味します。


5

まあ、Cコンパイラは伝統的に境界をチェックしません。プロセスに「属していない」場所を参照すると、セグメンテーション違反が発生する可能性があります。ただし、ローカル変数はスタックに割り当てられ、メモリの割り当て方法によっては、配列(array[10])のすぐ外側の領域がプロセスのメモリセグメントに属する場合があります。したがって、セグメンテーションフォールトトラップはスローされず、それが発生しているようです。他の人が指摘したように、これはCでは未定義の動作であり、コードは不安定であると見なされる場合があります。Cを学習しているので、コードの境界をチェックする習慣を身に付けた方がよいでしょう。


4

a[10]実際に上書きしようとするためにメモリが配置されている可能性を超えて、コードに最初にアクセスしiないと、最適化コンパイラがi10より大きい値でループテストに到達できないと判断する可能性もあります。存在しない配列要素a[10]

その要素にアクセスする試みは未定義の動作となるため、コンパイラーは、その時点以降のプログラムの動作に関して義務を負いません。より具体的には、コンパイラーは、ループインデックスが10を超える可能性がある場合には、それをチェックするコードを生成する義務がないため、コードを生成してコードを生成する義務はまったくありません。代わりに、<=10テストが常にtrueを生成すると想定できます。これは、コードが書き込むのa[10]ではなく読み取る場合でも当てはまります。


3

あなたが過去を反復するとき、あなたi==9は実際に配列を越えて配置されている「配列項目」にゼロを割り当てます、それであなたは他のいくつかのデータを上書きします。おそらくi、の後にある変数を上書きしますa[]。そうすればi変数をゼロにリセットしてループを再開するだけです。

iループで印刷すると、自分でそれを見つけることができます。

      printf("test i=%d\n", i);

ただの代わりに

      printf("test \n");

もちろん、その結果は、変数のメモリ割り当てに強く依存します。これは、コンパイラとその設定に依存するため、通常は未定義の動作です。そのため、異なるマシン、異なるオペレーティングシステム、異なるコンパイラでは結果が異なる場合があります。


0

エラーは部分array [10]にあり、w / cもiのアドレスです(int array [10]、i;)。array [10]を0に設定すると、iは0 w / cになり、ループ全体がリセットされ、無限ループが発生します。array [10]が0〜10の場合は無限ループになります。正しいループは(i = 0; i <10; i ++){...} int array [10]、i; for(i = 0; i <= 10; i ++)array [i] = 0;


0

私が上で見つけた何かを提案します:

array [i] = 20を割り当ててみてください。

私はこれがどこでもコードを終了するはずだと思います..(あなたがi <= 10またはllを維持する場合)

これが実行された場合、ここで指定された答えがすでに正しいことを確実に判断できます[たとえば、メモリを踏み鳴らすことに関連する答え]。


-9

ここで2つの問題があります。スタックに表示されるように、int iは実際には配列要素array [10]です。インデックス付けが実際にarray [10] = 0になるように許可しているため、ループインデックスiは10を超えることはありませんfor(i=0; i<10; i+=1)

i ++は、K&Rが言うように「悪いスタイル」です。これは、1ではなくiのサイズでiをインクリメントします。i++はポインタ演算用で、i + = 1は代数用です。これはコンパイラに依存しますが、移植性のための良い慣例ではありません。


5
-1完全に間違っています。変数はiNOTan配列要素であるa[10]、すぐにスタックにそれを置くようにコンパイラのための義務、あるいは示唆ありません、後には a[] -それは、同様に、配列の前に位置する、またはいくつかの追加のスペースで分離することができます。たとえば、CPUレジスタなど、メインメモリの外部に割り当てることもできます。++ポインタではなく整数ではないことも正しくありません。完全に間違っているのは、「i ++がiのサイズだけiをインクリメントしている」ということです–言語定義の演算子の説明を読んでください!
CiaPan 2015年

これが、一部のプラットフォームでは機能し、他では機能しない理由です。それがウィンドウで永久にループする理由の唯一の論理的な説明です。I ++に関しては、整数ではなくポインタ演算です。聖書を読む...「Cプログラミング言語」。
カーニガンとリッチによる

1
OPでソースコードを読み取り、変数の宣言を見つけますiint型です。それは整数ではなく、ポインタ。へのインデックスとして使用される整数array
CiaPan 2015年

1
私がしたので、私がしたようにコメントしました。コンパイラにスタックチェックが含まれていない限り、I = 10が実際に参照している場合のスタック参照は問題ないことを理解しておく必要があるかもしれません。一部のコンパイルでは、配列インデックスがスタック領域の境界内にあります。コンパイラは愚かな修正ができません。コンパイルはこれがそうであるように見えるようにフィックスアップを作成するかもしれませんが、cプログラミング言語の純粋な解釈はこの規約をサポートせず、OPが移植性のない結果をもたらすと言っています。
SkipBerne 2015年

@SkipBerne:否定的なポイントが「授与」される前に、回答を削除することを検討してください。
Peter VARGA、2015
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.