web-dev-qa-db-ja.com

ELFローダーは初期スタックサイズをどのように決定しますか?

私はELF仕様( http://www.skyfree.org/linux/references/ELF_Format.pdf )を研究していますが、プログラムの読み込みプロセスについて明確ではない点が1つあります。スタックが初期化され、初期ページサイズが何であるか。これがテストです(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バイトがあります。

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

[〜#〜] edit [〜#〜]:いくつかのデータがあると、何が起こっているのかさらに不確かになります。テストは次のとおりです。

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

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

8
Mario Carneiro

この質問がELFに関するものだとは思いません。私の知る限り、ELFはプログラムイメージを " flat pack "にファイルに変換し、最初の実行に備えて再構成する方法を定義しています。 OSの動作がPOSIXに昇格されていない場合、スタックの定義とその実装方法は、CPU固有とOS固有の間のどこかにあります。 ELF仕様では、スタック上で何が必要かについていくつかの要求がありますが、疑いはありません。

最小スタック割り当て

あなたの質問から:

スタックのすぐ下のページが「ガードページ」であり、それに書き込むと自動的に書き込み可能になり、「スタックが下がっていく」(おそらく単純なスタック処理が「うまくいく」ように)ことを知っていますが、巨大なスタックフレームを使用すると、ガードページとsegfaultをオーバーシュートする可能性があるため、プロセスの開始時にすでに適切に割り当てられているスペースの量を特定したいと思います。

これについての信頼できる参照を見つけるのに苦労しています。しかし、私はこれが正しくないことを示唆するのに十分な数の権限のない参照を見つけました。

私が読んだことから、ガードページは、スタックの最大割り当て以外のアクセスをキャッチするために使用されており、「通常の」スタックの成長のためではありません。実際のメモリ割り当て(メモリアドレスへのページのマッピング)は、オンデマンドで行われます。つまり、stack-baseとstack-base-max-stack-size + 1の間にあるメモリ内のマップされていないアドレスにアクセスすると、CPUによって例外がトリガーされる可能性がありますが、カーネルはページをマッピングすることによって例外を処理しますセグメンテーション違反をカスケードしないメモリの。

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

最大スタック割り当て

ドキュメントの調査は、スレッドの作成とイメージの読み込みに関するLinuxドキュメントの行に従う必要があります( fork(2)clone(2)execve(2) =)。 execveのドキュメント は興味深いものについて言及しています:

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

... snip ...

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

... snip ...

これは、制限がそれをサポートするアーキテクチャを必要とすることを確認し、制限されている場所も参照します( getrlimit(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ページを参照してください: https://xem.github.io/minix86/manual/intel-x86-and-64-manual-vol3/o_fe12b1e2a880e0ce.html

3
Philip Couling

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

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

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

スタック領域のオーバーランを検出するために、スタック領域の終わりの直後にガードページが配置されます。最近(2017年)、一部の人々は1つのガードページでは不十分であることに気付きました。プログラムがだまされて、スタックポインターが大量にデクリメントされる可能性があるため、スタックポインターが書き込みを許可する他の領域を指すようになる可能性があるためです。この問題の「解決策」は、4 kBのガードページを1 MBのガード領域に置き換えることでした。これを参照してください LWNの記事

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

3
Johan Myréen