Minimale Stapelzuordnung

Minimale Stapelzuordnung

Ich studiere die ELF-Spezifikation (http://www.skyfree.org/linux/references/ELF_Format.pdf), und ein Punkt, der mir beim Programmladevorgang nicht klar ist, ist, wie der Stapel initialisiert wird und wie groß die anfängliche Seitengröße ist. Hier ist der Test (auf 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]

Die ELF-Spezifikation sagt sehr wenig darüber aus, wie oder warum diese Stapelseite überhaupt existiert, aber ich kann Referenzen finden, die besagen, dass der Stapel mit SP initialisiert werden sollte, das auf argc zeigt, mit argv, envp und dem Hilfsvektor direkt darüber, und ich habe dies bestätigt. Aber wie viel Platz ist unterhalb von SP verfügbar? Auf meinem System sind 0x1FF00Bytes unterhalb von SP zugeordnet, aber vermutlich wird hier von oben auf dem Stapel bei heruntergezählt 0x7ffffffff000, und es gibt 0x21000Bytes in der vollständigen Zuordnung. Was beeinflusst diese Zahl?

Mir ist bewusst, dass die Seite direkt unter dem Stapel eine „Schutzseite“ ist, die automatisch beschreibbar wird und „im Stapel nach unten wächst“, wenn ich darauf schreibe (vermutlich, damit die naive Stapelverarbeitung „einfach funktioniert“). Wenn ich jedoch einen riesigen Stapelrahmen zuweise, könnte ich die Schutzseite überschreiten und einen Segmentierungsfehler verursachen. Daher möchte ich gleich beim Prozessstart feststellen, wie viel Speicherplatz mir bereits ordnungsgemäß zugewiesen wurde.

BEARBEITEN: Einige weitere Daten machen mich noch unsicherer, was los ist. Der Test ist der folgende:

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

Ich habe hier mit verschiedenen Werten der Konstante gespielt, 0x7fe000um zu sehen, was passiert, und für diesen Wert ist es nicht deterministisch, ob ich einen Segmentierungsfehler bekomme oder nicht. Laut GDB subqwird die Anweisung allein die Größe des mmap erweitern, was mir ein Rätsel ist (woher weiß Linux, was sich in meinem Register befindet?), aber dieses Programm lässt GDB beim Beenden aus irgendeinem Grund normalerweise abstürzen. Es kann nicht ASLR sein, das den Nichtdeterminismus verursacht, da ich weder einen GOT- noch einen PLT-Abschnitt verwende; die ausführbare Datei wird jedes Mal an denselben Stellen im virtuellen Speicher geladen. Ist das also eine Art Zufälligkeit des PID oder des physischen Speichers, der durchsickert? Alles in allem bin ich sehr verwirrt darüber, wie viel Stapel tatsächlich legal für den wahlfreien Zugriff verfügbar ist und wie viel beim Ändern von RSP oder beim Schreiben in Bereiche „knapp außerhalb des Bereichs“ des legalen Speichers angefordert wird.

Antwort1

Ich glaube nicht, dass diese Frage wirklich etwas mit ELF zu tun hat. Soweit ich weiß, definiert ELF eine Möglichkeit, „Flachgehäuse" ein Programmabbild in Dateien und setzt es dann für die erste Ausführung wieder zusammen. Die Definition des Stapels und seiner Implementierung liegt irgendwo zwischen CPU-spezifisch und betriebssystemspezifisch, wenn das Betriebssystemverhalten nicht auf POSIX angehoben wurde. Allerdings stellt die ELF-Spezifikation zweifellos bestimmte Anforderungen an den Stapel.

Minimale Stapelzuordnung

Aus Ihrer Frage:

Mir ist bewusst, dass die Seite direkt unter dem Stapel eine „Schutzseite“ ist, die automatisch beschreibbar wird und „im Stapel nach unten wächst“, wenn ich darauf schreibe (vermutlich, damit die naive Stapelverarbeitung „einfach funktioniert“). Wenn ich jedoch einen riesigen Stapelrahmen zuweise, könnte ich die Schutzseite überschreiten und einen Segmentierungsfehler verursachen. Daher möchte ich gleich beim Prozessstart feststellen, wie viel Speicherplatz mir bereits ordnungsgemäß zugewiesen wurde.

Ich habe Mühe, eine autoritative Referenz dafür zu finden. Aber ich habe eine ausreichend große Anzahl nicht autoritativer Referenzen gefunden, die darauf schließen lassen, dass dies falsch ist.

Soweit ich gelesen habe, wird die Schutzseite verwendet, um Zugriffe außerhalb der maximalen Stapelzuweisung abzufangen, und nicht für „normales“ Stapelwachstum. Die eigentliche Speicherzuweisung (Zuordnen von Seiten zu Speicheradressen) erfolgt bei Bedarf. D. h.: Wenn auf nicht zugeordnete Adressen im Speicher zugegriffen wird, die zwischen Stapelbasis und Stapelbasis – maximale Stapelgröße + 1 liegen, kann eine Ausnahme von der CPU ausgelöst werden, aber der Kernel behandelt die Ausnahme, indem er eine Speicherseite zuordnet und keinen Segmentierungsfehler kaskadiert.

Der Zugriff auf den Stack innerhalb der maximalen Zuweisung sollte also keinen Segmentierungsfehler verursachen. Wie Sie festgestellt haben

Maximale Stapelzuordnung

Die Dokumentation zur Untersuchung sollte den Anweisungen der Linux-Dokumentation zur Thread-Erstellung und zum Laden von Bildern folgen (Gabel(2),Klon(2),execve(2)). Die Dokumentation von execveerwähnt etwas Interessantes:

Beschränkungen der Argument- und Umgebungsgröße

...schnipp...

Ab Kernel 2.6.23 unterstützen die meisten Architekturen eine Größenbeschränkung, die von der Software abgeleitet ist.RLIMIT_STACKRessourcenlimit (siehegetrlimit(2))

...schnipp...

Dies bestätigt, dass die Beschränkung eine Architektur erfordert, die sie unterstützt, und verweist auch darauf, wo sie begrenzt ist (getrlimit(2)).

RLIMIT_STACK

Dies ist die maximale Größe des Prozessstapels in Bytes. Bei Erreichen dieser Grenze wird ein SIGSEGV-Signal generiert. Um dieses Signal verarbeiten zu können, muss ein Prozess einen alternativen Signalstapel verwenden (sigaltstack(2)).

Seit Linux 2.6.23 bestimmt diese Begrenzung auch die Menge an Speicherplatz, die für die Kommandozeilenargumente und Umgebungsvariablen des Prozesses verwendet wird; Details finden Sie unter execve(2).

Erweitern des Stacks durch Ändern des RSP-Registers

Ich kenne keinen x86-Assembler. Ich möchte Sie jedoch auf die „Stack Fault Exception“ aufmerksam machen, die von x86-CPUs ausgelöst werden kann, wenn das SS-Register geändert wird. Bitte korrigieren Sie mich, wenn ich falsch liege, aber ich glaube, auf x86-64 ist SS:SP einfach zu „RSP“ geworden. Wenn ich es also richtig verstehe, kann eine Stack Fault Exception durch dekrementiertes RSP ( subq $0x7fe000,%rsp) ausgelöst werden.

Siehe Seite 222 hier:https://xem.github.io/minix86/manual/intel-x86-and-64-manual-vol3/o_fe12b1e2a880e0ce.html

Antwort2

Jeder Prozessspeicherbereich (z. B. Code, statische Daten, Heap, Stack usw.) hat Grenzen, und ein Speicherzugriff außerhalb eines Bereichs oder ein Schreibzugriff auf einen schreibgeschützten Bereich erzeugt eine CPU-Ausnahme. Der Kernel verwaltet diese Speicherbereiche. Ein Zugriff außerhalb eines Bereichs breitet sich in Form eines Segmentierungsfehlersignals bis zum Benutzerbereich aus.

Nicht alle Ausnahmen werden durch den Zugriff auf den Speicher außerhalb der Regionen generiert. Auch ein Zugriff innerhalb der Region kann eine Ausnahme generieren. Wenn die Seite beispielsweise nicht dem physischen Speicher zugeordnet ist, behandelt der Seitenfehlerhandler dies transparent für den laufenden Prozess.

Der Hauptstapelbereich des Prozesses ist anfangs nur mit einer kleinen Anzahl von Seitenrahmen verknüpft, wächst jedoch automatisch, wenn über den Stapelzeiger weitere Daten dorthin übertragen werden. Der Ausnahmehandler überprüft, ob der Zugriff noch innerhalb des für den Stapel reservierten Bereichs erfolgt, und weist ggf. einen neuen Seitenrahmen zu. Aus Sicht des Codes auf Benutzerebene geschieht dies automatisch.

Eine Schutzseite wird direkt nach dem Ende des Stapelbereichs platziert, um einen Überlauf des Stapelbereichs zu erkennen. Kürzlich (2017) haben einige Leute erkannt, dass eine einzelne Schutzseite nicht ausreicht, da ein Programm möglicherweise dazu verleitet werden kann, den Stapelzeiger um einen großen Betrag zu dekrementieren, wodurch der Stapelzeiger möglicherweise auf einen anderen Bereich zeigt, der Schreibvorgänge zulässt. Die „Lösung“ für dieses Problem bestand darin, die 4-kB-Schutzseite durch einen 1-MB-Schutzbereich zu ersetzen. Siehe diesLWN-Artikel.

Es ist zu beachten, dass diese Sicherheitslücke nicht ganz einfach auszunutzen ist. Sie erfordert beispielsweise, dass der Benutzer die Speichermenge steuern kann, die ein Programm über einen Aufruf von zuweist alloca. Robuste Programme sollten den an übergebenen Parameter überprüfen alloca, insbesondere wenn er von Benutzereingaben abgeleitet ist.

verwandte Informationen