Jak działa alokacja stosu w systemie Linux?

18

Czy system operacyjny rezerwuje stałą ilość ważnego miejsca wirtualnego na stos lub coś innego? Czy jestem w stanie wygenerować przepełnienie stosu tylko przy użyciu dużych zmiennych lokalnych?

Napisałem mały Cprogram, aby sprawdzić moje założenia. Działa na 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;
}

Uruchomienie programu daje &a[0] = f0ceabe0i&a[n-1] = f16eabdf

Mapy proc pokazują stos: 7ffff0cea000-7ffff16ec000. (10248 * 1024B)

Potem próbowałem zwiększyć n = 11240 * 1024

Uruchomienie programu daje &a[0] = b6b36690i&a[n-1] = b763068f

Mapy proc pokazują stos: 7fffb6b35000-7fffb7633000. (11256 * 1024B)

ulimit -sdrukuje 10240na moim komputerze.

Jak widać, w obu przypadkach rozmiar stosu jest większy niż ulimit -sdaje. A stos rośnie wraz z większą zmienną lokalną. Na górze stosu jest jakieś 3-5kB więcej &a[0](AFAIK, czerwona strefa to 128B).

Jak więc alokuje się tę mapę stosów?

Amos
źródło

Odpowiedzi:

14

Wygląda na to, że limit pamięci stosu nie jest przydzielony (zresztą nie można tego zrobić przy nieograniczonym stosie). https://www.kernel.org/doc/Documentation/vm/overcommit-accounting mówi:

Wzrost stosu języka C powoduje niejawne mapowanie. Jeśli chcesz mieć absolutne gwarancje i zbliżać się do krawędzi, MUSISZ zmapować swój stos na największy rozmiar, jaki Twoim zdaniem będzie potrzebny. Dla typowego użycia stosu nie ma to większego znaczenia, ale jest to narożny przypadek, jeśli naprawdę zależy ci na tym

Jednak mapowanie stosu byłoby celem kompilatora (jeśli ma taką opcję).

EDYCJA: Po kilku testach na maszynie Debian x84_64 odkryłem, że stos rośnie bez żadnego wywołania systemowego (zgodnie z strace). Oznacza to, że jądro rozwija je automatycznie (to właśnie „domyślnie” oznacza powyżej), tzn. Bez wyraźnego mmap/ mremapz procesu.

Trudno było znaleźć szczegółowe informacje na ten temat. Polecam Understanding The Linux Virtual Memory Manager autorstwa Mel Gorman. Podejrzewam, że odpowiedź znajduje się w rozdziale 4.6.1 Obsługa błędu strony , z wyjątkiem „Region nie jest prawidłowy, ale znajduje się obok regionu rozwijalnego, takiego jak stos”, i odpowiadającej mu akcji „Rozwiń region i przydziel stronę”. Zobacz także D.5.2 Rozszerzanie stosu .

Inne odniesienia do zarządzania pamięcią systemu Linux (ale prawie nic o stosie):

EDYCJA 2: Ta implementacja ma wadę: w przypadkach narożnych kolizja stos-stos może nie zostać wykryta, nawet w przypadku, gdy stos byłby większy niż limit! Powodem jest to, że zapis w zmiennej na stosie może skończyć w przydzielonej pamięci sterty, w którym to przypadku nie występuje błąd strony i jądro nie może wiedzieć, że stos wymagał rozszerzenia. Zobacz mój przykład w dyskusji Cicha kolizja stosu stosu w GNU / Linux, którą zacząłem na liście pomocy gcc. Aby tego uniknąć, kompilator musi dodać kod przy wywołaniu funkcji; można to zrobić -fstack-checkza pomocą GCC (szczegóły patrz Ian Lance Taylor i strona podręcznika GCC).

vinc17
źródło
To wydaje się poprawna odpowiedź na moje pytanie. Ale bardziej mnie to myli. Kiedy zostanie wywołane połączenie mremap? Czy będzie to syscall wbudowane w program?
Amos
@amos Zakładam, że wywołanie mremap zostanie uruchomione, jeśli zajdzie potrzeba wywołania funkcji lub wywołania funkcji alga ().
vinc17
Dobrym pomysłem byłoby wspomnienie, czym jest mmap dla ludzi, którzy nie wiedzą.
Faheem Mitha
@FaheemMitha Dodałem trochę informacji. Dla tych, którzy nie wiedzą, co to jest mmap, patrz wyżej wspomniane FAQ dotyczące pamięci. W przypadku stosu byłoby to „anonimowe mapowanie”, aby nieużywane miejsce nie zajmowało pamięci fizycznej, ale jak wyjaśnił Mel Gorman, jądro wykonuje mapowanie (pamięć wirtualną) i fizyczną alokację w tym samym czasie .
vinc17
1
@max Próbowałem programu OP z ulimit -spodaniem 10240, jak w warunkach OP, i otrzymuję SIGSEGV zgodnie z oczekiwaniami (jest to wymagane przez POSIX: „Jeśli ten limit zostanie przekroczony, SIGSEGV zostanie wygenerowany dla wątku. „). Podejrzewam błąd w jądrze OP.
vinc17
6

Jądro Linux 4.2

Minimalny program testowy

Następnie możemy to przetestować za pomocą minimalnego 64-bitowego programu NASM:

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

Upewnij się, że wyłączasz ASLR i usuwasz zmienne środowiskowe, ponieważ będą one umieszczane na stosie i zajmują miejsce:

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

Limit jest gdzieś nieco poniżej mojego ulimit -s(8 MB dla mnie). Wygląda na to, że dzieje się tak z powodu dodatkowych danych określonych w Systemie V, które początkowo zostały umieszczone na stosie oprócz środowiska: Parametry wiersza polecenia systemu Linux 64 w zestawie | Przepełnienie stosu

Jeśli poważnie o tym mówisz, TODO zrób minimalny obraz initrd, który zacznie pisać od góry stosu i zejdzie w dół, a następnie uruchom go za pomocą QEMU + GDB . Umieść dprintfpętlę drukującą adres stosu i punkt przerwania na acct_stack_growth. Będzie chwalebnie.

Związane z:

Ciro Santilli
źródło
2

Domyślnie maksymalny rozmiar stosu jest skonfigurowany na 8 MB na proces,
ale można go zmienić za pomocą ulimit:

Pokazuje wartość domyślną w kB:

$ ulimit -s
8192

Ustaw na nieograniczony:

ulimit -s unlimited

wpływające na bieżącą powłokę i podpowłoki oraz ich procesy potomne.
( ulimitjest wbudowanym poleceniem powłoki)

Możesz pokazać rzeczywisty zakres adresów stosu w użyciu z:
cat /proc/$PID/maps | grep -F '[stack]'
w systemie Linux.

Volker Siegel
źródło
Więc gdy program zostanie załadowany przez bieżącą powłokę, system operacyjny sprawi, że segment pamięci ulimit -sKB będzie poprawny dla programu. W moim przypadku jest to 10240 KB. Ale kiedy deklaruję lokalną tablicę char a[10240*1024]i zestaw a[0]=1, program kończy się poprawnie. Dlaczego?
Amos,
Spróbuj także ustawić ostatni element. I upewnij się, że nie są zoptymalizowane.
vinc17
@amos Myślę, że co oznacza vinc17, to że nazwałeś region pamięci, który nie zmieściłby się na stosie w twoim programie , ale ponieważ tak naprawdę nie masz do niego dostępu w części, która nie pasuje , maszyna nigdy tego nie zauważa - to nie nawet uzyskać tę informację .
Volker Siegel,
@amos Spróbuj int n = 10240*1024; char a[n]; memset(a,'x',n);... seg wina.
goldilocks
2
@amos Jak widać, a[]nie został przydzielony do twojego stosu 10 MB. Kompilator mógł zobaczyć, że nie może być wywołania rekurencyjnego i dokonał specjalnego przydziału lub czegoś innego, jak nieciągłe stosy lub jakieś pośrednie.
vinc17