Rücksprungadresse

Rücksprungadresse

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.

Je nach Kontext werden auch die Bezeichnung Prozedur, Funktion, Methode und Operation benutzt. Die verschiedenen Bezeichnungen sind teils historisch entstanden, teils im Umfeld verschiedener Programmiersprachen und betonen jeweils einen bestimmten Aspekt. Nachfolgend wird grundsätzlich erläutert, wie Unterprogramme aufgebaut sind und im Maschinencode ablaufen.

Inhaltsverzeichnis

Beispiele zu Unterprogrammen

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 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 Variablen 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 Java-Beispiel stellt ein Unterprogramm dar, die eine Funktion im allgemeinsprachlichem Sinne ausführt. Der Rückgabewert ist lediglich der Folgeindex beziehungsweise die Anzahl der Daten oder -1, wenn kein Platz mehr zum Abspeichern vorhanden ist. 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. 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 Unterprogrammen 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 in Assembler geschriebenes Unterprogramm dar, und zwar für die Signalprozessorfamile 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.

Parameter oder 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 sind in diesem Kontext synonym. Den Unterprogrammen wird beim Aufruf über die tatsächliche Parameterliste bestimmte Werte übergeben, mit denen sie arbeiten können. Die Unterprogramme können über einen Rückgabewert 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 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 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 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 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 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ückschreiben von Ergebnissen

Ein Unterprogramm kann in Programmiersprachen wie C, C++ oder Java 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 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).

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 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 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 Compiler - 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++-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 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 geschweichten 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 eine Wert (value-)-Übergaben von Strukturen im Rückgabewert. 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 Rückgabewerten beispielsweise für komplexe Zahlen. Im Kontext des Aufrufes wird hier die Anlage einer extra Struktur für die Rückgabewertbergabe und deren Referenzierung eingespart.

Überladen und dynamisches Binden von Unterprogrammen

Das Wort überladen ist eine direkte Übersetzung aus dem englischem '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 Compiler sind Aufrufe folgender Art:

short y;
function(y);

Hier muss der Compiler 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 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 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 Compiler 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.

Gründe für das Aufteilen von Programmen in Unterprogrammen

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. 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 typischerweise heute eher kurz und übersichtlich.

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 fr 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 der 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 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 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


Wikimedia Foundation.

Игры ⚽ Поможем решить контрольную работу

Schlagen Sie auch in anderen Wörterbüchern nach:

  • Rekursive Programmierung — Bei der rekursiven Programmierung ruft sich eine Prozedur, Funktion oder Methode in einem Computerprogramm selbst wieder auf. Auch der gegenseitige Aufruf stellt eine Rekursion dar. Inhaltsverzeichnis 1 Einleitung 2 Beispiel 3 Effizienz …   Deutsch Wikipedia

  • Buffer-Overflow — Pufferüberläufe (engl. buffer overflow) gehören zu den häufigsten Sicherheitslücken in aktueller Software, die sich u. a. über das Internet ausnutzen lassen können. Im Wesentlichen werden bei einem Pufferüberlauf durch Fehler im Programm zu große …   Deutsch Wikipedia

  • Buffer Overflow — Pufferüberläufe (engl. buffer overflow) gehören zu den häufigsten Sicherheitslücken in aktueller Software, die sich u. a. über das Internet ausnutzen lassen können. Im Wesentlichen werden bei einem Pufferüberlauf durch Fehler im Programm zu große …   Deutsch Wikipedia

  • Buffer overflow — Pufferüberläufe (engl. buffer overflow) gehören zu den häufigsten Sicherheitslücken in aktueller Software, die sich u. a. über das Internet ausnutzen lassen können. Im Wesentlichen werden bei einem Pufferüberlauf durch Fehler im Programm zu große …   Deutsch Wikipedia

  • Heap-Overflow — Pufferüberläufe (engl. buffer overflow) gehören zu den häufigsten Sicherheitslücken in aktueller Software, die sich u. a. über das Internet ausnutzen lassen können. Im Wesentlichen werden bei einem Pufferüberlauf durch Fehler im Programm zu große …   Deutsch Wikipedia

  • Heap Overflow — Pufferüberläufe (engl. buffer overflow) gehören zu den häufigsten Sicherheitslücken in aktueller Software, die sich u. a. über das Internet ausnutzen lassen können. Im Wesentlichen werden bei einem Pufferüberlauf durch Fehler im Programm zu große …   Deutsch Wikipedia

  • Pointer overflow — Pufferüberläufe (engl. buffer overflow) gehören zu den häufigsten Sicherheitslücken in aktueller Software, die sich u. a. über das Internet ausnutzen lassen können. Im Wesentlichen werden bei einem Pufferüberlauf durch Fehler im Programm zu große …   Deutsch Wikipedia

  • Pufferüberlauf — Pufferüberläufe (engl. buffer overflow) gehören zu den häufigsten Sicherheitslücken in aktueller Software, die sich u. a. über das Internet ausnutzen lassen können. Im Wesentlichen werden bei einem Pufferüberlauf durch Fehler im Programm zu… …   Deutsch Wikipedia

  • Speicherüberlauf — Pufferüberläufe (engl. buffer overflow) gehören zu den häufigsten Sicherheitslücken in aktueller Software, die sich u. a. über das Internet ausnutzen lassen können. Im Wesentlichen werden bei einem Pufferüberlauf durch Fehler im Programm zu große …   Deutsch Wikipedia

  • Stack Overflow — Pufferüberläufe (engl. buffer overflow) gehören zu den häufigsten Sicherheitslücken in aktueller Software, die sich u. a. über das Internet ausnutzen lassen können. Im Wesentlichen werden bei einem Pufferüberlauf durch Fehler im Programm zu große …   Deutsch Wikipedia

Share the article and excerpts

Direct link
Do a right-click on the link above
and select “Copy Link”