web-dev-qa-db-ja.com

CでNULLポインターを逆参照すると、OSで何が起こりますか?

ポインタがあり、それをNULLで初期化するとします。

int* ptr = NULL;
*ptr = 10;

ptrがアドレスをポイントしておらず、値を割り当てているため、プログラムがクラッシュします。これは無効なアクセスです。では、問題は、OSの内部で何が起こるのかということです。ページ違反/セグメンテーション違反は発生しますか?カーネルはページテーブルも検索しますか?またはクラッシュはその前に発生しますか?

私はどのようなプログラムでもそのようなことをしないことを知っていますが、これはそのような場合にOSまたはコンパイラの内部で何が起こるかを知るためだけです。そして、それは重複した質問ではありません。

41
h4ck3d

短い答え:コンパイラ、プロセッサアーキテクチャ、特定のプロセッサモデル、OSなど、多くの要因に依存します。

長い答え(x86およびx86-64):最下位のレベルであるCPUに行きましょう。 x86およびx86-64では、そのコードは通常、次のような命令または命令シーケンスにコンパイルされます。

_movl $10, 0x00000000
_

「仮想メモリアドレス0に定数整数10を格納する」とあります。 インテル®64およびIA-32アーキテクチャーソフトウェア開発者向けマニュアル この命令が実行されたときに何が起こるかを詳しく説明しているので、要約して説明します。

CPUは、いくつかの異なるモードで動作できます。そのうちのいくつかは、はるかに古いCPUとの下位互換性のためのものです。最新のオペレーティングシステムは、ユーザーレベルのコードを保護モードと呼ばれるモードで実行します。このモードでは paging を使用して仮想アドレスを物理アドレスに変換します。

各プロセスについて、OSはアドレスのマッピング方法を示すページテーブルを保持します。ページテーブルは、CPUが理解できる特定の形式でメモリに格納されます(ユーザーコードで変更できないように保護されています)。発生するすべてのメモリアクセスについて、CPUはページテーブルに従ってそれを変換します。変換が成功すると、物理メモリの場所への対応する読み取り/書き込みが実行されます。

アドレス変換が失敗すると、興味深いことが起こります。すべてのアドレスが有効であるとは限らず、メモリアクセスによって無効なアドレスが生成されると、プロセッサはページ違反例外を発生させます。これにより、user mode(akacurrent privilege level(CPL))からの移行がトリガーされます3 on x86/x86-64)をカーネルモード(別名CPL 0)に変換し、割り込み記述子テーブル(IDT)。

カーネルは制御を取り戻し、例外からの情報とプロセスのページテーブルに基づいて、何が起こったかを把握します。この場合、ユーザーレベルのプロセスが無効なメモリ位置にアクセスしたことが認識され、それに応じて反応します。 Windowsでは、 構造化例外処理 を呼び出して、ユーザーコードが例外を処理できるようにします。 POSIXシステムでは、OSはSIGSEGVシグナルをプロセスに配信します。

他の場合では、OSはページ違反を内部的に処理し、何も起こらなかったかのように現在の場所からプロセスを再起動します。たとえば、スタックに大量のメモリを事前に割り当てるのではなく、スタックの最下部に ガードページ を配置して、スタックを必要に応じて制限まで拡張できます。同様のメカニズムが copy-on-write メモリを実現するために使用されます。

最近のOSでは、ページテーブルは通常、アドレス0を無効な仮想アドレスにするように設定されています。しかし、時にはそれを変更することが可能です。 Linuxでは、0を疑似ファイル_/proc/sys/vm/mmap_min_addr_に書き込むことにより、その後にmmap(2)を使用して仮想アドレス0をマップできます。その場合、nullポインターを逆参照してもページ違反は発生しません。

上記の説明は、元のコードがユーザー空間で実行されているときに何が起こるかについてのすべてです。しかし、これはカーネル内でも発生する可能性があります。カーネルは仮想アドレス0をマップすることができ(ユーザーコードよりもはるかに可能性が高いので)、そのようなメモリアクセスは正常です。しかし、マッピングされていない場合、何が起こるかはほぼ同じです。CPUがページフォールトエラーを発生させ、カーネルの事前定義されたポイントにトラップされ、カーネルが何が起こったかを調べ、それに応じて反応します。カーネルが例外から回復できない場合、通常は何らかの方法でパニックになります(kernel panickernel oops、またはWindowsのBSOD、たとえば)デバッグ情報をコンソールまたはシリアルポートに出力して停止する。

Linuxマシンでroot権限を獲得するために、攻撃者がカーネル内部からnullポインター逆参照のバグを悪用する方法の例については、 NULLについての大騒ぎ:カーネルのNULL逆参照の利用 も参照してください。

65
Adam Rosenfield

補足として、アーキテクチャの違いを強制するために、3文字の頭字語名で知られ、しばしば大きな原色と呼ばれる会社によって開発および保守されている特定のOSは、最も魅力的なNULL決定を持っています。

それらは、1つの巨大な「もの」のすべてのデータ(メモリとディスク)に128ビットの線形アドレス空間を利用します。それらのOSに従って、「有効な」ポインタmustがそのアドレス空間内の128ビット境界に配置されます。これは、ところで、ポインタを格納する構造体(パックされているかどうかにかかわらず)に魅力的な副作用を引き起こします。とにかく、プロセスごとの専用ページに隠されているのは、有効なポインターを置くことができるプロセスアドレス空間内のすべての有効な場所に対して1つのbitを割り当てるビットマップです。有効なメモリアドレスを生成して返し、それをポインタに割り当てることができるハードウェアとOS上のすべてのオペコードは、そのポインタ(ターゲットポインタ)が配置されているメモリアドレスを表すビットを設定します。

では、なぜ誰かが気にする必要があるのでしょうか。この単純な理由から:

int a = 0;
int *p = &a;
int *q = p-1;

if (p)
{
// p is valid, p's bit is lit, this code will run.
}

if (q)
{
   // the address stored in q is not valid. q's bit is not lit. this will NOT run.
}

本当に面白いのはこれです。

if (p == NULL)
{
   // p is valid. this will NOT run.
}

if (q == NULL)
{
   // q is not valid, and therefore treated as NULL, this WILL run.
}

if (!p)
{
   // same as before. p is valid, therefore this won't run
}

if (!q)
{
   // same as before, q is NOT valid, therefore this WILL run.
}

あなたが信じるために見なければならないその何か。特にポインター値をコピーしたり、動的メモリを解放したりするときに、そのビットマップを維持するためにハウスキーピングが行われることさえ想像できません。

6
WhozCraig

仮想メモリをサポートするCPUでは、メモリアドレス0x0で読み取ろうとすると、通常、ページフォールト例外が発行されます。 OSページフォールトハンドラーが呼び出され、OSはページが無効であると判断し、プログラムを中止します。

一部のCPUでは、メモリアドレス0x0にも安全にアクセスできることに注意してください。

C標準では、nullポインターの逆参照は未定義であると規定されているため、コンパイラーがコンパイル時(または実行時)にnullポインターの逆参照であることを検出できる場合、詳細なエラーメッセージでプログラムを中止するなど、必要な処理を実行できます。 。

(C99、6.5.3.2.p4)「ポインターに無効な値が割り当てられている場合、単項*演算子の動作は未定義です。87)」

87):「単項*演算子によってポインターを逆参照するための無効な値には、nullポインター、指し示されるオブジェクトのタイプに対して不適切に整列されたアドレス、およびその寿命の終了後のオブジェクトのアドレスがあります。」

4
ouah

typicalの場合、int *ptr = NULL;ptrをアドレス0を指すように設定します。C標準(およびC++標準)はnotを要求するように非常に注意していますが、extremelyです。それにもかかわらず一般的です。

あなたがするとき*ptr = 10;、CPUは通常、アドレス行に0を生成し、10データライン上で、R/Wラインを設定して書き込みを示します(そして、バスにそのようなものがある場合は、メモリ対I/Oラインをアサートして、I/Oではなくメモリへの書き込みを示します) 。

CPUがメモリ保護をサポートしている(そしてそれを有効にするOSを使用している)と仮定すると、CPUは(試みられた)アクセスが発生する前にそれをチェックします。たとえば、最新のIntel/AMD CPUは、仮想アドレスを物理アドレスにマップするページングテーブルを使用します。典型的なケースでは、アドレス0はany物理アドレスにマップされません。この場合、CPUはアクセス違反例外を生成します。かなり典型的な例の1つとして、Microsoft Windowsは最初の4メガバイトをマップされないままにするため、その範囲のanyアドレスは通常アクセス違反になります。

古いCPU(またはCPU保護機能を有効にしていない古いオペレーティングシステム)では、試行された書き込みは成功することがよくあります。たとえば、MS-DOSでは、NULLポインタを介して書き込むと、単にアドレス0に書き込まれます。小規模または中規模モデル(データに16ビットアドレスを使用)では、ほとんどのコンパイラーが既知のパターンをデータセグメントの最初の数バイトに書き込み、プログラムが終了すると、そのパターンがそのまま残っているかどうかを確認します(そして失敗した場合は、NULLポインタを介して書き込んだことを示すために何かを行います)。コンパクトモデルまたは大規模モデル(20ビットデータアドレス)では、警告なしにアドレス0に書き込むだけです。

3
Jerry Coffin

これはプラットフォームとコンパイラに依存していると思います。 NULLポインターは、NULLページを使用して実装できます。この場合、ページフォールトが発生します。または、拡張ダウンセグメントのセグメント制限を下回る場合(セグメンテーションフォールトが発生します)。

これは決定的な答えではなく、私の推測です。

0
Nathan Fellman