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 LoopFor 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
  HLTCMP 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
  HLTDecrementing a 1
sets the zero flag, as was also to be expected
; check flags (Z = 1, C = 0)
  MOV A,1
  DEC A
  HLTDecrementing 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
  HLTNOT 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
  HLTNOT 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!
  HLTXOR 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:
  RETThe 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],'>'
  HLTA 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:
  HLTA 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:
  HLTAB 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:
  HLTA 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:
  HLTA 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:
  HLTA 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
  HLTAB 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
  HLTThis 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.