blog · git · desktop · images · contact
2011-04-03
Okay, ich habe jetzt also meinen Stack und einen Frame Pointer. Der Stack ist aber nur ein kleiner Teil des Adressraums eines Prozesses. Was gibt es da sonst noch? Und wie kann ich das anzeigen lassen? Wie sieht überhaupt der Adressraum eines Prozesses aus?
Einen schönen Überblick liefert schon einmal dieser LWN-Artikel. Das möchte ich jetzt in der Praxis ausprobieren und nachvollziehen. Der Artikel ist ja auch schon ein paar Tage älter.
Vorweg sei kurz virtueller Speicher erwähnt. Jedes Programm denkt, es habe den gesamten Speicher an einem Stück zur Verfügung. Erst mithilfe von Paging werden diese virtuellen Adressen in echte Adressen übersetzt, wovon ein normaler Prozess aber nichts mitbekommt.
Den Anfang macht dieses Programm:
.text
.global _start
_start:
/* SIGSTOP an dich selbst senden. Jetzt Speicherlayout zum Beispiel
* mittels cat /proc/$(pgrep stp)/maps ansehen. Danach SIGCONT von
* extern senden. */
movl $37, %eax
movl $0, %ebx
movl $19, %ecx
int $0x80
/* _exit(0) */
movl $1, %eax
movl $0, %ebx
int $0x80
Übersetzung – analog werden auch alle folgenden Beispiele übersetzt:
$ as -o stp.o stp.s
$ ld -o stp stp.o
kill
hat die Syscall-Nummer 37 und SIGSTOP
ist 19. Das Programm
startet also und stoppt sich danach sofort, erhält es dann ein
SIGCONT
, beendet es sich über _exit(0)
. Jetzt kann man sich über
$ ./stp
[1]+ Stopped ./stp
$ cat /proc/$(pgrep stp)/maps
das Layout des Speichers anschauen:
08048000-08049000 r-xp 00000000 00:11 1164180 /tmp/stp
b77c1000-b77c2000 r-xp 00000000 00:00 0 [vdso]
bf929000-bf94a000 rwxp 00000000 00:00 0 [stack]
Eine ausführliche Beschreibung dieser Ausgabe findet sich in Documentation/filesystems/proc.txt.
Da es sich um ein minimales Programm handelt und keine libc involviert ist, sieht das sehr übersichtlich aus. Bildlich dargestellt (niedrige Adressen diesmal oben, damit das Bild mit der Ausgabe übereinstimmt):
+----------+ <-- 0x08048000
| Programm |
+----------+ <-- 0x08049000
| |
: : :
: : :
: : :
| |
+----------+ <-- 0xB77C1000
| vdso |
+----------+ <-- 0xB77C2000
: :
+----------+ <-- 0xBF929000
| Stack |
+----------+ <-- 0xBF94A000
vdso
lasse ich vorerst außen vor – vielleicht später ein eigenes
Posting dazu. Für den Anfang nur ein paar Links:
Was auffällt: Da steht nichts vom Heap. Das liegt daran, dass mit dem Heap noch nichts gemacht wurde. Linux spart es sich, da irgendwelchen Speicher für einen Heap zu reservieren, wenn er eh nicht benutzt wird.
Das will ich jetzt mal tun. Über den Syscall brk()
kann man den
„Program Break“ verschieben. Das ist der Pointer auf das Ende des
Speicherbereichs, in dem der Programmcode liegt. In obigem Bild ist also
theoretisch die Adresse 0x08049000 der Program Break. Das folgende
Programm ist eine Erweiterung:
brk()
und anhalten. Jetzt ist ein Heap sichtbar!brk()
und anhalten. Der Heap ist größer geworden.Der Code:
.text
.global _start
_start:
/* SIGSTOP an dich selbst senden. Jetzt Speicherlayout zum Beispiel
* mittels cat /proc/$(pgrep brk)/maps ansehen. Danach SIGCONT von
* extern senden. */
movl $37, %eax
movl $0, %ebx
movl $19, %ecx
int $0x80
/* Okay, es ging weiter. Nun hole mal über brk(0) den aktuellen
* Program Break, dann addiere da 4100 drauf (also mehr als 4096 und
* damit mehr als eine Page) und rufe nochmal brk(), damit der
* Program Break nach oben verschoben werden kann -- das alloziiert
* de facto Speicher auf dem Heap. */
movl $45, %eax
movl $0, %ebx
int $0x80
addl $4100, %eax
movl %eax, %ebx
movl $45, %eax
int $0x80
/* Wieder SIGSTOP an dich selbst senden, jetzt Speicher anschauen. */
movl $37, %eax
movl $0, %ebx
movl $19, %ecx
int $0x80
/* Dasselbe Spiel nochmal. Beobachte, wie der Heap wächst. */
movl $45, %eax
movl $0, %ebx
int $0x80
addl $4100, %eax
movl %eax, %ebx
movl $45, %eax
int $0x80
movl $37, %eax
movl $0, %ebx
movl $19, %ecx
int $0x80
/* _exit(0) */
movl $1, %eax
movl $0, %ebx
int $0x80
Das mit brk()
läuft so ab: Man ruft zuerst brk(0)
auf und als
Ergebnis (in EAX) erhält man den aktuellen Program Break. Auf diesen
Wert kann man etwas addieren, nämlich die Größe des gewünschten,
zusätzlichen Platzes. Mit diesem Wert als Parameter ruft man dann noch
einmal brk()
auf, jetzt wird der Program Break tatsächlich verschoben.
Programmiert man in C, kann man hierfür
sbrk()
verwenden.
Man sieht die Entwicklung sehr schön:
$ ./brk
[1]+ Stopped ./brk
$ cat /proc/$(pgrep brk)/maps
08048000-08049000 r-xp 00000000 08:08 730975 /tmp/brk
b78b2000-b78b3000 r-xp 00000000 00:00 0 [vdso]
bfb1f000-bfb40000 rwxp 00000000 00:00 0 [stack]
$ kill -CONT %1
[1]+ Stopped ./brk
$ cat /proc/$(pgrep brk)/maps
08048000-08049000 r-xp 00000000 08:08 730975 /tmp/brk
09f0a000-09f0c000 rwxp 00000000 00:00 0 [heap]
b78b2000-b78b3000 r-xp 00000000 00:00 0 [vdso]
bfb1f000-bfb40000 rwxp 00000000 00:00 0 [stack]
$ kill -CONT %1
[1]+ Stopped ./brk
$ cat /proc/$(pgrep brk)/maps
08048000-08049000 r-xp 00000000 08:08 730975 /tmp/brk
09f0a000-09f0d000 rwxp 00000000 00:00 0 [heap]
b78b2000-b78b3000 r-xp 00000000 00:00 0 [vdso]
bfb1f000-bfb40000 rwxp 00000000 00:00 0 [stack]
Man sieht hier auch deutlich, wie der Kernel nur ganze Pages (4096 KiB)
zuweist. Ferner ist ersichtlich, wie sich vdso
bei jedem
Programmaufruf (siehe oben) an einer anderen Position befindet.
+----------+ <-- 0x08048000
| Programm |
+----------+ <-- 0x08049000
: :
+----------+ <-- 0x09F0A000
| |
| Heap |
| |
+··········+ <-- 0x09F0C000 Initial
| |
+----------+ <-- 0x09F0D000 Nach dem zweiten SIGCONT
| |
: : :
: : :
: : :
| |
+----------+ <-- 0xB78B2000 Andere Adressen, vgl. mit erstem Bild
| vdso |
+----------+ <-- 0xB78B3000
: :
+----------+ <-- 0xBF929000
| Stack |
+----------+ <-- 0xBF94A000
So ganz stimmt die Semantik des „Program Breaks“ aber auch nicht. Zwischen dem Code und dem Heap ist ein Loch im Speicher. Dieses Loch ist immerhin fast 31 MiB groß. Es ist bei jedem Start unterschiedlich groß (erwähntes „Sicherheitspolster“) und beinhaltet bei entsprechenden Programmen auch Datensektionen. Hier liegen beispielsweise String-Literale von C-Programmen.
Wenn man genau hinschaut, ist die Größe des Stacks eigentlich
verwunderlich. Sie beträgt 33 Pages, also 132 KiB. Wieso? ulimit
behauptet etwas anderes:
$ ulimit -a | grep stack
stack size (kbytes, -s) 8192
Wie genau die Zahl 33 entsteht, kann ich noch nicht sagen, aber der Kernel scheint auch hier zu Anfang „faul“ zu sein. Mehr Speicher reserviert er erst, wenn er auch benutzt wird. Erweitert man das Programm oben um folgenden Abschnitt, dann sieht man, wie das Mapping größer wird:
.text
.global _start
_start:
/* SIGSTOP an dich selbst senden. Jetzt Speicherlayout zum Beispiel
* mittels cat /proc/$(pgrep stk)/maps ansehen. Danach SIGCONT von
* extern senden. */
movl $37, %eax
movl $0, %ebx
movl $19, %ecx
int $0x80
/* Jetzt der Stack. Schreibe etwas ziemlich weit unten. Dann wieder
* SIGSTOP. Man sieht in /proc/$PID/maps, wie der Stack nach unten
* hin wächst. */
subl $262144, %esp
movl $42, (%esp)
movl $37, %eax
movl $0, %ebx
movl $19, %ecx
int $0x80
/* _exit(0) */
movl $1, %eax
movl $0, %ebx
int $0x80
Hier der Status vor und nach diesem Schreiben:
$ ./stk
[1]+ Stopped ./stk
$ cat /proc/$(pgrep stk)/maps
08048000-08049000 r-xp 00000000 00:11 1036666 /tmp/stk
b7780000-b7781000 r-xp 00000000 00:00 0 [vdso]
bf8a7000-bf8c8000 rwxp 00000000 00:00 0 [stack]
$ kill -CONT %1
[1]+ Stopped ./stk
$ cat /proc/$(pgrep stk)/maps
08048000-08049000 r-xp 00000000 00:11 1036666 /tmp/stk
b7780000-b7781000 r-xp 00000000 00:00 0 [vdso]
bf885000-bf8c8000 rwxp 00000000 00:00 0 [stack]
Die untere Grenze des Stacks hat sich also von 0xBF8A7000 auf 0xBF885000 geändert.
Was nun noch fehlt, sind Speicherbereiche, die über mmap
eingeblendet
werden. Der Einfachheit halber werde ich hier einen anonymen Bereich
verwenden (also keine echte Datei), außerdem nehme ich den alten
mmap-Syscall, dem die Parameter über den Stack übergeben werden.
Stattdessen könnte man auch mmap2 hernehmen, was alle Parameter in
Registern erwartet.
Das wird der Ablauf sein:
cat /proc/$(pgrep
mmap)/maps
.munmap
-Syscalls wieder frei und
halte an. Anzeige des Layouts.Es gibt also vier Stopps und vier Ausgaben von cat /proc/$(pgrep
mmap)/maps
. Der eingeblendete Speicher ist anonym, hat also keinen
„Namen“ in der rechten Spalte. Man sieht, dass er direkt unterhalb von
vdso
eingeblendet wird. Nach der Freigabe der ersten 512 MiB entsteht
ein großes Loch zwischen vdso
und dem kleinen 512 KiB-Teil.
Das Programm:
.text
.global _start
_start:
pushl %ebp
movl %esp, %ebp
/* SIGSTOP an dich selbst senden. Jetzt Speicherlayout zum Beispiel
* mittels cat /proc/$(pgrep mmap)/maps ansehen. Danach SIGCONT
* von extern senden. */
movl $37, %eax
movl $0, %ebx /* === STOP 1 === */
movl $19, %ecx
int $0x80
/* Okay, es ging weiter. Call zu mmap (90), alle Parameter rückwärts
* auf den Stack und diese Adresse dann nach EBX. */
movl $90, %eax
pushl $0 /* offset */
pushl $-1 /* fd */
pushl $34 /* flags = MAP_PRIVATE | MAP_ANONYMOUS */
pushl $3 /* prot = PROT_READ | PROT_WRITE */
pushl $0x20000000 /* length = 512 * 1024 * 1024 = 512 MiB */
pushl $0 /* addr */
movl %esp, %ebx
int $0x80
/* Adresse, an der der neue Speicher eingeblendet wurde, steht jetzt
* in EAX. Diese Adresse auf dem Stack merken. Dazu vorher die
* Argumente vom mmap-Aufruf "entfernen". */
addl $24, %esp
pushl %eax
movl $37, %eax
movl $0, %ebx /* === STOP 2 === */
movl $19, %ecx
int $0x80
/* Noch einmal ein paar Byte. Schauen, wo die hinkommen! */
movl $90, %eax
pushl $0 /* offset */
pushl $-1 /* fd */
pushl $34 /* flags = MAP_PRIVATE | MAP_ANONYMOUS */
pushl $3 /* prot = PROT_READ | PROT_WRITE */
pushl $0x80000 /* length = 512 * 1024 = 512 KiB */
pushl $0 /* addr */
movl %esp, %ebx
int $0x80
movl $37, %eax
movl $0, %ebx /* === STOP 3 === */
movl $19, %ecx
int $0x80
/* Jetzt den als erstes angeforderten Block freigeben (munmap = 91).
* Was passiert? Vorher die Argumente für das zweite mmap verwerfen.
* */
addl $24, %esp
movl $91, %eax /* Syscall-Nummer */
popl %ebx /* damalige Anfangsadresse */
movl $0x20000000, %ecx /* length = 512 * 1024 * 1024 = 512 MiB */
int $0x80
movl $37, %eax
movl $0, %ebx /* === STOP 4 === */
movl $19, %ecx
int $0x80
/* _exit(0) */
movl $1, %eax
movl $0, %ebx
int $0x80
Die Ausgaben:
$ ./mmap
[1]+ Stopped ./mmap
$ cat /proc/$(pgrep mmap)/maps
08048000-08049000 r-xp 00000000 08:08 731032 /tmp/mmap
b7819000-b781a000 r-xp 00000000 00:00 0 [vdso]
bfcb3000-bfcd4000 rwxp 00000000 00:00 0 [stack]
$ kill -CONT %1
[1]+ Stopped ./mmap
$ cat /proc/$(pgrep mmap)/maps
08048000-08049000 r-xp 00000000 08:08 731032 /tmp/mmap
97819000-b7819000 rwxp 00000000 00:00 0
b7819000-b781a000 r-xp 00000000 00:00 0 [vdso]
bfcb3000-bfcd4000 rwxp 00000000 00:00 0 [stack]
$ kill -CONT %1
[1]+ Stopped ./mmap
$ cat /proc/$(pgrep mmap)/maps
08048000-08049000 r-xp 00000000 08:08 731032 /tmp/mmap
97799000-b7819000 rwxp 00000000 00:00 0
b7819000-b781a000 r-xp 00000000 00:00 0 [vdso]
bfcb3000-bfcd4000 rwxp 00000000 00:00 0 [stack]
$ kill -CONT %1
[1]+ Stopped ./mmap
$ cat /proc/$(pgrep mmap)/maps
08048000-08049000 r-xp 00000000 08:08 731032 /tmp/mmap
97799000-97819000 rwxp 00000000 00:00 0
b7819000-b781a000 r-xp 00000000 00:00 0 [vdso]
bfcb3000-bfcd4000 rwxp 00000000 00:00 0 [stack]
Und die vier Situationen noch einmal bildlich:
+----------+ 08048000 +----------+ 08048000 +----------+ 08048000 +----------+ 08048000
| Programm | | Programm | | Programm | | Programm |
+----------+ 08049000 +----------+ 08049000 +----------+ 08049000 +----------+ 08049000
: : : : : : : :
: : : : : : : :
: : : : : : : :
: : : : : : : :
: : : : +----------+ 97799000 +----------+ 97799000
: : : : | 512 KiB | | 512 KiB |
: : +----------+ 97819000 +----------+ 97819000 +----------+ 97819000
: : | | | | : :
: : | 512 MiB | | 512 MiB | : :
: : | | | | : :
+----------+ B7819000 +----------+ B7819000 +----------+ B7819000 +----------+ B7819000
| vdso | | vdso | | vdso | | vdso |
+----------+ B781A000 +----------+ B781A000 +----------+ B781A000 +----------+ B781A000
: : : : : : : :
+----------+ BFCB3000 +----------+ BFCB3000 +----------+ BFCB3000 +----------+ BFCB3000
| Stack | | Stack | | Stack | | Stack |
+----------+ BFCD4000 +----------+ BFCD4000 +----------+ BFCD4000 +----------+ BFCD4000
(1) (2) (3) (4)
An den Speicheradressen kann man die Größen nachvollziehen – oder, indem
man statt /proc/$PID/maps
die „Datei“ /proc/$PID/smaps
ausgibt,
welche die Größen explizit nennt.
Was der alte LWN-Artikel also sagte, stimmt grundsätzlich auch heute noch. :)
+----------+··+------:····························:------+------+·······:-------+··+--------+
| Programm | | Heap :--> <--: mmap | vdso | <--: Stack | | Kernel |
+----------+··+------:····························:------+------+·······:-------+··+--------+
^ ^ ^ ^
| | | |
0x08048000 0x08xxxxxx 0xBFxxxxxx 0xC0000000