Um das Verhalten von C-Programmen besser zu verstehen, ist es hilfreich, eine (vereinfachte) Vorstellung von Programmen und Prozessen zu haben.
Ein Programm (oder ein Teil eines Programmes) kann in mehreren Formen auftreten, unter anderem:
Die letztere Form, also das Programm während der Ausführung, heißt üblicherweise Prozeß.
Je nach verwendetem Rechnersystem können solche Prozesse unterschiedlich dargestellt werden, aber viele Gemeinsamkeiten lassen sich erkennen, wenn man die Betrachtung weit genug vereinfacht. Im Folgenden beschränke ich mich auf wenige Punkte, die für das Verständnis von C-Programmen wesentlich sind; mehr Einzelheiten sind in allgemeineren Einführungen in die Informatik zu finden.
Ein Prozeß kann nur laufen, wenn eine entsprechende Systemumgebung vorhanden ist:
Der Arbeitsspeicher, den ein Prozeß sieht, besteht einfach aus einer langen Folge von Werten, die jeweils über eine Adresse angesprochen werden können. Eine solche Adresse ist nichts weiter als eine ganze Zahl zwischen 0 und einer systemabhängigen Obergrenze (typischerweise einige Megabyte bis einige Gigabyte). Eine Adresse ist in C als Wert eines Zeigers darstellbar und kann für alle dem Compiler bekannten Objekte mit Adreßoperator (&) beschafft werden.
Tatsächlich wird bei üblichen Systemen zwischen dem realen und dem virtuellen Speicher unterschieden. Auf den Unterschied wird hier nicht eingegangen; im Zweifelsfall geht es im Folgenden nur um den virtuellen Speicher.
Ein Prozessor hat neben verschiedenen funktionalen Einheiten (Rechenwerke, Steuerwerk etc.) einen Satz von Registern. Das sind sehr schnelle Speicher für Rechenoperanden, Ergebnisse usw.; ihre Anzahl ist begrenzt und sehr klein im Vergleich zum gesamten Prozeß (in der Regel nur für wenige oder maximal einige Dutzend Werte).
Typische Prozessoren haben einige allgemein verwendbare Register für ganze Zahlen, meist auch welche für Gleitkommazahlen, sowie Adreßregister, die zum Ansprechen von Daten im Arbeitsspeicher verwendet werden.
Einige Register haben eine besondere Bedeutung:
Während ein Prozeß läuft, muß sein gesamter Programmcode im Arbeitsspeicher erreichbar sein.
Bei jedem Laden eines Befehls aus dem Speicher wird der PC um die Länge des gelesenen Befehls automatisch weitergesetzt.
Nach der Ausführung eines Befehls wird der nächste Befehl anhand des PC aus dem Speicher gelesen.
Programmsprünge werden durchgeführt, indem dieses Register mit einer anderen Adresse überschrieben wird.
Der Arbeitsspeicher eines Prozesses ist nun zumindest in folgende Bereiche, sogenannte Segmente unterteilt:
Solche Daten sind:
Dazu wird einfach bei Programmstart ein ausreichend großer Bereich im Speicher reserviert (eben der Stack). Der Stack besteht immer aus zwei Teilen: ein zusammenhängender Bereich, der bereits belegt ist (üblicherweise der obere Teil), sowie der gesamte Rest, der noch frei ist.
Der Stackpointer zeigt nun zu jedem Zeitpunkt genau auf die Grenze zwischen dem freien und dem belegten Teil.
Um weitere Werte im Stack abzulegen, wird der Stackpointer um die Länge des Wertes erniedrigt (falls wie üblich der belegte Bereich über dem noch freien liegt), und der zu speichernde Wert an die Stelle kopiert, auf die der SP zeigt.
Der unterste Wert auf dem Stack (und nur dieser) kann freigegeben werden, indem der SP um die richtige Länge erhöht wird.
Der Aufruf einer C-Funktion sieht nun so aus, daß die zu übergebenden Parameter (von rechts her) nacheinander auf dem Stack abgelegt werden, dann werden zu rettende Register (zumindest der program counter und der gleich noch beschriebene frame pointer) abgelegt, und letztlich wird der SP noch um soviele Byte erniedrigt, wie alle automatischen Variablen Platz benötigen.
Wenn eine laufende Funktion a() eine weitere (b()) aufruft, wiederholt sich dieses Spiel: die an b() zu übergebenden Parameter, zu rettende Register und die von b() benötigten Variablen werden unterhalb des bereits belegten Stacks angelegt. Dadurch besteht zu jedem Zeitpunkt der belegte Stack aus eine Folge sogenannter stack frames aller gerade aktiven Funktionen. Ein stack frame einer Funktion enthält also die Parameter an die Funktion, gerettete Register, und lokale automatische Variablen.
Wenn eine aufgerufene Funktion mit ihrem Programmcode beginnt, kann sie nun auf alle ihre automatischen Variablen ebenso wie auf ihre Parameter zugreifen, indem sie zum Wert des SP einen bestimmten Offset addiert (der dem Compiler bereits bekannt ist, weil er die Größen aller Datentypen und damit alle Positionen im stack frame kennt; dieser Mechanismus heißt SP-relative Adressierung).
Während die Funktion aber läuft, wird sie weitere Werte auf dem Stack ablegen (zum Beispiel für Unterprogrammaufrufe) und sie wieder entfernen. Dadurch ändert sich der Wert des SP laufend, und auf ein und dieselbe Variable müßte ständig mit anderen Offsets zugegriffen werden. Um diesen Aufwand zu vermeiden, wird beim Start jeder Funktion der gerade aktuelle Wert des SP in das frame pointer-Register (FP) kopiert; dieses Register bleibt dann während der Abarbeitung der Funktion unverändert, und es kann anstatt SP-relativ über den FP auf automatische Variablen und Parameter zugegriffen werden, während sich der SP durchaus ändern kann.
Wird nun eine weitere Funktion aufgerufen, dann muß logischerweise der Inhalt des FP auf dem Stack gerettet werden, damit nach der Rückkehr wieder die lokalen Variablen korrekt adressiert werden, auch wenn die aufgerufene Funktion den FP zur Adressierung ihrer Variablen benutzt hat.
Dieser Heap wird in beliebiger Reihenfolge stückweise reserviert und wieder freigegeben, in C üblicherweise durch malloc() und free().
Alle diese Zusammenhänge sind im Abbildung 4 skizziert.
Der dargestellte Quelltext definiert einige Objekte im Speicher (die Einfärbung der einzelnen Elemente entspricht den zugehörigen Speicherbereichen):
AnyWare@Wachtler.de