Tips to Writing RISC-V Assembly
Writing assembly is itself an art. When C, C++, or any other language is compiled, the compiler determines the art of writing assembly. However, this time, we will some of the techniques and decisions we can make to write these ourselves.
We will use RISC-V to see how to design logic, write up the logic, and translate the logic into assembly.
Tip 1: Design the Logic in a Comfortable Language
This is one of the hardest aspects of my students’ progress to perfect. A large number of students choose to sit down and compose the whole assignment. In the other hand, if you are not happy doing assembly, this solution is a lost cause. We ought to write in a language we understand instead of attempting to keep the logic apart from the language.
Pseudocode is great for students who do not know C or any lower-level vocabulary, so they can use it to explain their solutions. The higher the language is set, the more difficult the translation becomes, and the lower the language is set, the more difficult the logic architecture becomes. So, in my opinion, C or C++, or any other programming language of equal skill would be ideal.
It is handy to have an editor that lets you visually put the translated text next to the original text. It is difficult to keep a running list of instructions in your head, especially if you are working on a complicated programme.
Tip 2: Take Small Bites
My students do their homework assignments by trying to write the curriculum from start to end, without checking anything in between. I specialise in teaching step-by-step programming to beginning programmers. It is your job to try out the logic as soon as you complete a portion of it. To complete this step, one would only need to make use of a for loop, or manipulate the pointer using scale.
One way to check whether you have linked the C or C++ programme correctly is to connect the two programmes together. In C++, you can prototype the name of the assembly function to determine the one you want to use, and then swap between the two. It is important to remember that you must have two separate pieces to avoid being disappointed with your linker. I generally replace the C function with a C++ function in order to indicate that it is a C++ function. I want to keep the assembly names consistent because ultimately, we want all assembly functions to replace the C functions.
With what I’ve done above, we can call show to run the assembly function or cshow to run the C function. In C++, the names of the functions are mangled to allow for overloading. However, you can turn this off by doing the following:extern “C” { // Turn off name mangling void show(int *x);}
The extern “C” will tell C++ that the functions follow the C “calling convention”. We really care that name mangling is turned off so that we can just create a label named “show” and have our function.
Tip 3: Know Your Role
“Know your role” was the mantra Dwayne “The Rock” Johnson kept repeating. It is important to recognise what C/C++ was doing for us and what assembly fails to accomplish. Often known as the order of operations, which contains order of operations. For example, multiplying four by three four times would always order the operations to be performed in the following order: addition first, followed by multiplication. This works in the context of the previous statement, where we said that in assembly, we have to select the multiply instruction followed by the addition instruction. The process of “reordering” does not apply to us.
Tip 4: Know How to Call a Function
The majority of ISA architectures will come with a calling convention manual, such as ARM and RISC-V. They set down only some general guidelines to allow code to run regardless of the programming language it is written in. Fortunately, the ABI register names of the RISC-V registers allow for what they represent. In order to comply with these laws, please adhere to these regulations.
- Integer arguments go in a0-a7, floating point arguments go in fa0-fa7.
- Allocate by subtracting from the stack pointer, deallocate by adding it back.
- The stack must be allocated in chunks of 8.
- All a (argument) and t (temporary) registers must be considered destroyed after a function call.
- All s (saved) registers can be considered saved after a function call.
- If you use any of the saved registers, you must restore their original value before returning.
- Return data back to the caller through the a0 register.
- Don’t forget to save the one and only RA (return address) register any time you see a call instruction (jal or jalr) in your function.
Obviously, I’ve reinterpreted the rules in a more informal way, but you get the jist of what’s going on here..global mainmain:addi sp, sp, -8 sd ra, 0(sp) la a0, test_solve call solve mv a0, zero ld ra, 0(sp) addi sp, sp, 8 ret
You can see from the code above, we allocate our stack frame first, save all registers that need to be saved, execute, and then undo everything before returning.
Tip 5: Document!
Writing assembly from C or another language will have you writing multiple lines of assembly code for every single line of C code. This can get confusing and utterly frustrating if you’re trying to debug your program. So, I always write the C code as a comment for the assembly and then pull it apart and show each step of me doing it.# used |= 1 << ( x[i * 9 + col] – 1) mul t1, s3, t0 # t1 = i * 9 add t1, t1, s2 # t1 = i * 9 + col slli t2, t1, 2 # Scale by 4 add t2, t2, s6 # x + i * 9 + col lw t3, 0(t2) # x[i * 9 + col] addi t3, t3, -1 # x[i * 9 + col] – 1 li t4, 1 sll t4, t4, t3 # 1 << x[i * 9 + col] – 1 or s5, s5, t4 # used |= …
You can see from the code above, I have the original C code (first comment), and then inline comments for each piece. This keeps me honest when it comes to order of operations and that I’m doing each step correctly.