Im Magazin „MC" wurde im Juni 1986 der folgende Artikel abgedruckt.
Erzeugung von Inline-Dateien für Turbo Pascal 3.01.
Helmut Merz

Assembler-Routinen in Turbo-Pascal

Ein Programm zur Erzeugung von Inline-Code

Turbo-Pascal bietet mit dem Inline-Statement eine bequeme Möglichkeit, Maschinenspracheprogramme in ein Pascal-Programm einzubinden. Bei umfangreicheren Routinen ist dabei jedoch von Nachteil, daß die Codierung von Hand erfolgen muß. Abhilfe schafft hier ein Programm, das aus Relocatable-Code, wie er von Microsofts Macro-80-Assembler erzeugt wird, Inline-Code erzeugt, der über die $I-Compiler-Option oder mit Hilfe des Editors in ein Pascal-Programm eingebaut werden kann.

Macro-80 erzeugt aus dem Assembler-Quelltext eine Datei (mit der Typbezeichnung .REL), die sogenannten verschiebbaren Code enthält. In dieser Datei sind Sprung- und Datenadressen, die beim Assemblieren noch nicht festliegen, als relative Adressen gekennzeichnet. Der zum Assembler gehörende Linker (L80) berechnet dann hieraus die absoluten Werte und erzeugt eine lauffähige COM-Datei. Außerdem enthält die REL-Datei alle Informationen über externe oder globale Symbole, also etwa Adressen, über die verschiedene Module, die mit L80 zusammengebunden werden sollen, miteinander kommunizieren.

Damit enthält aber diese REL-Datei auch alle Informationen, die für die Erzeugung von Inline-Code für Turbo-Pascal benötigt werden. Variablen, über die die Maschinencode-Routine mit dem Pascal-Programm kommunizieren soll, werden im Assembler-Programm als external deklariert. Das hier vorgestellte Pascal-Programm PMLink erzeugt dann aus dem REL-Code Inline-Code (in einer Datei mit Typ .INL), der die als external deklarierten Namen an den entsprechenden Stellen enthält. Der Turbo-Pascal-Compiler setzt dann an den jeweiligen Stellen die Adressen der Variablen ein.

Bild 1 zeigt eine kurze Assembler-Routine, die ihr Argument Word um NBits Bits nach links rotiert und den so erhaltenen Wert im HL-Register als Funktionswert übergibt.

	.z80
	external Word,NBits

rotlft:	ld	HL,(Word)
	ld	A,(NBits)
	and	0F
	or	A
	ret	Z	;kein Rotieren,
			;wenn NBits=0
	ld	B,A
	ld	A,L
	ld	L,0

loop:	sla	A	;MSB im Carry
	rl	H	;     "
	adc	A,L	;Carry wiedereinsetzen
	djnz	loop

	ld	L,A	; Ergebnis in HL
	ret
	end	rotlft
Bild 1. Assembler-Quelle für die Funktion ROTLFT, die ihr integer-Argument Word um NBits nach links rotiert

Word und NBits werden im Assembler-Programm als external deklariert und erscheinen wieder im Inline-Code, der von PMLink erzeugt wurde (Bild 2).

type Bits = 0..15;

function RotLft (Word: integer; NBits: Bits): integer;
begin {Modul ROTLFT}
  InLine (
{0000}    $2A/WORD   /$3A/NBITS  /$E6/$0F/$B7/$C8/$47/$7D/$2E/$00/$CB/$27/
{0010}    $CB/$14/$8D/$10/$F9/$6F/$C9)
end;  {ROTLFT}
Bild 2. Inline-Code zu ROTLFT; der Prozedurkopf wurde von Hand eingegeben

Der Kopf des Unterprogramms muß natürlich von Hand eingesetzt werden; alles weitere ab begin wird von PMLink produziert.

Der Aufbau von PMLink

Das Programm ist in Bild 3 gezeigt.
[Listing]
Bild 3. Programmlisting von PMLink. Die in Bild 4 und 5 gezeigten Hilfsroutinen werden über die Include-Option eingelesen

Es benötigt zwei Durchläufe; im ersten (FirstPass) wird die REL-Datei eingelesen und eine interne Darstellung des Programms (Prog) erstellt, die zu jedem Byte Angaben über seine Bedeutung enthält und eine Symbol- und eine Offset-Tabelle umfaßt. Letztere wird benötigt, um Ausdrücke von der Form ld A,(Num+5) verarbeiten zu können (wobei Num als external deklariert wurde). Der entsprechende Inline-Code ($3A/NUM+5/) wird allerdings erst ab Turbo-Pascal-Version 3.0 verarbeitet - bei älteren Versionen sollten also Offsets keine Verwendung finden. Im zweiten Durchlauf wird entsprechend der internen Darstellung der Inline-Code erzeugt und auf die INL-Datei ausgegeben.

Die Angabe der Dateinamen erfolgt interaktiv, wobei mehrere REL-Files nacheinander bearbeitet werden können. Eine leere Eingabe für das REL-File beendet das Programm, während bei einer leeren Eingabe für das Inline-File dessen Name aus dem der REL-Datei erzeugt wird. Die Typbezeichnungen .REL und .INL werden automatisch ergänzt, sofern keine Typen explizit angegeben werden.

Zu PMLink gehören noch die Module PMLINK.UTL (Bild 4),
[Listing]
Bild 4. Einige von PMLink benötigte Hilfsroutinen, u.a. zur Eingabe der Dateinamen und zur Hexadezimaldarstellung von Zahlen
das einige Hilfsroutinen enthält, und PMLINK.BIT (Bild 5),
[Listing]
Bild 5. Die Routinen zum bitweisen Lesen der REL-Datei
das die Grundlage für ein bitweises Einlesen der REL-Datei schafft. Der von M80 erzeugte Code ist nämlich im Bitstrom organisiert, d.h. die einzelnen Elemente (Items) beginnen und enden nicht an Bytegrenzen. Das jeweils erste (höchstwertige) Bit eines Elements gibt an, ob es sich bei den folgenden acht Bits um ein absolut einzusetzendes Byte handelt. Hat dieses Bit den Wert 1, so entscheiden statt dessen die nächsten zwei Bits über das weitere Schicksal der folgenden Bits usw. (Einzelheiten hierüber findet man im Macro-80 Reference Manual von Microsoft, Anhang D [oder hier]). Durch diesen Aufbau im Bitstrom wird die REL-Datei klein gehalten, und es werden die Diskettenzugriffe beim Lesen minimiert. Wie die Routinen in PMLINK.BIT zeigen, ist bitweises Lesen in Turbo-Pascal recht einfach zu verwirklichen.

Ein umfangreicheres Beispiel

Bild 6 zeigt eine Version der Funktion GetBits in Z80-Assembler.

	.z80
	external NBits,BitCnt,WB,InByte

getbts:	ld	HL,0	;Ergebnis-Register
	ld	A,(NBits)
	ld	C,A	;C := NBits
	ld	A,(BitCnt)
	cp	C
	jr	NC,ok	;BitCnt>=NBits

	ld	B,A	;B := BitCnt
	ld	A,C	;A := NBits
	sub	B	;A : = NBits - BitCnt
	ld	(NBits),A
	ld	A,(WB)
	call	shl	;WB shl BitCnt
	push	HL	;Zwischenergebnis
	call	InByte	;neues Byte in L
	ld	A,(NBits)
	ld	B,A	;B := NBits
	ld	A,8	;
	sub	B	;A := 8 - NBits
	ld	(BitCnt),A
	ld	A,L	;neues  Byte
	pop	HL
	call	shl	;WB shl NBits
	ld	(WB),A
	ret

ok:	ld	B,C	;B := NBits
	sub	B	;A := BitCnt - NBits
	ld	(BitCnt),A
	ld	A,(WB)
	call	shl
	ld	(WB),A
	ret

shl:	sla	A	;MSB in Carry
	rl	L
	djnz	shl
	ret

	end	getbts
Bild 6. Eine Version der Funktion GetBits in Assembler
PMLink kann nun dazu verwendet werden, um sich selbst etwas effektiver zu machen (der Geschwindigkeitsvorteil wirkt sich allerdings nur bei der Verwendung von RAM-Disks aus, da im anderen Fall die Diskettenzugriffe die Arbeitsgeschwindigkeit des Programms bestimmen). Der mit PMLink erzeugte entsprechende Inline-Code ist in Bild 7 gezeigt.

function GetBits (NBits: Bits): byte;
begin {Modul GETBTS}
  inline (
{0000}    $21/$00/$00/$3A/NBITS  /$4F/$3A/BITCNT /$B9/$30/$23/$47/$79/$90/
{0010}    $2/NBITS   /$3A/WB     /$CD/*+40   /$E5/$CD/INBYTE /$3A/NBITS  /
{0020}    $47/$3E/$08/$90/$32/BITCNT /$7D/$E1/$CD/*+21   /$32/WB     /$C9/
{0030}    $41/$90/$32/BITCNT /$3A/WB     /$CD/*+6    /$32/WB     /$C9/$CB/
{0040}    $27/$CB/$15/$10/$FA/$C9)
end; {GETBTS}
Bild 7. Inline-Version von GetBits. Der Aufruf von InByte wird in dieser Form erst ab Turbo-Pascal Version 3.0 verarbeitet

Hieraus wird eine Einschränkung erkennbar: Alle in der REL-Datei enthaltenen Namen (also Programmname und als external deklarierte Symbole) dürfen nicht mehr als sechs Zeichen enthalten.

Aus diesem Grund wurde der Name des Unterprogramms auf GETBTS gekürzt und darauf geachtet, daß die Variablen- und Funktionsnamen, die GETBTS benutzt, nicht mehr als sechs Zeichen lang sind.

GETBTS zeigt unter anderem, daß es auch möglich ist, Prozeduren und Funktionen, die in Pascal definiert worden sind, aus einer Maschinensprache-Routine aufzurufen - wie hier die Funktion InByte. Allerdings werden solche Aufrufe erst ab Turbo-Pascal-Version 3.0 verarbeitet; ältere Versionen des Compilers brechen mit einer Fehlermeldung ab. Diese Einschränkung kann aber leicht umgangen werden: Man plaziere in diesem Fall unmittelbar vor die aufzurufende Pascal-Routine die Deklaration einer typisierten Byte-Konstante mit dem Wert 0, in unserem Beispiel vor InByte das Statement
const InBtAd: byte = 0,
und setze in der Maschinensprache-Routine den Namen dieser Konstante anstatt des Namens der Pascal-Routine ein (also call InBtAd statt call InByte, mit der entsprechenden external-Deklaration). Bei der Ausführung des call-Befehls trifft nun der Prozessor auf die Speicherzelle, in der das Byte mit dem Wert 0 abgelegt ist; dieser Wert wird als Opcode für NOP interpretiert und übersprungen, wodurch schließlich die gewünschte Prozedur erreicht wird.

Was noch zu beachten ist

Bei der Erstellung des Assembler-Codes ist darauf zu achten, daß sich hieraus sinnvoller Inline-Code erzeugen läßt; das Assemblerprogramm sollte z.B. keine aseg-, dseg-, common- oder .phase-Pseudo-Opcodes enthalten. Ebenso wenig ist es sinnvoll, Symbole als public zu deklarieren, da PMLink keinen Maschinensprache-Linker darstellt, d.h. es ist nicht möglich, vor der Erzeugung des Inline-Codes wie mit L80 verschiedene Assembler-Routinen zusammenzubinden. Allerdings können mehrere Module vor der Bearbeitung mit PMLink mit LIB80 zusammengefaßt werden; die Module erscheinen im Inline-Text durch begin und end eingefaßt und sind durch ihre Namen in Kommentarklammern gekennzeichnet.

Alle Items, die von PMLink nicht verarbeitet werden, führen zu einer entsprechenden Fehlermeldung; dabei wird das Programm nicht abgebrochen, sondern der Bitstrom korrekt weiterverarbeitet — der erzeugte Inline-Code ist jedoch nicht unbedingt richtig.

Bezüglich der Arbeitsweise des Pascal-Compilers wäre noch anzumerken, daß Funktionen, die ihre Werte unmittelbar im HL-Register übergeben, mit einem ret-Opcode abzuschließen sind, da der Compiler am Ende der Funktion noch Code ergänzt, durch den das HL-Register aus einer speziellen Speicherzelle geladen wird. Dieser Code sollte natürlich nicht zur Ausführung gelangen, wenn das Register bereits den Funktionswert enthält. Bei Prozeduren ist ein explizites ret nicht notwendig.

Arbeitet eine Maschinensprache-Routine mit var-Parametern, so ist zu beachten, daß der Wert, der unter der entsprechenden Adresse abgelegt ist, nicht den Wert der Variablen darstellt, sondern angibt, unter welcher Adresse dieser Wert zu finden ist — im Gegensatz zu Parametern, deren Wert unmittelbar übergeben wird, und Variablen, die innerhalb einer Prozedur oder Funktion deklariert worden sind.

Im Original-Programm ist es nicht möglich, den Programmzähler über die Assembler-Anweisung DS zu setzen. Ich fand, dass auch weitere Verschlimmbesserungen angebracht seien und habe hier eine auf den Joyce-Bildschirm abgestimmte Version daraus entwickelt.
[April 2007] Auf der Webseite von Hans Otten findet sich in der Rubrik „Pascal on CP/M MSX" in der Rubrik „Pascal articles" das INLINE program by J.A.C.G. van der Valk. Dieses Programm verarbeitet ebenfalls das .REL-80 Format. Es liegt dort vor in Form von eingescannten Seiten sowie als Textdatei (OCR der gescannten Seiten). Hier als Quelle INLINE.PAS

1. Auf Simtel fand sich eine kurze englische Übersetzung, die z.B. auf einem Walnut-Creek-Archiv zu finden ist.

Eingescanned von Werner Cirsovius
März 2003, April 2007, 2010
© Franzis' Verlag