OPがすでに回答の1つを受け入れていることは知っていますが、残念ながらMAP_GROWSDOWN
の理由を説明していません 時々動作するようです。このスタック オーバーフローの質問は、検索エンジンで最初にヒットしたものの 1 つなので、他の人のために私の回答を追加させてください。
MAP_GROWSDOWN
のドキュメント 更新が必要です。特に:
この成長は、マッピングが次の下位マッピングの上限のページ内に成長するまで繰り返すことができます。この時点で「保護」ページに触れると、SIGSEGV シグナルが発生します。
実際には、カーネルは MAP_GROWSDOWN
を許可していません stack_guard_gap
より近くなるマッピング 前のマッピングから離れたページ。デフォルト値は 256 ですが、カーネル コマンド ラインで上書きできます。コードではマッピングに必要なアドレスが指定されていないため、カーネルは自動的にアドレスを選択しますが、既存のマッピングの末尾から 256 ページ以内に収まる可能性が非常に高くなります。
編集 :
さらに、v5.0 より前のカーネルは、スタック ポインターより 64k+256 バイト以上下のアドレスへのアクセスを拒否します。詳細については、このカーネル コミットを参照してください。
このプログラムは、5.0 より前のカーネルでも x86 で動作します:
#include <sys/mman.h>
#include <stdint.h>
#include <stdio.h>
#define PAGE_SIZE 4096UL
#define GAP 512 * PAGE_SIZE
static void print_maps(void)
{
FILE *f = fopen("/proc/self/maps", "r");
if (f) {
char buf[1024];
size_t sz;
while ( (sz = fread(buf, 1, sizeof buf, f)) > 0)
fwrite(buf, 1, sz, stdout);
fclose(f);
}
}
int main()
{
char *p;
void *stack_ptr;
/* Choose an address well below the default process stack. */
asm volatile ("mov %%rsp,%[sp]"
: [sp] "=g" (stack_ptr));
stack_ptr -= (intptr_t)stack_ptr & (PAGE_SIZE - 1);
stack_ptr -= GAP;
printf("Ask for a page at %p\n", stack_ptr);
p = mmap(stack_ptr, PAGE_SIZE, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_STACK | MAP_ANONYMOUS | MAP_GROWSDOWN,
-1, 0);
printf("Mapped at %p\n", p);
print_maps();
getchar();
/* One page is already mapped: stack pointer does not matter. */
*p = 'A';
printf("Set content of that page to \"%s\"\n", p);
print_maps();
getchar();
/* Expand down by one page. */
asm volatile (
"mov %%rsp,%[sp]" "\n\t"
"mov %[ptr],%%rsp" "\n\t"
"movb $'B',-1(%%rsp)" "\n\t"
"mov %[sp],%%rsp"
: [sp] "+&g" (stack_ptr)
: [ptr] "g" (p)
: "memory");
printf("Set end of guard page to \"%s\"\n", p - 1);
print_maps();
getchar();
return 0;
}
置換:
volatile char *c_ptr_1 = mapped_ptr - 4096; //1 page below
と
volatile char *c_ptr_1 = mapped_ptr;
理由:
<ブロック引用>戻りアドレスは、プロセスの仮想アドレス空間で実際に作成されるメモリ領域よりも 1 ページ下にあります。 マッピングの下の「ガード」ページのアドレスに触れると、マッピングが 1 ページ分大きくなります。
このソリューションをテストしたところ、カーネル 4.15.0-45-generic で期待どおりに動作することに注意してください。
まず第一に、MAP_GROWSDOWN
は必要ありません 、そしてそれはメインスレッドスタックがどのように機能するかではありません。 pmap を使用したプロセスのメモリ マッピングの分析。 [スタック] それを使用するものはなく、すべきもほとんどありません これを使って。 「スタックに使用される」というマニュアルページの内容は間違っており、修正する必要があります。
バグがあるのではないかと思います (誰も使用していないため、通常、壊れても誰も気にせず、気付かないこともあります)。
mmap
を変更すると、コードが機能します 複数のページをマップする呼び出し。具体的には 4096 * 100
を試してみました . Linux 5.0.1 (Arch Linux) をベアメタル (Skylake) で実行しています。
/proc/PID/smaps
gd
を示しています
そして (asm をシングルステップ実行するとき) maps
エントリは実際にはより低い開始アドレスに変更されますが、同じ終了アドレスに変更されるため、400k マッピングで開始すると文字通り下方に成長します。これにより、上記の 400k の初期割り当てが得られます プログラムの実行時に 404kiB に成長する戻りアドレス。 (_GROWSDOWN
のサイズ マッピングはありません 成長限界かそのようなものです。)
https://bugs.centos.org/view.php?id=4767 が関連している可能性があります; CentOS 5.3 と 5.5 のカーネル バージョン間で何かが変更されました。および/または、VM での作業 (5.3) と、ベア メタルでの成長や障害 (5.5) の違いに関係していました。
ptr[-4095]
を使用するように C を簡略化しました など:
int main(void){
volatile char *ptr = mmap(NULL, 4096*100,
PROT_READ | PROT_WRITE,
MAP_ANONYMOUS | MAP_PRIVATE | MAP_STACK | MAP_GROWSDOWN,
-1, 0);
if(ptr == MAP_FAILED){
int error_code = errno;
fprintf(stderr, "Cannot do MAP_FIXED mapping."
"Error code = %d, details = %s\n", error_code, strerror(error_code));
exit(EXIT_FAILURE);
}
ptr[0] = 'a'; //address returned by mmap
ptr[-4095] = 'b'; // grow by 1 page
}
gcc -Og
でコンパイルする シングルステップにナイスな asm を提供します。
ところで、フラグが glibc から削除されたというさまざまな噂は明らかに間違っています。このソースはコンパイルされ、カーネルでもサポートされており、黙って無視されていないことは明らかです。 (400kiB の代わりにサイズ 4096 で見られる動作は、フラグが黙って無視されることと正確に一致していますが、 gd
VmFlag はまだ smaps
にあります であるため、その段階では無視されません。)
確認したところ、別のマッピングに近づくことなく成長する余地がありました。では、GD マッピングが 1 ページしかないときに、なぜ IDK が成長しなかったのか。私は数回試しましたが、そのたびにセグメンテーション違反が発生しました。初期マッピングが大きいため、エラーは発生しませんでした。
どちらの場合も、mmap の戻り値 (適切なマッピングの最初のページ) へのストアと、その下の 4095 バイトへのストアでした。