Direkter Kurs auf den Prozessor: Assembler Programmierung leicht verständlich, Teil 2

Assembler - keine andere Sprache gestattet dem Programmierer so direkten Zugriff auf den Computer, in keiner anderen entstehen so schnelle Programme.

Nachdem wir uns im ersten Teil dieses Assemblerkurses mit den notwendigen Grundlagen beschäftigt haben, steigen wir mit diesem Kursteil bereits voll in die Assembler-Programmierung ein. Und damit das Folgende nicht nur graue Theorie bleibt, finden Sie auf der aktuellen TOS-Diskette einen voll funktionsfähigen Shareware-Assembler, den Turbo-Ass. Lesen Sie dazu auch die beigelegte Textdatei über die Nutzungskonditionen und seine grundlegende Bedienung. Bevor wir uns mit neuen Assemblerbefehlen beschäftigen, gehen wir erst einmal auf die Struktur eines Programms ein. Jedes Betriebssystem, wie etwa AmigaDOS, DOS, TOS und UNIX, besitzt eine eigene Vorstellung, wie ein Programm aufgebaut sein muß. TOS lehnt sich dabei stark an DOS an. Ein TOS-Programm besteht aus drei Teilen (siehe auch Bild 1): dem Text-, dem Daten- und dem BSS-Bereich. Im Textbereich stehen die Assemblerbefehle. Der Datenbereich umfaßt vorinitialisierte Daten, beispielsweise Variablen und Tabellen. Der dritte Teil, das BSS (»Block Storage Segment«) ist ein Speicherbereich, der für Variablen vorgesehen ist, die beim Programmstart noch nicht initialisiert sind.

Der Grund, warum gleich zwei Programmteile für Daten existieren, ist einleuchtend: Vorinitialisierte Variablen muß der Assembler direkt in die Programmdatei schreiben, da ansonsten ihr Wert verloren geht. Variablen, die beim Programmstart noch nicht initialisiert sind, stehen dagegen nicht in der Programmdatei, sondern werden vom TOS erst beim Laden angelegt, indem es zusätzlichen Speicher reserviert.

Die Unterteilung des Programms nimmt Ihnen kein Assembler ab, doch stellen sie die Direktiven »TEXT«, »DATA« und »BSS« zur Verfügung. Direktiven sind keine Assemblerbefehle, sondern steuern lediglich den Assembler-Übersetzer. Beachten Sie, daß Sie bei einigen Assemblern die Direktiven mit einem Punkt einleiten müssen, z.B. ».TEXT«. Findet der Assembler keine der eben erwähnten Direktiven im Quelltext, so legt er alles im Textbereich ab. Ein ordnungsgemäß unterteiltes Programm sieht etwa so aus:

1:	TEXT
2:	move.l	suml,d0
3:	add.l	sum2,d0
4:	move.l d0,ergebnis 	; ergebnis=sum1+sum2
5:	...
6:	DATA
7:	sum1:	DC.L 10		; vorinitialisierte
8:	sum2:	DC.L 20		; Langwortvariablen
9:	BSS
10: 	ergebnis: DS.L 1	; uninit. Variable

Die »TEXT«-Direktive in Zeile 1 sagt dem Assembler, daß alle folgenden Informationen im Textbereich abzulegen sind. In der zweiten Zeile laden wir das Datenregister D0 mit dem Wert der vorinitialisierten Langwort-Variablen »suml«. Deren Definition folgt erst im Datensegment in Zeile 7. Zeile 3 addiert den Inhalt der ebenfalls im Datenbereich vorinitialisierten Langwort-Variablen »sum2« zu D0. Die letzte Anweisung legt das in D0 festgehaltene Ergebnis („ 30) in der noch uninitalisierten Langwortvariablen »ergebnis« ab. Diese ist im BSS-Bereich in Zeile 10 definiert.

Ab Zeile 6 beginnt durch die »DATA«-Direktive der Datenbereich. Darin stehen die im Textbereich angesprochenen Variablendefinitionen. Ähnlich wie die im ersten Kursteil behandelten Labels im Textbereich dienen auch Variablen als Ersatz für Speicheradressen. Erst beim Übersetzen in Maschinensprache ordnet der Assembler ihr eine Speicheradresse zu. Ergibt sich etwa, daß die Variable »sum1« an der Speicheradresse 1000 liegt, so würde der Assembler aus »move.l suml,D0« den Befehl »move.l 1000,D0« erzeugen. Beachten Sie bei der Definition von Labels und Variablennamen, daß kein Name doppelt Vorkommen darf. Außerdem beachten die meisten Assembler bei Namen lediglich die ersten acht Zeichen. Genaueres erfahren Sie im Handbuch des Assemblers.

Im Gegensatz zu Labels benötigt der Assembler bei einer Variablendefinition jedoch nicht nur den Variablennamen, sondern auch deren Größe. Dazu gibt es die Direktive »DC« (»Data Constant«). Darauf folgt unmittelbar die Größenangabe, wobei ».B« für Byte, ».W« für Wort- und »1« für Langwort steht. Zuletzt folgt noch der Wert, den die Variable beim Programmstart besitzt, »sum1« ist eine Langwortvariable mit dem Wert 10, »sum2« ist ebenfalls eine Langwortvariable und besitzt den Wert 20.

Ab Zeile 9 beginnt der BSS-Bereich, in dem die zu Programmstart noch uninitialisierten Variablen stehen. Dazu gehört die Langwortvariable »ergebnis«, der wir den Inhalt erst zur Laufzeit in Zeile 9 zuordnen. Für die Definition dieser Variablenart besitzt der Assembler die Direktive »DS« (»Data Storage«, Deutsch: »Datenblock«). Ihr folgt ebenfalls unmittelbar eine Größenangabe: ».B«, ».W« oder ».L«. Darauf erwartet der Assembler eine weitere Zahl. Diese gibt an, wieviele Byte, Worte bzw. Langworte diese Variable umfassen soll.

Für »ergebnis« benötigen wir lediglich ein Langwort, deshalb schreiben wir »1«.

Wollen Sie eine initialisierte Wertetabelle im Datenbereich definieren, erreichen Sie dies, indem Sie einerseits eine Folge von »DC«-Zeilen schreiben oder einfach durch Komma getrennt die Werte angeben: »tabelle: DC.B 10,20,30,40«. Eine uninitialisierte Tabelle im BSS-Bereich sieht etwa so aus: »tabelle: DS.B 4«. Dabei sagt der Wert »4«, daß »tabelle« vier Byte umfaßt.

Arithmetische Befehle

Wenden wir uns wieder neuen Assemblerbefehlen zu. Von den arithmetischen Befehlen haben wir bereits zwei kennengelernt: »add« und »sub«. Von beiden gibt es drei Variationen: »addq/subq«, »addx/subx« und »abcd/sbcd«. Die erste Variation »addq/subq« eignet sich zur Programmoptimierung. Das »q« im Befehlsnamen steht für »Immediate Quick« (Deutsch: »Schneller, direkter Wert«) und bezeichnet bereits ihren Zweck: die schnelle Addition/Subtraktion eines direkten Wertes, »addq/subq« arbeitet mit Byte, Wort und Langwort-Werten. Beide Befehle benötigen lediglich zwei Byte Speicherplatz. Dadurch ist allerdings der zu addierende Wert auf den Bereich 1 bis 8 beschränkt.

In folgenden Beispiel beschleunigt »subq« die Programmausführung gegenüber »sub« um rund 20 Prozent.

; lösche 400000 Byte ab Adresse 1000 
	lea	   1000,A0
	move.l #100000,D0 
loop:
	clr.l  (A0)+
	subq.l #1,D0		; D0 = D0-1
	bne.s  loop 		; solange D0 <> 0 weitermachen

Die zweite Variation »addx/subx« entspricht weitgehend »add/sub«. Der Unterschied besteht darin, daß bei der Addition/Subtraktion der Prozessor das Extended-Flag im CCR des Statusregisters berücksichtigt. Dieses Flag dient dazu, einen Überlauf im Ergebnis zu signalisieren. Addieren Sie beispielsweise in einer Byteoperation die beiden Werte 200 und 240, so wäre das Ergebnis 440. Das Ergebnis darf allerdings ebenfalls nur ein Byte sein. Da 440 allerdings außerhalb des Bereichs von 0 bis 255 (bzw. -128 bis 127) liegt, übernimmt der Prozessor das neunte Bit des Ergebnisses in das Extended-Flag.

Folgt auf diese Operation ein weiterer »addx« bzw. »subx«, so addiert/subtrahiert der Prozessor das Extended-Bit zum Ergebnis. Der praktische Nutzen dieser beiden Befehle liegt vor allem darin, in mehrere Byte, Worte oder Langworte dargestellte Zahlen ohne großen Aufwand zu addieren bzw. subtrahieren.

Im folgenden ein kleines Beispiel, das gleichzeitig in den Zeilen 4 und 5 eine neue Adressierungsart vorstellt: die indirekte Adressierung mit Pre-Dekrement. Diese Adressierungart ist genau das Gegenteil der indirekten Adressierung mit Post-Inkrement: Zunächst erniedrigt der Prozessor den Inhalt des Adreßregisters um die angegeben Zugriffsgröße (Byte, Wort oder Langwort). Auf diese neue Adresse erfolgt schließlich der Zugriff, »move.w -(A0),-(A1)« etwa erniedrigt zunächst A0 um zwei Byte und liest das Wort, auf das A0 zeigt. Darauf wird A1 um zwei Byte erniedrigt und das gelesene Wort in den Speicher geschrieben, auf den A1 zeigt.

1: lea zahl1+8,A0
2: lea zahl2+8,A1
3: add.b	#0,D0	; X-Flag löschen
4:	addx.l -(A0),-(A1)
5:	addx.l -(A0),-(A1)
6:	rts
7:DATA	;	high,low
8:	zahl1:	DC.L $,$ffffffff
9:	zahl2:	DC .L $,$0000002

Dieses Programm addiert die beiden Variablen »zahl1« und »zahl2«. Das besondere daran ist, daß die Variablen jeweils 64 Bit (zwei Langworte) umfassen, wobei das erste Langwort höherwertig ist. Da die Additionsbefehle aber maximal mit nur einem Langwort arbeiten, addiert das Programm zunächst die beiden niederwertigen Langworte ($ffffffff+ $0000002) unter Berücksichtigung des Extended-Flags miteinander.

Vor der Addition müssen wir das Extended-Flag sicherheitshalber zurüekselzen. Dies geschieht in Zeile 3: Durch Addition mit 0 kommt es keinesfalls zu einem Überlauf, und der Prozessor löscht das Extended-Flag. In Zeile 5 addieren wir unter Berücksichtigung des Extended-Flags die beiden höchstwertigen Langworte ($0+$0+ $1 für das gesetzte Extended-Flag). Das korrekte Ergebnis ($1,$1) steht danach in »zahl2«. Die Befehle »addx/subx« kennen nur zwei Adressierungsarten: »-(Ax),-(Ay)« und »Dx,Dy«.

Das TOS unterteilt ein Programm in drei Bereiche: TEXT (Assemblerbefehle), DATA (initialisierte Daten) und BSS (uninitialisierte Daten).
Die Befehle »abcd«, »sbcd« und »nbcd« sehen ihren Byteoperanden nicht als Byte, sondern als eine zweistellige binär kodierte Dezimalzahl (BCD)

Die dritte Variation von »add/sub« ist »abcd/sbcd« und steht für »Add/Sub Binary Coded Decimal«, addiere/ subtrahiere binär kodierte Dezimalzahl«. Diese Befehle arbeiten nur mit Byteoperanden. Sie sehen allerdings ein Byte nicht als eine Zusammenfassung von acht Bit mit einem Wertebereich von 0 bis 255, sondern als eine binär kodierte Dezimalzahl, kurz BCD, an (siehe Bild 2). Jede BCD enthält zwei Dezimalstellen, die jeweils vier Bits benötigen und einen Wertebereich von 0 bis 9 haben. Das Byte $78 repräsentiert etwa die Dezimalzahl 78. »abcd/sbcd« arbeiten ansonsten wie »addx/subx«: Sie berücksichtigen das Extended-Flag und kennen nur die zwei Adressierungsarten »-(Ax),-(Ay)« und »Dx,Dy«.

Für was sind BCDs überhaupt nützlich? Viele Programmierer verwenden BCDs zur Darstellung mehrstelliger Dezimalzahlen, auf die sie einen schnellen Zugriff benötigen. Das folgende Listing läßt sich etwa für ein Taschenrechner-Programm verwenden. Es subtrahiert zwei Zahlen mit jeweils vier Stellen (zwei Byte) und gibt das Ergebnis in den Datenregistern D0 und D1 zurück.

sub_bcd:
lea	zahl1,A0	; Zeiger auf Zahl 1
move.b	(A0) + ,D0	; höchstwertige Stellen
move.b	(A0),D1	; niederwertig
lea	zahl2,A0	; Zeiger auf Zahl 2
move.b	(A0)+,D2	; höchstwertig
move.b	(A0),D3	; niederwertig
add.b	#0,D0	; X-Flag löschen
sbcd	D3,D1	; zuerst niederwertig
sbcd	D2,D0	; dann höchstwertig
rts	; Ergebnis in D0/D1 zurück

Der 68000-Prozessor kennt außer Additionen und Subtraktionen natürlich noch weitere arithmetische Befehle. Da ist zunächst einmal »neg« (»Negate«, negiere). Dieser Befehl negiert seinen einzigen Operanden. Dazu liest er den Wert aus, zieht ihn von 0 ab und schreibt ihn zurück, »neg« wandelt etwa 10 in -10 um. In Tabelle 1 sehen Sie, wie der Prozessor vorzeichenbehaftete Zahlen darstellt. Von »neg« gibt es zwei Variationen: »negx«, das zusätzlich das Extended-Flag berücksichtigt, und »nbcd«, das seinen Byteoperanden als BCD-Zahl betrachtet.

Neben den bisher besprochenen und doch recht trivialen Befehlen ist der 68000 auch in der Lage, Multiplikationen und Divisionen durchzuführen. Dabei unterscheidet er zwischen Vorzeichen losen und vorzeichenbehafteten Operationen. Der Befehl »mu-lu« steht für »multiply unsigned« und multipliziert vorzeichenlos den Quell- mit dem Zieloperanden. Für den Quelloperanden sind alle Adressierungsarten außer ein Adreßregister erlaubt. Der Zieloperand darf nur ein Datenregister sein. Der Quell- und Zieloperand sind Worte und liegen somit im Wertebereich von 0 bis 65535. Das Ergebnis im Zieloperand ist stets ein Langwort.

»divu« steht für »Divide unsigned« und dividiert den Ziel- durch den Quelloperanden. Der Quelloperand ist ein Wort, der Zieloperand ein Langwort. Das Ergebnis belegt ebenfalls ein Langwort, wobei im unteren Wort der Quotient und im oberen Wort der Rest der Division steht. Teilen Sie etwa das Datenregister DO, das den Wert 2001 enthält, durch 100 (»divu #100,DO«), so erhalten Sie $00010014 zurück. $0001 ist dabei der Rest und $0014 (20) der Quotient. Die erlaubten Adressierungsarten der »divu«-Operanden entsprechen denen von »mulu«.

»muls« (»Multiply signed«) führt eine vorzeichenbehaftete Multiplikation durch. Dabei liegen die Operanden im Wertebereich von -32768 bis 32767. »divs« dividiert Vorzeichen behaftet den Ziel- durch den Quelloperanden. Ansonsten entsprechen sie ihren vorzeichenlosen Äquivalenten.

Unterroutinen

Stellen Sie sich vor, Sie müßten ein Programm schreiben, das die beiden Zahlen 10 und 20 jeweils mit 16 multipliziert (f(x) = x*16). Mit unseren bisherigen Kenntnissen der Assemblersprache läßt sich das Problem nur folgendermaßen lösen:

move.w	#10,d0
add. w	d0, d0	; d0 = d0+d0:	10*2
add.w	d0,d0	; 10x2x2
add.w	d0,d0	; 10x2x2x2
add.w	d0,d0	; 10x2x2x2x2 = 160
move.w	#20,d0
add.w	d0,d0	; 20x2
add.w	d0,d0	; 20x2x2
add.w	d0,d0	; 20x2x2x2
add.w	d0,d0	; 20x2x2x2x2 =320

Wie Sie sehen, verwenden wir zum Multiplizieren vier mal den Additionsbefehl, der den Quelloperanden (linke Seite) mit dem Zieloperanden (rechte Seite) addiert und das Ergebnis im Zieloperanden ablegt. In unserem Fall addieren wir das Datenregister DO mit sich selbst. Dieses Vorgehen ist für beide Werte 10 und 20 identisch - eine sehr unflexible und vor allem umständliche Lösung.

Wie auch jede Hochsprache kennt Assembler Befehle für die Definition und den Aufruf von Unterroutinen. Dies sind Programmteile, welche die Hauptroutine zur Erledigung von häufigen Aufgaben aufruft. Dabei übergibt sie gegebenenfalls Parameter und erhält je nach Aufgabe ein Ergebnis zurück. Im obigen Listing ist das Multiplizieren mit 16 eine Aufgabe, die wir unbedingt in einer Unterroutine ablegen sollten. Als Parameter übergeben wir den Multiplikant, als Ergebnis erhalten wir das Produkt zurück. Hier das angepaßte Listing:

1:	move.w	#10,d0
2:	jsr	mall6	; d0=106
3:	move.w	#20,d0
4:	jsr	mall6	; d0 = 206
5:	...
6:; den Wert d0 mit 16 multiplizieren 
7:; das Ergebnis steht ebenfalls in D0 
8: mall6:
9:	add.w	d0,d0	; D0x2
10: add.w	d0,d0	; D0x2x2
11: add.w	d0,d0	; D0x2x2x2
12: add.w	d0,d0	; d0x2x2x2x2
13: rts

In diesem Listing haben wir die Multiplikation in die mit dem Label »mall 6« benannte Unterroutine verbannt. Diese erwartet den Multiplikant im Datenregister D0. Das Ergebnis steht ebenfalls in D0. In der ersten Zeile setzen wir D0 auf 10. Die zweite Zeile enthält den neuen Befehl »jsr« (»Jump to SubRoutine«, springe in die Unterroutine). Als einzigen Operanden erwartet dieser Befehl die Adresse der aufzurufenden Unterroutine, in diesem Fall das Label »mall 6«.

Byte Zahl
$00 - $7f 0..127
$80 - $ff -128..-1
Wort Zahl
$0000 - $7fff 0..32767
$8000 - $ffff 32768..-1
Langwort Zahl
$00000000 - $7fffffff 0..2.14E9
$80000000 - $ffffffff -2.14E9..-1

Tabelle 1. Vorzeichenbehaftete Zahlen sind positiv, wenn das höchste Bit gelöscht ist. Sie sind negativ, wenn das höchste Bit gesetzt ist.

Der nächste vom Prozessor abzuarbeitende Befehl ist der erste Befehl (Zeile 9) der Unterroutine. In Zeile 13 stößt der Prozessor auf die Anweisung »rts« (»ReTurn from Subroutine«, Deutsch: »Kehre aus Unterroutine zurück«). Dieser Befehl weist den Prozessor an, die Programmausführung hinter dem zuletzt abgearbeiteten »jsr« fortzusetzen. Dies ist in unserem Fall Zeile 3. Hier setzen wir D0 auf 20 und rufen erneut die Multiplikations-Routine auf. Um zu wissen, von wo der Unterroutinenaufruf kommt, speichert der Prozessor die aktuelle PC-Adresse auf dem Stack (Adreßregister A7 bzw. SP) ab. Den Stack werden wir im dritten Kursteil noch genauer kennenlernen.

In obigem Beispiel übergeben wir den Parameter in D0 und erhalten dort auch das Ergebnis zurück. Für Unterroutinen, die mehrere Parameter benötigen, können Sie neben D0 auch alle anderen Daten- und Adreßregister außer A7, dem Stackregister, verwenden. Die Zieladresse von »jsr« kann wie bei »jmp« eine beliebige gerade Speicheradresse sein. Auch nahezu alle Adressierungsarten, wie etwa »jsr (A0)« sind zulässig.

Verwenden Sie einen direkten Sprung und ist die Distanz zwischen Aufruf und Ziel kleiner 32768 Byte, so ist der Befehl »bsr« (»Branch to SubRoutine«, verzweige in die Unterroutine) der »jsr«-Anweisung vorzuziehen. Seine Funktionsweise ist völlig identisch, doch der Assembler übersetzt ihn statt in sechs in nur vier Byte. Für Distanzen kleiner 128 Byte ist der Befehl »bsr.s« zu empfehlen, der lediglich zwei Byte belegt. Viele Assembler, so auch der Turbo-Ass, verfügen über einen Kode-Optimierer. Dieser nimmt unter anderem bei Sprunganweisungen den günstigen Befehl. Das obige »jsr mul16« ersetzt er z. B. durch »bsr.s mul16« und spart dadurch pro Sprungbefehl vier Byte ein.

Logische Verknüpfungen

Eine weitere wichtige Befehlsgattung neben den arithmetischen Befehlen sind die logischen Verknüpfungen. Der 68000 kennt vier Verknüpfungen: Und (»and«), Oder (»or«), Exklusiv-Oder (»eor«) und Nicht (»not«). Die Verknüpfungen arbeiten bitweise und mit allen Operandengrößen (Byte, Wort und Langwort). In einer Wortoperation finden demnach 16 einzelne Verknüpfungen statt. Dabei beginnt der Prozessor mit dem jeweils niedrigsten Bit des Quell- und Zieloperanden. Das Ergebnis der Bitverknüpfung speichert er in das aktuelle Bit des Zieloperanden und fährt daraufhin mit der Verknüpfung der nächsten beiden Bit fort. Die vier Verknüpfungsregeln finden Sie in Bild 3.

Die »Nicht«-Verknüpfung benötigt nur den Zieloperanden. Bei »and«, »or« und »eor« dürfen Sie als Zieloperand auch das Statusregister SR angeben; der Quelloperand muß jedoch in diesem Fall ein absoluter Wert sein. Damit sind Sie in der Lage, gezielt Flags zu setzen und zu löschen, »and.w #$ff00,sr« setzt etwa alle Flags des CCR zurück.

Aufgrund seiner Verknüpfungsregel eignet sich der »eor«-Befehl hervorragend, einen Speicherblock zu verschlüsseln. Zum Abschluß dieses Kursteils finden Sie deshalb ein Programm, das einen Text verschlüsselt und unverschlüsselt auf dem Bildschirm ausgibt, (ah)

Der 68000 kennt vier logische Verknüpfungen: Und, Oder, Exklusiv-Oder und Nicht.

Die Verknüpfungen erfolgen bitweise. Bei Byteoperanden gibt es demnach 8, bei Worten 16 und bei Langworten 32 einzelne Verknüpfungen.

lea			text,A0 
jsr			texteor	; Text kodieren
lea			text,A0
jsr			printA0	; Text ausgeben
lea			neuezeile,A0
jsr			printA0	; Zeilenwechsel
lea			text,A0
jsr			texteor	; Text dekodieren
lea			text,A0
jsr			printA0	; Text ausgeben
rts					; Programm verlassen
texteor:			; in A0 Zeiger	auf	Null-terminierten String
move.b		(A0),D0	; Zeichen lesen
beq.s		teorend	; wenn 0, dann Ende
eor.b		#15,D0	; ansonsten kodieren
move.b		D0,(A0)+	; kodiertes Z. schreiben
bra.s		texteor	; nächstes Zeichen
teorend:	rts
printA0				; in A0 Zeiger auf Null-terminierten String
move.l		A0,-(SP)	;??? Diese Befehle
move.w		#9,-(SP)	; ??? werden Sie
trap		#1		;??? nach dem 3. Teil
addq.l		#6,SP	;??? beherrschen!
DATA
text:		DC.B "I like Assembler",0
neuezeile:	DC.B 13,10,0

Kursübersicht

Teil 1: Der Prozessor, die Register, elementare Befehle

Teil 2: Programmstruktur, Unterroutinen, arithmetische Befehle und logische Verknüpfungen

Teil 3: Bit- und Schiebebefehle, der Stack, Interrupts, Exceptions, Traps, Assembler-Direktiven

Teil 4: Adressierungsarten, Makroprogrammierung, Optimierung, Fehlersuche, Hochspracheneinbindung

Teil 5: Programmprojekt: Assembler und das Betriebssystem


Martin Backschat
Aus: TOS 01 / 1991, Seite 80

Links

Copyright-Bestimmungen: siehe Über diese Seite