Zu den ursprünglichsten Formen der Computer-Programmierung gehört die Verwendung eines Assemblers.
Marco Schweighauser hat einen sehr schönen Simulator für einen einfachen 8-Bit Prozessor mit integriertem Assembler geschrieben, der einen guten Einblick in die Welt der maschinennahen Programmierung ermöglicht.
Falls Sie die Vorlesung "Grundlagen der Informatik" an der HFT Stuttgart hören, finden Sie hier neben den dort bereits erläuterten Beispielen weitere Programme zur Vertiefung der Thematik und für eigene Experimente.
Eine Anleitung erklärt Ihnen die Bedienung der Seiten in diesem Web-Auftritt, in den Einstellungen können Sie deren Verhalten anpassen.
Der Simulator kann online genutzt werden, eine Installation ist nicht erforderlich. Alle wichtigen Informationen zu dem System finden Sie auf Github.
Die Bedienoberfläche umfasst
Explizite Eingaben gestattet der Simulator nicht, alle Daten müssen Teil des Assembler-Programmes sein.
Die Anzeige von Register- und Speicher-Inhalten kann wahlweise hexadezimal oder dezimal erfolgen, außerdem können erkannte Befehle in der Speicheranzeige farblich markiert werden.
Falls ein Register eine Adresse beinhaltet, kann die referenzierte Speicherzelle auf Wunsch farblich markiert werden, so wie es für "Instruction Pointer" und "Stack Pointer" standardmäßig bereits geschieht.
Die Konsole wird (im Sinne einer "memory-mapped IO") auf einen Bereich im Hauptspeicher abgebildet, die betreffenden Zellen sind grau hinterlegt.
Direkt nach dem Laden der zugehörigen Web-Seite enthält der Editor des Assembler-Simulators bereits ein "Hello World!"-Programm.
Drücken Sie auf Assemble
, um dieses Programm zu übersetzen (und achten Sie darauf, wie der Speicher mit den Daten und Instruktionen aus dem Programm gefüllt wird).
Anschließend klicken Sie auf die grüne Run
-Taste und sehen Sie dem Prozessor dabei zu, wie der Ausgabebereich mit dem Text "Hello World!" gefüllt wird - danach bleibt der Prozessor stehen.
Reset
-Taste zurücksetzen. Der Simulator wäre nur halb so schön, könnte man nicht selbst aktiv werden und eigene Programme laufen lassen.
Die folgenden Beispiele sind dafür gedacht, mit möglichst wenig Befehlen sichtbare Ausgaben zu produzieren.
*
) in den Speicherbereich, der für die Konsole gedacht ist.
Assemble
, danach auf Run
und beobachten Sie den Ausgabebereich
; write a single asterisk onto the console
MOV [0xE8],'*'
HLT
; write two asterisks onto the console
MOV [0xE8],'*'
MOV [0xE9],'*'
HLT
; write two asterisks onto the console
MOV A,0xE8
MOV [A],'*'
INC A
MOV [A],'*'
HLT
; fill the console with asterisks
MOV A,0xE8
Loop:
MOV [A],'*'
INC A
JNC Loop ; incrementing 0xFF will set carry
HLT
; fill the console with asterisks
MOV A,0xE8
Loop:
MOV [A],'*'
INC A
JMP Loop
Für die Assembler-Programmierung ist es wichtig, das Verhalten des Prozessors sowie die Auswirkungen der einzelnen Befehle auf die Flags zu kennen.
Die folgenden Beispiele mögen deshalb vielleicht trivial wirken (sie sind es ja auch), tragen aber dennoch zum Verständnis des Systems bei. Werfen Sie deshalb am Ende jedes der folgenden Programme einen Blick auf die Register-Inhalte und Flag-Zustände!
CMP reg,0
explizit zu einem Test zwingen
; MOV does not set the zero flag
MOV A,0
HLT
; CMP triggers an explicit test
MOV A,0
CMP A,0
HLT
CMP
oder einen arithmetischen bzw. logischen Befehl, um die Flags zu aktualisieren - aber nicht immer verhält sich der Prozessor wie erwartet:
Inkrementieren des Wertes 255
setzt das Carry und löscht das Zero Flag, obwohl das verwendete Register 0 zeigt - aus semantischer Sicht ist dieses Verhalten aber absolut korrekt
; check flags (Z = 0 - sic!, C = 1)
MOV A,255
INC A
HLT
Dekrementieren einer 1
setzt das Zero Flag, so wie es auch zu erwarten war
; check flags (Z = 1, C = 0)
MOV A,1
DEC A
HLT
Dekrementieren einer 0
setzt das Carry Flag, welches jetzt die Funktion eines "Borrow" übernimmt
; check flags (Z = 0, C = 1)
MOV A,0
DEC A
HLT
NOT setzt stets das Carry Flag
und zwar unabhängig vom zu invertierenden Wert - ein solches Verhalten ist unerwartet.
; check flags (Z = 0, C = 1 - why?)
MOV A,0x0F
NOT A
NOT A
HLT
NOT invertiert 0xFF falsch
leider liefert NOT, auf 0xFF angewandt, nicht den Wert 0x00, sondern 0x100 (!) - also einen vollkommen ungültigen Wert - eine zweifache Invertierung liefert zwar wieder das ursprgl. 0xFF (wodurch der NOT-Befehl weiterhin zum Setzen des Carry Flag taugt), für eine einfache Invertierung ist der Befehl jedoch ungeeignet!
; NOT is broken
MOV A,0xFF
NOT A ; watch register: contains 100!
HLT
XOR anstelle von NOT
Abhilfe (für die Invertierung, nicht aber für das Setzen des Carry Flag) bietet in diesem Fall der XOR-Befehl: die XOR-Verknüpfung eines beliebigen Registers mit dem konstanten Wert 0xFF bewirkt de facto eine Invertierung des Registerinhaltes
; use XOR (instead of NOT) for negation
MOV A,0xFF
XOR A,0xFF
HLT
; check flags (Z = 0 - sic!, C = 1) like INC
MOV A,0xFF
ADD A,1
HLT
; check flags (Z = 1, C = 0) like DEC
MOV A,1
SUB A,1
HLT
; check flags (Z = 0, C = 1) like DEC
MOV A,0
SUB A,1
HLT
; check flags (Z = 1, C = 0)
MOV A,0
MUL 1
HLT
; check flags (Z = 0, C = 1)
MOV A,255
MUL 2
HLT
; check flags (Z = 1, C = 0)
MOV A,0
DIV 1
HLT
; check flags (do you see it?)
MOV A,0
DIV 0
HLT
; check flags (Z = 1, C = 0)
MOV A,0xFF
AND A,0x00
HLT
; check flags (Z = 0, C = 0)
MOV A,0xFF
AND A,0x55
HLT
; check flags (Z = 1, C = 0)
MOV A,0x00
OR A,0x00
HLT
; check flags (Z = 0, C = 0)
MOV A,0x00
OR A,0x55
HLT
; check flags (Z = 1, C = 0)
MOV A,0x55
XOR A,0x55
HLT
; check flags (Z = 0, C = 0)
MOV A,0x55
XOR A,0x00
HLT
; watch the stack!
PUSH 1
PUSH 2
PUSH 3
HLT
; watch the stack!
PUSH 1
PUSH 2
PUSH 3
POP A
POP B
HLT
; watch the stack!
POP A ; causes a runtime error
HLT
; watch the stack!
CALL Subroutine
HLT ; good style to protect routines
Subroutine:
HLT
; watch the stack!
CALL Sub_1
HLT ; good style to protect routines
Sub_1:
CALL Sub_2
HLT
Sub_2:
CALL Sub_3
RET
Sub_3:
RET
Die folgenden Beispiele beschäftigen sich nicht mehr mit dem Prozessor selbst, sondern lösen arithmetische Aufgaben. Nutzen Sie die Programme, um Ihre Kenntnisse im Umgang mit binären Zahlen zu vertiefen!
Mangels anderer Eingabemöglichkeiten müssen die zu bearbeitenden Zahlen direkt in das jeweilige Programm eingegeben werden - durch Füllen der beteiligten Register (A
...D
) mit den höher- bzw. niederwertigen Bytes der 16-Bit großen Operanden.
Auch die Ausgabe des (bzw. der) Ergebnisse erfolgt der Einfachheit halber (zumeist) über Register. Werfen Sie deshalb am Ende jeder Berechnung einen Blick auf die Registeranzeige im Simulator.
Ein- und Ausgabe erfolgen in der "big endian"-Reihenfolge: zuerst kommt das höherwertige, anschließend das niederwertige Byte (MSB vor LSB): A
= MSB, B
= LSB, C
= MSB, D
= LSB.
A
und B
, die zweite in die Register C
und D
ein. Nach Durchlaufen des Programmes zeigt die Konsole die Beziehung zwischen den beiden Registerpaaren AB
und CD
an: <
bedeutet, dass AB
kleiner als CD
ist, =
zeigt die Gleichheit der beiden Zahlen an, und >
erscheint, falls AB
größer als CD
ist.
; compare two 16-bit values
MOV A,0x12 ; compare AB with CD
MOV B,0x34
MOV C,0x12
MOV D,0x34
CMP A,C
JB below
JE equal_MSB
JA above
HLT
equal_MSB:
CMP B,D
JB below
JE equal
JA above
HLT
below:
MOV [0xE8],'<'
HLT
equal:
MOV [0xE8],'='
HLT
above:
MOV [0xE8],'>'
HLT
A
und B
ein, assemblieren Sie und lassen Sie das Programm laufen. Am Ende steht in dem Registerpaar auch das Rechenergebnis.
; increment a 16-bit value (see registers)
MOV A,0x12 ; increment AB
MOV B,0x34
INC B
JNC exit
INC A
exit:
HLT
; decrement a 16-bit value (see registers)
MOV A,0x12 ; decrement AB
MOV B,0x34
DEC B
JNC exit
DEC A
exit:
HLT
A
und B
ein, assemblieren Sie und lassen Sie das Programm laufen. Am Ende steht im Registerpaar AB
auch das Rechenergebnis.
; 16-bit 2th complement (see registers)
MOV A,0x12
MOV B,0x34
XOR A,0xFF ; instead of NOT A
XOR B,0xFF ; instead of NOT B
INC B
JNC exit
INC A
exit:
HLT
AB
bzw. CD
ein, assemblieren Sie und lassen Sie das Programm laufen. Am Ende steht im Registerpaar AB
auch das Rechenergebnis.
; add two 16-bit values (see registers,flags)
MOV A,0x12 ; compute AB + CD
MOV B,0x34
MOV C,0x12
MOV D,0x34
ADD B,D
JC LSB_carry
ADD A,C
JMP exit ; carry flag is properly set here
LSB_carry:
INC A
JC MSB_carry
ADD A,C
JMP exit ; carry flag is properly set here
MSB_carry:
ADD A,C ; will clear the carry flag
NOT A ; trick to explicitly set carry flag
NOT A
exit:
HLT
; subtract two 16-bit values (see regs,flags)
MOV A,0x12 ; compute AB - CD
MOV B,0x34
MOV C,0x12
MOV D,0x34
SUB B,D
JC LSB_carry
SUB A,C
JMP exit ; carry flag is properly set here
LSB_carry:
DEC A
JC MSB_carry
SUB A,C
JMP exit ; carry flag is properly set here
MSB_carry:
SUB A,C ; will clear the carry flag
NOT A ; trick to explicitly set carry flag
NOT A
exit:
HLT
A
und B
ein, assemblieren Sie und lassen Sie das Programm laufen. Am Ende steht im Registerpaar AB
auch das Ergebnis.
; shift 16-bit value right (see regs,flags)
MOV A,0x12 ; compute AB >> 1
MOV B,0x34
MOV C,A ; since rightmost bit of A is lost
SHL C,7
SHR A,1
SHR B,1
ADD B,C ; considers rightmost bit of A
exit:
HLT
A
und B
ein, assemblieren Sie und lassen Sie das Programm laufen.
AB
auch das Ergebnis.
Testen Sie insbesondere auch folgende Operanden: 0x0000, 0x0080, 0x8000 und 0xFFFF - achten Sie auch auf den Zustand des Carry Flag am Ende jedes Durchlaufes!
; shift 16-bit value left (see regs,flags)
MOV A,0x12 ; compute AB << 1
MOV B,0x34
SHL B,1
JC LSB_carry
SHL A,1
JMP exit ; carry flag is properly set here
LSB_carry:
SHL A,1
JC MSB_carry
ADD A,1 ; considers carry from B << 1
JMP exit ; carry flag is properly set here
MSB_carry:
ADD A,1 ; considers carry from B << 1
NOT A ; trick to explicitly set carry flag
NOT A
exit:
HLT
A
und B
ein, assemblieren Sie und lassen Sie das Programm laufen. Am Ende steht im Registerpaar CD
das Ergebnis der Berechnung.
; multiply two 8-Bit values (see regs, flags)
MOV A,0x12 ; compute A*B
MOV B,0x34
MOV C,0 ; will store MSB of result
MOV D,0 ; LSB
; find left-most bit of B
PUSH A ; we need this register ourself
MOV A,8 ; counter
bit_loop:
SHL B,1
JC upper_bit_found
DEC A
JNZ bit_loop
JMP exit ; B seems to be 0
upper_bit_found:
MOV D,[SP+1] ; start actual multiplication
multiplication_loop:
DEC A ; proceed to next bit
JZ exit
SHL C,1 ; don't care about carry
SHL D,1
JNC no_bit_transfer
ADD C,1 ; MSB of D transferred to C
no_bit_transfer:
SHL B,1
JNC upper_bit_not_set
ADD D,[SP+1] ; add former content of A
JNC upper_bit_not_set; no overflow detected
INC C ; considers carry of addition
upper_bit_not_set:
JMP multiplication_loop
exit:
INC SP ; throw backup of A away
HLT
AB
und den Divisor in das Register C
ein, assemblieren Sie und lassen Sie das Programm laufen. Am Ende steht im Registerpaar CD
das Ergebnis der (Ganzzahl-)Division und im Registerpaar AB
der verbleibende Divisionsrest.
; divide a 16-Bit value by an 8-Bit one (see regs, flags)
MOV A,0x12 ; compute AB / C
MOV B,0x34
MOV C,0x56 ; divisor
CMP C,0
JNE division
HLT ; error: division by zero
; how many iterations do we need?
division:
MOV D,9 ; that's the default
normalization_loop:
CMP C,0x80 ; test if upper bit is set
JAE start_division
INC D
SHL C,1 ; C is not 0, thus has bit(s) set
JMP normalization_loop
start_division: ; we need more "registers"!
PUSH 0 ; auxiliary "register", call it "I"
PUSH 0 ; LSB of result, call it "H"
PUSH 0 ; MSB of result, call it "G"
PUSH 0 ; call it "F"
PUSH C ; call it "E" now
division_loop:
CMP A,[SP+1] ; AB >= EF?
JB skip
JA subtract
CMP B,[SP+2]
JB skip
subtract: ; compute AB-EF
SUB B,[SP+2]
JNC no_borrow
DEC A
no_borrow:
SUB A,[SP+1]
MOV C,[SP+4]
OR C,0x01 ; set LSB in result
MOV [SP+4],C
skip:
DEC D ; proceed to next step
JZ exit ; finish, if no more steps needed
; shift EF right
MOV C,[SP+1] ; rightmost bit of E is lost
SHL C,7
MOV [SP+5],C ; store in "I"
MOV C,[SP+1]
SHR C,1
MOV [SP+1],C
MOV C,[SP+2]
SHR C,1
ADD C,[SP+5] ; considers rightmost bit of E
MOV [SP+2],C
; shift GH left
MOV C,[SP+4]
SHL C,1
MOV [SP+4],C
MOV C,[SP+3]
JNC no_carry
SHL C,1
ADD C,1
JMP continue
no_carry:
SHL C,1
continue:
MOV [SP+3],C
JMP division_loop
exit: ; AB is remainder, load CD with result
MOV C,[SP+3]
MOV D,[SP+4]
ADD SP,5 ; throw auxiliary "registers" away
HLT
Diese Web-Seite verwendet die folgenden Drittanbieter-Bibliotheken oder -Materialien bzw. StackOverflow-Antworten:
Der Autor dankt den Entwicklern und Autoren der genannten Beiträge für ihre Mühe und die Bereitschaft, ihre Werke der Allgemeinheit zur Verfügung zu stellen.