Anmelden

Schlüsseltechnologie· Staffel 1· Folge 45

STP045: Speicherstruktur aus Programmsicht

Donnerstag, 19. Oktober 2023 · 1 h 11

MP3

Shownotes

<p>In dieser Ausgabe greifen wir ein altes Thema erneut auf, bei dem ttimeless trotzdem stellenweise nur schwer folgen kann. Denn durch Wiederholung lernt man. Denn durch Wiederholung lernt man.</p><h2>Shownotes</h2><ul>
<li>
<p>Rückblick: wir hatten schon einige Folgen zu Speicherverwaltung</p>
<ul>
<li>STP007: Speicherhierarchie (Festplatte und SSD, Arbeitsspeicher/RAM, Prozessor-Cache, Registerbank)</li>
<li>STP019: Speicherschutz (virtueller Speicher, Auslagerungsspeicher/Swap, Direct Memory Access)</li>
<li>Nachtrag/Linktipp: <a href="linuxatemyram.com">https://www.linuxat…;
<li>das war alles aus Sicht des Betriebssystems; diesmal Abläufe innerhalb des Prozesses</li>
</ul>
</li>
<li>
<p>Was stellt uns das Betriebssystem bereit? -> <a href="de.wikipedia.org/w/index.php?title=Virtuelle_S… Speicher</a></p>
<ul>
<li>nominale Größe: entsprechend der Adressbreite des Prozessors (für 32 Bit sind das 2^32 Bytes = 4 GiB, für 64 Bit sind es 2^64 Bytes = 16 EiB = 16.777.216 TiB)</li>
<li>nutzbar aber nur dort, wo das Betriebssystem für uns eine tatsächlich existierende Speicherseite einblendet</li>
<li>initiale Belegung (siehe Beispiel unten): hauptsächlich Programmdatei und benötigte Programmbibliotheken mit ihren entsprechenden Segmenten</li>
<li>Belegung (und Rückgabe) weiterer Speichersegmente mittels Syscalls (siehe STP019)</li>
<li>klassische Struktur des virtuellen Speichers: oben der Stapel, unten ein Haufen</li>
</ul>
</li>
<li>
<p>vorher einige Bemerkungen zu klassischen Schutzmaßnahmen, die man im untenstehenden Beispiel sehen kann</p>
<ul>
<li>die untersten paar KiB des Adressbereiches werden nie vergeben
<ul>
<li>dies verhindert versehentlichen Zugriff auf Nullzeiger mit hoher Wahrscheinlichkeit</li>
</ul>
</li>
<li><a href="de.wikipedia.org/w/index.php?title=Address_Spa… Space Layout Randomization (ASLR)</a>: Programmdateien und Programmbibliotheken werden an zufällig gewählten Speicheradressen eingeblendet, um es Angreifern zu erschweren, das Programm so zu manipulieren, dass es ungeplant in deren Code hineinspringt
<ul>
<li>klingt nach einer einfach umsetzbaren Schutzmaßnahme</li>
<li>war aber lange nicht weit verbreitet, weil der benötigte <a href="en.wikipedia.org/w/index.php?title=Position-in… Independent Code (PIC)</a> mit einem kleinen Geschwindigkeitsverlust (1-3%) einhergeht</li>
</ul>
</li>
</ul>
</li>
<li>
<p>oberer Teil (d.h. bei hohen Adressen): <strong>Stack</strong> (Stapelspeicher)</p>
<ul>
<li>Programme sind meist als ineinander verschachtelte Unterprogramme (Subroutinen) aufgebaut (siehe STP011)</li>
<li>Daten, die nur für die Dauer des jeweiligen Unterprogramms gebraucht werden, können im Stack aufgestapelt werden</li>
<li>Idee: Hauptroutine kriegt den ganzen Stack und knapst sich oben soviel ab, wie es selber braucht</li>
<li>nächste Unterroutine kriegt den Rest des Stacks und verfährt in derselben Weise</li>
<li>wenn die Unterroutine zurückkehrt, kann der von ihr benutzte Stack an die nächste Unterroutine weitergereicht werden -> keine aufwändige Speicherverwaltung nötig</li>
<li>bei Prozessen mit mehreren Threads (parallel lauffähigen Teilprozessen) teilen sich alle Threads grundsätzlich denselben virtuellen Speicher, aber kriegen jeweils eigene Stacks</li>
<li>Stack ist vom Betriebssystem in einer festen Größe angelegt (im Beispiel unten 132 KiB) – bei zu vielen verschachtelten Subroutinen-Aufrufen kommt es zum berüchtigten <strong>Stack Overflow</strong></li>
</ul>
</li>
<li>
<p>unterer Teil (d.h. bei tiefen Adressen): <strong>Heap</strong> (Haufenspeicher)</p>
<ul>
<li>keine Strukturvorgaben durch das Betriebssystem oder die Prozessorarchitektur</li>
<li>notwendig für Daten, deren Lebenszeit nicht der Struktur von Unterprogrammen folgt, oder deren Größe die 132 KiB des Stacks übersteigt</li>
<li>neue Frage: Wie kann man diesen unstrukturierten Haufen sinnvoll verwalten? -> Teil 2 folgt in STP047</li>
</ul>
</li>
<li>
<p>Randnotiz zu unserem Beispiel unten: <a href="lwn.net/Articles/615809/">Was sind vvar, vdso und vsyscall?</a></p>
<ul>
<li>Syscalls sind zeitaufwändig (in der Größenordnung von dutzenden Mikrosekunden), da man vom Prozess in den Kernel umschalten muss und wieder zurück</li>
<li>Idee: manche Syscalls kann der Kernel mit geschickter Planung im Prozess selbst beantworten, ohne Kontextwechsel</li>
<li>Beispiel: <code>gettimeofday</code> (Abfrage der aktuellen Uhrzeit) kann man aus einem vorhergehenden Datenpunkt des Kernels anhand des Taktzählers des Prozessors fortschreiben (z.B. <a href="en.wikipedia.org/w/index.php?title=Time_Stamp_…; auf x86)</li>
<li>vsyscall: erste derartige Implementation, mit zwei großen Problemen (Platzbegrenzung, kein PIC = kein ASLR); nur aus Kompatibilitätsgründen angeboten</li>
<li><a href="en.wikipedia.org/w/index.php?title=VDSO&ol…; ("virtual dynamic shared object"): die aktuelle Implementation, verhält sich wie eine dynamisch nachgeladene Programmbibliothek</li>
<li>vvar: Datensegment für das vDSO</li>
</ul>
</li>
</ul>
<hr>
<pre><code>$ sleep 30 &
[1] 4970
$ cat /proc/$(pidof sleep)/maps
# address perms offset dev inode pathname
556ac29f4000-556ac29f6000 r--p 00000000 fe:00 10234368 /usr/bin/sleep
556ac29f6000-556ac29f9000 r-xp 00002000 fe:00 10234368 /usr/bin/sleep
556ac29f9000-556ac29fa000 r--p 00005000 fe:00 10234368 /usr/bin/sleep
556ac29fa000-556ac29fb000 r--p 00006000 fe:00 10234368 /usr/bin/sleep
556ac29fb000-556ac29fc000 rw-p 00007000 fe:00 10234368 /usr/bin/sleep
556ac2f81000-556ac2fa2000 rw-p 00000000 0 [heap]
7fe4dea00000-7fe4ded42000 r--p 00000000 fe:00 10321904 /usr/lib/locale/locale-archive
7fe4deefc000-7fe4deeff000 rw-p 00000000 0
7fe4deeff000-7fe4def21000 r--p 00000000 fe:00 10226583 /usr/lib/libc.so.6
7fe4def21000-7fe4df07e000 r-xp 00022000 fe:00 10226583 /usr/lib/libc.so.6
7fe4df07e000-7fe4df0d6000 r--p 0017f000 fe:00 10226583 /usr/lib/libc.so.6
7fe4df0d6000-7fe4df0da000 r--p 001d6000 fe:00 10226583 /usr/lib/libc.so.6
7fe4df0da000-7fe4df0dc000 rw-p 001da000 fe:00 10226583 /usr/lib/libc.so.6
7fe4df0dc000-7fe4df0eb000 rw-p 00000000 0
7fe4df11e000-7fe4df11f000 r--p 00000000 fe:00 10226556 /usr/lib/ld-linux-x86-64.so.2
7fe4df11f000-7fe4df145000 r-xp 00001000 fe:00 10226556 /usr/lib/ld-linux-x86-64.so.2
7fe4df145000-7fe4df14f000 r--p 00027000 fe:00 10226556 /usr/lib/ld-linux-x86-64.so.2
7fe4df14f000-7fe4df151000 r--p 00031000 fe:00 10226556 /usr/lib/ld-linux-x86-64.so.2
7fe4df151000-7fe4df153000 rw-p 00033000 fe:00 10226556 /usr/lib/ld-linux-x86-64.so.2
7ffe3da15000-7ffe3da36000 rw-p 00000000 0 [stack]
7ffe3da73000-7ffe3da77000 r--p 00000000 0 [vvar]
7ffe3da77000-7ffe3da79000 r-xp 00000000 0 [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 0 [vsyscall]
</code></pre>
<p>Randbemerkungen: Sofern ich die Ausgabe von <code>strace sleep 30</code> richtig lese, wurde <code>[heap]</code> hier mit <code>brk</code> verwaltet. Die beiden anonymen Segmente gehen höchstwahrscheinlich auf das Konto des Laufzeit-Linkers (hier <code>ld-linux-x86-64.so.2</code>), der die Programmbibliotheken (hier nur <code>libc.so.6</code>) in den Prozess lädt.</p>
<p>Um das zu bestätigen, habe ich dann <del>mal schnell</del> ein minimales Programm gebaut, dass ausschließlich <code>sleep</code> gefolgt von <code>exit</code> macht, und definitiv gar nichts anderes. Ich war in der Tat erfolgreich:</p>
<pre><code>$ strace ./target/release/minimal-memory-maps
execve("./target/release/minimal-memory-maps", ["./target/release/minimal-memory-"...], 0x7ffd0c104b00 /* 67 vars */) = 0
nanosleep({tv_sec=10, tv_nsec=0}, 0x7ffdaa084800) = 0
exit(0) = ?
+++ exited with 0 +++
</code></pre>
<p>Und dieses Programm hat nicht mal einen Heap, sondern nur den Stack und die Kernel-Schnittstellen:</p> …

Anmelden oder registrieren

Jetzt

0:00 / 0:00