Andreas Rozek
[ Imprint ]   [ Data Privacy Statement ]   [ Contact ]       [ deutsche Fassung ]   english version

Assembler Simulator

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 at a Glance

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

  • a large editor for the program to be executed
    Nota bene: do not forget to reassemble the program after making a change - only then will your change take effect!
  • an output area ("console") for up to 24 characters,
  • a display for the four multi-purpose registers, the "Instruction Pointer" and the "Stack Pointer" as well as the three flags ("Zero", "Carry", "Failure") of the simulated processor,
  • a display for the 256 bytes of the simulated memory and finally
  • a list of the "labels" defined in the assembler program, including the concrete address and the current content.

The simulator does not allow explicit inputs, all data must be part of the assembler program.

Fig. 1: View of the 8-bit Assembler Simulator
Fig. 1: View of the 8-bit Assembler Simulator

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.

A very first Program

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.

Nota bene: If you want to restart a program that has already run, you must first reset the processor using the Reset button.

Your first own Programs

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.

  • a single Asterisk on the Console.
    the first example simply writes the ASCII code for a asterisk (*) into the memory area intended for the console.
    Empty the editor and copy the source code for this example into it, click on Assemble, then on Run and observe the output area
    ; write a single asterisk onto the console

    MOV [0xE8],'*'
    HLT
  • two Asterisks on the Console
    the display fields of the console occupy consecutive memory cells. If you want to display a second asterisk, you simply have to write the ASCII code for it into the memory cell following 0xE8.
    ; write two asterisks onto the console

    MOV [0xE8],'*'
    MOV [0xE9],'*'
    HLT
  • two Asterisks on the Console (second variant)
    you do not have to explicitly specify the addresses of the output fields, you can also have them calculated and access the cells using "indirect addressing".
    ; write two asterisks onto the console

    MOV A,0xE8
    MOV [A],'*'

    INC A
    MOV [A],'*'
    HLT
  • "My God, it's full of Stars"
    calculating addresses is especially useful when using loops - this way you can easily fill the whole console with asterisks.
    Since the memory area for output reaches up to address 0xFF, you can detect the end e.g. by a range overflow after incrementing the output address.
    ; fill the console with asterisks

    MOV A,0xE8
    Loop:
    MOV [A],'*'
    INC A
    JNC Loop ; incrementing 0xFF will set carry
    HLT
  • ...and if you make a Mistake?
    syntactical (and also the one or other semantic) errors can be detected by the assembler itself. In general, however, only specific trial and error (i.e. "testing") helps to show the correctness of a program.
    One of the most difficult errors to find is accidental (sometimes deliberate) overwriting of the program itself. Try what happens if you set an unconditional jump instead of a conditional one in the previous program....
    ; fill the console with asterisks

    MOV A,0xE8
    Loop:
    MOV [A],'*'
    INC A
    JMP Loop

Get to know the Processor

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!

  • MOV leaves Flags untouched
    one of the most important features of the MOV instruction is the preservation of all flag states: hardly any assembler program could work if a processor behaved differently in this respect.
    On the other hand, this also means that simply loading a 0 into a register does not set the zero flag - you have to explicitly force the processor to do a test with a 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
  • INC and DEC change Flags - but be careful!
    generally you need either 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 is incorrect
    just like "real" processors, the simulator also contains a few bugs:
    • NOT always sets the Carry Flag
      regardless of the value to be inverted - such behaviour is unexpected.

      However, this peculiarity can also be used practically: if you want to set the carry flag explicitly (e.g. in the context of 16-bit arithmetic), two 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
  • arithmetic Commands
    the arithmetic commands behave as expected - try them out!
    ; 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

    By all means also test the division by 0 once
    ; check flags (do you see it?)

    MOV A,0
    DIV 0
    HLT
  • logical Commands
    the logical commands also hold no surprises
    ; 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
  • Stack Operations
    also very pleasant about the simulator is the support of a stack memory ("stack") and the way the stack is shown in the memory display.
    Run the following examples and see the respective stack state (e.g. the direction in which the stack grows and what remains in the memory after clearing the stack)!
    ; 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

16-Bit Arithmetic with the 8-Bit Processor

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.

  • Comparison of two 16-Bit Numbers
    The simplest example compares two 16-bit numbers.
    Enter the first number into registers 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
  • Increment and decrement a 16-Bit Number
    The simplest arithmetic operations are incrementing or decrementing a number by 1.
    Enter the desired operand into registers A and B, assemble and run the program. At the end, this register pair will also contain the calculation result.
    Test the following operands in particular: 0x0000, 0x00FF and 0xFFFF - also pay attention to the state of the carry flag at the end of the calculation!
    ; 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
  • Forming the 2's Complement for a 16-Bit Number
    It is not far from incrementing to form a 2's complement.
    Enter the desired operand into registers A and B, assemble and run the program. At the end, register pair AB will also contain the calculation result.
    Test the following operands in particular: 0x0000, 0x0001 and 0xFFFF!
    ; 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
  • Add and subtract two 16-Bit Numbers
    Adding and subtracting two 16-bit wide numbers is a bit more complicated - in particular, the "trick" for explicitly setting the carry flag is used here for the first time.
    Enter the desired summands into register pairs AB or CD, assemble and run the program. At the end, register pair AB will also contain the calculation result.
    When choosing the numbers for your tests, pay particular attention to the problem cases of the procedure - namely the overflows after processing the two LSBs as well as the final overflow after processing the MSBs!
    ; 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
  • Shift a 16-Bit Number one Position to the right
    The simplest form of dividing a binary number is to shift all bits one position to the right - corresponding to a division by 2.
    Enter the number to be shifted into registers A and B, assemble and run the program. At the end, the register pair AB will also contain the result.
    In particular, also test the following operands: 0x0000, 0x0001, 0x0100 and 0xFFFF!
    ; 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
  • Shift a 16-Bit Number one Position to the left
    The simplest form of multiplying a binary number is to shift all bits one place to the left - corresponding to a multiplication by 2.
    Enter the number to be shifted into registers A and B, assemble and let the program run.
    At the end, register pair AB will also contain the result.
    In particular, also test the following operands: 0x0000, 0x0080, 0x8000 and 0xFFFF - and pay attention to the state of the carry flag at the end of each run!
    ; 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
  • Multiply two 8-Bit Numbers to produce a 16-Bit wide Result
    Multiplying two 8-bit wide numbers is the first slightly more challenging example in this compilation.
    Enter the desired operands into registers A and B, assemble and run the program. At the end, the result of the calculation will be found in register pair CD.
    In particular, test multiplication by 0 or 1 as well as numbers whose product still or no longer fits into a single byte!
    ; 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
  • Division of a 16-Bit wide Number by an 8-Bit Number
    The 16-bit division is the most challenging example on this page.
    Enter the dividend into register pair 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.
    In particular, also test the division by 0, 1 or a power of 2!
    ; 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.