Wie funktioniert die Stapelzuweisung unter Linux?

18

Reserviert das Betriebssystem die festgelegte Menge des gültigen virtuellen Speicherplatzes für den Stapel oder etwas anderes? Kann ich einen Stapelüberlauf nur mit großen lokalen Variablen erzeugen?

Ich habe ein kleines CProgramm geschrieben, um meine Vermutung zu testen. Es läuft auf 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;
}

Das Ausführen des Programms gibt &a[0] = f0ceabe0und&a[n-1] = f16eabdf

Die Proc Maps zeigen den Stack: 7ffff0cea000-7ffff16ec000. (10248 * 1024B)

Dann habe ich versucht zu erhöhen n = 11240 * 1024

Das Ausführen des Programms gibt &a[0] = b6b36690und&a[n-1] = b763068f

Die Proc Maps zeigen den Stack: 7fffb6b35000-7fffb7633000. (11256 * 1024B)

ulimit -sdruckt 10240in meinem PC.

Wie Sie sehen, ist in beiden Fällen der Stapel größer als der angegebene ulimit -s. Und der Stapel wächst mit einer größeren lokalen Variablen. Die Oberseite des Stapels ist irgendwie 3-5kB mehr entfernt &a[0](AFAIK die rote Zone ist 128B).

Wie wird diese Stapelzuordnung zugewiesen?

Amos
quelle

Antworten:

14

Es sieht so aus, als ob das Stack-Speicherlimit nicht zugewiesen wurde (es konnte jedoch nicht mit unbegrenztem Stack geschehen). https://www.kernel.org/doc/Documentation/vm/overcommit-accounting sagt:

Das C-Language-Stack-Wachstum führt eine implizite mremap aus. Wenn Sie absolute Garantien wünschen und nah an der Kante laufen, MÜSSEN Sie Ihren Stapel auf die größte Größe einstellen, die Sie für erforderlich halten. Für die typische Stapelverwendung spielt dies keine große Rolle, aber es ist ein Eckfall, wenn Sie sich wirklich wirklich darum kümmern

Das Mapping des Stacks wäre jedoch das Ziel eines Compilers (wenn er eine Option dafür hat).

BEARBEITEN: Nach einigen Tests auf einem x84_64-Debian-Rechner habe ich festgestellt, dass der Stack ohne Systemaufruf wächst (laut strace). Das bedeutet also, dass der Kernel ihn automatisch vergrößert (was das "implizite" oben bedeutet), dh ohne explizite mmap/ mremapaus dem Prozess.

Es war ziemlich schwierig, detaillierte Informationen zu finden, die dies bestätigen. Ich empfehle, den Linux Virtual Memory Manager von Mel Gorman zu verstehen . Ich nehme an, die Antwort befindet sich in Abschnitt 4.6.1, „ Behandlung eines Seitenfehlers“, mit der Ausnahme, dass „Region nicht gültig ist, sich jedoch neben einer erweiterbaren Region wie dem Stapel befindet“ und der entsprechenden Aktion „Erweitern Sie die Region und weisen Sie eine Seite zu“. Siehe auch D.5.2 Erweitern des Stapels .

Andere Referenzen zur Linux-Speicherverwaltung (aber mit fast nichts über den Stack):

BEARBEITEN 2: Diese Implementierung hat einen Nachteil: In Eckfällen wird eine Stapel-Heap-Kollision möglicherweise nicht erkannt, selbst wenn der Stapel größer als das Limit wäre! Der Grund dafür ist, dass ein Schreibzugriff auf eine Variable im Stapel möglicherweise im zugewiesenen Heap-Speicher endet. In diesem Fall liegt kein Seitenfehler vor und der Kernel kann nicht erkennen, dass der Stapel erweitert werden muss. Siehe mein Beispiel in der Diskussion Silent Stack-Heap-Kollision unter GNU / Linux, die ich in der gcc-Hilfeliste gestartet habe. Um dies zu vermeiden, muss der Compiler beim Funktionsaufruf Code hinzufügen. Dies kann -fstack-checkfür GCC durchgeführt werden (siehe Ian Lance Taylors Antwort und die GCC-Manpage für Details).

vinc17
quelle
Das scheint die richtige Antwort auf meine Frage zu sein. Aber es verwirrt mich mehr. Wann wird der mremap-Aufruf ausgelöst? Wird es ein in das Programm integrierter Systemaufruf sein?
Amos
@amos Ich gehe davon aus, dass der mremap-Aufruf bei Bedarf bei einem Funktionsaufruf oder beim Aufruf von alloca () ausgelöst wird.
Vinc17
Es wäre wahrscheinlich eine gute Idee zu erwähnen, was mmap ist, für Leute, die es nicht wissen.
Faheem Mitha
@FaheemMitha Ich habe einige Informationen hinzugefügt. Für diejenigen, die nicht wissen, was mmap ist, lesen Sie die oben genannten Speicher-FAQ. Hier für den Stack wäre es "anonymes Mapping" gewesen, damit nicht genutzter Speicherplatz keinen physischen Speicher beansprucht, aber wie von Mel Gorman erklärt, nimmt der Kernel das Mapping (virtueller Speicher) und die physische Zuordnung gleichzeitig vor .
Vinc17
1
@max Ich habe das Programm des OP mit ulimit -s10240 versucht , wie unter den Bedingungen des OP, und ich erhalte erwartungsgemäß ein SIGSEGV (dies ist erforderlich für POSIX: "Wenn dieses Limit überschritten wird, soll SIGSEGV für den Thread generiert werden. "). Ich vermute einen Fehler im OP-Kernel.
Vinc17
6

Linux-Kernel 4.2

Minimales Testprogramm

Wir können es dann mit einem minimalen NASM 64-Bit-Programm testen:

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

Stellen Sie sicher, dass Sie ASLR deaktivieren und Umgebungsvariablen entfernen, da diese auf dem Stapel abgelegt werden und Speicherplatz belegen:

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

Das Limit liegt irgendwo etwas unter meinem ulimit -s(8MiB für mich). Dies liegt an den zusätzlichen System V-Daten, die anfänglich zusätzlich zur Umgebung auf dem Stapel abgelegt wurden: Linux 64-Befehlszeilenparameter in Assembly | Paketüberfluss

Wenn Sie dies ernst meinen, erstellen Sie mit TODO ein minimales initrd-Image , das vom Stapel nach oben und unten geschrieben wird, und führen Sie es dann mit QEMU + GDB aus . Setzen Sie ein dprintfauf die Schleife, die die Stapeladresse und einen Haltepunkt bei druckt acct_stack_growth. Es wird herrlich sein.

Verbunden:

Ciro Santilli ist ein Schauspieler
quelle
2

Standardmäßig ist die maximale Stapelgröße auf 8 MB pro Prozess konfiguriert.
Sie kann jedoch folgendermaßen geändert werden ulimit:

Den Standard in kB anzeigen:

$ ulimit -s
8192

Auf unbegrenzt setzen:

ulimit -s unlimited

Auswirkungen auf die aktuelle Shell und Subshells und ihre untergeordneten Prozesse.
( ulimitist ein Shell-Builtin-Befehl)

Sie können den tatsächlichen Stapeladressbereich anzeigen, der verwendet wird mit:
cat /proc/$PID/maps | grep -F '[stack]'
unter Linux.

Volker Siegel
quelle
Wenn also ein Programm von der aktuellen Shell geladen wird, macht das Betriebssystem ein Speichersegment von ulimit -sKB für das Programm gültig. In meinem Fall ist es 10240 KB. Aber wenn ich ein lokales Array deklariere char a[10240*1024]und setze a[0]=1, wird das Programm korrekt beendet. Warum?
Amos
Versuchen Sie auch das letzte Element zu setzen. Und stellen Sie sicher, dass sie nicht weg optimiert werden.
Vinc17
@amos Ich denke, was vinc17 bedeutet, ist, dass Sie einen Speicherbereich benannt haben, der nicht auf den Stapel in Ihrem Programm passt , aber da Sie in dem Teil, der nicht passt , nicht darauf zugreifen , merkt die Maschine das nie - tut es nicht Holen Sie sich sogar diese Informationen .
Volker Siegel
@amos Versuch int n = 10240*1024; char a[n]; memset(a,'x',n);... seg Fehler.
Goldlöckchen
2
Wie Sie sehen, a[]wurde @amos in Ihrem 10-MB-Stack nicht zugeordnet. Der Compiler hat möglicherweise festgestellt, dass es keinen rekursiven Aufruf gab und hat eine spezielle Zuordnung vorgenommen, oder etwas anderes wie einen diskontinuierlichen Stapel oder eine Indirektion.
Vinc17