私が理解したように、チップのMMUは仮想アドレスを取得し、それを物理メモリアドレスに変換します。 MMU は次のタスクを実行します。
(1) プロセス別ページテーブルを参照してください。
(2) 仮想アドレスに対応するページが Resident Set にある場合は、そのアドレスを物理アドレスに変換する。
(3) 仮想アドレスに対応するページが常駐セットにない場合、ページフォルトが発生しカーネルで処理されます。
これで、プロセスのさまざまな部分で、などのbrk()
ページを作成および削除するためのシステムコールが必要であることがわかります。したがって、これらのシステムコールが行われるたびに、カーネルは常にプロセスのページテーブルを更新する機会があります。sbrk()
mmap()
munmap()
ただし、実行中のプロセスでは、%rsp
スタックポインタを10,000減らす必要があり、スタック領域を増やすことができ、スタックの深さの増加に対応するために複数のページを割り当てる必要があります。
MMUの私の理解が正しい場合、%rsp
変更が発生したときにMMUはページエラーを生成しません(アドレスが最初からプロセステーブルにないため)。この場合、MMUはカーネルに通知するために何をしますか?
ベストアンサー1
実行中のプロセスは、スタックポインタ%rspを10,000減らすことでスタック領域を増やすことができ、スタックの深さの増加に対応するために複数のページを割り当てる必要があります。
MMUの私の理解が正しい場合、%rspが変更されてもMMUはページフォールトを生成しません(アドレスが最初からプロセステーブルにないため)。この場合、MMUはカーネルに通知するために何をしますか?
%rspを変更してもページエラーは発生しません。ページフォルトは、メモリの読み書き時にのみ発生します。
マップされていないページに触れるといつもページエラーが発生しますが、カーネルのページエラーハンドラはこれが「有効な」ページエラーであるかどうかを判断し、論理マッピングを増やし、ページをハードウェアページテーブルにリンクすることができます。または「無効」と判断し、SIGSEGVを送信します。
スタックの成長は特別なケースです。通常、ページフォルトは既存のマッピング内でのみ機能します(たとえば、遅延割り当て、書き込み時のコピー、またはページがスワップスペースまたはそれをサポートするファイルとしてページアウトされる場合など)。マッピングされた論理状態はハードウェアページテーブルと一致する必要はないため、「ソフト」または「ハード」ページエラーが発生する可能性があります。バラより「push」または「sub」x86命令を使用すると、スタックメモリはどのように割り当てられますか?詳細については。
一部の実装にはスタックマップ拡張のための特別な場合があります。他の人はこれを行わずにスタック領域全体を事前マッピングします。これにより、ページフォルトも同じように機能しますmmap()
。具体的な通話方法mmap()
は糸スタックが割り当てられます。少なくともこれは、以下を使用してpthreadを生成するときのデフォルト値です。glibc。対照的に、初期スレッドのスタックはカーネルによって生成され、成長を可能にする特別なケースを実装します。 Linuxがスタックを拡張する方法の詳細については、以下を参照してください。
スタックの終わりをスキップすると、他のマッピングへの誤ったアクセスが発生する可能性があります。つまり、ページフォルトを引き起こすことなく対応するメモリを破損する可能性があります。これは「スタック競合」セキュリティの脆弱性です。悪意のある入力により、このようなことが発生する可能性があります。たとえば、alloca
非常に大きな配列またはC99可変長配列が割り当てられ、スタックポインタがアイテムをスキップします。
これらのスタックオーバーフローをすべて検出する保証された方法は、1)スタックの最後に「ガードページ」をマッピングし、2)スタックメモリを割り当てるときに一度に1ページずつ検索することです。この記事を書く時点では、GCCはスタックプローブの作成を完全にサポートしていません。。
x86-64 System V ABIは、スタックが毎回1ページ以上増加するときの精度を保証するためにスタックプローブを必要としません。したがって、gccは明示的に指示された場合にのみスタックプローブをエクスポートします。 (ほとんどのx86アーキテクチャではなく、Linuxで使用されるABIは同じだと思います。)Linuxでは、スタック検索はスタックサイズ制限を超えてスタックを増やそうとしたときにプログラムでエラーが発生するかどうかを確認するためにのみ必要です。
(面白い事実:Windowsするスタックに大きな配列を割り当てるには、各ページを順番にタッチする必要があります。 Windows用コンパイラは、スタックポインタを2ページ以上移動したり、変数を移動したりする際の精度を確保するために、常にスタックプローブをエクスポートする必要があります。できる1ページ以上です。 )
あえてスタックを増やすのはなぜですか?
固定サイズのスタックマップを使用するには、少なくとも1つの欠点があるようです。 mmap()を使用してこれらのスタックを作成すると、物理メモリを割り当てなくても、Linuxはそれを「コミットされた」メモリとして扱います。
LinuxはデフォルトでRAM +スワップオーバーコミットを許可しますが、経験的な方法を使用して「明白なアドレス空間オーバーコミット」を拒否します。本当に頑張るとき使用メモリがRAM +スワップを超えると、OOM(Out of Memory Killer)は、十分なメモリが解放されるまで実行するプログラムの選択を開始します。割り当ての拒否など、さまざまなポリシーを設定でき、コミットがRAM / 2 +スワップを超える可能性があります。
バラよりvm.overcommit_memory および vm.overcommit_ratio。
このメモは、以下のWindowsブログ記事にも記載されています。おそらく実装の違いは、Linuxの乱用とOOMキラーに対する人々の苦情の要因かもしれません。 :-).
[glibcのようなCランタイム]できる最初はほとんど書き込み不可能または読み取り不可能にし、失敗時に変更しますが、シグナルハンドラが必要であり、このソリューションはアプリケーションのシグナルハンドラを妨げるため、POSIXスレッド実装では許可されていません。 -StackOverflowのユーザー「R..」
MAP_GROWSDOWN
Linuxは、上記で引用したものと基本的に同じですが、カーネルに実装されている代替メカニズムを提供します。これは、プロセスの初期スタックを作成するときにカーネルが使用するものです。ただし、これはLinuxがメインスタックを最大値まで増やすことができるように仮想メモリを予約するため、実際には意味がありますulimit -s
。安全に+正しく動作するようにするいくつかの「魔法」はを通じて得ることができないので、スレッドスタックではmmap(MAP_GROWSDOWN)
動作しません。それ以外の場合、これは有効な選択になります。
「R..」は、スレッドスタックのオンデマンド送信をサポートするようにカーネルを変更し続けることを提案します。
さまざまな参考資料:
- LinuxのLLVMはスタックナビゲーションを実装しません。これになった未解決の問題メモリに安全なRust言語の場合、この問題は今解決されました(「すべてのレベル1プラットフォームで動作」)。
- 一部のソースでは、GCCはスタックナビゲーションを実装していると述べていますが、
-fstack-check
「残念ながら-fstack-check
私たちの目的には適していません」いくつかの理由。 - 最近の:"ほとんどのターゲットはスタック衝突保護を完全にサポートしていません。ただし、これらのターゲットでは動的
-fstack-clash-protection
スタック割り当てが保護されます。-fstack-clash-protection
ターゲットがサポートしている場合は、静的スタック割り当てに対して制限された保護を提供することもできます。-fstack-check=specific
」。 - CVE-2010-2240- Linuxはついに
MAP_GROWSDOWN
。 - スタックの衝突2017 - Linuxはついにスタックプローブがなければ1ページでは不十分であることに気づく
- 必須StackClashに関するLWN.net記事。
- Windowsの実装を説明するRaymond Chenのブログ投稿の一部:https://devblogs.microsoft.com/oldnewthing/?p=29563