233

範囲外の配列に (C で) アクセスするのはどれほど危険ですか? 配列の外側から読み取ったり (プログラムの他の部分で使用されるメモリにアクセスしたり、それ以上のメモリにアクセスしたりすることがわかりました)、または配列の外側のインデックスに値を設定しようとしていることがあります。プログラムがクラッシュすることもあれば、ただ実行されることもあり、予期しない結果しか得られません。

ここで知りたいのは、これが実際にどれほど危険なのかということです。それが私のプログラムに損害を与えたとしても、それほど悪くはありません。一方で、まったく関係のないメモリにどうにかしてアクセスできたために、プログラムの外部で何かが壊れた場合、それは非常に悪いことだと思います。「何でも起こり得る」、「セグメンテーションは最も悪い問題ではないかもしれない」、「ハードディスクがピンク色に変わり、ユニコーンが窓の下で歌っているかもしれない」などの多くの記事を読みました。

私の質問:

  1. 配列の外側から値を読み取ると、プログラム以外の何かに損傷を与える可能性がありますか? 物事を見ただけでは何も変わらないと思いますか、それともたまたま到達したファイルの「最後に開いた時間」属性を変更しますか?
  2. 配列の外側に値を設定すると、プログラム以外の何かに損傷を与える可能性がありますか? この スタック オーバーフローの質問から、任意のメモリ位置にアクセスでき、安全性が保証されていないことがわかります。
  3. 私は今、XCode 内から小さなプログラムを実行しています。それは、それ自体のメモリの外に到達できない私のプログラムの周りに特別な保護を提供しますか? XCodeに害を及ぼす可能性はありますか?
  4. 本質的にバグのあるコードを安全に実行する方法に関する推奨事項はありますか?

OSX 10.7、Xcode 4.6 を使用しています。

4

11 に答える 11

131

ISO C 標準 (言語の公式定義) に関する限り、その範囲外の配列にアクセスすると、「未定義の動作」が発生します。これの文字通りの意味は次のとおりです。

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

非規範的な注記はこれを拡張します:

未定義の可能性のある動作は、状況を完全に無視して予測できない結果をもたらすことから、翻訳中またはプログラム実行中に、環境に特有の文書化された方法で動作すること (診断メッセージの発行の有無にかかわらず)、翻訳または実行の終了 (診断メッセージの発行を伴う) にまで及びます。診断メッセージの)。

それが理論です。現実は何ですか?

「最良の」ケースでは、現在実行中のプログラムが所有する (プログラムの誤動作を引き起こす可能性がある) か、現在実行中のプログラムが所有していない(おそらくプログラムがセグメンテーション違反のようなものでクラッシュします)。または、プログラムが所有するメモリに書き込みを試みても、読み取り専用とマークされている場合があります。これにより、プログラムがクラッシュする可能性もあります。

これは、同時に実行されているプロセスを互いに保護しようとするオペレーティング システムでプログラムが実行されていることを前提としています。コードが「ベアメタル」で実行されている場合、たとえばそれが OS カーネルまたは組み込みシステムの一部である場合、そのような保護はありません。あなたの不正なコードは、その保護を提供するはずだったものです。その場合、場合によってはハードウェア (または近くの物や人) への物理的な損傷を含め、損傷の可能性がかなり高くなります。

保護された OS 環境であっても、保護は常に 100% ではありません。たとえば、特権のないプログラムがルート (管理) アクセスを取得することを許可するオペレーティング システムのバグがあります。通常のユーザー権限を持っていても、誤動作しているプログラムはリソース (CPU、メモリ、ディスク) を過剰に消費し、システム全体をダウンさせる可能性があります。多くのマルウェア (ウイルスなど) は、バッファ オーバーランを悪用して、システムへの不正アクセスを取得します。

(1 つの歴史的な例:コア メモリを備えた一部の古いシステムでは、タイトなループで単一のメモリ位置に繰り返しアクセスすると、文字通りそのメモリのチャンクが溶ける可能性があると聞いたことがあります。他の可能性としては、CRT ディスプレイの破壊、読み取りの移動などがあります。 /ドライブキャビネットの高調波周波数でディスクドライブのヘッドを書き込み、テーブルを横切って床に落ちる.)

そして、常に心配するSkynetがあります。

要するに、意図的に悪いことをするプログラムを書くことができたとしても、少なくとも理論的には、バグのあるプログラムが誤って同じことをする可能性があるということです。

実際には、MacOS X システムで実行されているバグのあるプログラムが、クラッシュよりも深刻な事態を引き起こす可能性はほとんどありませんしかし、バグのあるコードが本当に悪いことをするのを完全に防ぐことはできません。

于 2013-03-26T21:20:23.557 に答える
26

一般に、今日のオペレーティング システム (人気のあるオペレーティング システム) は、仮想メモリ マネージャーを使用して保護されたメモリ領域ですべてのアプリケーションを実行します。プロセスに割り当てられた/割り当てられた領域の外側のREAL空間に存在する場所を単純に読み書きするのは、(それ自体)それほど簡単ではないことがわかりました。

直接の答え:

  1. 読み取りが別のプロセスに直接ダメージを与えることはほとんどありませんが、プログラム/プロセスの暗号化、復号化、または検証に使用される KEY 値を読み取った場合、間接的にプロセスにダメージを与える可能性があります。範囲外の読み取りは、読み取り中のデータに基づいて決定を下している場合、コードに多少の悪影響/予期しない影響を与える可能性があります

  2. メモリアドレスからアクセス可能な場所に書き込むことで実際に何かを損傷する唯一の方法は、書き込み先のメモリアドレスが実際にハードウェアレジスタ (実際にはデータストレージ用ではなく、ハードウェアの一部を制御するための場所) である場合です。 ) RAM の場所ではありません。実際、再書き込み可能ではない一度だけプログラム可能な場所 (またはその性質のもの) を書き込んでいない限り、通常は何かを損傷することはありません。

  3. 通常、デバッガー内から実行すると、コードがデバッグ モードで実行されます。デバッグ モードで実行すると、コードがより速く停止する傾向があります (常にではありません)。

  4. マクロを使用したり、配列インデックス境界チェックが既に組み込まれているデータ構造を使用したりしないでください。

追加 上記の情報は、メモリ保護ウィンドウを備えたオペレーティング システムを使用しているシステムのみを対象としていることを付け加えておきます。組み込みシステム、またはメモリ保護ウィンドウ (または仮想アドレス指定ウィンドウ) を持たないオペレーティング システム (リアルタイムまたはその他) を使用するシステムのコードを記述する場合は、メモリの読み取りと書き込みにもっと注意を払う必要があります。また、これらの場合、セキュリティの問題を回避するために、常に SAFE および SECURE コーディング プラクティスを採用する必要があります。

于 2013-03-26T20:59:29.707 に答える
11

境界をチェックしないと、セキュリティ ホールなどの厄介な副作用につながる可能性があります。厄介な問題の 1 つは、任意のコードの実行です。古典的な例: 固定サイズの配列strcpy()があり、そこにユーザー指定の文字列を配置するために使用する場合、ユーザーは、バッファーをオーバーフローさせ、他のメモリ位置を上書きする文字列を与えることができます。これには、関数が CPU から返されるコード アドレスが含まれます。終わります。

つまり、ユーザーは、プログラムが実質的に を呼び出す文字列を送信できることを意味します。これにより、その文字列がexec("/bin/sh")シェルになり、すべてのデータの収集やマシンのボットネット ノードへの変換など、システム上で実行したいことはすべて実行されます。

これを行う方法の詳細については、楽しみと利益のためにスタックを破壊するを参照してください。

于 2013-03-27T11:50:41.333 に答える
9

あなたが書く:

「何でも起こり得る」、「セグメンテーションは最も悪い問題ではないかもしれない」、「ハードディスクがピンク色に変わり、ユニコーンが窓の下で歌っているかもしれない」などの多くの記事を読みました。

そのように言いましょう:銃を装填します。特に狙いを定めずに窓の外に向けて発砲。危険は何ですか?

問題は、あなたが知らないということです。コードがプログラムをクラッシュさせる何かを上書きしても、定義された状態で停止するので問題ありません。ただし、クラッシュしない場合は、問題が発生し始めます。あなたのプログラムの管理下にあるリソースはどれですか?また、それらにどのような影響を与える可能性がありますか? このようなオーバーフローによって引き起こされた重大な問題を少なくとも 1 つ知っています。この問題は、実稼働データベースの無関係な変換テーブルを台無しにする、一見無意味な統計関数にありました。その結果、後で非常にコストのかかるクリーンアップが行われました。実際、この問題でハードディスクがフォーマットされていれば、はるかに安価で簡単に処理できたはずです...言い換えれば、ピンクのユニコーンはあなたにとって最も問題がないかもしれません.

オペレーティング システムが保護してくれるという考えは楽観的です。可能であれば、範囲外の書き込みを避けるようにしてください。

于 2013-03-26T20:54:00.227 に答える
7

プログラムをルートまたは他の特権ユーザーとして実行しないことは、システムに害を及ぼさないため、一般的にはこれが良い考えです。

ランダムなメモリ位置にデータを書き込むことにより、各プロセスが独自のメモリ空間で実行されるため、コンピューターで実行されている他のプログラムを直接「損傷」することはありません。

プロセスに割り当てられていないメモリにアクセスしようとすると、オペレーティング システムはプログラムの実行をセグメンテーション違反で停止します。

したがって、直接 (root として実行して /dev/mem などのファイルに直接アクセスすることなく)、プログラムがオペレーティング システムで実行されている他のプログラムに干渉する危険はありません。

とはいえ、これは危険という観点から聞いたことがあるかもしれませんが、ランダムなデータをランダムなメモリ位置に誤って書き込むと、損傷できるものはすべて損傷する可能性があります。

たとえば、プログラムは、プログラムのどこかに保存されているファイル名で指定された特定のファイルを削除したい場合があります。誤ってファイル名が保存されている場所を上書きした場合、代わりにまったく異なるファイルを削除する可能性があります。

于 2013-03-26T20:50:28.903 に答える
4

コードをテストするときに、 Valgrindmemcheckでこのツールを使用してみてください。スタック フレーム内の個々の配列境界違反は検出されませんが、他の多くの種類のメモリの問題を検出する必要があります。単一の機能の範囲外の問題。

マニュアルから:

Memcheck は、メモリ エラー検出器です。C および C++ プログラムで一般的な次の問題を検出できます。

  • ヒープ ブロックのオーバーランとアンダーラン、スタックのトップのオーバーラン、解放後のメモリへのアクセスなど、すべきでないメモリへのアクセス。
  • 未定義の値、つまり初期化されていない値、または他の未定義の値から派生した値を使用する。
  • ヒープ ブロックの二重解放、malloc/new/new[] と free/delete/delete[] の使用の不一致など、ヒープ メモリの不適切な解放
  • memcpy および関連する関数での src および dst ポインタの重複。
  • メモリ リーク。

ETA:ただし、Kaz の回答が示すように、これは万能薬ではなく、特に刺激的なアクセス パターンを使用している場合は、常に最も役立つ出力が得られるとは限りません。

于 2013-03-27T01:33:52.180 に答える
4

NSArrayObjective-C の s には、特定のメモリ ブロックが割り当てられます。配列の境界を超えると、配列に割り当てられていないメモリにアクセスすることになります。これの意味は:

  1. このメモリは任意の値を持つことができます。データ型に基づいてデータが有効かどうかを知る方法はありません。
  2. このメモリには、秘密鍵やその他のユーザー資格情報などの機密情報が含まれる場合があります。
  3. メモリ アドレスが無効であるか、保護されている可能性があります。
  4. メモリは、別のプログラムまたはスレッドによってアクセスされているため、値が変化する可能性があります。
  5. メモリ マップド ポートなど、メモリ アドレス空間を使用するものもあります。
  6. 不明なメモリ アドレスにデータを書き込むと、プログラムがクラッシュし、OS のメモリ空間が上書きされ、通常は太陽が内破する可能性があります。

プログラムの観点からは、コードがいつ配列の境界を超えているかを常に知りたいと思うでしょう。これにより、不明な値が返され、アプリケーションがクラッシュしたり、無効なデータが提供されたりする可能性があります。

于 2013-03-26T20:54:09.777 に答える
3

システム レベルのプログラミングや組み込みシステムのプログラミングを行う場合、ランダムなメモリ位置に書き込むと、非常に悪いことが起こる可能性があります。古いシステムと多くのマイクロコントローラはメモリ マップド IO を使用するため、特に非同期で行われる場合、ペリフェラル レジスタにマップされるメモリ位置への書き込みは大混乱を引き起こす可能性があります。

例として、フラッシュ メモリのプログラミングがあります。メモリ チップのプログラミング モードは、チップのアドレス範囲内の特定の場所に特定のシーケンスの値を書き込むことによって有効になります。それが行われている間に別のプロセスがチップ内の他の場所に書き込むと、プログラミング サイクルが失敗します。

場合によっては、ハードウェアがアドレスをラップする (アドレスの最上位ビット/バイトは無視される) ため、物理アドレス空間の末尾を超えてアドレスに書き込むと、実際にはデータが途中で書き込まれることになります。

最後に、MC68000 のような古い CPU は、ハードウェアのリセットのみが再開できるポイントまでロックする可能性があります。数十年間それらに取り組んでいませんでしたが、例外を処理しようとしているときにバスエラー (存在しないメモリ) が発生したときだと思います。ハードウェアリセットがアサートされるまで停止するだけです。

私の最大の推奨事項は、製品のあからさまなプラグインですが、個人的には興味がなく、それらとはまったく関係がありません - しかし、信頼性が重要であった C プログラミングと組み込みシステムの数十年に基づいて、Gimpel の PC Lint はこの種のエラーを検出するだけでなく、悪い習慣について絶えず非難することで、より優れた C/C++ プログラマーになれるようにします。

また、誰かからコピーを入手できる場合は、MISRA C コーディング標準を読むことをお勧めします。最近のものは見たことがありませんが、昔は、彼らがカバーすることをすべき/すべきでない理由について、良い説明をしてくれました.

あなたのことはわかりませんが、アプリケーションからコアダンプまたはハングアップが発生するのは 2 回目か 3 回目です。4回目か5回目、そしてパッケージが何であれ、棚に置かれ、入ってきたパッケージ/ディスクの中央に木製の杭を打ち込み、二度と戻ってこないようにします。

于 2013-04-04T01:53:20.837 に答える
2

私は DSP チップ用のコンパイラを使用しています。このコンパイラは、C コードから配列の終わりを超えてアクセスするコードを意図的に生成しますが、そうではありません!

これは、反復の終わりに次の反復のために一部のデータをプリフェッチするようにループが構造化されているためです。したがって、最後の繰り返しの最後にプリフェッチされたデータは、実際には使用されません。

そのような C コードを書くと、未定義の動作が発生しますが、それは移植性を最大化することに関する標準文書の形式にすぎません。

多くの場合、範囲外にアクセスするプログラムは巧妙に最適化されていません。それは単にバグです。コードはガベージ値をフェッチし、前述のコンパイラの最適化されたループとは異なり、コードはその後の計算で値を使用するため、値が破損します。

そのようなバグをキャッチすることは価値があるため、その理由だけでも動作を未定義にする価値があります。ランタイムが「main.c の 42 行目の配列オーバーラン」のような診断メッセージを生成できるようにするためです。

仮想メモリを備えたシステムでは、後に続くアドレスが仮想メモリのマップされていない領域にあるように配列が割り当てられることがあります。アクセスすると、プログラムが爆撃されます。

余談ですが、C では、配列の末尾を 1 つ過ぎたポインタを作成することが許可されていることに注意してください。そして、このポインターは、配列の内部へのポインターよりも大きくなければなりません。これは、C 実装がメモリの最後に配列を配置できないことを意味します。この場合、1 プラスのアドレスがラップアラウンドし、配列内の他のアドレスよりも小さく見えます。

それにもかかわらず、初期化されていない値や範囲外の値へのアクセスは、移植性が最大ではない場合でも有効な最適化手法である場合があります。これは、たとえば、Valgrind ツールが初期化されていないデータへのアクセスが発生したときに報告せず、プログラムの結果に影響を与える可能性のある何らかの方法で値が後で使用された場合にのみ報告する理由です。「xxx:nnn の条件付き分岐は初期化されていない値に依存します」のような診断が表示され、その発生源を突き止めるのが難しい場合があります。このようなすべてのアクセスがすぐにトラップされた場合、適切に手動で最適化されたコードだけでなく、コンパイラで最適化されたコードからも多くの誤検知が発生します。

そういえば、Linux に移植して Valgrind で実行すると、これらのエラーが発生するベンダーのコーデックを使用していました。しかし、ベンダーは、数ビットしかないと私に確信させました使用されている値の実際には初期化されていないメモリからのものであり、それらのビットはロジックによって慎重に回避されました。値の適切なビットのみが使用され、Valgrind には個々のビットまで追跡する機能がありません。初期化されていない素材は、エンコードされたデータのビット ストリームの末尾を超えて 1 ワードを読み取った結果ですが、コードはストリーム内のビット数を認識しており、実際のビット数よりも多くのビットを使用することはありません。ビット ストリーム配列の末尾を超えたアクセスは、DSP アーキテクチャに悪影響を与えないため (配列の後に仮想メモリがなく、メモリ マップ ポートがなく、アドレスがラップされない)、これは有効な最適化手法です。

ISO Cによると、C標準で定義されていないヘッダーを単にインクルードしたり、プログラム自体またはC標準で定義されていない関数を呼び出したりすることは、未定義の例であるため、「未定義の動作」は実際にはあまり意味がありません行動。未定義の動作は、「地球上の誰にも定義されていない」という意味ではなく、単に「ISO C 標準で定義されていない」という意味です。しかしもちろん、未定義の動作は実際には誰にも定義されていないことがあります。

于 2013-03-26T23:02:44.597 に答える
1

あなた自身のプログラムを除いて、あなたが何かを壊すことはないと思います.最悪の場合、カーネルがプロセスに割り当てなかったページに対応するメモリアドレスから読み書きしようとし、適切な例外を生成します.そして殺されます(つまり、あなたのプロセス)。

于 2013-03-26T20:49:03.390 に答える