- Unterprogramm
-
Ein Unterprogramm oder eine Subroutine ist ein Teil eines Programms, der aus gegebenenfalls mehreren anderen Programmteilen oder Programmen heraus aufgerufen werden kann und nach Abschluss der Abarbeitung jeweils in das aufrufende Programm wieder zurückkehrt.
Je nach Kontext werden auch die Bezeichnungen Prozedur, Funktion, Methode, Modul und Operation benutzt. Die verschiedenen Bezeichnungen sind teils historisch entstanden, teils im Umfeld verschiedener Programmiersprachen, und betonen jeweils einen bestimmten Aspekt.
Unterprogramme können hinsichtlich ihrer Zugehörigkeit zu den aufrufenden Programmen unterschiedlich implementiert sein:
- Als logisch in sich geschlossener Funktionsteil im Quellcode eines Programms, das nach dem Prinzip der modularen Programmierung erstellt wurde. Hinsichtlich der administrativen Verwaltung (z. B. bei Änderungen) und der technischen Ausführung auf einem Computer gehören solche, z. T. ebenfalls "Unterprogramm" genannte Codeteile fest zum Gesamtprogramm. Je nach Programmiersprache (z. B. bei Cobol) und Programmierstil hat das Unterprogramm Zugriff auf alle im Quellcode definierten Daten und Funktionen.
- Als administrativ und zur Ausführung eigenständige Programme. Sie werden erst zur Ausführungszeit ('dynamisch') geladen und über Parameter mit dem/n aufrufenden Programm(en) verbunden.
- Eine Mischform aus beiden sind Unterprogramme mit ebenfalls eigenständigem Quellcode und getrennter Compilierung. Ihr Maschinencode wird jedoch zusammen mit dem Code sie aufrufender Programme zu einem ausführbaren Programm 'statisch gebunden'.
Die Abschnitte ab den Beispielen zeigen Details zur Programmierung mit Unterprogrammen, wobei Codebeispiele und die dazu gehördenden Erläuterungen hauptsächlich für die Programmiersprachen C, C++ oder / und Java gelten.
Inhaltsverzeichnis
Gründe für das Aufteilen von Programmen in Unterprogramme
Aus der ursprünglichen Sicht der Assemblerprogrammierung war der Grund der Aufteilung die mehrfache Verwendung der gleichen Befehlsfolge, damit Einsparung von Speicherplatz und die Vermeidung von Codewiederholungen.
In modernen Technologien des Softwareengineering ist ein weiterer wichtiger Grund allerdings die Strukturierung des Softwareentwurfes. Ein Unterprogramm sollte eine in sich abgeschlossene und gut beschreibbare Teilaufgabe erledigen. Das ist ebenfalls das Konzept der Methoden in der objektorientierten Programmierung. Unterprogramme sind heute zu Gunsten der besseren Wartbarkeit und Programmierfehlerbehebung vorzugsweise kurz und übersichtlich, da der rechnerinterne Zeit- und Verwaltungsaufwand zum Aufruf von Unterprogrammen auf modernen Rechenmaschinen praktisch keine Rolle mehr spielt.
Beispiele zu Unterprogrammen
Beispiel Java
float parabel(float x, float a, float b, float c) { float y = a*x*x + b*x + c; return y; }
Dieses Beispiel wird der Bezeichnung Funktion im mathematischem Sinne gerecht: x wird auf y abgebildet, die Funktion benutzt keine externen Daten. Man könnte x als Argument bezeichnen, a, b und c dagegen als Parameter. Die Bezeichnung y, die mathematisch das Ergebnis bezeichnet, kommt nur in der lokalen Variablen zum tragen.
int setNextValue(float value) { if (idx < 0 || idx > (idxMax - 1)) { throw new OutOfBoundsException(); } dataArray[idx] = value; return idx; }
Dieses Java-Beispiel stellt ein Unterprogramm dar, das eine Funktion im allgemeinsprachlichem Sinne ausführt. Der Rückgabewert ist lediglich der Folgeindex beziehungsweise die Anzahl der Daten. In diesem Unterprogramm werden Daten verwendet, die außerhalb deklariert sind. Wäre das ein C-Beispiel, dann kann es sich nur um globale Variablen handeln. In C++ könnten es sinnvollerweise Klassenvariablen sein, in Java sind es jedenfalls Klassenvariablen.
Beispiel Assembler (ADSP61xx)
.GLOBAL _set_floatExtend; //float set_floatExtend(_floatExtend* dst, float nVal); _set_floatExtend: I4=R4; //_floatExtend* dst PX=F8; //float nVal dm(0,I4)=PX1; //store 40 bit in 2 32 bit memory locations dm(1,I4)=PX2; ! FUNCTION EPILOGUE: i12=dm(-1,i6); jump (m14,i12) (DB);!.return F0=F8; //return value nVal to R0/F0 RFRAME; !FUNCTION END
Dieses Beispiel stellt in Assembler geschriebenes Unterprogramm dar, und zwar für die Signalprozessorfamilie ADSP61xx von Analog Devices. Dieses Beispiel wird weiter unten bezüglich Übergabe von Argumenten und Rückgabewert diskutiert. Die Kommentierungen zeigen, wie dieses Unterprogramm in einer C- oder C++-Umgebung verwendet werden soll.
Beispiel Großrechner IBM-Welt
Das Beispiel zeigt, beispielhaft und Cobol-ähnlich angedeutet, den Code eines aufrufenden und eines Unterprogramms, und zeigt, wie der Unterprogrammaufruf auf Systemen der Serie IBM System/390 verläuft. Technische Details wie etwa Register sichern und rückladen sind nicht dargestellt.
- Code des rufenden Programms:
* Daten: A-Daten. Struktur von A-Daten, z.B.: Ax >Formatdefinition(en) Ay ... B-Daten. B1 (definitionen) B2 (definitionen) B3 (definitionen) * Funktionscode: A-ROUTINE. A-1. >Befehle-1 Call UPRO Using A-Daten, B2. A-2. >Befehle-2, z.B. Auswerten und Verarbeiten Rückgabewert(e) A-ROUTINE Exit. >beliebige weitere Routinen / Befehle * Programm-Ende
- Code des Unterprogramms - ggf. in einer anderen Programmiersprache
* Datendefinitionen: A-DAT Format und Struktur wie in A-Daten! B-2 dto. C-x z.B. eigene Definitionen von UPRO * Funktionscode: Entry UPRO Using A-DAT, B-2. Feldbezeichnungen von Using ggf. abweichend, Reihenfolge identisch zu 'Call' >Befehle des Unterprogramms: Mit vollem Zugriff (auch ändernd) auf die Struktur (Einzelfelder) von A-Daten und B2. Ggf. setzen Returncode, z.B. in B-2 (= B2) >Exit = UPRO-Ende
- Ablauf des Unterprogramm-Aufrufs (Call und Return):
- (von den Compilern generierte Funktionen)
* Call im rufenden Programm: Setzt in einer Adressliste mit 2 Einträgen die Adresse von A-Daten und von B2 Setzt einen Zeiger (Register) auf die Adressliste Setzt einen Zeiger (Register) auf die Rückkehradresse A-2. Verzweigt zum Entry-Point von UPRO (ggf. nach dem Laden von UPRO) * Entry im Unterprogramm: Übernehmen der übergebenen Adressen: Adresse(A-DAT) = aus Adressliste(1), Adr(B-2) = aus Adressliste(2) >Verarbeitung - mit Zugriff auf alle Datendefinitionen * Rücksprung ins rufende Programm: Exit: Rückkehr zu Adresse A-2 im rufenden Programm
Parameter / Argumente
Unterprogramme verarbeiten Daten und liefern Werte zurück. Dazu haben Unterprogramme in ihrer Abbildung in höheren Programmiersprachen eine sogenannte Formale Parameterliste. Dieser Ausdruck wurde bereits in den 1960er Jahren für die damals als Lehrbeispiel entstandene Sprache ALGOL benutzt und ist noch heute üblich. Die Begriffe Parameter oder Argument werden in diesem Kontext oft synonym verwendet, wobei sich 'Parameter' genau genommen auf die Funktionsdefinition bezieht, 'Argument' hingegen auf den tatsächlichen Aufruf. Den Unterprogrammen wird beim Aufruf über die tatsächliche Parameterliste (genauer: Argumentliste) bestimmte Werte übergeben, mit denen sie arbeiten können. Die Unterprogramme liefern in der Regel Rückgabewerte zurück.
Alle genannten Werte können auch Referenzen, Zeiger oder Adressen auf Speicherbereiche sein. Die genannten Begriffe sind ebenfalls synonym. Im C++-Jargon wird allerdings oft streng zwischen Referenz und Zeiger unterschieden, mit Referenz wird die mit Type& deklarierte Variante bezeichnet, Zeiger dagegen mit Type*. Der Unterschied besteht darin, das eine Referenz im Gegensatz zum Zeiger nicht uninitialisiert oder leer sein darf. Das bedeutet, bei Verwendung einer Referenz muss immer ein gültiges Objekt des entsprechenden Typs übergeben werden. Referenzen auf Grunddatentypen sind ebenfalls erlaubt (z. B. int &).
Übergabe der Parameter/Argumente über den Stack
Für ein Verständnis der Funktionsweise von Unterprogrammen ist folgendes Basiswissen notwendig:
Grundsätzlich ist der Speicher von Prozessoren unterteilt in
- Programmspeicher: Dort steht der Maschinencode, der als Befehle abgearbeitet wird.
- Datenspeicher: Dort sind Daten abgespeichert. Wird auch als Heap bezeichnet.
- Stack: Das ist ein besonderer Datenbereich, dessen Verwendung insbesondere bei Unterprogrammen eine Rolle spielt.
Jeder Thread hat seinen eigenen Stackbereich. Im Stack werden gespeichert:
- Die Rücksprungadressen für die Fortsetzung der Programmbearbeitung nach Abarbeitung des Unterprogramms
- Die tatsächlichen Parameter
- Alle Daten, die lokal in einer Prozedur vereinbart werden
- Rückgabewerte
Der Stack wird bei der Programmabarbeitung im Maschinencode über ein spezielles Adressregister adressiert, den Stackpointer. Dieser adressiert immer das untere Ende des als Stack benutzen Speicherbereiches. Hinzu kommt zumeist ein Basepointer, der eine Basisadresse der Variablen und tatsächlichen Parameter innerhalb des Stacks adressiert. Der Begriff Stack ist im deutschen als Stapel übersetzbar, auch der Begriff Kellerspeicher wird benutzt. Im Stack werden Informationen gestapelt und nach dem LIFO-Prinzip (last in, first out) gespeichert und wieder herausgelesen. Allerdings kann der Zugriff auch auf beliebige Adressen innerhalb des Stacks erfolgen.
Die Parameterübergabe erfolgt über den Stack. Jeder tatsächliche Parameter wird in der Reihenfolge der Abarbeitung, üblicherweise von links nach rechts (gemäß einer strikt festgelegten Aufrufkonvention), auf den Stack gelegt. Dabei erfolgt, falls notwendig, eine Konvertierung auf das Format, das vom Unterprogramm benötigt wird.
Bei Aufruf des Unterprogramms wird dann der sogenannte Basepointer auf die nunmehr erreichte Adresse des Stacks gesetzt. Damit sind die Parameter des Unterprogramms relativ über die Adresse, die im Basepointer gespeichert ist, erreichbar, auch wenn der Stack für weitere Speicherungen benutzt wird.
Werte oder Referenzen/Zeiger als Parameter
Werden in der tatsächlichen Parameterliste nicht einfache Werte wie int oder float angegeben, sondern komplette Datenstrukturen, dann werden im Stack meist nicht die Werte der Datenstruktur selbst, sondern Referenzen (Adressen) auf die Datenstrukturen übergeben. Das hängt allerdings vom Aufruf und der Gestaltung der tatsächlichen Parameterliste ab. In C und C++ ergeben sich folgende Verhältnisse:
void function(type* data) // Funktionskopf, formale Parameterliste … struct { int a, float b} data; // Datendefinition function(&data); // Funktionsaufruf, tatsächliche Parameterliste
In diesem Fall erfolgt beim Aufruf die explizite Angabe der Adresse der Daten, ausgedrückt mit dem & als Referenzieroperator. Beim Aufruf ist data ein Zeiger (engl. pointer) auf die Daten. Allgemein ausgedrückt kann von Referenz auf die Daten gesprochen werden.
Die Angabe
function(data)
ohne den Referenzierungsoperator & führt zu einem Syntaxfehler. In C allerdings nur, wenn der Prototyp der gerufenen Funktion bekannt ist.
In C++ kann der Funktionskopf im gleichen Beispielzusammenhang mit
void function(type& data)
geschrieben werden. Dann ist der Aufruf mit
function(data)
zu gestalten. Der Übersetzer erkennt automatisch aufgrund des in C++ notwendigerweise bekannten Funktionsprototyps, dass die Funktion laut formaler Parameterliste eine Referenz erwartet und kompiliert im Maschinencode das Ablegen der Adresse der Daten auf den Stack. Das entlastet den Programmierer von Denkarbeit, der Aufruf ist einfacher. Allerdings ist beim Aufruf nicht ersichtlich, ob die Daten selbst (call by value) oder die Adresse der Daten übergeben wird.
In C oder C++ ist es auch möglich, anstelle der meist sinnvollen und gebräuchlichen Referenzübergabe eine Wertübergabe zu programmieren. Das sieht wie folgt aus:
void function(type data) // Funktionskopf, formale Parameterliste … struct { int a, float b } data; // Datendefinition function(data); // Funktionsaufruf, tatsächliche Parameterliste
Beim Aufruf wird der Inhalt der Struktur insgesamt auf den Stack kopiert. Das kann sehr viel sein, wenn die Struktur umfangreich ist. Dadurch kann es zum Absturz des gesamten Ablaufes kommen, wenn die Stackgrenzen überschritten werden und dies in der Laufzeitumgebung nicht erkannt wird. Eine Wertübergabe ist allerdings sinnvoll in folgenden Fällen:
- Übergabe einer kleinen Struktur
- Einkalkulierung der Tatsache, dass der Inhalt der originalen Struktur während der Abarbeitung verändert wird. Die Inhalte der Struktur beim Aufrufer bleiben unverändert, da eine Kopie der Daten angelegt und übergeben wird.
Rückgabe von Ergebnissen
Ein Unterprogramm kann in Programmiersprachen wie C, C++ oder Java – diese Restriktion gilt jedoch nicht für alle Programmiersprachen – genau einen Wert als Rückgabewert zurückgeben. Im einfachsten Fall ist das ein einfacher Wert wie int oder float. Dieser einfache Wert passt immer in ein CPU-Register, damit ist dies für die meisten Übersetzer/Laufzeitsysteme die richtige Wahl. Der Wert im Register wird entweder innerhalb eines Ausdruckes unmittelbar weiterverarbeitet:
… + function(parameter) * …
oder er wird gespeichert:
variable = function(parameter).
Das Unterprogramm selbst hat dann keine Wirkung.
Rückschreiben über referenzierte Daten
Allerdings ist ein Rückschreiben auch über Referenzen, die als Parameter des Unterprogramms übergeben wurden, möglich:
void function(Type* data) { data->a = data->b*2; // Wert in data->a wird veraendert. }
Das gilt gleichermaßen für Java. Das Rückschreiben kann ungewollt sein, weil Nebenwirkungen (Nebeneffekte) verhindert werden sollen. Ein Unterprogramm soll die Werte von bestimmten Datenstrukturen nur lesend verarbeiten und wirkungsfrei darauf sein. In C++ (bzw. in C) ist es möglich, zu formulieren:
void function(Type const* data) { data->a = data->b*2; // Hier meldet der Übersetzer einen Syntaxfehler. }
Die hier verwendete Schreibweise mit dem const vor dem * soll deutlich machen, dass der gezeigerte (referenzierte) Bereich als konstant zu betrachten ist. Möglicherweise wird const Type* geschrieben, was syntaktisch und semantisch identisch ist.
Nur in diesem Fall ist es möglich, einen als const deklarierten Speicherbereich überhaupt zu übergeben. Die Konstruktion
const struct Type{ int a, float b } data = { 5, 27.2 }; … function(Type* data) … // Funktionsdefinition … function(&data)
führt in C++ zu einem Syntaxfehler, weil es nicht gestattet ist, als const bezeichnete Daten an eine nicht const-Referenz zu übergeben. In C werden Zeigertypen nicht so genau getestet, so dass dieser Code - abhängig vom verwendeten Übersetzer - in solchen Fällen möglicherweise lediglich eine warning auslösen würde.
Allerdings ist es in C++ möglich, innerhalb der Funktion den Typ des Zeigers zu wandeln und sozusagen "durch die Hintertür" dennoch schreibend auf den Speicherbereich zuzugreifen. Das ist allerdings ein nicht teamgerechter, möglicherweise als nachlässig zu bezeichnender Programmierstil.
In Java ist es nicht möglich, den schreibenden oder nicht schreibenden Zugriff auf eine Instanz in einem Referenzparameter zu unterscheiden, das Sprachkonzept sieht das nicht vor. Ein final-Zusatz bedeutet nicht, dass das referenzierte Objekt nicht geändert wird, sondern nur, dass die Referenz selbst nicht geändert wird.
Rückgabewertübergabe in Form einer kompletten Struktur von Daten
Es ist möglich, nicht einen einfachen skalaren Wert wie int oder float als Rückgabewert zurückzugeben, sondern eine komplette Struktur von Werten. Dabei gibt es mehrere Möglichkeiten:
Folgende Struktur wird von den meisten C++-Übersetzern mindestens als warning bewertet und ist grundlegend falsch:
struct DataType { int a; float b }; DataType* function() { DataType data; // Daten werden hier angelegt, data.a = 5; data.b = 27.2; // und belegt return &data; // und nach außen als Referenz bekanntgegeben. }
Der Fehler besteht darin, dass die Daten im Stack angelegt werden und eine Referenz auf den Stackbereich zurückgegeben wird, der Stackbereich aber dann für anderweitige Verwendung freigegeben wird. Es kommt auf die weitere Stacknutzung an, ob der Bereich tatsächlich überschrieben wird, so dass ein solcher grober Fehler zunächst gegebenenfalls gar nicht auffällt.
Es gibt in C und C++ zwei Varianten, das Problem richtig zu lösen, in Java geht nur die erste:
DataType* function() { DataType* data = new DataType; // Daten werden stattdessen im Heap angelegt, data->a = 5; data->b = 27.2; // und belegt return data; // und nach außen als Referenz bekanntgegeben. }
Dieses Beispiel geht adäquat in Java, in C++ muss noch geklärt werden, wer für das Löschen der Daten verantwortlich ist. Ansonsten bleibt Speichermüll stehen, was bei längerer Laufzeit zum crash des Systems führen kann. In Java kann das nicht passieren, dort gibt es den Garbage-Collector, zu deutsch den Müllaufsammler.
In C++ ist es auch möglich zu schreiben:
DataType function() { DataType data; // Daten werden hier angelegt, data.a = 5; data.b = 27.2; // und belegt return data; // und nach außen kopiert. }
Der Unterschied zu der Schreibweise oben ist nuanciert aber bedeutend. In diesem Fall werden aber die Daten auf einen Speicherbereich kopiert, der zwar im Stack liegt, aber nicht in dem vom Unterprogramm verantworteten Bereich. In der Umgebung des Aufrufes muss beispielsweise folgendes stehen:
int x = (function()).a;
oder
DataType data2 = function();
Im ersten Fall wird auf ein Element der erzeugten Daten zugegriffen, im zweiten Fall erfolgt bei der Zuweisung das Kopieren der Daten aus dem Stackbereich in denjenigen Datenbereich, der von data2 belegt wird. Nach dem Programmblock (an der schließenden geschweiften Klammer) wird der Stackbereich, der für den Rückgabewert reserviert und von diesem belegt wurde, wieder freigegeben.
In den hier in C und C++ vorgestellten Beispielen handelt es sich um Wert-(value-)-Übergaben von Strukturen im Rückgabewert. Diese kann (meist) nicht mehr über Register erfolgen, sondern erfolgt durch Umkopieren im Stack. Bei der Signalprozessorfamilie 216× von Analog Devices wird allerdings eine solche Wertübergabe von Strukturen, die zwei 32-bit-Werte umfassen, dennoch über Register ausgeführt - über R0 und R1. Das ist laufzeitoptimal und ist in diesem Fall getrimmt auf die Übergabe von Rückgabewerten beispielsweise für komplexe Zahlen. Im Kontext des Aufrufes wird hier die Anlage einer extra Struktur für die Rückgabewertübergabe und deren Referenzierung eingespart.
Überladen und dynamisches Binden von Unterprogrammen
Das Wort überladen ist eine direkte Übersetzung aus dem englischen „overload“ und nicht unbedingt aus sich heraus verständlich. Mit Überladen wird hier das Umdefinieren des Bezeichners (engl. name mangling) eines Unterprogramms in Abhängigkeit von der Parameterauswahl verstanden. Eine besser verständliche Bezeichnung wäre Parametersensibilität, die sich aber in Fachkreisen bis heute nicht durchsetzen konnte. Die nachstehenden Beispiele sind nur in C++ oder Java möglich; nicht aber in reinem C, wo die Überladung von Funktionen nicht vorgesehen ist und der Versuch, eine solche zu realisieren, beim Kompilieren einen Fehler auslösen würde.
void function(int x);
ist eine gänzlich andere Funktion als
void function(float x);
Beide Funktionen haben verschiedene Implementierungen, verschiedene Bezeichnungen in der Objektdatei und haben nichts weiter miteinander zu tun als dass sie den gleichen Namen in der Anwendung tragen. Überladen ist also nur der Funktionsname.
Problematisch für das Verständnis und für den Übersetzer sind Aufrufe folgender Art:
short y; function(y);
Hier muss der Übersetzer selbständig entscheiden, ob er besser nach int castet und die int-Variante aufruft, oder nach float castet und die float-Variante aufruft. Naheliegend wäre der erste Fall, dennoch hängt hier einiges von der Meinung des Übersetzer ab; der Programmierer ahnt nicht, was sich im Untergrund des Maschinencodes tut. Einige Übersetzer verhalten sich in solchen Fällen nett zum unbedarften Programmierer und wählen das mutmaßlich richtige (was im konkreten Fall falsch sein kann), andere Übersetzer, beispielsweise GNU, neigen eher dazu, einen Fehler auszugeben, um vom Anwender eine Entscheidung zu verlangen. Er kann beispielsweise mit der Schreibweise
function((float)(y));
mit dem angegebenen casting die Auswahl festlegen.
Im Allgemeinen ist es besser, die Möglichkeit des Überladens nicht zu frei zu nutzen, sondern nur für deutliche Unterschiede wie Varianten von Unterprogrammen mit unterschiedlicher Parameteranzahl. Aber auch hier führt die Kombination mit Parametern mit default-Argumenten zu Irritationen. Als sicher kann ein parametersensitiver Funktionsaufruf mit Zeigern verschiedenen Types, die nicht über Basisklassen (Vererbung) ableitbar sind, bezeichnet werden. Hier prüft der Übersetzer jedenfalls die Zeigertyprichtigkeit und meldet entweder einen Fehler oder verwendet genau das passende Unterprogramm:
class ClassA; class ClassB; function(class A*); // Ist deutlich unterschieden von function(class B*);
wenn ClassA und ClassB in keiner Weise voneinander abgeleitet (vererbt) sind.
Mit Überladen wird im direktem Sinn des Wortes aber auch das dynamisches Binden bezeichnet. Hier wird tatsächlich eine Methode (= Unterprogramm) einer Basisklasse von der gleichnamigen und gleichparametrischen Methode der abgeleiteten Klasse überdeckt. Zur Laufzeit wird diejenige Methode gerufen, die der Instanz der Daten entspricht. Das wird vermittelt durch die Tabelle virtueller Methoden, ein Grundkonzept der Objektorientierten Programmierung.
Umsetzung auf Maschinenebene
Das Konzept des Stack wurde weiter oben im Abschnitt "Übergabe der Parameter/Argumente über den Stack" bereits erläutert.
Für Unterprogramme auf Maschinensprachniveau (Assembler) ist es an sich gleichgültig beziehungsweise liegt in der Hand des Programmierers, wie er die Parameterübergabe und die Rücksprungadresse verwaltet. Möglich ist auch die Übergabe und Speicherung ausschließlich in Prozessorregistern. Allerdings ist bei der Verwaltung der Rücksprungadresse die Notwendigkeit eines geschachtelten Aufrufs mehrerer (typisch verschiedener) Unterprogramme ineinander zu beachten. Nur bei ganz einfachen Aufgaben ist eine Beschränkung auf wenige oder nur eine Ebene sinnvoll. Es gibt aber tatsächlich bei zugeschnittenen Prozessoren und Aufgabenstellungen auch solche Konzepte.
- Die Rücksprungadresse, das ist die Folgeadresse nach dem Aufruf der Unterprogramme für die Fortsetzung des aufrufenden Programmes, wird auf den Stack gelegt.
- Zuvor werden die Aufrufparameter auf den Stack gelegt.
- Noch zuvor wird ein gegebenenfalls notwendiger Speicherplatz für Rückgabewerte auf dem Stack reserviert, wenn notwendig.
- der Basepointer wird auf den Stack gelegt.
- Danach wird das Unterprogramm aufgerufen, das heißt, der Instruction pointer wird geändert auf die Startadresse des Unterprogramms.
- Am Beginn des Unterprogramms wird der Basepointer auf den Wert des Stackpointers gesetzt als Adress-Bezug der Lage der Parameter, des Rücksprunges und der lokalen Variablen.
- Der Stackpointer wird gegebenenfalls weiter dekrementiert, wenn das Unterprogramm lokal Variablen benötigt. Diese liegen auf dem Stack.
- Am Ende des Unterprogramms wird der ursprüngliche Wert des Basepointer aus dem Stack geholt und damit rekonstruiert.
- Dann wird die Rücksprungadresse aus dem Stack geholt und der Instruction Pointer damit wieder restauriert.
- Der Stackpointer wird incrementiert um den Wert, um den vorher decrementiert wurde.
- Damit wird das aufrufende Programm fortgesetzt.
In Assembler muss man diese Dinge alle richtig selbst programmieren. In C/C++ übernimmt das der Übersetzer. In Java erfolgt innerhalb der Speicherbereiche der Virtuellen Maschine das Gleiche, organisiert vom Bytecode (erzeugt vom Java-Übersetzer) und dem Maschinencode in der virtuellen Maschine.
Als Illustration sei hier der erzeugte Assembler-Code von folgender einfachen Methode gezeigt:
float parabel(float x) { return x*x; }
Maschinencode am Aufruf: float y = parabel(2.0F);
push 40000000h // Der Wert 2.0 wird in den Stack gelegt. call parabel // Aufruf des Unterprogramms; // call legt den Instructionpointer in den stack add esp,4 // Addieren von 4, das ist Byteanzahl des Parameters fst dword ptr [ebp - 4] // Abspeichern des Ergebnisses in y
Maschinencode des Unterprogramms:
parabel: push ebp // Der Basepointer wird im Stack gespeichert mov ebp,esp // Der Basepointer wird mit dem Wert des Stackpointer geladen sub esp,40h // 64 Byte Stack werden reserviert. push ebx // CPU-Register, die hier verwendet = geändert werden, push esi // werden im Stack zwischengespeichert. push edi fld dword ptr [ebp + 8] // Der Wert des Parameters x wird relativ zum Basepointer geladen fmul dword ptr [ebp + 8] // und in der floating-point-unit mit selbigem multipliziert. pop edi // Register werden restauriert. pop esi pop ebx mov esp,ebp // Der Stackpointer wird genau auf den Stand wie beim Aufruf // des Unterprogramms gebracht pop ebp // Der Basepointer wird aus dem Stack restauriert ret // Der Instruction pointer wird aus dem Stack restauriert // und damit wird nach dem call (oben) fortgesetzt.
Folgendes Beispiel zeigt einen handgeschriebenen Assemblercode für den Signalprozessor ADSP 216x von Analog devices für folgende aus C zu rufende Funktion:
float set_floatExtend(_floatExtend* dst, float nVal);
Dabei handelt es sich um eine Funktion, die einen in nVal stehenden Wert auf der Adresse dst speichern soll. Das besondere hierbei ist, dass der floatwert 40 Bit umfasst, und auf zwei 32-bit-Speicherlocations geschrieben werden soll.
.GLOBAL _set_floatExtend; // Sprunglabel global sichtbar _set_floatExtend: // Sprunglabel angeben, das ist der Name des Unterprogramms, // aus C ohne Unterstrich anzugeben. I4 = R4; // Im Register R4 wird der erste Parameter _floatExtend* dst übergeben. // Da es eine Adresse ist, wird diese in das Adressregister I4 umgeladen. PX = F8; // Der zweite Parameter float nVal wird aus F8 in das Register PX geladen. dm(0,I4) = PX1; // Ein Teil des Inhaltes von PX, in PX1 sichtbar, wird auf // der Adresse gespeichert, die von I4 gezeigert wird. dm(1,I4) = PX2; // Speicherung des zweiten Teils auf der Folgeadresse ! FUNCTION EPILOGUE: // Standard-Abschluss des Unterprogramms: i12 = dm(-1,i6); // Das Adressregister i12 wird aus einer Adresse relativ zum Basepointer // (hier i6) geladen. Das ist die Rücksprungadresse. jump (m14,i12) (DB) // Das ist der Rücksprung unter Nutzung des Registers i12. F0 = F8; // nach dem Rücksprung werden die noch im cashe stehenden Befehl verarbeitet, // hier wird der Wert in F8 nach dem Register R0 geladen, für return. RFRAME; // dieser Befehl korrigiert den Basepointer i6 und Stackpointer i7.
Sammlungen von Unterprogrammen
Unterprogramme werden oft vorübersetzt und zu Bibliotheken zusammengefasst.
Siehe auch
Kategorien:- Programmiersprachelement
- Unterprogramme
Wikimedia Foundation.