Privacy Policy Cookie Policy Terms and Conditions Unterprogramm - Wikipedia

Unterprogramm

aus Wikipedia, der freien Enzyklopädie

Dieser Artikel beschäftigt sich mit dem Begriff der Funktion, wie er in Programmiersprachen wie C++ oder Java verwendet wird.
  • Im Artikel Funktion (Programmierung) wird der Begriff der Funktion im Hinblick auf den Unterschied der imperativen gegenüber der funktionalen Programmierung erläutert.

Ein Unterprogramm oder eine Subroutine ist ein Teil eines Programmes, der aus gegebenenfalls mehreren anderen Programmteilen heraus gerufen werden kann und nach Abschluss der Abarbeitung jeweils in das aufrufende Programm wieder zurückkehrt.

Die Bezeichnung Prozedur (procedure) für eine in sich abgeschlossene Teil-Routine in einer höheren Programmiersprache bedeutet das Gleiche.

Die Bezeichnung Funktion (function) bezeichnet als Abgrenzung zu procedure in einigen Programmiersprachen (Algol, PL/1, Pascal) eine Routine, die einen Wert zurückliefert, in Anlehnung an die Bedeutung einer mathematischen Funktion. In der Programmiersprache C++ wird jedoch nicht zwischen Funktion und Prozedur unterschieden. Jede Subroutine wird als Funktion bezeichnet. Der Rückgabewert void wurde syntaktisch eingeführt, wenn nichts zurückgegeben wird. Die Bedeutung von Funktion ist hier nicht mehr mathematisch orientiert (Abbildung x auf y), sondern im allgemeinsprachlichen Sinne von Funktion.

Als Methode wird eine Routine in der objektorientierten Programmierung bezeichnet, die auf eine Klasse bezogen ist. Der Begriff Methode betont stärker die Sicht auf die Daten, die mit einer Methode bearbeitet werden. Teilweise wird dafür auch die Bezeichnung Operation verwendet.

Es gibt keinen tatsächlichen Bedeutungsunterschied zwischen diesen Begriffen. Die verschiedenen Begrifflichkeiten sind teils historisch entstanden, teils im Umfeld verschiedener Programmiersprachen und betonen jeweils einen bestimmten Aspekt. Nachfolgend wird grundsätzlich erläutert, wie Subroutinen aufgebaut werden und im Maschinencode ablaufen. Dabei wird durchgängig einheitlich der allgemeingültige Begriff Subroutine verwendet. Die Beispiele sind auf die Programmiersprachen C++ (bzw. C) und teilweise Java bezogen, auch Assembler wird angegeben.

Inhaltsverzeichnis

[Bearbeiten] Beispiele zu Subroutinen

float parabel(float x, float a, float b, float c)
{ 
  float y = a*x*x +  b*x +c;
  return y;
}

Diesem Beispiel wird die Bezeichnung Funktion im mathematischem Sinne gerecht. x wird auf y abgebildet. Die Funktion benutzt keine fremden 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 Variable zum tragen.

int setNextValue(float value)
{ 
  boolean bOk = true;
  
  if     (idx < 0)           { idx = 0; }  //start
  else if(idx < (idxMax-1) ) { idx +=1; }  //next;
  else
  { 
    bOk = false;  //idx nicht verändern!
  }
  
  if(bOk == true)
  { 
    dataArray[idx] = value;
    return idx;
  }
  else
  { 
    return -1;
  }
}

Dieses Beispiel stellt eine Subroutine dar, die eine Funktion im allgemeinsprachlichem Sinne ausführt. Der Returnwert ist lediglich der Folgeindex beziehungsweise die Anzahl der Daten oder -1, wenn kein Platz mehr zum Abspeichern vorhanden ist. In dieser Subroutine 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 Klassenvariable sein, in Java sind es jedenfalls Klassenvariable. In diesem Beispiel ist ein bestimmter zu empfehlender Programmierstil dargestellt: Man könnte auch auf die Zwischenvariable bOk verzichten und stattdessen den Rücksprung mit -1 weiter oben einbringen. Bei umfangreicheren Subroutinen wird das aber unübersichtlich. Hier ist eine saubere Trennung zwischen Vorbehandlung und Ausführung vorgenommen.

.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 eine Subroutine in Assembler dar, und zwar für die Signalprozessorfamile ADSP61xx von Analog Devices. Dieses Beispiel wird weiter unten bezüglich Übergabe von Argumenten und Returnwert diskutiert. Die Kommentierungen zeigen, wie diese Subroutine in einer C- oder C++-Umgebung verwendet werden soll.

[Bearbeiten] Parameter oder Argumente

Subroutinen verarbeiten Daten und liefern Werte zurück. Dazu haben Subroutinen in ihrerer Abbildung in höheren Programmiersprachen eine sogenannte Formale Parameterliste. Dieser Ausdruck wurde bereits in den 60-er Jahren für die damals als Lehrbeispiel entstandene Sprache ALGOL benutzt und ist noch heute üblich. Die Begriffe Parameter oder Argument sind in diesem Kontext synonym. Den Subroutinen wird beim Aufruf über die aktuelle Parameterliste bestimmte Werte übergeben, mit denen sie arbeiten können. Die Subroutinen können über einen Returnwert genau einen Wert zurückliefern. Soweit die einfache Theorie.

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 Varianten bezeichnet, Zeiger dagegen mit Type*. Realisierungstechnisch sind beide Varianten auch in C++ identisch.

[Bearbeiten] Übergabe der Parameter / Argumente über den Stack

Für ein Verständnis der Funktionsweise von Subroutinen 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.
  • Stack: Das ist ein besonderer Datenbereich, dessen Verwendung insbesondere bei Subroutinen eine Rolle spielt.

Jeder Thread hat seinen eigenen Stackbereich. Im Stack werden gespeichert:

  • Die Rücksprungadressen für die Fortsetzung der Programmbearbeitung nach Abarbeitung der Subroutine
  • Die aktuellen Parameter
  • Alle Daten, die lokal in einer Prozedur vereinbart werden
  • Returnwerte

Der Stack wird bei der Programmabarbeitung im Maschinencode adressiert mittels eines speziellen Adressregisters, dem sogenannten Stackpointer. Dieser adressiert immer das untere Ende des als Stack benutzen Speicherbereiches. Hinzu kommt zumeist ein Basepointer, der eine Basisadresse der Variablen und aktuellen 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 aktuelle Parameter wird in der Reihenfolge der Abarbeitung, üblicherweise von links nach rechts, auf den Stack gelegt. Dabei erfolgt, falls notwendig, eine Konvertierung auf das Format, das von der Subroutine benötigt wird.

Bei Aufruf der Subroutine wird dann der sogenannte Basepointer auf die nunmehr erreichte Adresse des Stacks gesetzt. Damit sind die Parameter der Subroutine relativ über die Adresse, die im Basepointer gespeichert ist, erreichbar, auch wenn der Stack für weitere Speicherungen benutzt wird. Dieses Ablaufmodell gilt für Subroutinen insbesondere bei Verwendung der Programmiersprache C/C++, die Grundaussage ist aber generell richtig. Bei Java wird ein eigener Stack unabhängig vom Stack des Prozessors von der Virtuellen Maschine verwaltet.

[Bearbeiten] Werte oder Referenzen/Zeiger als Parameter

Werden in der aktuellen Paramterliste 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 aktuellen Parameterliste ab. In C und C++ ergeben sich folgende Verhältnisse:

void function(type* data)        //Funktionskopf, formale Parameterlist
...
struct { int a, float b} data;   //Datendefinition
function(&data);                 //Funktionsaufruf, aktuelle Paramterliste

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 gleichem Beispielzusammenhang mit

void function(type& data)

geschrieben werden. Dann ist der Aufruf mit

function(data)

zu gestalten. Der Compiler erkennt automatisch aufgrund des in C++ notwendigerweise bekannten Funktionsprototyps, dass die Funktion laut formaler Parameterliste eine Referenz erwartet und compiliert 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 Parameterlist
...
struct { int a, float b} data;  //Datendefinition
function(data);                 //Funktionsaufruf, aktuelle Paramterliste

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 Wertbergabe ist allerdings sinnvoll in folgenden Fällen:

  • Übergabe einer kleinen Struktur
  • Einkalkulierung der Tatsache, dass der Inhalt der originalen Struktur während der Abarbeitung beispielsweise wegen Multithreading während der Abarbeitung verändert wird, die übergebene Kopie bleibt unverändert.

In Java werden generell nur Referenzen übergeben. Es besteht keine andere Möglichkeit. Damit ist ein Stack-crash wegen zu großer Strukturen ausgeschlossen. Für die Probleme einer Veränderung des Inhaltes der referenzierten Daten in einem anderen Thread gibt es Maßnahmen des gegenseitigen Ausschlusses (Mutex). Gegebenenfalls muss der Inhalt einer Datenstruktur kopiert werden, wofür es Methoden wie Object.clone() oder System.arraycopy() gibt.

[Bearbeiten] Rückschreiben von Ergebnissen

Eine Subroutine kann in Programmiersprachen wie C/C++ oder Java genau einen Wert als Returnwert 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 Compiler/Laufzeitsysteme die richtige Wahl. Der Wert im Register wird entweder innerhalb eines Ausdruckes unmittelbar weiterverarbeitet:

... + function(parameter) * ...

oder er wird gespeichert:

variable = function(parameter).

Die Subroutine selbst hat dann keine Wirkung.

[Bearbeiten] Rückschreiben über referenzierte Daten

Allerdings ist ein Rückschreiben auch über Referenzen, die als Parameter der Subroutine ü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. Eine Subroutine 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 Compiler 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 declarierten 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, abhängig vom Compiler gibt es in solchen Fällen möglicherweise nur eine warning.

Allerdings ist es in C++ möglich, innerhalb der Funktion den Zeiger zu casten und 'hintenrum' doch auf die Daten zu schreiben. Das ist allerdings ein nicht teamgerechter, möglicherweise als nachlässig zu bezeichnenter Programmierstil.

In Java ist anstatt const das Schlüsselwort final zu verwenden, um nach außen anzuzeigen, dass Daten in der Referenz nicht unmittelbar manipuliert werden. Ein casting 'hintenrum' ist hier nicht möglich.

[Bearbeiten] Returnwertübergabe in Form einer kompletten Struktur von Daten

Es ist möglich, nicht einen einfachen skalaren Wert wie int oder float als Returnwert zurckzugeben, sondern eine komplette Struktur von Werten. Dabei gibt es mehrere Möglichkeiten:

Folgende Struktur wird von den meisten C++-Compilern 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.
}

Diese 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 von der Subroutinen verantwortetem Bereich. In der Umgebung des Aufrufes muss beispiesweise 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 geschweichten Klammer) wird der Stackbereich, der für den Returnwert reserviert und von diesem belegt wurde, wieder freigegeben.

In den hier in C und C++ vorgestellten Beispielen handelt es sich um eine Wert (value-)-Übergaben von Strukturen im Returnwert. Diese kann (meist) nicht mehr ber Register erfolgen, sondern erfolgt ber Umkopieren im Stack. Bei der Signalprozessorfamile 216x 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 Returnwerten beispielsweise für komplexe Zahlen. Im Kontext des Aufrufes wird hier die Anlage einer extra Struktur für die Returnwertbergabe und deren Referenzierung eingespart.

[Bearbeiten] Wirkung von Subroutinen und saubere Programmierung

In imperativen Programmiersprachen kann in Subroutinen alles Mögliche getan, sprich alle denkbaren und undenkbaren Wirkungen = Datenänderungen erzielt werden. Einiges wird zwar wie oben mit const (C/C++) oder final (Java) syntaktisch abgefangen, aber kaum ausreichend. Es kommt daher auf die Disziplin der Programmierer an. Eine Subroutine sollte nur genau die Daten ändern, die in einer übersichtlichen und leicht verständlichen Beschreibung, die für die gesamte Subroutine gilt, angegeben werden. Eine Reihe von Subroutinen soll nur lesend auf Daten zugreifen. In C++ ist es bei Klassenmethoden dazu möglich, im Funktionskopf nach der schließenden Klammer der Paramterliste ein const anzugeben:

type ClassType::function(...) const;

Das bedeutet syntaktisch, dass der Zeiger auf die Instanzvariablen this als const bezeichnet wird, der Compiler meldet schreibende Zugriffe oder den Aufruf von nicht-const-Methoden als Fehler.

Ausdrücke mit Subroutinen, die nur lesend arbeiten, gleichen in dieser Beziehung dem Programmierparadigma der funktionalen Programmiersprachen mit ihren dort bekannten Vorteilen. Eine strenge Unterscheidung zwischen Subroutinen, die nur lesend arbeiten, und solchen, die Wirkungen auf Daten haben, ist zu beachten.

[Bearbeiten] Überladen und dynamisches Binden von Subroutinen

Das Wort überladen ist eine direkte Übersetung aus dem englischem 'overload' und nicht unbedingt aus sich heraus verständlich. Mit Überladen wird hier das Umdefinieren des Bezeichners einer Subroutine in Abhängigkeit von der Parameterauswahl verstanden. Eine besser verständliche Bezeichnung wäre Parametersensibilität, diese ist aber nicht gebräuchlich:

void function(int x);

ist eine gänzlich andere Funktion wie

void function(float x);

Beide Funktionen haben verschiedene Implementierungen, verschiedene Label auf Ebene des Objectfiles beziehungsweise Linkers und haben nichts weiter miteinander zu tun als dass sie den gleichen Namen tragen. Überladen ist also nur der Name.

Problematisch für das Verständnis und für den Compiler sind Aufrufe folgender Art:

short y;
function(y);

Hier muss der Compiler selbständig entscheiden, ob er besser nach int casted und die int-Variante aufruft, oder nach float casted und die float-Variante aufruft. Naheliegend wäre der erste Fall, dennoch hängt hier einiges von der Meinung des Compilers ab, der Programmierer ahnt nicht, was sich im Untergrund des Maschinencodes tut. Einige Compiler verhalten sich in solchen Fällen nett zum unbedarften Programmierer und wählen das mutmaßlich richtige (was im konkretem Fall falsch sein kann), andere Compiler, 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 Subroutinen 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 Compiler jedenfalls die Zeigertyprichtigkeit und meldet entweder einen Fehler oder verwendet genau die passende Subroutine:

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 (= Subroutine) 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.

[Bearbeiten] Gründe für das Aufteilen von Programmen in Subroutinen

Aus der ursprünglichen Sicht der Assemblerprogrammierung war der Grund der Aufteilung die mehrfache Verwendung der gleichen Befehlsfolge, damit Einsparung von Speicherplatz.

In moderenen Technologien des Softwareegeneering ist der Hauptgrund allerdings die Strukturierung des Softwareentwurfes. Eine Subroutine sollte eine in sich abgeschlossene und gut beschreibbare Teilaufgabe erledigen. Das ist ebenfalls das Konzept der Methoden in der objektorientierten Programmierung. Subroutinen sind typischerweise heute eher kurz und übersichtlich.

[Bearbeiten] Umsetzung auf Maschinenebene

Das Konzept des Stack wurde weiter oben im Abschnitt "Übergabe der Parameter / Argumente über den Stack" bereits erläutert.

Für Subroutinen 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) Subroutinen 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 Subroutine 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 fr Rückgabewerte auf dem Stack reserviert, wenn notwendig.
  • der Basepointer wird auf den Stack gelegt.
  • Danach wird die Subroutinen aufgerufen, das heißt, der Instruction pointer wird geändert auf die Startadresse der Subroutine.
  • Am Beginn der Subroutine 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 die Subroutine lokal Variablen benötigt. Diese liegen auf dem Stack.
  • Am Ende der Subroutine 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 Compiler. In Java erfolgt innerhalb der Speicherbereiche der Virtuellen Maschine das Gleiche, organisiert vom Bytecode (erzeugt vom Java-Compiler) 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 der Subroutine,
                                 //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 der Subroutine:

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 Paramters 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 der
                                 //Subroutine 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 der Subroutine,
                              //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 der Subroutine:
  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.

[Bearbeiten] Sammlungen von Unterprogrammen

Unterprogramme werden oft vorübersetzt und zu Bibliotheken zusammengefasst.

[Bearbeiten] Siehe auch

Static Wikipedia 2008 (no images)

aa - ab - af - ak - als - am - an - ang - ar - arc - as - ast - av - ay - az - ba - bar - bat_smg - bcl - be - be_x_old - bg - bh - bi - bm - bn - bo - bpy - br - bs - bug - bxr - ca - cbk_zam - cdo - ce - ceb - ch - cho - chr - chy - co - cr - crh - cs - csb - cu - cv - cy - da - de - diq - dsb - dv - dz - ee - el - eml - en - eo - es - et - eu - ext - fa - ff - fi - fiu_vro - fj - fo - fr - frp - fur - fy - ga - gan - gd - gl - glk - gn - got - gu - gv - ha - hak - haw - he - hi - hif - ho - hr - hsb - ht - hu - hy - hz - ia - id - ie - ig - ii - ik - ilo - io - is - it - iu - ja - jbo - jv - ka - kaa - kab - kg - ki - kj - kk - kl - km - kn - ko - kr - ks - ksh - ku - kv - kw - ky - la - lad - lb - lbe - lg - li - lij - lmo - ln - lo - lt - lv - map_bms - mdf - mg - mh - mi - mk - ml - mn - mo - mr - mt - mus - my - myv - mzn - na - nah - nap - nds - nds_nl - ne - new - ng - nl - nn - no - nov - nrm - nv - ny - oc - om - or - os - pa - pag - pam - pap - pdc - pi - pih - pl - pms - ps - pt - qu - quality - rm - rmy - rn - ro - roa_rup - roa_tara - ru - rw - sa - sah - sc - scn - sco - sd - se - sg - sh - si - simple - sk - sl - sm - sn - so - sr - srn - ss - st - stq - su - sv - sw - szl - ta - te - tet - tg - th - ti - tk - tl - tlh - tn - to - tpi - tr - ts - tt - tum - tw - ty - udm - ug - uk - ur - uz - ve - vec - vi - vls - vo - wa - war - wo - wuu - xal - xh - yi - yo - za - zea - zh - zh_classical - zh_min_nan - zh_yue - zu -