blog · git · desktop · images · contact & privacy · gopher


Linux-Kunde: Adressraum eines Prozesses

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:

  1. Start und anhalten. Betrachtung des Layouts möglich.
  2. brk() und anhalten. Jetzt ist ein Heap sichtbar!
  3. Noch einmal 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:

  1. Halte ganz zu Beginn an. Dadurch kann man das initiale Layout betrachten. Das ist die erste Ausgabe von cat /proc/$(pgrep mmap)/maps.
  2. Blende 512 MiB anonym ein, halte danach an. Wieder Betrachtung des Layouts.
  3. Blende weitere 512 KiB ein, anhalten, Anzeige des Layouts.
  4. Gib die ersten 512 MiB mittels des 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