Course pages 2018–19
ECAD and Architecture Practical Classes
Assembler programming guide
Some tips on assembly language programming for RISC-V...
Registers
Rather than local variables, we have registers to store temporary values and pass data to and from functions. The RISC-V calling convention dictates whether each register must be preserved by the caller or the callee, and which have special purposes. In particular, it specifies which registers are used for function arguments and return values.
To preserve data we can use the stack. This just involves decrementing the stack pointer `sp` appropriately and then storing data to the location that `sp` points to. Since you will only be writing simple assembly functions this may not be needed.
Instructions
The RISC-V Green Card has a full listing (note that our architecture is RV32I). Here are the instructions you will likely find most useful:
beq rs1, rs2, label # branch if rs1 == rs2 # similarly: bne (!=), blt (<), bge (>=) add rd, rs1, rs2 # rd := rs1 + rs2 sub rd, rs1, rs2 # rd := rs1 - rs2 # similarly: xor, or, and sll rd, rs1, rs2 # rd := rs1 << rs2 srl rd, rs1, rs2 # rd := rs1 >> rs2 sra rd, rs1, rs2 # rd := rs1 >>> rs2 (arithmetic shift sign-extends the top bits of rs1) slt rd, rs1, rs2 # if rs1 < rs2 then rd := 1 else rd := 0 addi rd, rs, immediate # rd := rs1 + immediate. The immediate must be at most 12-bits. # similarly xori, ori, andi, slli, srli, srai, slti sw rs2, offset(rs1) # *(rs1 + offset) := rs2. Store the value in rs2 to the address (rs1 + offset). lw rd, offset(rs1) # rd := *(rs1 + offset). Load the value at address (rs1 + offset) into rd.
The assembler also provides a variety of pseudo-instructions, which correspond to one or two more complicated instructions but make life easier for the pogrammer. You may find the following useful:
li rd, immediate # rd := immediate mv rd, rs # rd := rs bgt rs1, rs2, label # branch to label if rs1 > rs2 ble rs1, rs2, label # branch to label if rs1 <= rs2 beqz rs, label # branch to label if rs1 == 0 # similarly: bnez, blez, bgez, bltz, bgtz j label # unconditional jump to label call label # call subroutine (jump to label and store the return address into register ra) ret # return from subroutine (jump to the address in register ra)
We have also provided some special functionality for debugging:
ecall # stops the simulator when testing in ModelSim csrw 0x7B2, rs # output a debug message in the trace containing the value of rs
Control flow
In high level languages `goto` is to be avoided, but in assembler that is the only control flow mechanism. This means we must translate for-loops and if-else statements into linear versions using branches instead.
For instance, a conditional statement in C such as:
if (i > 42) { // true path } else { // false path }
Might become in assembly:
li t1, 42 # initialize state for conditiom ble t0, t1, else # if condition not met, go to false path ... # true path j end else: ... # false path end: ...
And a simple for loop in C:
for (int i = 0; i < 10; i++) { // something terribly clever }
Becomes in assembly:
li t0, 0 # initialize the loop variable li t1, 10 # initialize the condition variable loop: bge t0, t1, end # if the loop condition is not met, exit ... # something terribly clever addi t1, t1, 1 # increment the loop variable j loop # loop end: ...
In many cases the number of branches required can be reduced by updating the loop variable at the start of the loop or checking the condition at the end. Extra care must be taken when there are `break` statements within the loop to ensure that the loop variable is always updated.
Do and while loops are implemented similarly.