PofoWiki

Die ultimative Informationsquelle zum ATARI Portfolio

Benutzer-Werkzeuge

Webseiten-Werkzeuge


software:diy:assembler:kkurs

Unterschiede

Hier werden die Unterschiede zwischen zwei Versionen angezeigt.

Link zu dieser Vergleichsansicht

Beide Seiten der vorigen RevisionVorhergehende Überarbeitung
Nächste Überarbeitung
Vorhergehende Überarbeitung
software:diy:assembler:kkurs [21/12/2005 22:12] 8088software:diy:assembler:kkurs [Unbekanntes Datum] (aktuell) – Externe Bearbeitung (Unbekanntes Datum) 127.0.0.1
Zeile 8: Zeile 8:
  
 Doch jetzt zur Sache. Bekanntlich tut im Portfolio ein Intel-Prozessor namens 8088 seinen Dienst. Das Faszinierende an dieser 1979 mit dem 8086 (16-Bit-Datenbus, sonst wie 8088) eingeführten Architektur ist, daß sie uns bis in heutige PCs verfolgt. Wenn man aus einem Pentium-PC das Letzte herausholen möchte, ist deshalb auch heute das Erlernen der 8086-Maschinensprache durchaus noch aktuell. Doch jetzt zur Sache. Bekanntlich tut im Portfolio ein Intel-Prozessor namens 8088 seinen Dienst. Das Faszinierende an dieser 1979 mit dem 8086 (16-Bit-Datenbus, sonst wie 8088) eingeführten Architektur ist, daß sie uns bis in heutige PCs verfolgt. Wenn man aus einem Pentium-PC das Letzte herausholen möchte, ist deshalb auch heute das Erlernen der 8086-Maschinensprache durchaus noch aktuell.
 +
  
 ==== Maschinensprache und Assembler ==== ==== Maschinensprache und Assembler ====
Zeile 17: Zeile 18:
 In den Zeiten als die Programme noch kurz, Disketten Luxus und Mailboxen die Domäne von Hackern waren, wurden Listings ja gern in so einer Form gedruckt. Zur Programmentwicklung ist diese Darstellung jedoch denkbar ungeeignet. Um sich nicht für jeden Maschinenbefehl eine Zahl merken zu müssen, bedient man sich einer anderen Schreibweise. Für alle Maschinenbefehle des Prozessors existieren eingängige Abkürzungen, die meist aus drei Buchstaben bestehen und Mnemonics genannt werden. Ein Assemblerquelltext besteht hauptsächlich aus diesen Mnemonics und den zum jeweiligen Maschinenbefehl gehörenden Parametern. Dabei beginnt man für jeden Befehl eine neue Zeile, läßt links Platz für Sprungmarken und fügt eventuell rechts nach einem Strichpunkt einen Kommentar an. Obiges Beispiel stellt sich damit wie folgt dar: In den Zeiten als die Programme noch kurz, Disketten Luxus und Mailboxen die Domäne von Hackern waren, wurden Listings ja gern in so einer Form gedruckt. Zur Programmentwicklung ist diese Darstellung jedoch denkbar ungeeignet. Um sich nicht für jeden Maschinenbefehl eine Zahl merken zu müssen, bedient man sich einer anderen Schreibweise. Für alle Maschinenbefehle des Prozessors existieren eingängige Abkürzungen, die meist aus drei Buchstaben bestehen und Mnemonics genannt werden. Ein Assemblerquelltext besteht hauptsächlich aus diesen Mnemonics und den zum jeweiligen Maschinenbefehl gehörenden Parametern. Dabei beginnt man für jeden Befehl eine neue Zeile, läßt links Platz für Sprungmarken und fügt eventuell rechts nach einem Strichpunkt einen Kommentar an. Obiges Beispiel stellt sich damit wie folgt dar:
  
-            mov dx,8020h  ; Portadresse  +<code asm> 
-            xor ax,ax     ; Zähler auf 0  +        mov dx,8020h  ; Portadresse  
-    Marke:  out dx,al     ; Ton erzeugen  +        xor ax,ax     ; Zähler auf 0  
-            dec ax        ; Zähler -1  +Marke:  out dx,al     ; Ton erzeugen  
-            jnz Marke     ; Schleife  +        dec ax        ; Zähler -1  
-            mov ax,4c80h  +        jnz Marke     ; Schleife  
-            out dx,al     ; Ton aus  +        mov ax,4c80h  
-            int 21h       ; beenden+        out dx,al     ; Ton aus  
 +        int 21h       ; beenden 
 +</code>
  
 ==== Unser Handwerkszeug ==== ==== Unser Handwerkszeug ====
Zeile 30: Zeile 33:
 Dieser Quelltext muß nun in zwei Schritten mit einem Assembler und einem Linker in ein lauffähiges Programm übersetzt werden. Man kann diese zwei Schritte mit einem Batchprogramm zusammenfassen. Die bekanntesten Assembler sind MASM von Microsoft (zugehöriger Linker: LINK), TASM (Linker: TLINK) von Borland sowie der Sharewareassembler A86. Ich selbst benutze Borlands Turbo Assembler, jedoch scheint mir A86 den einfachsten Einstieg zu ermöglichen, denn dieser kann ohne externen Linker obigen Beispielquelltext direkt in ein .com-Programm übersetzen. Zudem ist er mit 23kB (Version 3.22) mit Abstand der kleinste Vertreter seiner Gattung und läuft damit sogar direkt auf dem Portfolio. A86.COM befand sich auf der Begleitdiskette zur ersten Ausgabe der Pofoinfo, ist aber auch auf der Club-CD, in der Club-Mailbox und in vielen Shareware-Archiven vorhanden. Turbo-Pascal (6.0ff) wartet mit einem integrierten Assembler auf. Allerdings bettet es den damit erzeugten Code in ein unhandliches EXE-Programm ein. Wer noch keines dieser Programme besitzt, kann zum Experimentieren vorerst auf DEBUG zurückgreifen. Dieser Quelltext muß nun in zwei Schritten mit einem Assembler und einem Linker in ein lauffähiges Programm übersetzt werden. Man kann diese zwei Schritte mit einem Batchprogramm zusammenfassen. Die bekanntesten Assembler sind MASM von Microsoft (zugehöriger Linker: LINK), TASM (Linker: TLINK) von Borland sowie der Sharewareassembler A86. Ich selbst benutze Borlands Turbo Assembler, jedoch scheint mir A86 den einfachsten Einstieg zu ermöglichen, denn dieser kann ohne externen Linker obigen Beispielquelltext direkt in ein .com-Programm übersetzen. Zudem ist er mit 23kB (Version 3.22) mit Abstand der kleinste Vertreter seiner Gattung und läuft damit sogar direkt auf dem Portfolio. A86.COM befand sich auf der Begleitdiskette zur ersten Ausgabe der Pofoinfo, ist aber auch auf der Club-CD, in der Club-Mailbox und in vielen Shareware-Archiven vorhanden. Turbo-Pascal (6.0ff) wartet mit einem integrierten Assembler auf. Allerdings bettet es den damit erzeugten Code in ein unhandliches EXE-Programm ein. Wer noch keines dieser Programme besitzt, kann zum Experimentieren vorerst auf DEBUG zurückgreifen.
  
-Neben einem Assembler sollten zur Grundausstattung aber auch Nachschlagewerke zu den Themen Prozessorarchitektur, Befehlssatz und Betriebssystem gehören, denn eine ausführliche Behandlung im Rahmen dieses Kurses ist kaum möglich. Eine große Hilfe ist hier eine Online-Referenz, wie z.B. das Shareware-Programm HelpPC für DOS von David Jurgens, das alle genannten Themenkomplexe abdeckt (Quelle z.B.ftp://ftp.simtel.net/pub/simtelnet/msdos/info/helppc21.zip, Größe ca. 255K)+Neben einem Assembler sollten zur Grundausstattung aber auch Nachschlagewerke zu den Themen Prozessorarchitektur, Befehlssatz und Betriebssystem gehören, denn eine ausführliche Behandlung im Rahmen dieses Kurses ist kaum möglich. Eine große Hilfe ist hier eine Online-Referenz, wie z.B. das Shareware-Programm HelpPC für DOS von David Jurgens, das alle genannten Themenkomplexe abdeckt (Quellez.B. [[ftp://ftp.simtel.net/pub/simtelnet/msdos/info/helppc21.zip]], Größe ca. 255 KiB).
  
 ==== Das erste Programm ==== ==== Das erste Programm ====
Zeile 42: Zeile 45:
 Die wichtigsten Befehle, die in Debug immer hinter einem Minuszeichen als Prompt eingegeben werden, lauten: Die wichtigsten Befehle, die in Debug immer hinter einem Minuszeichen als Prompt eingegeben werden, lauten:
  
-    ?      Hilfe (nur neuere Versionen)  +^ Kommando ^ Funktion                     ^ 
-    Adr  Eingabe einer Bytefolge  +|   ?      Hilfe (nur neuere Versionen) | 
-    a      Assemble  +|   //Adresse//  Eingabe einer Bytefolge       
-    u      Unassemble  +|   a      Assemble                     |  
-    nName  Dateiname festlegen  +|   u      Unassemble                   |  
-    r cx   Dateilänge festlegen  +|   n//Name//  Dateiname festlegen           
-    w      Write+|   r cx   Dateilänge festlegen         |  
 +|   w      Write                        
  
 Weiterführende Erläuterungen zu Debug sowie eine ältere Version (knapp 10KB), die auch auf dem PF funktioniert, finden sich auf der Club-CD. Unser Programm können wir damit bereits eingeben, und alle Debug-Neulinge sollten dies jetzt einmal versuchen. Zur Orientierung diene dabei das folgende Bildschirmprotokoll: Weiterführende Erläuterungen zu Debug sowie eine ältere Version (knapp 10KB), die auch auf dem PF funktioniert, finden sich auf der Club-CD. Unser Programm können wir damit bereits eingeben, und alle Debug-Neulinge sollten dies jetzt einmal versuchen. Zur Orientierung diene dabei das folgende Bildschirmprotokoll:
Zeile 90: Zeile 94:
 Der klassische Weg vom Quelltext zum Code führt über die sogenannte Objektdatei. Sie wird vom Assembler aus dem Quelltext erzeugt und enthält einen Mix aus Symbolen und Code. Erst durch das anschließende Linken gelangt man zum ausführbaren Programm. Dieses Verfahren ist zwar vielseitiger (man kann z.B. mehrere Objektdateien zusammenlinken), für den Einsteiger allerdings erst einmal umständlicher als die direkte Methode von A86. Damit aber nicht genug: Der Quelltext bedarf noch ein paar Ergänzungen: Der klassische Weg vom Quelltext zum Code führt über die sogenannte Objektdatei. Sie wird vom Assembler aus dem Quelltext erzeugt und enthält einen Mix aus Symbolen und Code. Erst durch das anschließende Linken gelangt man zum ausführbaren Programm. Dieses Verfahren ist zwar vielseitiger (man kann z.B. mehrere Objektdateien zusammenlinken), für den Einsteiger allerdings erst einmal umständlicher als die direkte Methode von A86. Damit aber nicht genug: Der Quelltext bedarf noch ein paar Ergänzungen:
  
-    .model tiny  +<code asm> 
-    .code  +.model tiny  
-    ORG 100h  +.code  
-    Start:  mov dx,8020h  ; Portadresse  +ORG 100h  
-            xor ax,ax     ; Zähler auf 0  +Start:  mov dx,8020h  ; Portadresse  
-    Marke:  out dx,al     ; Ton erzeugen  +        xor ax,ax     ; Zähler auf 0  
-            dec ax        ; Zähler -1  +Marke:  out dx,al     ; Ton erzeugen  
-            jnz Marke     ; Schleife  +        dec ax        ; Zähler -1  
-            mov ax,4c80h  +        jnz Marke     ; Schleife  
-            out dx,al     ; Ton aus  +        mov ax,4c80h  
-            int 21h       ; beenden  +        out dx,al     ; Ton aus  
-    END Start+        int 21h       ; beenden  
 +END Start 
 +</code>
  
 Die hinzugekommenen Assemblerdirektiven machen Angaben über das Speichermodell, den Beginn des Codesegments und die Einsprungadresse. Die hier verwendeten Minimaleinstellungen dürften für jedes Portfolioprogramm ausreichend sein und können jedesmal so übernommen werden. Hat man TON.ASM wie oben ergänzt, kann die Assemblierung folgen (hier auf dem Desktop-PC): Die hinzugekommenen Assemblerdirektiven machen Angaben über das Speichermodell, den Beginn des Codesegments und die Einsprungadresse. Die hier verwendeten Minimaleinstellungen dürften für jedes Portfolioprogramm ausreichend sein und können jedesmal so übernommen werden. Hat man TON.ASM wie oben ergänzt, kann die Assemblierung folgen (hier auf dem Desktop-PC):
Zeile 127: Zeile 133:
 Das umständlichste - pardon professionellste - Werkzeug stammt, wie sollte es anders sein, aus dem Hause Microsoft. Die hier verwendete MASM-Version 1.25 ist allerdings schon leicht in die Jahre gekommen, so daß das Beispiel eventuell nicht mit aktuelleren Versionen nachvollziehbar ist. Trotzdem hier der angepaßte Quelltext: Das umständlichste - pardon professionellste - Werkzeug stammt, wie sollte es anders sein, aus dem Hause Microsoft. Die hier verwendete MASM-Version 1.25 ist allerdings schon leicht in die Jahre gekommen, so daß das Beispiel eventuell nicht mit aktuelleren Versionen nachvollziehbar ist. Trotzdem hier der angepaßte Quelltext:
  
-    code    SEGMENT  +<code asm> 
-            ASSUME CS:code +code    SEGMENT  
 +        ASSUME CS:code 
             ORG 100h              ORG 100h 
-    Start:  mov dx,8020h  ; Portadresse  +Start:  mov dx,8020h  ; Portadresse  
-            xor ax,ax     ; Zähler auf 0  +        xor ax,ax     ; Zähler auf 0  
-    Marke:  out dx,al     ; Ton erzeugen  +Marke:  out dx,al     ; Ton erzeugen  
-            dec ax        ; Zähler -1  +        dec ax        ; Zähler -1  
-            jnz Marke     ; Schleife  +        jnz Marke     ; Schleife  
-            mov ax,4c80h  +        mov ax,4c80h  
-            out dx,al     ; Ton aus  +        out dx,al     ; Ton aus  
-            int 21h       ; beenden  +        int 21h       ; beenden  
-    code    ENDS  +code    ENDS  
-    END Start +END Start  
 +</code>
  
 Bis zum Endprodukt sind jetzt drei Schritte erforderlich. Zunächst wie beim Borland-Produkt die Assemblierung und das Linken. LINK erzeugt allerdings eine ausführbare Datei im .EXE-Format (783 Bytes), die danach noch ins COM-Format konvertiert werden muß: Bis zum Endprodukt sind jetzt drei Schritte erforderlich. Zunächst wie beim Borland-Produkt die Assemblierung und das Linken. LINK erzeugt allerdings eine ausführbare Datei im .EXE-Format (783 Bytes), die danach noch ins COM-Format konvertiert werden muß:
Zeile 177: Zeile 185:
 Mit der Anweisung ASM lassen sich Assemblerbefehle auch in ein Pascalprogramm einbauen. Dabei ist zu beachten, daß Labels mit dem Klammeraffen beginnen müssen und Kommentare in geschweifte Klammern gestellt werden müssen:  Mit der Anweisung ASM lassen sich Assemblerbefehle auch in ein Pascalprogramm einbauen. Dabei ist zu beachten, daß Labels mit dem Klammeraffen beginnen müssen und Kommentare in geschweifte Klammern gestellt werden müssen: 
  
-    begin  +<code asm> 
-      ASM  +begin  
-            mov dx,8020h {Portadresse  }  +  ASM  
-            xor ax,ax    {Zähler auf 0 }  +        mov dx,8020h {Portadresse  }  
-    @Marke: out dx,al    {Ton erzeugen }  +        xor ax,ax    {Zähler auf 0 }  
-            dec ax       {Zähler -1    }  +@Marke: out dx,al    {Ton erzeugen }  
-            jnz @Marke   {Schleife     }  +        dec ax       {Zähler -1    }  
-            mov ax,4c80h  +        jnz @Marke   {Schleife     }  
-            out dx,al    {Ton aus      }  +        mov ax,4c80h  
-            int 21h      {beenden      }  +        out dx,al    {Ton aus      }  
-      end;  +        int 21h      {beenden      }  
-    end.+  end;  
 +end. 
 +</code>
  
 Kompiliert mit TP 6.0 ergibt dieser Quelltext ein 1488 Bytes langes .EXE-Programm. Da der Befehl int 21h das Programm vorzeitig beendet, sollte man diese Zeile besser weglassen. Es ist aber auch möglich (z.B. mit DEBUG), unsere 15 Bytes aus dem .EXE Programm herauszuschneiden und als .COM-Programm zu speichern. Der betreffende Abschnitt reicht übrigens vom 16. bis zum 30. der 1488 Bytes.  Kompiliert mit TP 6.0 ergibt dieser Quelltext ein 1488 Bytes langes .EXE-Programm. Da der Befehl int 21h das Programm vorzeitig beendet, sollte man diese Zeile besser weglassen. Es ist aber auch möglich (z.B. mit DEBUG), unsere 15 Bytes aus dem .EXE Programm herauszuschneiden und als .COM-Programm zu speichern. Der betreffende Abschnitt reicht übrigens vom 16. bis zum 30. der 1488 Bytes. 
Zeile 212: Zeile 222:
 Diese Register sind am vielseitigsten einsetzbar. Die oberen bzw. unteren 8 Bits lassen sich auch getrennt verwenden, d.h. es stehen hiermit bis zu acht 8-Bit-Register zur Verfügung. Für die ersten Assembler-Gehversuche reichen die Allzweckregister bereits. Diese Register sind am vielseitigsten einsetzbar. Die oberen bzw. unteren 8 Bits lassen sich auch getrennt verwenden, d.h. es stehen hiermit bis zu acht 8-Bit-Register zur Verfügung. Für die ersten Assembler-Gehversuche reichen die Allzweckregister bereits.
  
-    AX = AH|AL Akkumulator        MUL, DIV  +^    Register   ^ Name/Funktion   ^ Typische Operation ^ 
-    BX = BH|BL Basisregister      XLAT  +|    AX = AH+AL Akkumulator     |  MUL, DIV          | 
-    CX = CH|CL Count-Register     LOOP  +   BX = BH+BL Basisregister   |  XLAT              | 
-    DX = DH|DL Daten-Register     IN, OUT+   CX = CH+CL Count-Register  |  LOOP              | 
 +   DX = DH+DL Daten-Register  |  IN, OUT           |
  
 __2. Die Index- und Pointerregister__ __2. Die Index- und Pointerregister__
Zeile 221: Zeile 232:
 Abgesehen von SP und IP dürfen auch diese Register nach Belieben verwendet werden. Insbesondere eignen sie sich aber zur indirekten Adressierung und für die sog. Stringbefehle. Abgesehen von SP und IP dürfen auch diese Register nach Belieben verwendet werden. Insbesondere eignen sie sich aber zur indirekten Adressierung und für die sog. Stringbefehle.
  
-    IP Instruction Pointer (Finger weg!)  +^ Register ^ Name/Funktion                     ^ Typische Operation 
-    SP Stackpointer (Finger weg!) PUSH, POP  +|    IP    Instruction Pointer (Finger weg!) |                     | 
-    BP Basepointer  +   SP    Stackpointer (Finger weg!)        PUSH, POP           | 
-    SI Sourceindex                LODSB  +   BP    Basepointer                       |                     | 
-    DI Dest.index                 STOSB +   SI    Sourceindex                       | LODSB               | 
 +   DI    | Destination index                 STOSB               |
  
  
Zeile 232: Zeile 244:
 Die Segmentregister haben eine spezielle Funktion, die später noch genauer erklärt wird. Nur ES steht zur freien Verfügung. Die Segmentregister haben eine spezielle Funktion, die später noch genauer erklärt wird. Nur ES steht zur freien Verfügung.
  
-    CS Codesegment (Finger weg!)  +^ Register ^ Name/Funktion             ^ Typische Operation                              ^ 
-    DS Datensegment (Vorsicht!)  +|    CS    Codesegment (Finger weg!) |                                                 | 
-    SS Stacksegment (Vorsicht!)  +   DS    Datensegment (Vorsicht!)  | Ändern des Datensegments: MOV DS,AX             | 
-    ES Extrasegment               STOSB+   SS    Stacksegment (Vorsicht!)  | Adressierung mit Segment-Präfix: MOV AL,[SS:BX] | 
 +   ES    Extrasegment              STOSB                                           |
  
 __4. Das Flag-Register__ __4. Das Flag-Register__
Zeile 246: Zeile 259:
  
 Wie die Namen der Segmentregister vermuten lassen, ist es vorgesehen, für Programmcode, Daten und den Stack (Zwischenspeicher für Unterprogramme) jeweils ein eigenes Segment zu spendieren. Für uns spielt das aber keine Rolle, weil das Betriebssystem bei COM-Programmen die drei Segmentregister CS, DS und SS mit dem selben Wert initialisiert. Es besteht also zum Glück kaum die Chance, einen Befehl zu verwenden, der sich nicht auf das gewünschte Segment bezieht. Allerdings muß sich ein COM-Programm deshalb mit höchstens 64 KByte begnügen, was aber für die meisten Portfolio-Programme mehr als ausreichend ist. Zudem sind compilierte Assemblerprogramme so kompakt, daß ihre Größe in der Regel nur etwa 1/10 von der des Quelltexts beträgt. Wie die Namen der Segmentregister vermuten lassen, ist es vorgesehen, für Programmcode, Daten und den Stack (Zwischenspeicher für Unterprogramme) jeweils ein eigenes Segment zu spendieren. Für uns spielt das aber keine Rolle, weil das Betriebssystem bei COM-Programmen die drei Segmentregister CS, DS und SS mit dem selben Wert initialisiert. Es besteht also zum Glück kaum die Chance, einen Befehl zu verwenden, der sich nicht auf das gewünschte Segment bezieht. Allerdings muß sich ein COM-Programm deshalb mit höchstens 64 KByte begnügen, was aber für die meisten Portfolio-Programme mehr als ausreichend ist. Zudem sind compilierte Assemblerprogramme so kompakt, daß ihre Größe in der Regel nur etwa 1/10 von der des Quelltexts beträgt.
 +
  
 ==== Eine handvoll Befehle ==== ==== Eine handvoll Befehle ====
Zeile 255: Zeile 269:
 Der vielseitigste und meist genutzte Assemblerbefehl ist sicherlich der MOV-Befehl (move). Er überträgt ein Datenwort z.B. aus dem Speicher in ein Register oder aus einem Register in ein anderes. Zudem unterstützt dieser Befehl verschiedene Adressierungsarten für Speicherzugriffe. Hierzu eine Auswahl an Beispielen: Der vielseitigste und meist genutzte Assemblerbefehl ist sicherlich der MOV-Befehl (move). Er überträgt ein Datenwort z.B. aus dem Speicher in ein Register oder aus einem Register in ein anderes. Zudem unterstützt dieser Befehl verschiedene Adressierungsarten für Speicherzugriffe. Hierzu eine Auswahl an Beispielen:
  
 +
 +<code asm>
     mov al,2      ; Lädt das Register al mit dem Wert 2      mov al,2      ; Lädt das Register al mit dem Wert 2 
     mov al,[2]    ; Lädt al mit dem Byte-Wert, der an der      mov al,[2]    ; Lädt al mit dem Byte-Wert, der an der 
Zeile 261: Zeile 277:
     mov al,[bx]   ; Lädt al mit dem Byte-Wert, der an der Speicherstelle      mov al,[bx]   ; Lädt al mit dem Byte-Wert, der an der Speicherstelle 
                   ; bx im Datensegment steht (indirekte Adressierung)                    ; bx im Datensegment steht (indirekte Adressierung) 
-    mov cl,[si+3] ; Lädt al mit dem Byte-Wert, der an der Speicherstelle +    mov cl,[si+3] ; Lädt cl mit dem Byte-Wert, der an der Speicherstelle 
                   ; si+3 im Datensegment steht (indirekte indizierte Adr.)                    ; si+3 im Datensegment steht (indirekte indizierte Adr.) 
     mov [7],dh    ; Speichert dh an der Speicherstelle 7 im Datensegment      mov [7],dh    ; Speichert dh an der Speicherstelle 7 im Datensegment 
 +</code>
  
 Diese Beispiele wickeln den Datenaustausch mit dem Speicher nur über die 8-Bit-"Halbregister" ab, wodurch genau 1 Byte im Speicher angesprochen wird. Es sind aber auch 16-Bit-Speicherzugriffe möglich. Dabei ist es oft hilfreich zu wissen, daß gemäß Intel-Konvention an der angegebenen Speicherstelle das niederwertige Byte (Lowbyte) und an der darauffolgenden Speicherstelle das höherwertige Byte (Highbyte) angesiedelt ist.  Diese Beispiele wickeln den Datenaustausch mit dem Speicher nur über die 8-Bit-"Halbregister" ab, wodurch genau 1 Byte im Speicher angesprochen wird. Es sind aber auch 16-Bit-Speicherzugriffe möglich. Dabei ist es oft hilfreich zu wissen, daß gemäß Intel-Konvention an der angegebenen Speicherstelle das niederwertige Byte (Lowbyte) und an der darauffolgenden Speicherstelle das höherwertige Byte (Highbyte) angesiedelt ist. 
 Wer sich mit DEBUG einmal den Maschinencode für obige Varianten des MOV-Befehls ansieht, kann feststellen, daß wir es hier eigentlich mit verschiedenen Maschinenbefehlen zu tun haben: Wer sich mit DEBUG einmal den Maschinencode für obige Varianten des MOV-Befehls ansieht, kann feststellen, daß wir es hier eigentlich mit verschiedenen Maschinenbefehlen zu tun haben:
  
 +<code asm>
     B002      MOV   AL,02      B002      MOV   AL,02 
     A00200    MOV   AL,[0002]      A00200    MOV   AL,[0002] 
Zeile 274: Zeile 292:
     8A4C03    MOV   CL,[SI+03]      8A4C03    MOV   CL,[SI+03] 
     88360700  MOV   [0007],DH      88360700  MOV   [0007],DH 
 +</code>
  
 Besonders effizient codiert wurden die ersten beiden Zeilen: Die Befehlscodes (OP-Codes) bestehen dort aus einem einzigen Byte (B0 bzw. A0), gefolgt von einem bzw. zwei Bytes für den zweiten Parameter. Man sieht auch, daß die 16-Bit-Konstante 0002 im Format Lowbyte/Highbyte gespeichert ist. Die anderen vier - etwas weniger gebräuchlichen - Varianten benötigen jeweils 2 Bytes für den OP-Code und bis zu zwei weitere Bytes für Konstanten. Besonders effizient codiert wurden die ersten beiden Zeilen: Die Befehlscodes (OP-Codes) bestehen dort aus einem einzigen Byte (B0 bzw. A0), gefolgt von einem bzw. zwei Bytes für den zweiten Parameter. Man sieht auch, daß die 16-Bit-Konstante 0002 im Format Lowbyte/Highbyte gespeichert ist. Die anderen vier - etwas weniger gebräuchlichen - Varianten benötigen jeweils 2 Bytes für den OP-Code und bis zu zwei weitere Bytes für Konstanten.
Zeile 300: Zeile 319:
 Dieser Befehl ruft die durch Nummer spezifizierte Interruptroutine auf. Der INT-Befehl stellt die Schnittstelle zum Betriebssystem dar, denn sowohl BIOS als auch DOS belegen etliche Interruptvektoren mit Routinen, die von Anwendungsprogrammen genutzt werden können (und sollen). Je nach Funktion müssen zuvor diverse Parameter in bestimmten Registern abgelegt werden. Beispiel: Dieser Befehl ruft die durch Nummer spezifizierte Interruptroutine auf. Der INT-Befehl stellt die Schnittstelle zum Betriebssystem dar, denn sowohl BIOS als auch DOS belegen etliche Interruptvektoren mit Routinen, die von Anwendungsprogrammen genutzt werden können (und sollen). Je nach Funktion müssen zuvor diverse Parameter in bestimmten Registern abgelegt werden. Beispiel:
  
 +<code asm>
     mov ah,7   ; 7=Zeicheneingabe nach al      mov ah,7   ; 7=Zeicheneingabe nach al 
     int 21h    ; 21=wichtigster DOS-Interrupt      int 21h    ; 21=wichtigster DOS-Interrupt 
Zeile 305: Zeile 325:
     mov ah,6   ; 6=Zeichenausgabe (dl=ASCII)      mov ah,6   ; 6=Zeichenausgabe (dl=ASCII) 
     int 21h     int 21h
 +</code>
  
 Übrigens lautet der Maschinencode für Int 21h 'CD 21' (dezimal 205, 33) und dürfte eine der häufigsten Bytekombinationen in DOS-Programmen darstellen. Übrigens lautet der Maschinencode für Int 21h 'CD 21' (dezimal 205, 33) und dürfte eine der häufigsten Bytekombinationen in DOS-Programmen darstellen.
Zeile 312: Zeile 333:
 Eine wichtige Eigenart der 80x86-Prozessorfamilie sind die Portadressen. Parallel zum Arbeitsspeicher existiert ein 64 KByte großer Adreßraum, über den Peripheriebausteine (z.B. paralleles Interface) angesprochen werden. Der Prozessorbefehl IN liest einen 8-Bit- oder 16-Bit-Wert von der Portadresse dx ein. Folgendes Beispiel übernimmt den Zustand der Statusleitungen des Druckerports in das Register al: Eine wichtige Eigenart der 80x86-Prozessorfamilie sind die Portadressen. Parallel zum Arbeitsspeicher existiert ein 64 KByte großer Adreßraum, über den Peripheriebausteine (z.B. paralleles Interface) angesprochen werden. Der Prozessorbefehl IN liest einen 8-Bit- oder 16-Bit-Wert von der Portadresse dx ein. Folgendes Beispiel übernimmt den Zustand der Statusleitungen des Druckerports in das Register al:
  
 +<code asm>
     mov dx,807Ah      mov dx,807Ah 
     in al,dx ; nur dx möglich!     in al,dx ; nur dx möglich!
 +</code>
  
 **OUT dx,Register** **OUT dx,Register**
Zeile 324: Zeile 347:
 Addiert zum Operand1 den Operand2. Falls das Ergebnis zu groß für Operand1 ist, wird das Carry-Flag gesetzt (Abfrage z.B. über den bedingten Sprungbefehl JC - jump if carry), welches somit das im Ergebnis fehlende höchste Bit repräsentiert. Beispiel: Addiert zum Operand1 den Operand2. Falls das Ergebnis zu groß für Operand1 ist, wird das Carry-Flag gesetzt (Abfrage z.B. über den bedingten Sprungbefehl JC - jump if carry), welches somit das im Ergebnis fehlende höchste Bit repräsentiert. Beispiel:
  
 +<code asm>
     mov cx,1997   ; cx=1997      mov cx,1997   ; cx=1997 
     add cx,3      ; cx=2000     add cx,3      ; cx=2000
 +</code>
  
 **SUB Operand1,Operand2** **SUB Operand1,Operand2**
Zeile 331: Zeile 356:
 Subtraktion analog zu ADD. Beispiel: Subtraktion analog zu ADD. Beispiel:
  
 +<code asm>
     mov bx,0605h  ; bh=6, bl=5      mov bx,0605h  ; bh=6, bl=5 
     sub bl,bh     ; bl=FFh     sub bl,bh     ; bl=FFh
 +</code>
  
 Hier wird das Carry-Flag gesetzt, was bedeutet, daß FFh als -1 zu interpretieren ist. Hier wird das Carry-Flag gesetzt, was bedeutet, daß FFh als -1 zu interpretieren ist.
Zeile 340: Zeile 367:
 Der pure Luxus: Strukturierte Programmierung in Maschinensprache durch Unterprogramme!  CALL springt an den Anfang des Unterprogramms und sichert automatisch die Rücksprungadresse auf dem Stack. RET beendet das Unterprogramm, indem es die Rücksprungadresse wieder vom Stack holt, um das Programm mit dem Befehl hinter 'CALL' fortzusetzen. Ein anständiges Unterprogramm sichert zunächst alle verwendeten Register mit PUSH auf dem Stack und stellt sie am Ende mit POP wieder her (Diese Befehle sind allerdings - wie auch CALL und RET - mit etwa 20 Takten recht langsam). Beispiel: Der pure Luxus: Strukturierte Programmierung in Maschinensprache durch Unterprogramme!  CALL springt an den Anfang des Unterprogramms und sichert automatisch die Rücksprungadresse auf dem Stack. RET beendet das Unterprogramm, indem es die Rücksprungadresse wieder vom Stack holt, um das Programm mit dem Befehl hinter 'CALL' fortzusetzen. Ein anständiges Unterprogramm sichert zunächst alle verwendeten Register mit PUSH auf dem Stack und stellt sie am Ende mit POP wieder her (Diese Befehle sind allerdings - wie auch CALL und RET - mit etwa 20 Takten recht langsam). Beispiel:
  
 +<code asm>
              ...               ... 
              call PortAus               call PortAus 
Zeile 351: Zeile 379:
              pop dx       ; Reihenfolge               pop dx       ; Reihenfolge 
              ret              ret
 +</code>
  
 **Weitere Prozessorbefehle** **Weitere Prozessorbefehle**
Zeile 356: Zeile 385:
 Aus Platzgründen kommen hier leider viele nützliche Befehle zu kurz. Wenigstens nicht unerwähnt bleiben sollten diese Instruktionen: Aus Platzgründen kommen hier leider viele nützliche Befehle zu kurz. Wenigstens nicht unerwähnt bleiben sollten diese Instruktionen:
  
-|  AND, OR, XOR    | bitweise Verknüpfungen          +^  Mnemonic        ^ Funktion                          ^ 
-|  INC, DEC        | Operand um 1 erhöhen/vermindern | +|  AND, OR, XOR    | Bitweise Verknüpfungen            
-|  ADC, SBC        | Addition/Subtr. mit Übertrag    +|  INC, DEC        | Operand um 1 erhöhen/vermindern   
-|  LOOP            | Schleife mit cx als Zähler      +|  ADC, SBC        | Addition/Subtraktion mit Übertrag | 
-|  SHL,SHR ROL,ROR | Schieben und Rotieren           +|  LOOP            | Schleife mit cx als Zähler        
-|  LODSB, STOSB    | Stringbefehle                   |+|  SHL,SHR ROL,ROR | Bitweises Schieben und Rotieren   
 +|  LODSB, STOSB    | Stringbefehle (Laden/Speichern von Bytes mit automatischer Indexerhöhung |
  
 ==== Ein Wort zur Ausführungsgeschwindigkeit ==== ==== Ein Wort zur Ausführungsgeschwindigkeit ====
Zeile 428: Zeile 458:
     Summe: DW ? ; wird 900     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:+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 übersichtlicher darstellen läßt:
  
           mov ax,Zahl1            mov ax,Zahl1 
Zeile 440: Zeile 470:
 ==== Auf Nummer sicher ==== ==== 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.+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 "Speicherfllstand" 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.+Eine einfache Möglichkeit, den "Speicherfüllstand" 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: 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:
Zeile 500: Zeile 530:
 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: 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:
  
 +^ Adresse ^ Funktion                              ^
 | 8000 | Keyboard-Scancode (>128 => losgelassen)  | | 8000 | Keyboard-Scancode (>128 => losgelassen)  |
 | 8010 | LCD controller Datenregister             | | 8010 | LCD controller Datenregister             |
Zeile 544: Zeile 575:
 |      ;     51    |    N     59    | |      ;     51    |    N     59    |
 |      =     53    |    O     12    | |      =     53    |    O     12    |
-   links |   43    |    P     33    | + links   |   43    |    P     33    | 
-   rechts|   44    |    Q     10    | + rechts  |   44    |    Q     10    | 
-   oben    29    |    R     20    | +|  oben    |   29    |    R     20    | 
-   unten |   37    |    S     32    | + unten   |   37    |    S     32    | 
-   lShift|   27    |    T     21    | + lShift  |   27    |    T     21    | 
-   rShift|   36    |    U     11    | + rShift  |   36    |    U     11    | 
-    Fn   |   54    |    V     57    | +|   Fn     |   54    |    V     57    | 
-    Esc  |   63    |    W     17    | +  Esc    |   63    |    W     17    | 
-   Enter |   22    |    X     55    | + Enter   |   22    |    X     55    | 
-   Space |   50    |   Z/ |   23    | + Space   |   50    |   Z/ |   23    | 
-     Ä   |   30    |   Y/ |   49    | +    Ä    |   30    |   Y/ |   49    | 
-     Ü   |   28    |   Alt  |    9    | +    Ü    |   28    |   Alt  |    9    | 
-   \ / < |   48    |   Atari|    0    | +  \ / <  |   48    |  Atari |    0    | 
-   + / ] |   31    |    BS  |   14    | +  + / ]  |   31    |    BS  |   14    | 
-   Caps  |   45    |   Ctrl |   18    | +  Caps   |   45    |  Ctrl  |   18    | 
-    Del  |    8    |                 ||+  Del    |    8    |                 ||
  
 Zur Verdeutlichung wieder ein konstruierter Programmausschnitt, in dem die Cursortasten zur Steuerung eines Ordinatenwerts dienen: Zur Verdeutlichung wieder ein konstruierter Programmausschnitt, in dem die Cursortasten zur Steuerung eines Ordinatenwerts dienen:
Zeile 618: Zeile 649:
 ==== Verkehrte Welt ==== ==== Verkehrte Welt ====
  
-Völlig unverständlich, weil ineffizient, ist die Art, auf die jeweils 8 Pixel vom BIOS zu einem Byte zusammengefaßt werden. Obwohl wie gesagt die Organisation des Bildspeichers inkompatibel zu allen PC-Grafikmodi ist, ordneten die Entwickler in Anlehnung zum PC dem linken Pixel einer Achtergruppe das höchstwertige Bit im Byte zu. Dagegen wäre nichts einzuwenden, wenn nicht der LCD-Controller genau die umgekehrte Bitreihenfolge verwenden würde. Darunter leidet bereits die Performance der "Putpixel"-Funktion (0Ch) von Int 10h. Regelrecht bei der Arbeit zusehen kann man aber der "Refresh"-Funktion (12h) von Int 61h, die den Grafikspeicher zum LCD überträgt. Die Bitmuster der Achtergruppen werden dazu auf bemerkenswert langsame Weise gespiegelt. Es sind deshalb diverse alternative Routinen bekannt, die mit Leichtigkeit eine Beschleunigung um den Faktor 10 oder mehr erzielen (siehe [1] und [2]). Im Grunde kann man aber auch sehr gut ganz auf diese Art der Refresh-Funktion verzichten, indem man einen eigenen Bildspeicher anlegt, der eine identische Kopie des LCD-RAMs enthält. Dieser Bildspeicher könnte sich durchaus bei B800:0 befinden, einfacher ist es jedoch, direkt im Datensegment 1920 Bytes zu reservieren. Abbildung veranschaulicht hierzu die Organisation des LCD-Bildspeichers.+Völlig unverständlich, weil ineffizient, ist die Art, auf die jeweils 8 Pixel vom BIOS zu einem Byte zusammengefaßt werden. Obwohl wie gesagt die Organisation des Bildspeichers inkompatibel zu allen PC-Grafikmodi ist, ordneten die Entwickler in Anlehnung zum PC dem linken Pixel einer Achtergruppe das höchstwertige Bit im Byte zu. Dagegen wäre nichts einzuwenden, wenn nicht der LCD-Controller genau die umgekehrte Bitreihenfolge verwenden würde. Darunter leidet bereits die Performance der "Putpixel"-Funktion (0Ch) von Int 10h. Regelrecht bei der Arbeit zusehen kann man aber der "Refresh"-Funktion (12h) von Int 61h, die den Grafikspeicher zum LCD überträgt. Die Bitmuster der Achtergruppen werden dazu auf bemerkenswert langsame Weise gespiegelt. Es sind deshalb diverse alternative Routinen bekannt, die mit Leichtigkeit eine Beschleunigung um den Faktor 10 oder mehr erzielen (siehe [1] und [2]). Im Grunde kann man aber auch sehr gut ganz auf diese Art der Refresh-Funktion verzichten, indem man einen eigenen Bildspeicher anlegt, der eine identische Kopie des LCD-RAMs enthält. Dieser Bildspeicher könnte sich durchaus bei B800:0 befinden, einfacher ist es jedoch, direkt im Datensegment 1920 Bytes zu reservieren. Die folgende Abbildung veranschaulicht hierzu die Organisation des LCD-Bildspeichers:
  
-{{software:diy:assembler:vidram.gif}}+{{software:diy:assembler:vidram.png}}
  
 Wenn das komplette Bild am Stück berechnet werden kann und nicht wieder ausgelesen werden muß, kann man davon absehen, überhaupt ein Abbild der Grafik im Arbeitsspeicher zu erzeugen und stattdessen die Bilddaten direkt Byte für Byte dem LCD-Controller übergeben. Wenn die Bilddaten noch nicht komplett im Speicher vorbereitet wurden, kann zudem die ohnehin nötige Wartezeit (ca. 25 Taktzyklen beim Standard-Pofo, getunt entsprechend mehr, siehe auch [6]) zwischen den Zugriffen auf den LCD-Controller sinnvoll genutzt werden. Dieser schnellen Variante habe ich in dem Spiel "FoliDash" den Vorzug gegeben. Wenn das komplette Bild am Stück berechnet werden kann und nicht wieder ausgelesen werden muß, kann man davon absehen, überhaupt ein Abbild der Grafik im Arbeitsspeicher zu erzeugen und stattdessen die Bilddaten direkt Byte für Byte dem LCD-Controller übergeben. Wenn die Bilddaten noch nicht komplett im Speicher vorbereitet wurden, kann zudem die ohnehin nötige Wartezeit (ca. 25 Taktzyklen beim Standard-Pofo, getunt entsprechend mehr, siehe auch [6]) zwischen den Zugriffen auf den LCD-Controller sinnvoll genutzt werden. Dieser schnellen Variante habe ich in dem Spiel "FoliDash" den Vorzug gegeben.
Zeile 628: Zeile 659:
 Die Kommunikation mit dem bereits mehrfach angesprochenen LCD-Controller des Portfolio erfolgt über nur zwei Portadressen:  Die Kommunikation mit dem bereits mehrfach angesprochenen LCD-Controller des Portfolio erfolgt über nur zwei Portadressen: 
  
-    8010h = Datenregister  +^ Adresse (hex) ^ Funktion          ^ 
-    8011h = Befehlsregister +|      8010     Datenregister     | 
 +|      8011     Befehlsregister   |
  
 Ein Kommando wird übergeben, indem zuerst ein gültiger Kommandocode in das Befehlsregister und anschließend das zugehörige Argument ins Datenregister geschrieben wird. Für unsere Zwecke sind folgende Kommandos des HD61830 von Interesse: Ein Kommando wird übergeben, indem zuerst ein gültiger Kommandocode in das Befehlsregister und anschließend das zugehörige Argument ins Datenregister geschrieben wird. Für unsere Zwecke sind folgende Kommandos des HD61830 von Interesse:
  
-Ein Kommando wird übergeben, indem zuerst ein gültiger Kommandocode in das Befehlsregister und anschließend das zugehörige Argument ins Datenregister geschrieben wird. Für unsere Zwecke sind folgende Kommandos des HD61830 von Interesse: +Kommando ^ Funktion                              ^ 
- +    8 Bildspeicher-Startadresse Low festlegen  | 
-     Bildspeicher-Startadresse Low festlegen  +    9 Bildspeicher-Startadresse High festlegen | 
-     Bildspeicher-Startadresse High festlegen  +   10 Cursorposition Low festlegen             | 
-    10 Cursorposition Low festlegen  +   11 Cursorposition High festlegen            | 
-    11 Cursorposition High festlegen  +   12 Byte(s) zum LCD übertragen               | 
-    12 Byte(s) zum LCD übertragen  +   14 Bit setzen                               | 
-    14 Bit setzen  +   15 Bit löschen                              |
-    15 Bit löschen+
  
 Die weiteren Erläuterungen und Programmbeispiele setzen voraus, daß der LCD-Controller wie beschrieben in den Grafikmodus versetzt wurde. Die weiteren Erläuterungen und Programmbeispiele setzen voraus, daß der LCD-Controller wie beschrieben in den Grafikmodus versetzt wurde.
Zeile 736: Zeile 767:
 So viel zum Thema Grafik auf dem Portfolio. Weitere Informationen rund um den LCD-Controller finden sich in [3], [4] und [5]. Ganz im Sinne von Multimedia geht es im nächsten (bislang nicht erschienenen) Teil weiter mit Wissenswertem über die Tonerzeugung beim Portfolio. So viel zum Thema Grafik auf dem Portfolio. Weitere Informationen rund um den LCD-Controller finden sich in [3], [4] und [5]. Ganz im Sinne von Multimedia geht es im nächsten (bislang nicht erschienenen) Teil weiter mit Wissenswertem über die Tonerzeugung beim Portfolio.
  
-Referenzen+ 
 +---- 
 + 
 + 
 +**Autor:** Klaus Peichl, http://leute.server.de/peichl/main.htm 
 + 
 +**Referenzen**
  
 [1]    Prozedur "grefresh" der Pascal-Unit auf Gunnar Thöles WWW-Seite [1]    Prozedur "grefresh" der Pascal-Unit auf Gunnar Thöles WWW-Seite
software/diy/assembler/kkurs.1135202086.txt.gz · Zuletzt geändert: 16/02/2024 17:02 (Externe Bearbeitung)