One of the most basic forms of computer programming is the use of an assembler.
Marco Schweighauser has written a very nice simulator for a simple 8-bit processor with integrated assembler, which provides a good insight into the world of machine-oriented programming.
If you are listening to the lecture "Fundamentals of Computer Science" at the HFT Stuttgart, you will find here, in addition to the examples already explained there, further programs to deepen the subject matter and for your own experiments.
A guide explains how to use the pages in this website, their behaviour may be adjusted in the settings.
The simulator can be used online, installation is not required. All important information about the system can be found on Github.
The user interface includes
The simulator does not allow explicit inputs, all data must be part of the assembler program.
The display of register and memory contents can be either hexadecimal or decimal; in addition, recognised instructions can be colour-coded in the memory display.
If a register contains an address, the referenced memory cell can be colour-coded if desired, as is already done by default for "instruction pointers" and "stack pointers".
The console is mapped (in the sense of a "memory-mapped IO") to an area in the main memory, the cells concerned are greyed out.
Immediately after loading the corresponding web page, the editor of the assembler simulator already contains a "Hello World!" program.
Press Assemble
to translate this program (and watch the memory being filled with the data and instructions from the program).
Then press the green Run
button and watch the processor fill the output area with the text "Hello World!" - then the processor stops.
Reset
button. The simulator would only be half as nice if you couldn't get active yourself and run your own programs.
The following examples are intended to produce visible output with as few commands as possible.
*
) into the memory area intended for the console.
Assemble
, then on Run
and observe the output area
; 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
For assembler programming, it is important to know the behaviour of the processor as well as the effects of the individual instructions on the flags.
The following examples may therefore seem trivial (they are), but nevertheless contribute to the understanding of the system. Therefore, take a look at the register contents and flag states at the end of each of the following programs!
CMP reg,0
.
; MOV does not set the zero flag
MOV A,0
HLT
; CMP triggers an explicit test
MOV A,0
CMP A,0
HLT
CMP
or an arithmetic or logic command to update the flags - but the processor does not always behave as expected:
Incrementing 255
sets the carry and clears the zero flag, although the register used shows 0 - from a semantic point of view, however, this behaviour is absolutely correct
; check flags (Z = 0 - sic!, C = 1)
MOV A,255
INC A
HLT
Decrementing a 1
sets the zero flag, as was also to be expected
; check flags (Z = 1, C = 0)
MOV A,1
DEC A
HLT
Decrementing a 0
sets the carry flag, which now takes over the function of a "Borrow".
; check flags (Z = 0, C = 1)
MOV A,0
DEC A
HLT
NOT always sets the Carry Flag
regardless of the value to be inverted - such behaviour is unexpected.
NOT
instructions in a row (applied to any but the same register) are sufficient, and the carry flag is set, but the register content is left unchanged.
; check flags (Z = 0, C = 1 - why?)
MOV A,0x0F
NOT A
NOT A
HLT
NOT does not invert 0xFF properly
unfortunately, NOT applied to 0xFF does not return the value 0x00, but 0x100 (!) - i.e. a completely invalid value - a double inversion returns the original 0xFF (which means that the NOT command is still suitable for setting the carry flag), but the command is unsuitable for a simple inversion!
; NOT is broken
MOV A,0xFF
NOT A ; watch register: contains 100!
HLT
XOR instead of NOT
in this case, the XOR instruction offers a remedy (for the inversion, but not for setting the carry flag): the XOR operation of any register with the constant value 0xFF causes a de facto inversion of the register content.
; 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
The following examples no longer deal with the processor itself, but solve some arithmetic problems. Use these programs to deepen your knowledge in dealing with binary numbers!
Due to the lack of other input possibilities, the numbers to be processed must be entered directly into the respective program - by filling the registers involved (A
...D
) with the higher- or lower-order bytes of the 16-bit operands.
For the sake of simplicity, the output of result(s) is also (mostly) done via registers. Therefore, take a look at the register display in the simulator at the end of each calculation.
Input and output are done in "big endian" order: first comes the high-order byte, then the low-order byte (MSB before LSB): A
= MSB, B
= LSB, C
= MSB, D
= LSB.
A
and B
, the second one into registers C
and D
. After running the program, the console will display the relationship between the two register pairs AB
and CD
: <
means that AB
is less than CD
, =
indicates that the two numbers are equal, and >
appears if AB
is greater than CD
.
; 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
and B
, assemble and run the program. At the end, this register pair will also contain the calculation result.
; 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
and B
, assemble and run the program. At the end, register pair AB
will also contain the calculation result.
; 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
or CD
, assemble and run the program. At the end, register pair AB
will also contain the calculation result.
; 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
and B
, assemble and run the program. At the end, the register pair AB
will also contain the result.
; 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
and B
, assemble and let the program run.
AB
will also contain the result.
; 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
and B
, assemble and run the program. At the end, the result of the calculation will be found in register pair CD
.
; 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
and the divisor into register C
, assemble and run the program. At the end, register pair CD
will contain the result of the (integer) division and register pair AB
will contain the division remainder.
; 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
This web page uses the following third-party libraries, assets or StackOverflow answers:
The author would like to thank the developers and authors of the above-mentioned contributions for their effort and willingness to make their works available to the general public.