Assemblerkurs, Teil 3

Wie versprochen stehen in diesem dritten Teil unseres Assemblerkurses verschiedene Ein- und Ausgabemethoden auf dem Programm, wie sie in fast jedem Programm benötigt werden. Außerdem müssen wir uns noch mit der Speicherreservierung für konstante oder variable Daten befassen.

Hello, World!

Aus guter Tradition steckt man sich beim Einstieg in eine Programmiersprache zunächst gerne das Ziel, ein Programm zu erstellen, das lediglich eine simple Meldung auf den Bildschirm schreibt. Wer nun fürchtet, einen Textstring Byte für Byte in den Bildschirmspeicher übertragen zu müssen, kann beruhigt werden: Die sehr häufig gebrauchte Funktion Nr. 9 des DOS-Interrupts 21h (oft auch mit "Int 21,9" bezeichnet) gibt nämlich bereits ganze Zeichenketten auf dem Bildschirm aus und stellt so gewissermaßen den PRINT-Befehl in Assembler dar. Die Zeichenkette selbst darf sich an beliebiger Stelle im Datensegment befinden. Ihre Position, d.h. der Offset des ersten Zeichens innerhalb des Datensegments, muß vor dem Aufruf des Interrupts 21h in das Register DX geladen werden und das Ende des auszugebenden Strings muß mit einem "$"-Zeichen gekennzeichnet sein.
Hier nun das komplette Programm (ohne assemblerspezifischen Header):

          mov dx,Offset Meldung
          mov ah,9  ; Funktion 9
          int 21h   ; Print
          mov ax,4C00h
          int 21h   ; beenden
Meldung:  DB 'Hello, World!'
          DB 10,13,'$'

Datenbereiche im Programm

Die Zeichenkette "Hello,World!" sowie ein Linefeed-, ein Carriage-Return- und das Dollarzeichen liegen im Programm direkt hinter dem eigentlichen Code. Die Assemblerdirektive "DB" (define Byte) bindet beliebige Bytefolgen - die meisten Assembler akzeptieren auch Zeichenketten - direkt in das Programm ein. Entsprechend setzt man die Direktiven "DW" und "DD" zur Definition von Word- bzw. Doubleword-Speicherplätzen (16 bzw. 32 Bit) ein.
Das Label "Meldung" im Programm dient als Referenz auf die Zeichenkette: Der Ausdruck "Offset Meldung" wird beim Assemblieren automatisch durch die Position der Zeichenkette im Datenegment ersetzt.

Variablenspeicher

Datenbereiche mit variablem Inhalt bedürfen oft gar keiner Initialisierung. Für diesen Fall erlauben die Direktiven DB, DW und DD die Verwendung eines Fragezeichens als Argument. Solche Datenbereiche sollten stets am Ende des Programms angeordnet werden, damit der Assembler sie nicht unnötiger Weise mit Nullen gefüllt ins Programm einfügen muß. Besonders gilt dies für umfangreiche Byte-Felder (Arrays) wie im folgenden Beispiel, das unter Verwendung eines Stringbefehls mit Präfix (REP) 100 Bytes von Feld1 nach Feld2 kopiert:

           mov si,Offset Feld1
           mov di,Offset Feld2
           mov cx,100 ; Anzahl
           cld        ; aufwärts
           REP movsb
           ...
    Feld1: DB 100 Dup(?)
    Feld2: DB 100 Dup(?)
    Dummy: DB ?

Natürlich ist es auch möglich, ohne den Umweg über die Register SI oder DI auf Speicherplätze zuzugreifen. Beispiel für 16-Bit- (Word-) Speicherzugriffe (Byte-Zugriffe funktionieren analog):

           mov ax,Word Ptr [Offset Zahl1]
           add ax,Word Ptr [Offset Zahl2]
           mov Word Ptr [Offset Summe],ax
           ...
    Zahl1: DW 400
    Zahl2: DW 500
    Summe: DW ? ; wird 900

Die meisten Assembler unterstützen für die hier auftretenden Speicherreferenzen eine spezielle Form von Labels ohne Doppelpunkt, mit der sich das vorangehende Beispiel deutlich über sichtlicher darstellen läßt:

          mov ax,Zahl1
          add ax,Zahl2
          mov Summe,ax
          ...
    Zahl1 DW 400
    Zahl2 DW 500
    Summe DW ? ; wird 900

Auf Nummer sicher

Dank der "Pseudo-Initialisierung" mit "?" sind die beiden Arrays nicht Bestandteil des COM- Programms. Allerdings ist nun nicht mehr sichergestellt, daß DOS einen Programmstart verhindert, wenn nicht genügend Speicher zur Verfügung steht. Natürlich könnte man argumentieren, daß eine Speichererschöpfung bei einem sehr kurzen Programm mit womöglich nur wenigen uninitialisierten Bytes in der Praxis kaum vorkommt, aber auf solch unkalkulierbare Risiken sollte sich niemand einlassen.
Eine einfache Möglichkeit, den "Speicherstand" während der Laufzeit zu testen besteht in der Auswertung des Stackpointers (Register SP). Beim Programmstart wird SP von DOS nämlich so hoch initialisiert, wie es die Menge des freien Speichers gestattet (jedoch maximal 0FFFEh, weil das der letzte 16-Bit-Speicherplatz im gemeinsamen Code-/Daten-/Stack-Segment ist). Da der Stapel abwärts dem Programmcode entgegenwächst, muß ein gewisser Sicherheitsabstand (200 Bytes sollten es schon sein) zum letzten verwendeten Speicherplatz vorhanden sein. In unserem Beispiel würde man prüfen, ob die Differenz von SP und Offset Dummy mindestens 200 beträgt.
Noch nobler ist es aber, dem Betriebssystem zur Laufzeit mitzuteilen, wieviel Speicher das Programm tatsächlich benötigt. Die Speicherverwaltung von DOS gibt dann den überschüssigen Speicher wieder frei, was beim Portfolio den Vorteil hat, daß sich dann meist die internen Applikationen parallel nutzen lassen. Ein universeller Programmanfang, der diese sogenannte Anpassung der Segmentlänge vornimmt, gestaltet sich wie folgt:

    Start:        mov bx,Offset Stapel+200
                  mov sp,bx     ; Vorsicht!
                  mov cl,4
                  shr bx,cl     ; bx/16
                  inc bx
                  mov ah,4Ah    ; Segment
                  int 21h       ; anpassen
                  jnc Speicher_ok
                  mov dx,Offset Fehler
                  mov ah,9
                  int 21h
                  mov ah,4Ch
                  int 21h       ; beenden
    Speicher_ok:  ...
    Fehler:       DB 'No Mem!',10,13,'$'
    Stapel:       DB 200 Dup(?)
 
Hier wird zuerst der Stapelspeicher in den 200 Bytes großen Bereich hinter der Stringkonstante verlegt (es dürfen sich keine Daten auf dem Stapel befinden), um anschließend mit der Funktion 4Ah des Interrupts 21h allen weiteren Speicher freizugeben. Letzteres ist nur in 16-Byte-Portionen, den sogenannten Paragraphen, möglich. Die Anzahl der benötigten Paragraphen wird durch eine Division der höchsten Speicheradresse durch 16 mit anschließendem Aufrunden (Rechts-Shiften um 4 Bit und Erhöhen um 1) ermittelt. Anmerkung: Die Berechnung zur Laufzeit ist eigentlich unschön, aber mir ist es leider nicht gelungen, meinem Assembler den Ausdruck (Offset Stapel)/16+1 schmackhaft zu machen.
Im Fehlerfall (nicht genug Speicher vorhanden) setzt DOS das Carry-Flag, worauf die Fehlermeldung ausgegeben wird und das Programm abbricht.

Endlich: Tastatureingaben

Nach diesen zugegebenermaßen etwas abstrakten Betrachtungen soll im folgenden wieder ein (be)greifbares Objekt im Mittelpunkt stehen: die Tastatur.
Auch für Eingaben hält der Interrupt 21h passende Funktionen parat. Am gebräuchlichsten dürften die Funktionen 1 und 8 sein, die auf ein Zeichen von der Tastatur warten und dessen ASCII-Wert im Register AL ablegen. Gleichzeitig stellt die Funktion 1 das getippte Zeichen auf dem Bildschirm dar (Echo), wodurch sie sich von der Funktion 8 unterscheidet.
Eine Erwähnung verdient auch die Funktion 0Bh, die mit AL=0FFh signalisiert, daß sich Zeichen im Tastaturpuffer befinden (leerer Puffer: AL=0).
Zur Veranschaulichung folgt ein kurzes Programm, das den Benutzer so lange Zeichen tippen läßt, bis die Escape-Taste (ASCII-Wert 27) betätigt wurde:

    Start:  mov ah,1
            int 21h
            cmp al,27   ; ESC?
            jne Start
            mov ah,4Ch
            int 21h     ; Ende

DOS oder BIOS?

Auch der vom BIOS belegte Interrupt 16h stellt ähnliche Funktionen zur Tastaturabfrage bereit. So ist beispielsweise
    mov ah,8
    int 21h  ; DOS
austauschbar durch
    mov ah,0
    int 16h  ; BIOS.
Beide Programmfragmente warten auf eine Taste, wobei der ASCII-Wert danach im Register AL steht. Die DOS-Variante bietet aber den Vorteil, daß auch Eingabeumleitungen - etwa aus einer Steuerdatei - verarbeitet werden können.

Das Tor zur Hardware

Die Bedeutung der Ein-/Ausgabeports und der zugehörigen Instruktionen (IN und OUT) wurde in der letzten Ausgabe bereits angesprochen. Kommen wir also gleich zur Sache. Die folgende Übersicht zeigt die wichtigsten (hexadezimalen) Portadressen des Portfolio:
 
    8000 Keyboard-Scancode (>128 => losgelassen)
    8010 LCD controller Datenregister
    8011 LCD controller Addressregister
    8020 Soundchip (128 = aus)
    8030 Power management
    8040 Zähler (2 Hz)
    8051 Batterie-Status (C2h=ok, 82h=leer)
    8060 LCD-Kontrast
    8070 Serial Interface
    8078 Parallel Interface Datenregister (out)
    8079 Parallel Interface Steuerregister (out)
    807A Parallel Interface Statusregister (in)

Grüße von der Atari-Taste

Man mag es kaum glauben, aber es gibt Situationen, in denen einem weder DOS noch das BIOS weiterhelfen. Oder kennt vielleicht jemand den ASCII-Wert der Atari-Taste? Und wenn ja, wie lange wurde sie gedrückt? Oder: Wieviele Tasten werden im Augenblick gedrückt gehalten?
Zwei Zeilen Assemblercode genügen, um festzustellen, welche Taste zuletzt niedergedrückt oder losgelassen wurde:

    mov dx,8000h
    in al,dx

Jede der 63 Tasten des Portfolio besitzt einen eindeutigen "Make Code", der beim Niederdrücken an der Portadresse 8000h erscheint. Beim Loslassen einer Taste geschieht dasselbe mit ihrem "Break Code", der stets um 128 größer ist als der Make Code. Leider folgt die Numerierung der Tasten einem sehr eigenwilligen Schema, doch mit Hilfe der untenstehenden Tabelle ist es ein leichtes, beliebige Tasten zu detektieren.
Zur Ehrenrettung des BIOS muß noch ergänzt werden, daß der portfoliospezifische Interrupt 61h sehr wohl eine eigene Funktion (2Fh) zur Detektion der Atari-Taste besitzt (bei der "Fn"- Taste läßt es einen dann aber doch im Stich). Mit dem Interrupt 61h werden wir uns sicher noch einmal beschäftigen.
 
            Make-            Make-
    Taste   Code     Taste   Code
    ------------------------------
      ,      38        A      62
      -      26        B      58
      .      52        C      56
      /      61        D       5
      0      24        E      19
      1       2        F      40
      2       3        K      47
      3       4        G      35
      4      34        H      41
      5       6        I      25
      6       7        J      42
      7      13        K      47
      8      46        L      39
      9      15        M      60
      ;      51        N      59
      =      53        O      12
    links    43        P      33
    rechts   44        Q      10
    oben     29        R      20
    unten    37        S      32
    lShift   27        T      21
    rShift   36        U      11
     Fn      54        V      57
     Esc     63        W      17
    Enter    22        X      55
    Space    50       Z/Y     23
      Ä      30       Y/Z     49
      Ü      28       Alt      9
    \ / <    48       Atari    0
    + / ]    31        BS     14
    Caps     45       Ctrl    18
     Del      8

Zur Verdeutlichung wieder ein konstruierter Programmausschnitt, in dem die Cursortasten zur Steuerung eines Ordinatenwerts dienen:

              mov dx,8000h
              in al,dx
              cmp al,43 ; links?
              jne nicht_li
              dec x_Koo
    nicht_li: cmp al,44 ; rechts?
              jne nicht_re
              inc x_Koo
    nicht_re: ...
    x_Koo     DB ?

Vorschau

Nachdem sich nun Textausgaben als wenig spektakulär entpuppt haben, steht als nächstes der Einstieg in die Grafikprogrammierung bevor. Die relativ komfortablen BIOS-Funktionen eignen sich leider kaum für zeitkritische Anwendungen, weswegen es unumgänglich ist, sich mit dem LCD-Controller auseinanderzusetzen. Das und mehr gibt's beim nächsten Mal. Bleiben Sie dran!

Und wie immer: Fragen und Anmerkungen bitte an .
Eventuelle Korrekturen oder Ergänzungen werden zu finden sein auf der WWW-Seite:
http://leute.server.de/peichl/pf.htm
 



Zum 2. Kursteil
Zum 4. Kursteil
Zum Hauptmenü