Linuxでスタック割り当てはどのように機能しますか?


18

OSは、スタックまたは他の何かのために有効な仮想スペースの固定量を予約しますか?大きなローカル変数を使用するだけでスタックオーバーフローを生成できますか?

C仮定をテストするための小さなプログラムを作成しました。X86-64 CentOS 6.5で実行されています。

#include <string.h>
#include <stdio.h>
int main()
{
    int n = 10240 * 1024;
    char a[n];
    memset(a, 'x', n);
    printf("%x\n%x\n", &a[0], &a[n-1]);
    getchar();
    return 0;
}

プログラムを実行する&a[0] = f0ceabe0&a[n-1] = f16eabdf

procマップはスタックを示します: 7ffff0cea000-7ffff16ec000. (10248 * 1024B)

それから私は増加しようとしました n = 11240 * 1024

プログラムを実行する&a[0] = b6b36690&a[n-1] = b763068f

procマップはスタックを示します: 7fffb6b35000-7fffb7633000. (11256 * 1024B)

ulimit -s10240PCで印刷します。

ご覧のとおり、どちらの場合もスタックサイズはそれよりも大きくなりulimit -sます。そして、スタックはより大きなローカル変数で成長します。スタックの一番上は3〜5kB離れています&a[0](赤いゾーンは128Bです)。

それでは、このスタックマップはどのように割り当てられますか?

回答:


14

スタックメモリの制限は割り当てられていないようです(とにかく、無制限のスタックではできませんでした)。https://www.kernel.org/doc/Documentation/vm/overcommit-accountingのコメント:

C言語スタックの成長は、暗黙的なmremapを行います。絶対的な保証が必要で、エッジの近くで実行する場合は、必要と思われる最大サイズにスタックをmmapする必要があります。典型的なスタックの使用では、これはそれほど重要ではありませんが、本当に本当に気にするなら、それはコーナーケースです

ただし、スタックのmmappingは、コンパイラの目標になります(オプションがある場合)。

編集:Debianのx84_64マシンでのいくつかのテストの後、システムコールなしでスタックが増大することがわかりました(によるとstrace)。したがって、これはカーネルが自動的にそれを成長させることを意味します(これは上記の「暗黙的」が意味するものです)。つまり、プロセスから明示的にmmap/ なしでmremap

これを確認する詳細情報を見つけるのは非常に困難でした。Mel GormanによるLinux Virtual Memory Managerの理解をお勧めします。答えはセクション4.6.1 ページフォールトの処理にあり、例外は「領域は無効ですが、スタックのような拡張可能な領域の横にある」と、対応するアクション「領域を拡張してページを割り当てる」にあると思います。D.5.2 スタックの拡張も参照してください。

Linuxのメモリ管理に関するその他の参照(ただし、スタックについてはほとんど何もありません):

編集2:この実装には欠点があります。コーナーケースでは、スタックが制限よりも大きい場合でも、スタックヒープの衝突が検出されないことがあります!その理由は、スタック内の変数への書き込みが割り当てられたヒープメモリ内で終了する可能性があるためです。この場合、ページフォールトはなく、カーネルはスタックを拡張する必要があることを認識できません。gcc-helpリストで始めたGNU / Linuxでのサイレントスタックとヒープの衝突に関する議論の私の例を参照してください。これを回避するには、コンパイラーは関数呼び出し時にコードを追加する必要があります。これは-fstack-checkGCC で行うことができます(詳細については、Ian Lance Taylorの回答とGCCのマニュアルページを参照してください)。


それが私の質問に対する正しい答えのようです。しかし、それは私をもっと混乱させます。mremap呼び出しはいつトリガーされますか?プログラムに組み込まれたシステムコールになりますか?
アモス14

@amos関数呼び出しが必要な場合、またはalloca()が呼び出されたときにmremap呼び出しがトリガーされると想定しています。
vinc17 14

知らない人のために、mmapが何であるかを言及するのはおそらく良い考えでしょう。
ファヒームミタ14

@FaheemMithaいくつかの情報を追加しました。mmapが何なのかわからない場合は、上記のメモリに関するFAQをご覧ください。ここでは、スタックについては、「匿名マッピング」であるため、未使用のスペースは物理メモリを消費しませんが、Mel Gormanが説明したように、カーネルはマッピング(仮想メモリ)と物理割り当てを同時に行います。
vinc17

1
@max私はOPのプログラムを試してみたulimit -sOPの条件の下でのように、10240を与え、そして私は(これはPOSIXで要求されるものです期待通りSIGSEGVを取得する:「この制限を超えた場合、SIGSEGVがスレッドのために生成されなければなりません。 ")。OPのカーネルにバグがあると思われます。
vinc17

6

Linuxカーネル4.2

最小限のテストプログラム

その後、最小限のNASM 64ビットプログラムでテストできます。

global _start
_start:
    sub rsp, 0x7FF000
    mov [rsp], rax
    mov rax, 60
    mov rdi, 0
    syscall

ASLRをオフにし、環境変数を削除することを確認してください。環境変数はスタックに移動してスペースを占有するためです。

echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
env -i ./main.out

制限は、私ulimit -s(8MiB)の少し下です。このように見えるのは、環境に加えて最初にスタックに追加された追加のSystem V指定データが原因です:アセンブリのLinux 64コマンドラインパラメーター| スタックオーバーフロー

これを真剣に考えている場合、TODO はスタックの先頭から書き込みを開始してダウンする最小のinitrdイメージを作成し、それをQEMU + GDBで実行します。dprintfループにスタックアドレスを出力し、ブレークポイントをに置きますacct_stack_growth。それは見事です。

関連:


2

デフォルトでは、最大スタックサイズはプロセスごとに8MBに設定されていますが、次
を使用して変更できますulimit

デフォルトをkBで表示:

$ ulimit -s
8192

無制限に設定:

ulimit -s unlimited

現在のシェルおよびサブシェルとそれらの子プロセスに影響します。
ulimitシェル組み込みコマンドです)


cat /proc/$PID/maps | grep -F '[stack]'
Linux で使用中の実際のスタックアドレス範囲を表示できます。


したがって、現在のシェルによってプログラムがロードされると、OSはulimit -sそのプログラムに対してKBのメモリセグメントを有効にします。私の場合は10240KBです。しかし、ローカル配列を宣言してchar a[10240*1024]設定するa[0]=1と、プログラムは正しく終了します。どうして?
アモス14

最後の要素も設定してみてください。そして、それらが最適化されていないことを確認してください。
vinc17 14

@amos vinc17の意味は、プログラムのスタック収まらないメモリ領域に名前を付けたことですが、収まらない部分で実際にアクセスしないと、マシンはそれに気付かない- それはしませんその情報さえ入手してください
フォルカーシーゲル14

@amos Try int n = 10240*1024; char a[n]; memset(a,'x',n);... seg fault。
goldilocks 14

2
@amosしたがって、ご覧のとおりa[]、10MBスタックには割り当てられていません。コンパイラーは、再帰呼び出しができず、特別な割り当て、または不連続スタックや間接参照のような何かを行ったことに気付いたかもしれません。
vinc17 14
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.