ASSEMBLER

Dr. Erhard Henkes,  Stand: 03.07.2007



1. Einstieg in die Assembler-Programmierung am PC

Programmiert man möglichst nahe an der „Maschine“, so versteht man die Funktion eines Betriebssystems und den Ablauf eines Programms besser als beim Einsatz einer Hochsprache. Der Einstieg in diese Materie erfordert jedoch Geduld und Übung. Wir beginnen Schritt für Schritt. Zunächst beschaffen wir einen Assembler.

 

Was ist das eigentlich genau? Ein Assembler ist ein Programm, das Befehlsfolgen von Assemblersprache in Maschinensprache übersetzt. Ein Programm, das in der Assemblersprache (kurz: Assembler) geschrieben wurde, wird analog einem Hochsprachenprogramm einfach als Text gespeichert und ist für die Entwickler gut lesbar. Bekannte Assembler sind Microsoft’s Assembler (MASM) oder Borland’s Assembler (TASM). Wir verwenden in diesem Tutorial zunächst den Netwide Assembler (NASM), da man ihn einfach und kostenfrei erhält. Programmbeispiele dieses Assemblers sind verstärkt anzutreffen. Die Syntax ist klar und gut verständlich.

 

Will man z.B. den Wert 0x41 in das Register dl (Low Byte des Registers dx) überführen, sieht die Anweisung wie folgt aus:

 

Maschinensprache:       B2 41 (hexadezimal) bzw. 10110010 01000001 (binär)

Assembler:                   mov dl,0x41

 

In Assembler ist die Bedeutung der Anweisung für Menschen selbstverständlich besser verständlich als in binärem Maschinencode. Die Anweisung „mov“ ist ein sogenanntes Mnemonic für den Transferbefehl (move). Ein Assembler liest ein Source-Programm und wandelt die Mnemonics in Maschinensprache um. Bei Hochsprachen wird dieser Vorgang durch einen sogenannten Compiler durchgeführt. Ein Assembler funktioniert jedoch viel einfacher als ein Compiler, denn jedes Mnemonic entspricht direkt einem Befehl in Maschinensprache.


Prozessoren gibt es in PCs (z.B. Intel, AMD), aber auch in Microcontroller (z.B. Atmel, Pic). Jeder Prozessortyp verfügt über einen spezifischen Befehlsvorrat, so dass die Maschinensprache sich in Abhängigkeit von der eingesetzten CPU stark unterscheidet. Hier sind Hochsprachen bezüglich der Portabilität im Vorteil. Aus diesem Grund wird Assemblercode häufig auch in Kombination mit einer Hochsprache eingesetzt. Es lohnt sich auf jeden Fall, sich mit den nur durch die Maschine selbst begrenzten Möglichkeiten der „Maschinensprache“ zu beschäftigen. Die geistige und praktische Wanderung entlang der Entwicklungsgeschichte der PC’s auf Basis der Intel 80x86-Familie stellt einen interessanten historischen Lehrpfad von 1978 bis heute dar. Sehr interessant ist auch die Programmierung von  Microcontroller in Assembler.

 

Nun wenden wir uns der Praxis am PC zu. Ich gehe davon aus, dass Sie ein PC-Betriebssystem wie MS DOS oder MS Windows mit einer Konsole („DOS Box“) für unsere Experimente zur Verfügung haben.

 

Besorgen Sie sich bitte auf diesen Seiten die Programme und die Dokumentation, die wir für NASM benötigen:

Übersicht über NASM Downloads: http://sourceforge.net/project/showfiles.php?group_id=6208

Download der Version NASM 0.98.36 (MS DOS, 16 bit): http://prdownloads.sourceforge.net/nasm/nsm09836.zip?download

 

Sie sollten nun über die Programme nasm.exe und ndisasm.exe verfügen, die wir für erste Gehversuche im 16-bit-DOS einsetzen. Als Editor für unseren Sourcecode verwenden wir z.B. unter MS Windows den Texteditor notepad.exe (im Zubehör). Erstellen Sie folgenden Sourcecode und speichern Sie die Textdatei unter bsp1.asm (die Endung txt bitte entfernen).

 

 

Nehmen wir an, Sie haben die Dateien nasm.exe, ndisasm.exe und unser kleines Beispiel im gemeinsamen Unterverzeichnis C:\ASS\NASM\Beispiele abgelegt, dann erfolgt das Assemblieren wie folgt:


nasm sourcecode

nasm sourcecode -o ausführbare_datei

 

 

Als Resultat erhält man auf diese Weise die ausführbare Datei bsp1.com, die nach dem Starten den Text „Hello, World!“ am Bildschirm ausgibt. Der klassische Einstieg! Man kann auch einfach „nasm bsp1.asm“ eingeben. Daraufhin erhält man ein File namens „bsp1“, das man zur Ausführung zuerst nach „bsp1.com“ umbenennen muss. Dieses Umbennen erledigt die Anweisung „-o“ beim Assemblieren.

 

Die Anweisung org 0x100 ist an NASM gerichtet und bedeutet, dass das Programm ab der Speicheradresse 0x100 (dezimal: 256) geladen werden soll. Diesen Wert geben wir an, weil com-Files generell an diese Adresse geladen werden. Der erste Befehl ist mov (move), der eine Information von der Quelle zum Ziel transferiert. Die Anordnung der Parameter erfolgt gemäß der Intel-Syntax, also zuerst das Ziel, dann die Quelle.

 

Anschließend verwenden wir sogenannte interrupt service routines (ISR) des Betriebssystems MS DOS, die uns in der „DOS-Box“ zur Verfügung stehen. Diese Software-Interrupts funktionieren analog zu Hardware-Interrupts. Man muss diese nur selbst über die Anweisung INT … auslösen. MS-DOS ISR werden mit INT 0x21 ausgelöst. Einen Überblick erhält man hier:

 

http://spike.scu.edu.au/~barry/interrupts.html

 

Nachfolgend finden Sie die Beschreibung unserer beiden Beispiele:

 

AH = 09h : DISPLAY STRING

Display string of characters found in memory starting at the address given by [DS:DX] and ending with a "$" (an ASCII$ string);

the "$" character is not displayed.

 

AH = 4Ch : TERMINATE PROGRAM WITH ERRORLEVEL

Terminate program execution and return an "errorlevel" code equal to the value in the AL register; a value of 00h normally is used to indicate a normal (non-abort) program termination. For .COM-style programs (only) an alternate method of program termination is with a different interrupt, "int 20h"; note that this method only works properly with .COM programs and does not allow for a return code value.

 

Bei der ISR “DISPLAY STRING” übergeben wir die Adresse unseres Strings an das Daten-Register DX.

 

db bedeutet define byte.

 

message db "Hello, World!",0x0D,0x0A,"$"

 

message steht hier als Symbol für die Adresse unseres Textstrings, 0x0D,0x0A sind die ASCII-Werte der Kombination „carriage return / line feed“, und das $-Zeichen wird von unserer MS-DOS-Routine als Abschluß des Strings erwartet. 

 

Durch die direkte Zuordnung zwischen Maschinensprache und Assembler kann man den Prozess des Assemblierens auch umkehren. Das nennt man Disassemblieren. Unser diesbezügliches Werkzeug ist ndisasm.exe. Wir wenden dies zur Übung auf unser Programm bsp1.com an, und erhalten folgendes Bild:

 

 

Wir erkennen, das dies mit unseren Anweisungen gut klappt. Der Message-Bereich wird allerdings sinnfrei als „Befehlsfolge“ interpretiert. Man sieht, dass die Speicheradresse 0x10B in das Register DX übertragen wird. Das kommt daher, dass wir org 0x100 als Startadresse angegeben haben. Unser String beginnt ab der Offset-Speicherstelle 0x00B, zusammen ergibt dies 0x10B. Auf diese Weise wird die Adresse unserer Daten gefunden.

 

Ändern Sie versuchsweise die org-Anweisung ab auf org 0x101, dann erhalten Sie als Ausgabe „ello, World!“

Schauen Sie sich das anschließend genau mit der Anweisung „ndisasm bsp1.com“ an. Nun wird die Adresse 0x10c nach DX übertragen. Das ist erfolgt, da wir NASM angewiesen haben, unser Programm ab 0x101 zu beginnen. 0x101 und 0x00B ergibt 0x10C. Ein com-File wird aber nach 0x100 (und nicht nach 0x101) geladen. Damit zeigt unsere Adresse ein Zeichen zuweit nach hinten. Sie sehen, wie wichtig in diesem Fall die korrekte Angabe org 0x100 ist.

 

Merke:

ORG legt die “origin address” fest, an der NASM den Beginn des Programms erwartet, nachdem es in den Speicher geladen wurde.

 

 

Dieses Beispiel ist mit seinen 27 Byte bereits recht übersichtlich, aber wir wollen es weiter vereinfachen, damit die Zusammenhänge noch klarer werden:

 

 

Noch weniger Sourcecode! Erzeugen Sie das entsprechende com-File bitte mittels nasm bsp0.asm -o bsp0.com.

 

Informieren Sie sich über die verwendete DOS ISR, z.B. an folgender Stelle:

 

http://www.ctyme.com/intr/rb-2554.htm

 

Man überführt den Zeichencode in das Datenregister dl, gibt diesen aus und beendet das Programm.

 

 

Der ASCII-Zeichencode 0x41 (siehe: http://de.wikipedia.org/wiki/ASCII-Tabelle) wird bei der Ausgabe in das große A umgewandelt. Wenn wir unser Endprodukt bsp0.com wieder disassemblieren, finden wir unseren denkbar einfachen Code:

 

 

Schauen Sie sich das com-File an. Es ist 10 Byte groß. Sie kennen jedes dieser Bytes auf Maschinenebene mit Unterstützung des Disassemblers.

Dies sind die Bytes in kompakter Hexadezimal-Schreibweise: 

 

B2 41 B4 02 CD 21 B4 4C CD 21

 

In Binärform (Maschinensprache) sieht unser Programm damit wie folgt aus:

 

10110010 01000001

10110100 00000010

11001101 00100001

10110100 01001100

11001101 00100001

 

Klarer geht es nicht mehr. Damit sind die ersten Grundlagen im Umgang mit Assemblersprache und NASM gelegt. Erstellt man Programme z.B. wie hier für das Betriebssystem MS DOS, so muss man sich verdeutlichen, dass man hier mit völlig unterschiedlichen Bestandteilen arbeitet: Hardware, BIOS und Betriebssystem. Als Speichermodell verwendet man in diesem Fall zunächst den sogenannten Real Mode.

 

Ab dem Jahre 1978 vermarktete Fa. Intel den Intel-Prozessor 8086 der inzwischen legendären 80x86-Familie. Seine 16-Bit-Architektur war Ende der siebziger Jahre des letzten Jahrhunderts relativ fortschrittlich. Durch den 20-Bit-Adressbus konnte man mit Hilfe eines Tricks insgesamt 1 MB Speicher ansprechen. Hierbei geht es darum, diese 20 Bit mit16 Bit breiten Registern darzustellen.

 

Der Wert eines 16-Bit-Segmentregisters wurde hierbei um 4 Bit nach links geschoben (entspricht Multiplikation mit 16, hexadezimal wird einfach eine 0 angehängt) und dann ein 16-Bit-Offset eines Vielzweckregisters addiert. Diese Art der Speicheradressierung nennt man Real Mode. Aus Gründen der Kompatibilität wurde dies bis zum Pentium II erhalten.


Ab dem Intel 80286, der im IBM PC AT eingesetzt wurde, gab es dann zusätzlich den sogenannten Protected Virtual Adress Mode, kurz Protected Mode, der eine 32 Bit Adressierung mit 32-Bit-Registern bis zu 4 GB Speicher erlaubt.

 

Zunächst ist ein grundlegendes Verständnis der CPU, der Register und der Adressierung notwendig:

http://www.inf.hs-anhalt.de/Service/Assembler/Register8086.htm

 

Merke:

Akkumulator    AX  

Basisregister  BX   

Zählregister   CX  

Datenregister  DX   

 

Die geschichtliche Entwicklung der Intel 80x86-Familie sehen Sie hier im Überblick: http://www.bernd-leitenberger.de/computer-geschichte-artikel.shtml

 

Die Befehlsübersicht des Intel 8086 findet man an vielen Stellen, z.B. hier:

http://www.inf.hs-anhalt.de/Service/Assembler/Assemblerbefehle8086.htm

 

Bisher wurde die Ausgabe eines Textstrings und eines Zeichens realisiert. Wichtig ist auch die Eingabe von Zeichen über die Tastaur. Dafür stehen unter MS DOS natürlich ebenfalls Routinen zur Verfügung. Wir schauen zunächst bei INT 0x21:

 

AH = 01h : KEYBOARD INPUT AND DISPLAY

Wait for keyboard character; echo character on display; return ASCII code for character in AL. Terminate program if Ctrl-C or Ctrl-Break is typed.

 

Wir verwenden folgenden Sourcecode für unser Programm bsp2.asm:

 

;bsp2.asm
org 0x100
 
    mov dx,message
    mov ah,9
    int 0x21
 
keyboard:
    mov ah,1
    int 0x21
    jmp keyboard
 
    mov ah,0x4C
    int 0x21
 
   message db "Bitte Text eingeben:",0x0D,0x0A,"$"

  

 

Das funktioniert doch schon sehr gut. Mit dem unter MS DOS bekannten CTRL C kann man der Schleife entfliehen und das Programm beenden. Wenn wir die heute eher gebräuchliche ESC-Taste zusätzlich zum Abbruch zulassen wollen, benötigen wir eine Kontrollstruktur, in der wir das eingegebene Zeichen

mit dem ASCII-Wert der Escape-Taste vergleichen.

 

 

2. Kontrollstrukturen

 

2.1. if/then

 

In einer Hochsprache wie C / C++ ist eine if / then - Struktur einfach zu verstehen. Im Assembler-Stil sieht das leider etwas verworrener aus, aber das ist alles Gewöhnung (Pseudo-Code):

 

if (Bedingung) goto L2
 
L1: ... 
    ... ; Bedingung nicht erfüllt (false)
 
goto L3
 
L2: ... 
    ... ; Bedingung erfüllt (true)
 
L3: ... ; Hier geht es anschliessend weiter

 

Zunächst schreiben wir das obige Programm etwas um, damit wir auch andere ISR von MS DOS erproben können. Wir zerlegen die Tastenabfrage und die Darstellung am Bildschirm in zwei Schritte:

 

; bsp3.asm

org 0x100

 

    mov dx,message

    mov ah,9

    int 0x21

 

keyboard:

    mov ah,8 ; Zeichencode -> al

    int 0x21

    mov dl,al

   

    cmp al,0x1b ; ESC-Taste?

    je ende

 

    mov ah,2 ; dl -> Anzeige

    int 0x21

    jmp keyboard

 

ende:

    mov ah,0x4C

    int 0x21

 

    message db "Bitte Text eingeben:",0x0D,0x0A,"$"

 

 

Die entscheidende Stelle, an der wir in unserem Programm abfragen, ob die ESC-Taste gedrückt wurde, ist der folgende Zweizeiler:

 

    cmp al,0x1b ; ESC-Taste?

    je ende

 

JE bedeutet „jump if equal“. Das Programm springt zur angegebenen Zielmarke, wenn das Zero-Flag gesetzt ist. JE ist übrigens identisch mit JZ („jump if zero“).

Hier noch einmal eine Übersicht über die bedingten Sprungbefehle des 8086:

 

JA  / JB

Sprung nach Vergleich vorzeichenloser Zahlen (above / below)

JAE / JBE

(above or equal / below or equal)

JG  / JL

Sprung nach Vergleich von Zahlen mit Vorzeichen (greater / less)

JGE / JLE

(greater or equal / less or equal)

JE  / JNE 

Sprung in Abhängigkeit von ZF (equal / not equal)

JZ  / JNZ

Sprung in Abhängigkeit von ZF (zero /not zero)

JC  / JNC

Sprung in Abhängigkeit von CF

JO  / JNO

Sprung in Abhängigkeit von OF

JP  / JNP

Sprung in Abhängigkeit von PF

JS  / JNS

Sprung in Abhängigkeit von SF

JCXZ

Sprung, wenn CX gleich Null

 

 

 2.2. Warteschleife

 

Zunächst schreiben wir ein Programm, das den Zeichenvorrat unseres PC ausgibt. Dafür benötigen wir eine einfache Schleife. Das kennen Sie im Prinzip schon:

 

; bsp4.asm

org 0x100

 

L1:    mov dl,0x20

L2:    mov ah,0x02  ; bringt den Inhalt von dl zur Anzeige       

       int 0x21           

       inc dl       ; erhöht dl um 1 (increase)

       cmp dl,0xFF

       je L1

       jmp L2

 

Als Abkürzung für die anzuspringenden Labels verwenden wir hier L1, L2, ..., man findet auch M1, M2, ... Wir geben hier in bsp4.com sich wiederholend die Zeichen von ASCII-Code 0x20 bis 0xFF aus. Wenn Sie gerne akustisch verwöhnt werden wollen, können Sie die 0x20 durch 0x00 ersetzen. Viel Spass! Da Assembler schnell ist, benötigen wir für bestimmte Operationen eine einstellbare Warteschleife, wie z.B. hier bei der Bildschirmausgabe, denn wir wollen die einzelnen Zeichen wahrnehmen können. Wie wird dies in Assemblersprache realisiert?

 

; bsp5.asm

org 0x100

 

L1:    mov dl,0x00

L2:    mov ah,0x02               ; bringt den Inhalt von dl zur Anzeige       

       int 0x21

       call Warteschleife        ; Aufruf der Subroutine Warteschleife

       inc dl                    ; erhöht dl um 1 (increase)

       cmp dl,0xFF

       je L1

       jmp L2

 

Warteschleife:                   ; Unterprogramm Warteschleife

       mov bl,0xF0 

M2:    mov cx,0xFFFF

M1:    dec cx                    ; erniedrigt cx um 1 (decrease)

       jnz M1

       dec bl

       jnz M2

       ret                       ; Rücksprung

 

 

Im obigen Programm fügten wir eine Warteschleife als Subroutine (Unterprogramm) ein. Die Warteschleife besteht aus einer äußeren Schleife, die den Wert 0xF0 im Register bl in Einerschritten bis auf null erniedrigt. Zusätzlich verwenden wir eine innere Schleife, die das Register cx von 0xFFFF bis auf null durchzählt.

Die Länge der Warteschleife stellt man in bl ein. Nun kann man die Zeichen schon etwas geruhsamer betrachten. Wir haben uns nun auch getraut, von Zeichencode 0x00 beginnend auszugeben. Jetzt sollte die „Bell“ erträglich sein.

 

Sie sehen an diesem einfachen Beispiel, wie man ein Unterprogramm mit call aufruft. Die Rückkehr zum aufrufenden Programmteil erfolgt durch ret.