Assembler

An assembler is a specialized software tool in the build process that translates assembly language generated by the compiler into machine code, which is directly executable by a computer’s processor. The RISC-V assembler translates each source RISC-V *.s file into a *.o object file.

Understanding

The assembler is the second step of the programming build pipeline. As mentioned, the RISC-V assembler translates all (pseudo)instructions and outputs the .o file, which contains:

  1. A file header, which indicates the size and position of the components mentioned below.
  2. A text segment, which contains the machine code (stored in .text).
  3. A data segment, containing binary representations of all static data (in .data).
  4. A symbol table that keeps track of the addresses of labels, as well as static and global .data variables referenced within that file and other files.
    • By the native file, used to store symbols and replace their references when not yet defined in the code (forward references).
    • By other files, used to replace their external references to labels and variables in that file.
  5. A relocation table keeping track of the locations that need to be updated with actual addresses during linking, internally or externally.
    • Internally, used by absolute jumps (jal, jalr).
    • Externally, used by all

Examples

Two-Pass RISC-V Assembler

Consider the following simple RISC-V file, file_a.s:

1 func_name:
2 addi sp sp -4
3 beq a0 x0 loop
4 # [8 instructions here]
12 jal ra malloc
13
14 loop:
15 bneq t0 x0 done
16 # [3 instructions here]
19 j loop
20
21 done:
22 ret

>[!example]+ First Pass
>Assume that the function `func_name` starts at some address `fa_start`, which is the line that indicates the start of `file_a.s`. The assembler places this in the file's *symbol table*. 
>
>Going down, the assembler recognizes that line 3 references `loop`. However, it has no collection of `loop`'s address in the *symbol table*. As such, `loop` is placed in the *relocation table* for the second pass. 
>
>At line 12, the same is done for `malloc`.
>
>At line 14, the assembler recognizes the `loop` label, and places the address of `loop` into the *symbol table* as `fa_start + (13 * 4)`, since the first instruction of `loop` is $15-2=13$ lines/instructions away from `fa_start` and each instruction is 4 bytes in size. 
>
>At line 15, it stores the reference of `done` in the *relocation table* for the second pass. 
>
>At line 19, since it already has the address of `loop` in the *symbol table*, the reference to `loop` is replaced with the address `fa_start + 52`. Since this reference is resolved in the first pass, there is no need to store it in the *relocation table*.
>
>At line 21, the assembler recognizes `done`, saving it in the *symbol table* as `fa_start + (20 * 4)` using the same reasoning as before. This concludes our first pass of the assembler.

>[!example]+ Second Pass
>There are 3 references which need relocation in the second pass. This includes `loop` (3), `malloc` (12), and `done` (15). The assembler proceeds through the code again, setting the address of `loop` (3) to `fa_start + 52`.
>
>However, it cannot recognize `malloc` anywhere in the *symbol table*. As such, it skips the reference as it is external. External references are resolved in the linking stage.
>
>It then sets the address of `done` (15) to `fa_start + 80`.
>
>The second pass is now complete.