blog · git · desktop · images · contact
2015-10-25
Vor einiger Zeit habe ich einige Blogpostings zum grundlegenden Memory Management von Userspace-Prozessen unter Linux geschrieben: 0, 1.
Schauen wir uns noch einmal kurz das berühmte Bild an:
+----------+··+------:····························:------+------+·······:-------+··+--------+
| Programm | | Heap :--> <--: mmap | vdso | <--: Stack | | Kernel |
+----------+··+------:····························:------+------+·······:-------+··+--------+
^ ^ ^ ^
| | | |
0x08048000 0x08xxxxxx 0xBFxxxxxx 0xC0000000
Anmerkungen: Dieses Bild wurde erstellt, um den Adressraum auf einem
32-Bit-Linux darzustellen. Bei 64 Bit sieht das minimal anders aus.
Außerdem: Das „Programm“-Segment ist auch als .text
bekannt.
Wie wird dieser Speicher nun benutzt? Wo werden Variablen gewöhnlicherweise abgelegt, wenn man in C programmiert? Meine vorigen Artikel haben sich hiermit beschäftigt:
malloc
. Die libc entscheidet dann, wo sie
den Speicher hernimmt. Üblicherweise kommt er vom Heap oder aus dem
mmap-Bereich. Diese Daten können dann so lange leben, wie das
Programm läuft. (Intern fragt die libc natürlich den Kernel nach
Speicher.)Es gibt aber noch zwei andere „Klassen“ von Variablen in C: Globale Variablen und Variablen in einer Funktion, die dort als „static“ markiert sind. Wie funktionieren diese? Wo werden die abgelegt?
static
vs. static
Das Keyword static
hat mehrere Bedeutungen:
Der erste Punkt ist nicht wirklich relevant für diesen Artikel. Es wird der Geltungsbereich einer Variablen verändert, aber das passiert auf einer ganz anderen Ebene. Im Endeffekt sind auch solche Variablen nur irgendwo im Speicher abgelegt und können von überall aus angesprochen werden – sofern man ihre Adressen kennt.
Globale Variablen – ob static
oder nicht – sind interessant. Sie leben
nicht auf dem Heap, nicht im mmap-Bereich und auch nicht auf dem Stack.
Und statische Variablen in Funktionen sind interessant, weil sie mehrere
Funktionsaufrufe überleben. Folglich können auch sie nicht wie
gewöhnlich auf dem Stack liegen.
Aber wo leben diese Variablen dann?
Man betrachte folgenden Code:
#include <signal.h>
#include <stdio.h>
#include <sys/types.h>
int global_i;
int global_i_initialized = 123;
int
main()
{
static int function_i;
static int function_i_initialized = 456;
printf("%p\n", (void *)&global_i);
printf("%p\n", (void *)&global_i_initialized);
printf("%p\n", (void *)&function_i);
printf("%p\n", (void *)&function_i_initialized);
kill(0, SIGSTOP);
return 0;
}
Wenn man das laufen lässt, dann gibt das Programm zunächst ein paar Adressen aus und hält sich dann selbst an. Daraufhin kann man den Adressbereich des Programms untersuchen:
$ ./bla
0x6009a8
0x60099c
0x6009a4
0x600998
[1]+ Stopped ./bla
$ cat /proc/$(pgrep bla)/maps
00400000-00401000 r-xp 00000000 00:21 325277 /tmp/bla
00600000-00601000 rw-p 00000000 00:21 325277 /tmp/bla <---------
7f4ee52fa000-7f4ee5495000 r-xp 00000000 08:01 10227509 /usr/lib/libc-2.22.so
7f4ee5495000-7f4ee5694000 ---p 0019b000 08:01 10227509 /usr/lib/libc-2.22.so
7f4ee5694000-7f4ee5698000 r--p 0019a000 08:01 10227509 /usr/lib/libc-2.22.so
7f4ee5698000-7f4ee569a000 rw-p 0019e000 08:01 10227509 /usr/lib/libc-2.22.so
7f4ee569a000-7f4ee569e000 rw-p 00000000 00:00 0
7f4ee569e000-7f4ee56c0000 r-xp 00000000 08:01 10227099 /usr/lib/ld-2.22.so
7f4ee5888000-7f4ee588b000 rw-p 00000000 00:00 0
7f4ee58be000-7f4ee58bf000 rw-p 00000000 00:00 0
7f4ee58bf000-7f4ee58c0000 r--p 00021000 08:01 10227099 /usr/lib/ld-2.22.so
7f4ee58c0000-7f4ee58c1000 rw-p 00022000 08:01 10227099 /usr/lib/ld-2.22.so
7f4ee58c1000-7f4ee58c2000 rw-p 00000000 00:00 0
7ffe48b2f000-7ffe48b50000 rw-p 00000000 00:00 0 [stack]
7ffe48bd1000-7ffe48bd3000 r--p 00000000 00:00 0 [vvar]
7ffe48bd3000-7ffe48bd5000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
$ fg
./bla
Man sieht also, dass alle vier Variablen in einem Speicherbereich über
.text
liegen, der les- und schreibbar ist (00600000-00601000). Das ist
auch schon die Antwort auf unsere erste Frage. Ja, die fraglichen
Variablentypen leben nicht auf dem Stack oder Heap oder mmap.
Stattdessen bekommen sie ihren ganz eigenen Speicher und sind dadurch
tatsächlich während der gesamten Laufzeit des Programms verfügbar.
So weit, so gut.
Es macht einen Unterschied, ob man Variablen initialisiert oder nicht. Deswegen habe ich oben vier verschiedene eingeführt. Zur Laufzeit liegen die zwar möglicherweise im selben Bereich, sie leben aber in unterschiedlichen Sektionen. Das ist ein recht subtiler Unterschied. Deutlicher wird es, wenn man sich den Assembler-Code anschaut:
$ gcc -Wall -Wextra -S -o bla.S bla.c
Ich beschränke mich hier auf die relevanten Abschnitte. Schauen wir uns zunächst initialisierte Variablen an, da diese etwas einfacher zu verstehen sind.
Am Anfang des Codes findet man das hier:
.data
.align 4
.type global_i_initialized, @object
.size global_i_initialized, 4
global_i_initialized:
.long 123
Und am Ende das:
.data
.align 4
.type function_i_initialized.2802, @object
.size function_i_initialized.2802, 4
function_i_initialized.2802:
.long 456
Über .data
wird die gleichnamige Sektion in der finalen ELF-Datei
zusammengestellt. Einfach ausgedrückt: Hier kann man Daten ablegen und
zur Laufzeit wird diese Sektion samt Daten dann in den Speicher geladen,
irgendwo in die Nähe von .text
. Ganz einfach, oder? Interessant zu
sehen ist, dass beide Variablen hier gespeichert werden. Das heißt also,
dass der einzige Unterschied zwischen den beiden der Scope auf C-Ebene
ist. Man kann function_i_initialized
nicht außerhalb von main
(oder
wo auch immer sie deklariert wurde) über ihren Namen ansprechen.
Okay, jetzt zu uninitialisierten Variablen. Am Anfang des Codes kann man nun sehen:
.comm global_i,4,4
Und am Ende wiederum:
.comm function_i.2801,4,4
Es ist nun wichtig, zu betonen, dass beide Variablen nur „deklariert“
wurden – ihre Größe und Alignment wurde bekanntgegeben –, aber ihnen
wurde kein Wert zugewiesen. Noch nicht einmal „0“. Klar, warum sollte
man hier auch einen Wert festlegen? Der Programmierer hat die Variablen
nicht initialisiert, also ist es ihm auch egal, welchen Wert sie
(initial) haben. Was er eigentlich will, ist eine gewisse
Speichermenge. Und das ist, was tatsächlich passiert: Diese
„Deklarationen“ erhöhen einen Counter. Mehr nicht. Am Ende wird die
resultierende Größe in der Binary gespeichert. Führt man das Programm
dann aus, allokiert der Loader den notwendigen Speicher. All das
passiert innerhalb der .bss
-Sektion.
Oftmals wird Speicher direkt über die Adresse referenziert oder relativ
zum aktuellen Stack Frame. Benutzt man aber .data
und .bss
, dann
wird ein anderes interessantes Verfahren zur Adressierung offenbar.
Man nehme folgenden Code:
int
main()
{
static int function_i;
function_i = 5;
return 0;
}
Schaut man sich den Assembler-Code dazu an, dann springt diese Instruktion ins Auge:
movl $5, function_i.2285(%rip)
Das wird „RIP-relative addressing“ genannt. Das Register RIP beinhält
die Adresse der nächsten Instruktion. Dadurch, dass .data
und .bss
so nah an .text
liegen, ist der Weg nicht weit, und es bietet sich an,
Variablen darin über eine Adresse in .text
als Basis zu adressieren.
Was für einen Zweck hat das? Ist das nicht unnötig kompliziert? Das Ding ist: „RIP-relative addressing“ vereinfacht das Erzeugen von position-independent code.
Manchmal schreibt man als Anfänger Code dieser Art:
#include <stdio.h>
char *
foo(void)
{
char s[] = "hello world";
return s;
}
int
main()
{
char *t;
t = foo();
printf("%s\n", t);
return 0;
}
Glücklicherweise warnt der Compiler heute sehr deutlich. Das Problem
ist, dass man die Adresse von etwas aus der Funktion zurückgibt, was
eigentlich nur innerhalb des Stack Frames genau dieser Funktion
existiert. Wenn foo()
also fertig ist, dann zeigt t
nach wie vor in
den ehemaligen Stack Frame. Es ist jetzt aber unbekannt, was sich nun
an dieser Stelle im Speicher befindet.
(Der Knackpunkt ist, zu verstehen, dass man nur einen Pointer zurückgibt und nicht den gesamten String.)
Die Lösung des Problems ist nun NICHT, die Variable s
als static
zu deklarieren.
Man betrachte:
#include <stdio.h>
char *
foo(char c)
{
static char s[] = "hello world";
s[0] = c;
return s;
}
int
main()
{
char *t, *u;
t = foo('x');
u = foo('y');
printf("%s\n", t);
printf("%s\n", u);
return 0;
}
Was wäre hier zu erwarten? Löst das nicht unser Problem? Hast du nicht irgendwo gelesen, dass „statische Variablen nicht verschwinden, wenn die Funktion zuende ist“? Außerdem spuckt der Compiler keine Warnung aus! Das ist es!
Nein, ist es nicht.
Was foo()
jetzt zurückgibt, ist eine Adresse aus der .data
-Sektion.
Und es gibt dieselbe Adresse bei jedem Aufruf zurück. Ich will darauf
hinaus: static s
sorgt NICHT dafür, dass man eine Variable erzeugt,
die halt einfach nach dem Funktionsaufruf weiterlebt. Mit dieser
Denkweise könnte man nämlich meinen, dass bei jedem Funktionsaufruf eine
neue „unsterbliche“ Variable erzeugt wird. Nein!
Man kriegt nur eine Variable.
.data
-Sektion gespeichert. Sind sie uninitialisiert, dann wird
einfach nur ihre Gesamtgröße als Größe der .bss
-Sektion
hinterlegt.Das war nun ein sehr grober Überblick. Da gibt es noch viel zu lernen.