Assembly Language

The assembly language is close to the heart of the instruction set you will program for, for example for our SUBLEQ instruction set it is pretty simple. We don't actually have general purpose registers or any other operations.

START:
  subleq 7,6, END
  subleq 8,8, START
  subleq 3,3, 0
END:
  subleq 8,8, END

After we compile the program the actual machine code will be 7 6 9 8 8 0 3 3 0 8 8 9

If we write a program for a processor that implements the RISC-V (RISC Five) instruction set we have access to 32 registers, and all kinds of operations, add, subtract, shift etc, we can load from RAM into register, store from register into ram, and so on. Those operations are common on almost all modern CPUs, but they differ slightly and each architecture has its own assembler language.

Lets examine the same count to 3 program but in RISC-V assembly:

addi x5, x0, 3
loop: 
  addi x5, x5, -1
  bne x5, x0, loop
end:
  jal x0, end

Takes a second to get used to the symbols. Don't panic.

First we start with addi x5, x0, 3. x5 is one of the general purpose registers we could use, addi takes 3 parameters, destination register (rd), source register (rs) and an immidiate value (imm), it adds the source register plus the immidiate value and stores the result into the destination register rd = rs + imm. x0 is a special zero register, you always read zero from it, you can write to it, and it does nothing, its always zero, so addi x5, x0, 3 is the same x5 = zero + 3 so x5 will become 3.

Then we have addi x5, x5, -1 which is x5 = x5 + -1 which decrements x5, in the first iteration it goes from 3 to 2.

bne x5, x0, loop means if x5 != x0: jump to loop, so if the content of x5 is the same as x0 it will set the program counter to where the label loop is. The computer does not understand labels, in RISC-V the branch instructions are relative to the branch instruction itself, and also in the RISC-V32I we use all instructions are 32 bit, or 4 bytes, so bne x5, x0, loop will be compiled to bne x5, x0, -4, and branch means set the program counter to some value, if x5 != x0: pc = pc - 4. The assembler must know where things are going to be, where each instruction is in memory and how big it is, in order to calculate where the labels are.

jal x0, end means x0 = pc + 4; pc = pc + end, or store the next instruction address in x0, and set the program counter to wherever the label end is, again the instruction is relative, and in our case we want to jump to ourselves, so x0 = pc + 4; pc = pc + 0. JAL means Jump And Link, it is usually used with x1, also called the return address register, or ra, so that you can jump into a subroutine and then from there you want to come back to continue your program, but in our case we dont want to remember, we just want to jump, so we link to the zero register x0.

The compiled program will be 0x00300293 0xfff28293 0xfe029ee3 0x0000006f or as decimal 3146387 4294083219 4261584611 111. The processor will fetch one instruction, decode it, and execute it, then go to the next one, wherever the program counter is set to. Very similar to our SUBLEQ processor, but we did not have the "decode" step, because we had only one instruction, to decode it means basically to pick a mini program to be executed from the control unit.

The same program written for other architectures:


ARM:

    mov r5, #3
  loop:
    sub r5, r5, #1
    cmp r5, #0
    bne loop
  end:
    b end


x86:

    mov ecx, 3
  loop:
    dec ecx
    cmp ecx, 0
    jne loop
  end:
    jmp end


Z80:

    ld a, 3
  loop:
    dec a
    cp 0
    jr nz, loop
  end:
    jr end


6502:

    lda #3
    sta count
  loop:
    dec count
    lda count
    cmp #0
    bne loop
  end:
    jmp end
  
  count:  .byte 0

The idea is the same, they are different and yet they are the same. In this book we will use RISC-V because I think it is the coolest one, it is open source, and it is very very well thought, there are hundreds of emmulators and simmulators for it, and there are many very very cheap computers like esp32c3 which uses it.

Before we continue I will explain the most important RISC-V instructions.

I will actually ask Claude to write a list of the important instruction with their explanations, since RISCV is an open source project, Claude has been trained on it for sure, and I know enough to know when its wrong. The prompt I used: i want to add most important riscv instructions to my book, can you make a list with descriptions, explanations and also examples please.


Essential RISC-V Instructions

Arithmetic Instructions

ADD (Add)

  • Format: add rd, rs1, rs2
  • Description: Adds the values in two source registers and stores the result in the destination register
  • Example:
    add x5, x6, x7    # x5 = x6 + x7
    

ADDI (Add Immediate)

  • Format: addi rd, rs1, immediate
  • Description: Adds a 12-bit immediate value to a source register and stores the result in the destination register
  • Example:
    addi x5, x6, 10    # x5 = x6 + 10
    addi x5, x0, 42    # Load immediate value 42 into x5
    

SUB (Subtract)

  • Format: sub rd, rs1, rs2
  • Description: Subtracts the value in rs2 from rs1 and stores the result in rd
  • Example:
    sub x5, x6, x7    # x5 = x6 - x7
    

Logical Instructions

AND

  • Format: and rd, rs1, rs2
  • Description: Performs bitwise AND operation between two registers
  • Example:
    and x5, x6, x7    # x5 = x6 & x7
    

OR

  • Format: or rd, rs1, rs2
  • Description: Performs bitwise OR operation between two registers
  • Example:
    or x5, x6, x7     # x5 = x6 | x7
    

XOR

  • Format: xor rd, rs1, rs2
  • Description: Performs bitwise XOR operation between two registers
  • Example:
    xor x5, x6, x7    # x5 = x6 ^ x7
    

Load/Store Instructions

LW (Load Word)

  • Format: lw rd, offset(rs1)
  • Description: Loads a 32-bit word from memory into a register
  • Example:
    lw x5, 8(x6)      # Load word from address (x6 + 8) into x5
    

SW (Store Word)

  • Format: sw rs2, offset(rs1)
  • Description: Stores a 32-bit word from a register into memory
  • Example:
    sw x5, 12(x6)     # Store word from x5 into address (x6 + 12)
    

Branch Instructions

BEQ (Branch if Equal)

  • Format: beq rs1, rs2, offset
  • Description: Branches to offset if rs1 equals rs2
  • Example:
    beq x5, x0, loop  # Jump to loop if x5 equals zero
    

BNE (Branch if Not Equal)

  • Format: bne rs1, rs2, offset
  • Description: Branches to offset if rs1 is not equal to rs2
  • Example:
    bne x5, x0, loop  # Jump to loop if x5 is not zero
    

BLT (Branch if Less Than)

  • Format: blt rs1, rs2, offset
  • Description: Branches to offset if rs1 is less than rs2 (signed comparison)
  • Example:
    blt x5, x6, loop  # Jump to loop if x5 is less than x6
    

Jump Instructions

  • Format: jal rd, offset
  • Description: Jumps to offset and stores return address (pc+4) in rd
  • Example:
    jal x1, function  # Jump to function, store return address in x1
    
  • Format: jalr rd, rs1, offset
  • Description: Jumps to address in rs1 plus offset and stores return address in rd
  • Example:
    jalr x0, x1, 0    # Return from function (when x1 holds return address)
    

Shift Instructions

SLL (Shift Left Logical)

  • Format: sll rd, rs1, rs2
  • Description: Shifts rs1 left by the amount specified in rs2 (logical shift)
  • Example:
    sll x5, x6, x7    # x5 = x6 << x7
    

SRL (Shift Right Logical)

  • Format: srl rd, rs1, rs2
  • Description: Shifts rs1 right by the amount specified in rs2 (logical shift)
  • Example:
    srl x5, x6, x7    # x5 = x6 >> x7 (zero-extended)
    

SRA (Shift Right Arithmetic)

  • Format: sra rd, rs1, rs2
  • Description: Shifts rs1 right by the amount specified in rs2 (arithmetic shift)
  • Example:
    sra x5, x6, x7    # x5 = x6 >> x7 (sign-extended)
    

Important Register Conventions

  • x0: Zero register (always contains 0)
  • x1: Return address (ra)
  • x2: Stack pointer (sp)
  • x3: Global pointer (gp)
  • x4: Thread pointer (tp)
  • x5-x7: Temporary registers (t0-t2)
  • x8-x9: Saved registers (s0-s1)
  • x10-x11: Function arguments/results (a0-a1)
  • x12-x17: Function arguments (a2-a7)
  • x18-x27: Saved registers (s2-s11)
  • x28-x31: Temporary registers (t3-t6)

Common Programming Patterns

Initialize a Register

addi x5, x0, 42     # Load immediate value 42 into x5

Simple Loop

    addi x5, x0, 10    # Initialize counter to 10
loop:
    addi x5, x5, -1    # Decrement counter
    bne x5, x0, loop   # Loop if counter != 0

Function Call

    jal x1, function   # Call function
    # ... more code ...
function:
    # function body
    jalr x0, x1, 0     # Return

Memory Access

    # Store value
    sw x5, 8(x2)       # Store x5 to address in x2+8
    
    # Load value
    lw x6, 8(x2)       # Load from address in x2+8 to x6

Now its back to me.

You are quite familiar witht he jumps and the arithmetic operations, but we did not have lw and sw in our SUBLEQ computer, we could build up to them, in the same way we made the MOV subroutine, but they are not native to the machine.

RISC-V is very consistent with data size, w means word which is 32 bits, or 4 bytes, h is half word, 16 bits or 2 bytes, b is byte: 8 bits, 1 byte.

lw means Load Word, or load one word size of data, 32 bits, from memory and store it in a register. sw means Store Word, or take 32 bits from the register and store it in memory. The syntax is a bit strange, lw x6, 8(x2) is the same as x6 = memory[x2 + 8], and sw x5, 8(x2) is memory[x2 + 8] = x5. You cant use absolute addresses, e.g. if you want to read address 64, memory[64], you cant do lw x6, 64. You must first load 64 in some register, and then use it in lw.

Like this:

addi x5, x0, 64
lw x6, 0(x5)

It is the same with sw you cant just store the value in memory. If you want to store the value 7 at address 64, you can't just do sw 7, 64, you have to put 7 in a register, then 64 in another register, and then do sw.

addi x5, x0, 7
addi x6, x0, 64
sw x5, 0(x6)

It takes a bit of time to get used to, but the assembler is very consistent and things make a lot of sense, if you get confused ask Claude or ChatGPT and it will help you out. There are also many resources about RISC-V online, all kinds of guides and simmulators, like https://github.com/TheThirdOne/rars or https://www.cs.cornell.edu/courses/cs3410/2019sp/riscv/interpreter/ and instruction decoders, and debuggers and so on.

We will use RISC-V assembly to write a higher level language, we could write C, but I dont think that is very educational, so I will make a Forth compiler and interpreter, in the spirit of our infinite loop book, Forth is probably the best language for the purpose, as it modifies itself, and most of it is written in itself.