Im Magazin „MC" wurde im Juni 1985 der folgende Artikel abgedruckt.
Basierend auf dem Microsoft Aufrufstandard werden hier Nutzungen von verschiedenen Programmiersprachen aufgezeigt.
Ralf Wiegandt

CP/M-Spracherweiterungen

Dieser Artikel beschreibt eine Methode, unter Ausnutzung eines sprachenunabhängigen Aufrufstandards Assembler-Routinen zu erstellen, die an eine Anzahl verschiedener Compiler-Sprachen angebunden und somit als Elemente einer allgemeinen Spracherweiterung verwendet werden können. Das ist nützlich, weil es doch immer wieder Probleme gibt, die weder von einer Hochsprache noch vom Betriebssystem gelöst werden.

Sicherlich hat jeder Programmierer schon vor der Aufgabe gestanden, bestimmte, immer wiederkehrende Verarbeitungsvorgänge eines Anwendungsgebietes in Form von standardisierten Unterprogrammen zu realisieren und sie damit einer großen Zahl von Programmen zugänglich zu machen. Die übliche Vorgehensweise dabei ist, diese Unterprogramme in der gerade verwendeten Programmiersprache, d.h. in der gleichen Sprache wie die sie aufrufenden Hauptprogramme abzufassen. Der wesentliche Nachteil bei dieser Methode ist jedoch, daß man bei jedem Wechsel der Programmiersprache (z.B. von Basic zu Fortran) alle Unterprogramme - und seien sie auch noch so allgemein gehalten - neu schreiben muß. Dies stellt vor allem für Anwender, die regelmäßig mit verschiedenen Programmiersprachen arbeiten, eine große Belastung dar. Sie kann nur dann vermieden werden, wenn es gelingt, Unterprogramme und Funktionsroutinen in einer Programmiersprache abzufassen, die es möglich macht - unter Verwendung eines geeigneten Bindeprogramms - diese Routinen von in verschiedenen Sprachen geschriebenen Hauptprogrammen in gleicher Weise aufrufen zu lassen.

Einen solchen „Aufrufstandard" hat die Firma Microsoft entwickelt und in ihren Programmiersprachen Basic-80 (Compilerversion: Bascom), Fortran-80 und Cobol-80 konsequent verwirklicht. Diese drei Sprachen sind gleichermaßen kompatibel zu dem komfortablen Macro-Assembler Macro-80 von Microsoft; und zwar dergestalt, daß Assembler und Compiler bei der Programmübersetzung die gleiche Art von verschiebbarem „Objekt-Code" erzeugen. Die so entstehenden Programm-Module (Dateien mit dem Zusatz „.REL") können unter Verwendung des Bindeprogramms Link-80 zu einem vollständigen Programm zusammengefaßt werden. Dabei ist es zum Beispiel möglich, ein Hauptprogramm in Basic oder in Cobol zu schreiben, die benötigten Unterprogramme jedoch - je nach ihrer Aufgabenstellung - in Fortran oder Assembler zu entwickeln. Damit wird dem Anwender eine Möglichkeit in die Hand gegeben, sich eine Bibliothek von immer wieder benötigten Standardprozeduren anzulegen, die dann sowohl in Basic- oder Fortran- als auch in Cobol- oder Assembler-Programmen verwendet werden können.

Da das mit Sicherheit günstigste Verhältnis von Verarbeitungsgeschwindigkeit zu Codegröße bei der Verwendung der Assemblersprache erreicht wird, die dazu noch die umfangreichsten Möglichkeiten der direkten Daten- und Adreßmanipulation bietet, soll das Verfahren zur Erstellung von Standardroutinen hier anhand von Assembler-Unterprogrammen klargemacht werden. Da die vorgestellten Programme auf einem Apple-II mit Z80-Prozessor und CP/M 2.2 entwickelt und getestet wurden, sind die Unterprogramme im Z80-Code abgefaßt. Bis auf wenige Ausnahmen wurde jedoch auf die Verwendung spezieller Z80-„Tricks" verzichtet, so daß es leicht fallen dürfte, die Routinen in den 8080-Code umzusetzen.

Der Microsoft-Aufrufstandard

Ein Aufrufstandard besteht aus festgelegten Regeln für die Anbindung von Unterprogrammen (Prozeduren und Funktionen) an ein Hauptprogramm. Insbesondere muß dabei festgelegt werden, wie die Parameter an das Unterprogramm übergeben bzw. von diesem wieder zurückgeliefert werden. Für den hier beschriebenen CP/M-Standard der Firma Microsoft gelten die folgenden allgemeinen Regeln:
  1. Grundsätzlich werden alle Parameter dem Unterprogramm in Form von Adressen übergeben, d.h. das aufgerufene Unterprogramm erhält nicht den Wert der Variablen selbst, sondern lediglich deren Speicheradresse. Diese Methode wird als „Aufruf per Referenz" („Call by reference") bezeichnet; ausgetauscht werden dabei lediglich „Zeiger" (Pointer) auf die tatsächlichen Parameter in Form von 16-Bit-Adressen.

  2. Die übergebenen Adressen können sowohl in den Doppelregistern (HL, DE, BC) des Prozessors als auch im RAM-Speicher übergeben werden. Dabei gilt folgendes:
    Werden bis zu drei Parameter an das Unterprogramm übergeben, so stehen deren Adressen einfach in den Registerpaaren HL, DE und BC und können durch das Unterprogramm von dort übernommen werden. Wird z.B. das Unterprogramm XYZ mit den Parametern A, B und C (deren Datentyp beliebig sein kann) durch
    CALL XYZ(A,B,C)
    aufgerufen, wobei die Werte der Variablen A, B, C bei den Adressen '120'H, '12E'H und '13C'H (hexadezimal) abgespeichert seien, so findet das Unterprogramm die folgende Registerbelegung:

    HL :0120
    DE :012E
    BC :013C.

    Ist dagegen die Zahl der Parameter größer als drei, so ist die Situation etwas komplizierter. Nach wie vor stehen die Adressen der ersten beiden Werte in den Registerpaaren HL und DE. Das Doppelregister BC enthält nun jedoch einen Zeiger auf die Liste der Adressen der übrigen Parameter, d.h. die Adresse einer Liste von Adressen! Diese zunächst schwer verständliche Regel wird an folgendem Beispiel deutlich:
    Das Unterprogramm SUB soll mit den Parametern A, B, C, D und E versorgt werden. Diese seien im Speicher unter den Adressen 'C030'H, 'C032'H, 'C034'H, 'C036'H und 'C038'H abgelegt (gemeint ist immer die Adresse des ersten Bytes des jeweiligen Variablenwertes). Beim Aufruf des Programms durch
    CALL SUB (A,B,C,D,E)
    
    werden dann die Adressen von A und B in den Registerpaaren HL und DE abgelegt. Für die Adressen von C, D und E reserviert der Compiler einen Speicherbereich von 6 Byte (jede Adresse belegt 2 Byte!), der z.B. bei der Adresse C050H beginnt. Dort werden nacheinander die Adressen der verbleibenden 3 Parameter abgelegt. (Adressen werden - wie Integer-Zahlen - stets so abgespeichert, daß das erste Byte den niederwertigen Teil, das zweite den höherwertigen Teil enthält!) Das Unterprogramm findet also folgende Situation vor:

    Register:
    HL : C030
    DE : C032
    BC : C050

    Speicher:
    C050 : 34 C0 (Adresse C034)
    C052 : 36 C0 (Adresse C036)
    C054 : 38 C0 (Adresse C038).

  3. Weder die Compiler noch der Linker prüfen, ob die Zahl und Art der Parameter im Unterprogrammaufruf mit denen im Unterprogramm selbst übereinstimmt. Es ist daher einzig und allein die Aufgabe des Programmierers, dafür zu sorgen, daß die im Aufruf verwendeten Parameter in Anzahl und Reihenfolge richtig ausgewertet werden.

  4. Alle oben beschriebenen Unterprogramme können bei der Rückkehr in das Hauptprogramm einen Integer-Wert zurückliefern. Dies ist die Zahl, die zum Zeitpunkt der Ausführung des „Return-Statements" (RET) im Registerpaar HL steht. Hierbei wird allerdings der Inhalt von HL nicht als Adresse, sondern als Zahlenwert interpretiert. Ein Aufruf der Form
    ISTAT = XXX(parameter)
    
    in einem Fortran-Programm beispielsweise würde die Variable ISTAT mit dem Inhalt von HL belegen. (Diese Eigenart kann man auf elegante Weise ausnutzen, um die Speicheradresse einer beliebigen Variablen zu ermitteln: Dazu schreibt man ein Assembler-Unterprogramm LOC, welches lediglich aus einem RET-Statement besteht, und ruft dieses mit
    IADR = LOC(variable)
    
    auf. Da die Adresse der Variablen in HL an das Unterprogramm übergeben und in diesem nicht verändert wird, wird sie auf diese einfache Weise nach IADR transportiert!) Vielfach wird die Konvention verwendet, in Unterprogrammen, die nicht direkt einen Funktionswert zurückliefern, HL unmittelbar vor dem ordnungsgemäßen Rücksprung mit dem Wert 1 zu laden. Dieser Wert kann dann als „Return-Status" ausgewertet werden.

Parameterübernahme durch das Unterprogramm

In der Regel wird man sich im Assembler-Unterprogramm einen eigenen Zwischenspeicherbereich für die Parameter bzw. deren Adressen anlegen und diese zunächst dort abspeichern, um die Register für die Verarbeitung ausnutzen zu können. Das Sichern der ersten zwei Parameter (sowie des dritten, falls keine weiteren folgen) geschieht einfach durch 16-Bit-Transferbefehle:
LD (paradr1),HL
LD (paradr2),DE
LD (paradr3),BC.
Die Adressen der drei Parameter sind damit an den Stellen 'paradr1', 'paradr2' und 'paradr3' gesichert. Werden 4 oder mehr Parameter übergeben, so kann eine kleine Hilfsroutine verwendet werden, um die Adressen der Parameter 3,4,5... in den Zwischenspeicherbereich zu transferieren. Diese Routine findet sich unter dem Namen $AT in der Fortran-Library Forlib und ist in Bild 1 abgedruckt.

Vor ihrem Aufruf muß das Registerpaar HL die Startadresse des eigenen Zwischenspeicherbereichs für die Parameteradressen 3,4,5... und das Register A die Zahl der zu transferierenden Parameter enthalten. Sollen also z.B. 6 Parameteradressen unter den Speicherstellen 'paradr1' bis 'paradr6' gesichert werden (wobei 'paradr3' bis 'paradr6' hintereinander liegen müssen), so beginnt das Unterprogramm folgendermaßen:
LD (paradr1),HL	; Adresse von Parameter 1 sichern
LD (paradr2),DE	; Adresse von Parameter 2 sichern
LD HL,paradr3	; Adresse 'paradr3' nach HL laden
LD A,0004	; Konstante 4 (Anzahl restlicher Parameter) nach A laden
Call $AT##	; Routine $AT („Extern") aufrufen.
Sollen Zahlenwerte an das Hauptprogramm zurückgegeben werden, so müssen diese vor der Rückkehr mit Transferbefehlen an die entsprechende Speicheradresse geladen werden. (Dabei ist zu beachten, daß - in Basic- und Fortran-Programmen - Integer-Zahlen stets in der Reihenfolge Low-Byte, High-Byte abgespeichert werden!) Soll z.B. die Hexadezimalzahl 'F3FE'H als Parameter 3 zurückgeliefert werden, so endet das Unterprogramm mit den Befehlen:
LD HL,(paradr3)	; HL mit der Adresse von Parameter 3 laden
LD A,00FEH	; Low-Byte nach A laden
LD (HL),A	; Im ersten Byte von Parameter 3 ablegen
INC HL		; Adresse um 1 erhöhen
LD A,00F3H	; High-Byte nach A laden
LD (HL),A	; Im zweiten Byte von Parameter 3 ablegen
RET		; Rücksprung in das Hauptprogramm
Die Bilder 2 und 3 enthalten sinnvolle Beispiele für Unterprogramme mit drei Parametern. Die Routine BFILL (Bild 2) dient dazu, einen Speicherbereich (z.B. ein Array) mit identischen Bytes aufzufüllen.

Das Unterprogramm kann z.B. dazu verwendet werden, ein Integer-Feld zu löschen, ohne eine entsprechende Schleife programmieren zu müssen. Die Routine wird mit den drei Parametern
<start> : Startadresse des Bereichs (angegeben durch einen Variablen-
          oder Arraynamen)
<count> : Anzahl zu füllender Bytes (als Integer-Zahl)
<fill>  : Wert des einzusetzenden Bytes (als Integer-Zahl zwischen 0 und 255)
aufgerufen. Ähnlich ist die Routine BMOVE (Bild 3) anzuwenden, die einen Speicherbereich an eine andere Stelle kopiert.

Sie kann z.B. genutzt werden, um ein Array mit den Werten eines anderen zu belegen oder Teile eines Arrays zu verschieben (komprimieren oder erweitern). Die Routine ist so ausgelegt, daß Quell- und Zielbereich sich auch überlappen dürfen! Die Aufrufparameter sind
<source>       : Startadresse des Quellbereichs (angegeben durch einen
                 Variablen- oder Arraynamen)
<destination>  : Startadresse des Zielbereichs (wie oben)
<count>        : Anzahl der zu kopierenden Bytes.
(In diesem Programm wurde von den speziellen Z80-Befehlen LDIR und LDDR Gebrauch gemacht!)
Die Routine BLOC (Bild 4) ist ein Beispiel für die Programmierung eines Unterprogramms mit 5 Parametern.

Das Programm wird verwendet, um in einem Speicherbereich nach dem Auftreten einer bestimmten Bytefolge zu suchen. Es kann z.B. in Fortran-Programmen benutzt werden, um in einem als Byte-Array abgelegten Text bestimmte „Sub-strings" aufzufinden und deren Position zurückzuliefern. Die folgenden vier Parameter müssen vom aufrufenden Programm vorgegeben werden:
<buffer> : Startadresse des zu durchsuchenden Bereichs (angegeben
           durch einen Variablen- oder Arraynamen)
<countb> : Länge des zu durchsuchenden Bereiches in Bytes
<sbuf>   : Startadresse des zu findenden Bytemusters
           (Variablen- oder Arrayname)
<counts> : Länge des Bytemusters in Bytes.
Zurückgeliefert wird der fünfte Parameter:
<pos>    : Position des ersten gefundenen Bytes im durchsuchten Bereich
           (gezählt ab 1). Wird das Bytemuster nicht gefunden, so ist pos = 0.
Die Parameter <countb>, <counts> und <pos> müssen vom Typ Integer sein, d.h. 2 Bytes im Speicher belegen!
Der Gebrauch der drei Beispiel-Routinen in einem Basic-Programm wird in Bild 5 demonstriert.

Dieses Programm sucht in einem mit den ersten 10 geraden Zahlen gefüllten Integer-Array nach der Zahlenfolge „8, 10, 12" (Aufruf von BLOC), um dann den Teil des Arrays, der mit der Zahl 8 beginnt, in ein Integer-Array TEMP zu transferieren (Routine BMOVE). Zuvor muß das Array TEMP mit Hilfe der Routine BFILL gelöscht werden. (Solche - in diesem Beispiel etwas konstruiert erscheinenden - Operationen treten in ähnlicher Weise bei der Verwaltung linearer Datenketten auf.)
Genau in der gleichen Weise könnten die Routinen auch in einem Fortran-Programm verwendet werden. Hier würde man z.B. zwei Arrays vom Typ Byte definieren, beide mit einem ASCII-String füllen, um dann den zweiten String im ersten zu suchen (Teilstringsuche).

Strings in Basic- und Fortran-Programmen

Selbstverständlich setzt die Verwendung der gleichen Unterprogramme in verschiedenen Programmiersprachen nicht nur einen einheitlichen Aufrufstandard, sondern auch die Gleichheit der Abspeicherungsmethoden für Variablen voraus. In Basic- und Fortran z.B. ist diese Forderung für Variablen vom Typ „Integer" und „Floating Point" erfüllt - nicht jedoch für ASCII-Strings. Während in Fortran dieser Datentyp überhaupt nicht vorgesehen ist und daher durch Arrays vom Typ Byte simuliert werden muß, gibt es in Basic String-Variablen (Variablennamen mit angehängtem Dollarzeichen) mit dynamischer Länge, die bis zu 255 Zeichen enthalten dürfen.
In Fortran ist die Adresse des für einen String verwendeten Arrays bereits die Startadresse der abgespeicherten Zeichenfolge, so daß bei Unterprogrammaufrufen nur der Arrayname angegeben werden muß, um die jeweilige Routine mit der Startadresse des ASCII-Strings zu versorgen. In Basic hingegen zeigt die Adresse einer Stringvariablen nicht auf den String selbst, sondern auf einen 3 Bytes langen „Deskriptor", in dem die aktuelle Stringlänge sowie die tatsächliche Startadresse des Strings vermerkt sind. Assembler-Unterprogramme, in denen Strings verarbeitet werden, können daher nicht in gleicher Weise von Basic und Fortran-Programmen aus aufgerufen werden. Statt dessen ist für ein Basic-Programm eine kleine „Analyseroutine" zwischenzuschalten, die zunächst den String-Deskriptor auswertet und erst anschließend das Standard-Unterprogramm mit den entsprechenden Parametern aufruft.
Ein Beispiel für dieses Verfahren liefert das Programm UPCASE (Bild 6), welches in einem vorgegebenen ASCII-String alle Kleinbuchstaben in Großbuchstaben umwandelt.

Das Unterprogramm erwartet zwei Eingangsparameter:
<buffer>	: Startadresse des Strings (Arrayname)
<count>		: Länge des Strings in Bytes,
und kann damit von einem Fortran-Programm direkt mit
CALL UPCASE(TEXT,LNG)
aufgerufen werden, wenn TEXT ein Byte-Array ist. Ein unmittelbarer Aufruf in Basic ist aus den oben genannten Gründen nicht möglich, so daß für den Anschluß an Basic-Programme eine „Zwischenroutine" UCASE (Bild 7) geschrieben werden muß, die die Startadresse und die Stringlänge aus dem Deskriptor ermittelt.

Die Stringlänge wird daraufhin in einem Arbeitsspeicher (2 Bytes) zwischengespeichert, dessen Adresse - zusammen mit der Stringadresse - in den entsprechenden Registerpaaren (Stringadresse in HL, Längenadresse in DE) an das eigentliche Unterprogramm UPCASE übergeben werden kann. Der Basic-Aufruf muß daher lauten:
CALL UCASE(ST$).
Die Anwendung der Routinen UCASE und UPCASE ist ebenfalls in dem Beispielprogramm Bild 5) demonstriert.

Übersetzen und Binden der Programm-Module

Der Microsoft-Standard erlaubt es, Programme modular aufzubauen, d.h. einzelne Verarbeitungsvorgänge in abgeschlossenen Programmeinheiten zu realisieren, die in getrennten Dateien abgespeichert und auch getrennt compiliert werden können. Die Übersetzung der Programme geschieht mit den Compilern Bascom (Basic), F80 (Fortran), dem hier nicht behandelten Cobol-Compiler sowie dem Macro-Assembler M80. Fortran-Module werden mit:
F80 programm,=programm
übersetzt, Basic-Programme mit:
BASCOM programm,=programm/O/C
(Dabei bedeutet der Zusatz O, daß später beim Binden ein allein für sich lauffähiges Programm erzeugt werden soll welches nicht die Runtime-Library MRUN.COM benötigt; der Zusatz C ist notwendig, wenn die sonst in Basic notwendigen Zeilennummern entfallen sollen.) Assembler-Routinen schließlich werden mit:
M80 programm,=programm
übersetzt. In allen drei Fällen entstehen verschiebbare Objekt-Files (Dateibezeichnung „xxx.REL"), die nun mit dem Microsoft-Linker L80 zu einem lauffähigen Programm zusammengefügt werden können:
L80 hauptprogramm,unter1,unter2,unter3,programname/N/E
Soll eine größere Zahl von Standard-Unterprogrammen an das Hauptprogramm angeschlossen werden, so empfiehlt es sich, alle REL-Files der Unterprogramme mit Hilfe des Programms LIB80 in einer „Bibliothek" (Library) zusammenzufassen. Solche Standardbibliotheken sind bereits für Fortran (Bibliothek FORLIB) und Basic (Bibliothek OBSLIB) vorhanden und werden in der Regel automatisch mit angebunden. Die eigene zusätzliche „Standardbibliothek" könnte z.B. den Namen MYLIB erhalten. Beim Binden der Programme ist dann darauf zu achten, daß sowohl die notwendigen Microsoft-Bibliotheken (FORLIB oder OBSLIB) als auch die eigene Bibliothek MYLIB vom Linker durchsucht werden. Ist das Hauptprogramm beispielsweise ein Fortran-Programm, so könnte die Kommandozeile zum Binden lauten:
L80 MAIN,MYLIB/S,FORLIB/S,MAIN/N/E
Der Linker stellt dann fest, welche externen Symbole im Programm MAIN verwendet werden, und durchsucht daraufhin zunächst die Bibliothek MYLIB und dann die Bibliothek FORLIB nach den entsprechenden Einsprungadressen. (Die Reihenfolge der dem Linker angegebenen Bibliotheken kann wichtig sein, wenn z.B. in der Bibliothek MYLIB wiederum Routinen aus der Bibliothek FORLIB - wie z.B. das oben erwähnte $AT - aufgerufen werden!)

Anwendungsmöglichkeiten

Obwohl die unter CP/M verfügbaren Compilersprachen bereits standardmäßig viele Möglichkeiten bieten, gibt es doch eine ganze Anzahl von stets wiederkehrenden Verarbeitungen oder speziellen Anwendungen (etwa bei der maschinennahen Programmierung), die nicht direkt durch die Sprachen selbst abgedeckt werden und daher als eigene „Standardroutinen" realisiert werden könnten. Einige Beispiele für die realisierbaren (und zum Teil auch durch den Autor bereits realisierten) Routinen sollen als Abschluß noch genannt werden:

DUMP-Formatierte Ausgabe von Speicherbereichen
BMOVE-Kopieren von Speicherbereichen
BFILL-Byteweises Füllen von Speicherbereichen
IFILL-Wortweises Füllen von Speicherbereichen
LOC-Ermitteln von Speicheradressen
COMLIN-Ablesen der vollständigen CP/M-Kommandozeile zur Ermittlung von Programmparametern
FREE-Ermittlung des freien Speicherplatzes auf der Diskette
RESET-Zurücksetzen des Disk-Systems
RDSECT-Lesen eines physikalischen Diskettensektors
WRSECT-Schreiben eines physikalischen Diskettensektors
sowie Stringverarbeitungsroutinen für Fortran, Umwandlungsroutinen für verschiedene Zahlensysteme, Betriebssystem-Aufrufe und viele andere mehr.

Eingescanned von Werner Cirsovius
November 2002
© Franzis' Verlag