最小スタック割り当て

最小スタック割り当て

私はELF仕様を勉強しています(http://www.skyfree.org/linux/references/ELF_Format.pdf) ですが、プログラムの読み込みプロセスに関して、スタックがどのように初期化されるか、また初期ページ サイズがどれくらいかがはっきりしません。テストは次のとおりです (Ubuntu x86-64 上)。

$ cat test.s
.text
  .global _start
_start:
  mov $0x3c,%eax
  mov $0,%edi
  syscall
$ as test.s -o test.o && ld test.o
$ gdb a.out -q
Reading symbols from a.out...(no debugging symbols found)...done.
(gdb) b _start
Breakpoint 1 at 0x400078
(gdb) run
Starting program: ~/a.out 

Breakpoint 1, 0x0000000000400078 in _start ()
(gdb) print $sp
$1 = (void *) 0x7fffffffdf00
(gdb) info proc map
process 20062
Mapped address spaces:

          Start Addr           End Addr       Size     Offset objfile
            0x400000           0x401000     0x1000        0x0 ~/a.out
      0x7ffff7ffa000     0x7ffff7ffd000     0x3000        0x0 [vvar]
      0x7ffff7ffd000     0x7ffff7fff000     0x2000        0x0 [vdso]
      0x7ffffffde000     0x7ffffffff000    0x21000        0x0 [stack]
  0xffffffffff600000 0xffffffffff601000     0x1000        0x0 [vsyscall]

ELF 仕様では、そもそもこのスタック ページが存在する仕組みや理由についてはほとんど触れられていませんが、スタックは SP が argc を指し、argv、envp、補助ベクトルがそのすぐ上に配置されるように初期化する必要があるという参考文献が見つかり、これは確認済みです。しかし、SP の下にはどれだけのスペースが使用可能でしょうか。私のシステムでは、SP の下には0x1FF00バイトがマップされていますが、これはおそらくスタックの先頭からカウントダウンしており0x7ffffffff000、完全なマッピングにはバイトがあります0x21000。この数値に影響を与えるものは何でしょうか。

スタックのすぐ下のページは「ガード ページ」であり、書き込むと自動的に書き込み可能になり、「スタックを下方向に拡大」する (おそらく、単純なスタック処理が「そのまま機能する」ようにするため) ことはわかっていますが、巨大なスタック フレームを割り当てると、ガード ページをオーバーシュートしてセグメント違反が発生する可能性があるため、プロセスの開始時にすでに適切に割り当てられているスペースの量を確認する必要があります。

編集: さらにデータを見ると、何が起こっているのかさらに不明瞭になります。テストは次のとおりです。

.text
  .global _start
_start:
  subq $0x7fe000,%rsp
  movq $1,(%rsp)
  mov $0x3c,%eax
  mov $0,%edi
  syscall

ここで定数のさまざまな値を試して0x7fe000何が起こるかを確認しましたが、この値ではセグメント違反が発生するかどうかは非決定的です。GDB によると、命令subq自体が mmap のサイズを拡張しますが、これは私には不可解です (Linux はレジスタの内容をどのように認識するのでしょうか?)。ただし、このプログラムは通常、何らかの理由で終了時に GDB をクラッシュさせます。GOT や PLT セクションを使用していないため、非決定性の原因が ASLR であるはずがありません。実行ファイルは、常に仮想メモリ内の同じ場所に毎回ロードされます。つまり、これは PID または物理メモリのランダム性が漏れ出ているのでしょうか? 全体として、ランダム アクセスに実際に合法的に使用できるスタックの量と、RSP の変更時または合法メモリの「範囲外」の領域への書き込み時に要求される量について、非常に混乱しています。

答え1

この質問はELFとはあまり関係ないと思います。私の知る限り、ELFは「フラットパック「プログラム イメージをファイルに分割し、最初の実行に備えて再アセンブルします。スタックの定義とその実装方法は、OS の動作が POSIX に昇格されていない場合、CPU 固有と OS 固有の間のどこかになります。 ただし、ELF 仕様では、スタックに何が必要かについていくつかの要求があることは間違いありません。

最小スタック割り当て

あなたの質問から:

スタックのすぐ下のページは「ガード ページ」であり、書き込むと自動的に書き込み可能になり、「スタックを下方向に拡大」する (おそらく、単純なスタック処理が「そのまま機能する」ようにするため) ことはわかっていますが、巨大なスタック フレームを割り当てると、ガード ページをオーバーシュートしてセグメント違反が発生する可能性があるため、プロセスの開始時にすでに適切に割り当てられているスペースの量を確認する必要があります。

これに関する信頼できる参考文献を見つけるのに苦労しています。しかし、これが間違っていることを示唆する、信頼できない参考文献を多数見つけました。

私が読んだところによると、ガード ページは最大スタック割り当て外のアクセスをキャッチするために使用され、「通常の」スタック増加には使用されません。実際のメモリ割り当て (ページをメモリ アドレスにマッピング) は、オンデマンドで行われます。つまり、スタック ベースとスタック ベース - 最大スタック サイズ + 1 の間にあるメモリ内のマッピングされていないアドレスにアクセスすると、CPU によって例外がトリガーされる可能性がありますが、カーネルは、セグメンテーション エラーをカスケードするのではなく、メモリのページをマッピングすることで例外を処理します。

したがって、最大割り当て内のスタックにアクセスしてもセグメンテーション違反は発生しません。

最大スタック割り当て

調査ドキュメントは、スレッド作成とイメージ読み込みに関するLinuxドキュメントの行に従う必要があります(フォーク(2)クローン(2)実行(2))。 execveのドキュメント興味深いことを述べています:

引数と環境のサイズの制限

...をちょきちょきと切る...

カーネル2.6.23以降では、ほとんどのアーキテクチャがソフトから得られるサイズ制限をサポートしています。RLIMIT_STACKリソース制限(参照取得制限(2)

...をちょきちょきと切る...

これは、制限がアーキテクチャでサポートされていること、また制限されている場所を参照していることを確認します(取得制限(2))。

RLIMIT_STACK

これはプロセス スタックの最大サイズ (バイト単位) です。この制限に達すると、SIGSEGV シグナルが生成されます。このシグナルを処理するには、プロセスは代替シグナル スタック (sigaltstack(2)) を使用する必要があります。

Linux 2.6.23 以降では、この制限によってプロセスのコマンドライン引数と環境変数に使用されるスペースの量も決まります。詳細については execve(2) を参照してください。

RSPレジスタを変更してスタックを拡張する

x86 アセンブラは分かりません。 しかし、SS レジスタが変更されたときに x86 CPU によってトリガーされる可能性がある「スタック エラー例外」に注目してください。 間違っていたら訂正してくださいしかし、x86-64 では SS:SP が単に「RSP」になったと私は信じています。したがって、私の理解が正しければ、スタック エラー例外は RSP の減少 ( subq $0x7fe000,%rsp) によってトリガーされる可能性があります。

こちらの222ページをご覧ください:出典: intel

答え2

すべてのプロセス メモリ領域 (コード、静的データ、ヒープ、スタックなど) には境界があり、領域外のメモリ アクセス、または読み取り専用領域への書き込みアクセスは CPU 例外を生成します。カーネルはこれらのメモリ領域を管理します。領域外のアクセスは、セグメンテーション エラー信号の形でユーザー空間に伝播します。

すべての例外が、領域外のメモリにアクセスすることによって生成されるわけではありません。領域内アクセスでも例外が生成されることがあります。たとえば、ページが物理メモリにマップされていない場合、ページ フォールト ハンドラーは実行中のプロセスに対してこれを透過的に処理します。

プロセスのメイン スタック領域には、最初は少数のページ フレームのみがマップされていますが、スタック ポインターを介してさらにデータがプッシュされると、自動的に拡大します。例外ハンドラーは、アクセスがまだスタック用に予約された領域内にあるかどうかを確認し、そうである場合は新しいページ フレームを割り当てます。これは、ユーザー レベル コードの観点からは自動的に行われます。

ガード ページはスタック領域の終わりの直後に配置され、スタック領域のオーバーランを検出します。最近 (2017 年)、ガード ページ 1 つだけでは不十分であることが判明しました。これは、プログラムがスタック ポインターを大幅に減らすように仕向けられる可能性があり、その結果、スタック ポインターが書き込みを許可する別の領域を指すようになる可能性があるためです。この問題の「解決策」は、4 KB のガード ページを 1 MB のガード領域に置き換えることでした。こちらを参照してください。LWN記事

この脆弱性を悪用するのは決して簡単ではないことに注意してください。たとえば、 の呼び出しを介してプログラムが割り当てるメモリの量をユーザーが制御できる必要がありますalloca。堅牢なプログラムでは、 に渡されるパラメータalloca、特にユーザー入力から派生したパラメータをチェックする必要があります。

関連情報