範囲外の配列にアクセスしてもエラーは発生しません、なぜですか?


177

C ++プログラムで次のように範囲外の値を割り当てています。

#include <iostream>
using namespace std;
int main()
{
    int array[2];
    array[0] = 1;
    array[1] = 2;
    array[3] = 3;
    array[4] = 4;
    cout << array[3] << endl;
    cout << array[4] << endl;
    return 0;
}

プログラムはとを印刷34ます。それは不可能であるべきです。私はg ++ 4.3.3を使用しています

ここにコンパイルして実行するコマンドがあります

$ g++ -W -Wall errorRange.cpp -o errorRange
$ ./errorRange
3
4

割り当て時のみarray[3000]=3000、セグメンテーション違反が発生します。

gccが配列の境界をチェックしない場合、後で深刻な問題が発生する可能性があるため、プログラムが正しいかどうかをどのように確認できますか?

上記のコードを

vector<int> vint(2);
vint[0] = 0;
vint[1] = 1;
vint[2] = 2;
vint[5] = 5;
cout << vint[2] << endl;
cout << vint[5] << endl;

これもエラーにはなりません。



16
もちろん、コードにはバグがありますが、未定義の動作を生成します。未定義は、完了するまで実行される場合とされない場合があることを意味します。クラッシュの保証はありません。
dmckee ---元モデレーターの子猫、

4
生の配列をめちゃくちゃにしないことで、プログラムが正しいことを確認できます。C ++プログラマーは、組み込み/ OSプログラミングを除き、代わりにコンテナークラスを使用する必要があります。コンテナーを使用する理由については、こちらをお読みください。parashift.com/c++-faq-lite/containers.html
jkeys

8
ベクトルは必ずしも[]を使用して範囲をチェックするわけではないことに注意してください。.at()の使用は[]と同じですが、範囲チェックを行います。
David Thornley、

4
範囲外の要素にアクセスする場合、A vector 自動サイズ変更しません!ただのUBです!
Pavel Minaev 2009

回答:


364

すべてのC / C ++プログラマーの親友、未定義の動作へようこそ。

さまざまな理由から、言語標準で指定されていないものはたくさんあります。これはその1つです。

一般に、未定義の動作が発生した場合は常に、何かが起こる可能性があります。アプリケーションがクラッシュしたり、フリーズしたり、CD-ROMドライブがイジェクトされたり、悪魔が鼻から出たりする可能性があります。ハードドライブをフォーマットしたり、すべてのポルノを祖母にメールで送信したりできます。

本当に運が悪い場合でも、正しく動作しているように見えるかもしれません。

言語は、配列の境界の要素にアクセスした場合に何が起こるかを単純に述べています。境界を超えるとどうなるかは定義されていません。コンパイラーでは、今日は機能しているように見えるかもしれませんが、CまたはC ++は合法ではなく、次にプログラムを実行するときにも機能するという保証はありません。それとも、今でも上書きされた重要なデータを持っていない、とあなたはそれがあること、問題が発生していないことをされた原因に行く-まだ。

なぜ何の境界チェックがない、答えにカップルの側面があります:

  • 配列は、Cからの残り物です。C配列は、取得可能な限りプリミティブです。連続したアドレスを持つ一連の要素。生のメモリを公開するだけなので、境界チェックはありません。堅牢な境界チェックメカニズムを実装することは、Cではほとんど不可能でした。
  • C ++では、クラス型で境界チェックが可能です。しかし、配列はまだ古いC互換の配列です。クラスではありません。さらに、C ++は、境界チェックを非理想的なものにする別のルールにも基づいています。C ++の指針となる原則は、「使用しないものについては支払いをしない」です。コードが正しければ、境界チェックは不要です。また、実行時の境界チェックのオーバーヘッドを強制されることはありません。
  • そのため、C ++はstd::vectorクラステンプレートを提供し、両方を許可します。operator[]効率的になるように設計されています。言語標準では、境界チェックを実行する必要はありません(ただし、これも禁止されていません)。ベクトルには、境界チェックの実行が保証されているat()メンバー関数もあります。したがって、C ++では、ベクトルを使用すると、両方の長所が得られます。あなたは、配列のような境界チェックなしのパフォーマンスを得る、そしてあなたがそれをしたいときには、使用範囲チェックがアクセスする能力を取得します。

5
@Jaif:この配列のものを長い間使用していますが、それでもなぜこのような単純なエラーをチェックするテストがないのですか?
seg.server.fault 2009

7
C ++の設計原則は、同等のCコードより遅くなることはなく、Cは配列の境界チェックを行いません。Cの設計原則は、システムプログラミングを目的としているため、基本的に高速でした。配列の境界チェックには時間がかかるため、実行されません。C ++でのほとんどの用途では、とにかく配列ではなくコンテナを使用する必要があります。それぞれ.at()または[]を介して要素にアクセスすることにより、境界チェックまたは非境界チェックを選択できます。
KTC

4
@segそのようなチェックは、いくらかコストがかかります。正しいコードを書けば、その代償を払う必要はありません。そうは言っても、チェックされているstd :: vectorのat()メソッドへの完全な変換になりました。それを使用すると、「正しい」コードだと私が思ったものにかなりの数のエラーが現れました。

10
GCCの古いバージョンは、特定のタイプの未定義の動作に遭遇したときに、実際にEmacsとハノイの塔のシミュレーションを起動したと思います。私が言ったように、が起こるかもしれません。;)
jalf

4
すべてがすでに述べられているので、これは小さな補遺を保証するだけです。デバッグビルドは、リリースビルドと比較すると、これらの状況で非常に寛容になる場合があります。デバッグ情報はデバッグバイナリに含まれているため、重要なものが上書きされる可能性は低くなります。そのため、リリースビルドがクラッシュしても、デバッグビルドは正常に動作するように見えることがあります。
リッチ

31

g ++を使用すると、コマンドラインオプションを追加できます -fstack-protector-all

あなたの例では、次のようになりました:

> g++ -o t -fstack-protector-all t.cc
> ./t
3
4
/bin/bash: line 1: 15450 Segmentation fault      ./t

それは本当にあなたが問題を見つけたり解決を支援しませんが、少なくとも、セグメンテーション違反は、あなたがいることを知るようになる何かが間違っています。


10
さらに良いオプションを見つけました:-fmudflap
Hi-Angel

1
@ Hi-Angel:最新の同等機能は-fsanitize=address、コンパイル時(最適化の場合)と実行時の両方でこのバグをキャッチします。
ネイト・エルドレッジ

@NateEldredge +1、最近では私も使用しています-fsanitize=undefined,address。ただし、サニタイザーによって境界外のアクセスが検出されない場合、stdライブラリではまれなまれなケースがあることに注意してください。このため-D_GLIBCXX_DEBUG、さらにチェックを追加するオプションを追加で使用することをお勧めします。
Hi-Angel

12

g ++は配列の境界をチェックしないため、3,4で何かを上書きしている可能性がありますが、それよりも大きな数値を使用すると、クラッシュが発生します。

スタックの使用されていない部分を上書きしているだけで、スタックに割り当てられたスペースの最後に到達し、最終的にクラッシュするまで続けることができます

編集:あなたはそれに対処する方法がありません、おそらく静的コードアナライザーはそれらの失敗を明らかにするかもしれませんが、それはあまりに単純で、静的アナライザーであっても検出されない同様の(しかしより複雑な)失敗があるかもしれません


6
そこからarray [3]とarray [4]のアドレスに「本当に重要なもの」がない場合、どこで入手できますか?
namezero

7

私の知る限り、これは未定義の動作です。それで大きなプログラムを実行すると、途中でクラッシュします。境界チェックは生の配列(またはstd :: vector)の一部ではありません。

std::vector::iterator代わりにstd :: vectorを's とともに使用して、心配する必要がないようにします。

編集:

楽しみのために、これを実行して、クラッシュするまでの時間を確認します。

int main()
{
   int array[1];

   for (int i = 0; i != 100000; i++)
   {
       array[i] = i;
   }

   return 0; //will be lucky to ever reach this
}

Edit2:

それを実行しないでください。

Edit3:

OK、配列とポインタとの関係についての簡単なレッスンがあります。

配列のインデックスを使用すると、実際には変装したポインタ( "参照"と呼ばれます)が使用され、自動的に逆参照されます。これが、*(array [1])の代わりに、array [1]が自動的にその値の値を返す理由です。

次のように、配列へのポインタがある場合:

int array[5];
int *ptr = array;

次に、2番目の宣言の「配列」は、最初の配列へのポインタに実際に減衰しています。これはこれと同等の動作です。

int *ptr = &array[0];

割り当てた範囲を超えてアクセスしようとすると、実際には他のメモリへのポインタを使用しているだけです(これはC ++では問題になりません)。上記の私のプログラム例を見ると、これはこれと同等です。

int main()
{
   int array[1];
   int *ptr = array;

   for (int i = 0; i != 100000; i++, ptr++)
   {
       *ptr++ = i;
   }

   return 0; //will be lucky to ever reach this
}

プログラミングでは、多くの場合、他のプログラム、特にオペレーティングシステムと通信する必要があるため、コンパイラは文句を言いません。これは、ポインタを使用してかなり行われます。


3
最後の例で「ptr」を増やすのを忘れたと思います。明確に定義されたコードを誤って作成しました。
ジェフレイク

1
はは、生の配列を使用すべきではない理由を見てください。
jkeys 2009

「これが、*(array [1])の代わりに、array [1]が自動的にその値の値を返す理由です。」*(array [1])が正しく機能しますか?私はそれが*(array + 1)であると思う ps:笑、それは過去にメッセージを送るようなものです。しかし、とにかく:
ムユスタン

5

ヒント

範囲エラーチェックを備えた高速な制約サイズの配列が必要な場合は、boost :: arrayを使用してみてください(次のC ++仕様では、std :: tr1 :: array<tr1/array>も標準のコンテナーになります)。std :: vectorよりもはるかに高速です。int array []のように、ヒープまたはクラスインスタンス内のメモリを予約します。
これは簡単なサンプルコードです:

#include <iostream>
#include <boost/array.hpp>
int main()
{
    boost::array<int,2> array;
    array.at(0) = 1; // checking index is inside range
    array[1] = 2;    // no error check, as fast as int array[2];
    try
    {
       // index is inside range
       std::cout << "array.at(0) = " << array.at(0) << std::endl;

       // index is outside range, throwing exception
       std::cout << "array.at(2) = " << array.at(2) << std::endl; 

       // never comes here
       std::cout << "array.at(1) = " << array.at(1) << std::endl;  
    }
    catch(const std::out_of_range& r)
    {
        std::cout << "Something goes wrong: " << r.what() << std::endl;
    }
    return 0;
}

このプログラムは印刷します:

array.at(0) = 1
Something goes wrong: array<>: index out of range

4

CまたはC ++は、配列アクセスの境界をチェックしません。

スタックに配列を割り当てています。配列のインデックス付けはarray[3]*と同等です(array + 3)。ここで、arrayは&array [0]へのポインターです。これにより、未定義の動作が発生します。

Cで時々これをキャッチする1つの方法は、スプリントなどの静的チェッカーを使用することです。実行すると:

splint +bounds array.c

オン、

int main(void)
{
    int array[1];

    array[1] = 1;

    return 0;
}

次に警告が表示されます:

array.c:(関数main内)array.c:5:9:範囲外の可能性が高いストア:array [1]制約を解決できません:前提条件を満たすために0> = 1が必要です:必要なmaxSet(array @ array .c:5:9)> = 1メモリの書き込みは、割り当てられたバッファを超えたアドレスに書き込む場合があります。


修正:OSまたは別のプログラムによって既に割り当てられています。彼は他の記憶を上書きしています。
jkeys 2009

1
「C / C ++は境界をチェックしない」と言うのは完全に正しくはありません-特定の準拠した実装がデフォルトで、またはいくつかのコンパイルフラグを使用してそうすることを妨げるものは何もありません。それはそれらのどれも気にしないというだけです。
Pavel Minaev 2009

3

あなたは確かにあなたのスタックを上書きしていますが、プログラムは非常にシンプルなので、その影響は気付かれません。


2
スタックが上書きされるかどうかは、プラットフォームによって異なります。
クリスクリーランド

3

これをValgrindで実行すると、エラーが表示される場合があります。

Falainaが指摘したように、valgrindはスタック破損の多くのインスタンスを検出しません。私はvalgrindの下でサンプルを試してみましたが、実際にエラーは報告されません。ただし、Valgrindは他の多くのタイプのメモリの問題を見つけるのに役立ちます。--stack-checkオプションを含めるようにbulidを変更しない限り、この場合は特に役に立ちません。サンプルをビルドして実行すると

g++ --stack-check -W -Wall errorRange.cpp -o errorRange
valgrind ./errorRange

valgrind エラー報告します。


2
実際、Valgrindは、スタック上の不正な配列アクセスを判断するのが非常に困難です。(そして当然のことながら、それができる最善のことは、スタック全体を有効な書き込み場所としてマークすることです)
Falaina

@Falaina-良い点ですが、Valgrindは少なくともいくつかのスタックエラーを検出できます。
トッドスタウト

そして、コンパイラは配列を最適化してリテラル3と4を出力するだけなので、valgrindはコードに何の問題もありません。gccが配列の境界をチェックする前に最適化が行われるため、範囲外の警告gccが行われます。持っていません。
Goswin von Brederlow

2

あなたの好意で働いている未定義の動作。どうやらあなたが盗みをしているどんな記憶でも、重要なものは何も持っていません。CとC ++は配列の境界チェックを行わないので、そのようなものはコンパイル時または実行時にキャッチされないことに注意してください。


5
いいえ、未定義の動作は、正常にクラッシュした場合に「動作します」。それが機能するように見えるとき、それは最悪の可能なシナリオについてです。
jalf

@JohnBode:次に、jalfのコメントに従って文言を修正した方がいいでしょう
Destructor

1

で配列を初期化するとint array[2]、2つの整数用のスペースが割り当てられます。しかし、識別子はarray単にそのスペースの始まりを指します。次にarray[3]and array[4]にアクセスすると、配列が十分に長い場合、コンパイラーはそのアドレスをインクリメントして、それらの値がどこにあるかを示します。array[42]最初にそれを初期化せずに何かにアクセスしてみてください、あなたはその場所ですでにメモリに存在することが起こったあらゆる値を取得することになります。

編集:

ポインタ/配列の詳細:http : //home.netcom.com/~tjensen/ptr/pointers.htm


0

int array [2]を宣言すると、それぞれ4バイトの2つのメモリ空間を予約します(32ビットプログラム)。コードでarray [4]と入力しても、有効な呼び出しに対応しますが、実行時にのみ未処理の例外がスローされます。C ++は手動のメモリ管理を使用します。これは実際にはプログラムをハッキングするために使用されたセキュリティ上の欠陥です

これは理解を助けることができます:

int * somepointer;

somepointer [0] = somepointer [5];


0

私が理解しているように、ローカル変数はスタックに割り当てられているため、自分のスタックの境界を超えると、他のローカル変数が上書きされるだけです。関数内で宣言された他の変数がないため、副作用は発生しません。最初の変数/配列の直後に別の変数/配列を宣言してみて、それがどうなるかを確認してください。


0

Cで 'array [index]'と書くと、機械語に変換されます。

翻訳は次のようになります:

  1. 「配列のアドレスを取得」
  2. '配列のオブジェクトタイプのサイズを取得する'
  3. '型のサイズにインデックスを掛ける'
  4. '結果を配列のアドレスに追加します'
  5. 「結果のアドレスにあるものを読む」

結果は、配列の一部である場合とそうでない場合があるものに対処します。機械命令の猛スピードと引き換えに、あなたはあなたのために物事をチェックするコンピュータのセーフティネットを失います。あなたが細心の注意を払っていれば、それは問題ではありません。ずさんなことをしたり、間違いをしたりすると、やけどを負うことになります。例外を引き起こす無効な命令を生成する場合とそうでない場合があります。


0

私がよく目にして実際に使用されてきた素晴らしいアプローチuint THIS_IS_INFINITY = 82862863263;は、配列の最後にいくつかのNULL型要素(またはのような作成された要素)を挿入することです。

次に、ループ条件チェックで、TYPE *pagesWordsある種のポインタ配列があります。

int pagesWordsLength = sizeof(pagesWords) / sizeof(pagesWords[0]);

realloc (pagesWords, sizeof(pagesWords[0]) * (pagesWordsLength + 1);

pagesWords[pagesWordsLength] = MY_NULL;

for (uint i = 0; i < 1000; i++)
{
  if (pagesWords[i] == MY_NULL)
  {
    break;
  }
}

配列がstruct型で満たされた場合、この解決策は意味をなさない。


0

今質問でstd :: vector :: atを使用して述べたように、問題を解決し、アクセスする前に境界チェックを行います。

最初のコードとしてスタックにある一定サイズの配列が必要な場合は、C ++ 11の新しいコンテナーstd :: arrayを使用します。ベクトルにはstd :: array :: at関数があります。実際、この関数は、意味を持つすべての標準コンテナに存在します。つまり、operator []が定義されている場合:(deque、map、unordered_map)std :: bitsetがstd :: bitsetと呼ばれる場合を除きます。 :テスト。


0

gccの一部であるlibstdc ++には、エラーチェック用の特別なデバッグモードがあります。コンパイラフラグで有効にし-D_GLIBCXX_DEBUGます。特にstd::vector、パフォーマンスを犠牲にして境界チェックを行います。これは、gccの最新バージョンを使用したオンラインデモです。

そのため、実際にはlibstdc ++デバッグモードで境界チェックを実行できますが、通常のlibstdc ++モードと比較してパフォーマンスが著しく低下するため、テスト時にのみ実行する必要があります。


0

プログラムを少し変更した場合:

#include <iostream>
using namespace std;
int main()
{
    int array[2];
    INT NOTHING;
    CHAR FOO[4];
    STRCPY(FOO, "BAR");
    array[0] = 1;
    array[1] = 2;
    array[3] = 3;
    array[4] = 4;
    cout << array[3] << endl;
    cout << array[4] << endl;
    COUT << FOO << ENDL;
    return 0;
}

(大文字の変更-これを試す場合は、小文字にしてください。)

変数fooがゴミ箱に移動したことがわかります。コード存在しないarray [3]とarray [4]に値を格納し、それらを適切に取得できますが、実際に使用されるストレージはfooからです。

したがって、元の例では配列の境界を超えて「逃げる」ことができますが、他の場所に損傷を与えることを犠牲にして、損傷を診断するのは非常に難しい場合があります。

なぜ自動境界チェックがないのか-正しく書かれたプログラムはそれを必要としません。いったんそれが行われると、実行時の境界チェックを行う理由はなく、そうすることはプログラムを遅くするだけです。設計およびコーディング中にすべてを把握するのが最善です。

C ++は、アセンブリ言語にできる限り近づくように設計されたCに基づいています。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.