1. Introduction and Problem Statement
A good way to learn what a compiler really does when transforming C source code into a binary is to disassemble the binary and compare it with the C source code. While this was traditionally true for 8-bit microcontrollers, it is equally insightful for modern 32-bit RISC architectures like RISC-V (RV32I).
To test our 32-bit toolchain (GCC / Clang-LLVM), we will use the exact same Universal Test Corpus C source code:
* ========================================================================== *
* Universal Test Corpus - Heterogeneous Architecture Analysis *
* ========================================================================== */
#include <stdint.h>
// 1. Global variables (testing absolute/relative addressing modes)
volatile uint32_t global_var_32 = 0xDEADBEEF;
volatile uint8_t global_var_8 = 0x42;
const char string_const[] = "TARGET_STRING";
// 2. Function with parameter passing and local variables (stack / Frame Pointer test)
int32_t callee_function(int16_t a, int16_t b) {
volatile int32_t local_result = 0;
// Basic and mixed arithmetic operations (8, 16, 32 bits)
local_result += (int32_t)(a * b);
local_result -= (int32_t)(a / (b | 1)); // Avoid division by zero
// Shift tests and logical operations (highly variable depending on ISAs)
local_result = (local_result << 2) ^ 0x55AA55AA;
local_result = (local_result >> 1) | (int32_t)global_var_8;
return local_result;
}
// 3. Main function grouping complex control flows
int main(void) {
volatile int32_t accumulator = 0;
int16_t i;
// Loop test (Conditional jumps, decrement, comparison tests)
for (i = 0; i < 10; i++) {
if (i == 5) {
accumulator += 100;
} else {
accumulator += i;
}
}
// Multiple branching test (Switch / Jump Table or cascaded if-else)
switch (global_var_8) {
case 0x10:
accumulator += 10;
break;
case 0x20:
accumulator += 20;
break;
default:
accumulator -= 5;
break;
}
// Function call (Stack management, save registers Link Register/PC)
accumulator += callee_function((int16_t)accumulator, 3);
// Pointer and indirect memory access test
volatile uint32_t *ptr = (volatile uint32_t *)&global_var_32;
*ptr = (uint32_t)accumulator;
// Terminal infinite loop (classic for raw binaries / microcontrollers)
while (1) {
accumulator ^= *ptr;
}
return 0;
}
To compile this code for a bare-metal RISC-V 32-bit target (RV32I), we use the official GNU toolchain (riscv64-unknown-elf-gcc or riscv32-unknown-elf-gcc with -mabi=ilp32 -march=rv32i flags):
riscv32-unknown-elf-gcc -O0 -mabi=ilp32 -march=rv32i -nostartfiles -T link.ld "$SRC/test1.c" -o "32/test1_rv32i.elf"
riscv32-unknown-elf-objcopy -O binary "32/test1_rv32i.elf" "32/bin/test1_rv32i.bin"
riscv32-unknown-elf-objdump -d "32/test1_rv32i.elf" > "32/test1_rv32i.asm"
To simulate and analyze the execution step-by-step, we use QEMU for RISC-V (qemu-system-riscv32 or qemu-riscv32) linked with gdb-multiarch.
2. RISC-V 32-bit (RV32I) Presentation
Unlike legacy architectures, RISC-V is an open-standard Instruction Set Architecture (ISA) designed around strict Load-Store RISC principles. In its base 32-bit variant (RV32I), the core provides 32 general-purpose 32-bit registers, making register allocation and compiler optimizations dramatically smoother than on 8-bit systems.
Here is a concise list of the core RV32I registers:
- x0 (zero): Hardwired to constant 0. Any write to x0 is discarded.
- x1 (ra): Return Address register, set automatically by call instructions (jal/jalr).
- x2 (sp): Stack Pointer, pointing to the current boundary of the stack.
- x3 (gp) & x4 (tp): Global Pointer and Thread Pointer for memory/thread addressing.
- x5 – x7 & x28 – x31 (t0–t6): Temporary registers used for intermediate calculations.
- x8 (s0/fp) & x9, x18 – x27 (s1–s11): Saved registers (preserved across function calls) / Frame Pointer.
- x10 – x11 (a0–a1): Function arguments / Return values.
- x12 – x17 (a2–a7): Function arguments.
- PC (Program Counter): Tracks the address of the current instruction being executed.
Notice that there are no dedicated condition flag registers (like PSW in 8051 or EFLAGS in x86). In RISC-V, conditional branches directly compare two registers (beq, bne, blt, bge), eliminating hidden pipeline state dependencies.
3. The RISC-V Instruction Set (RV32I)
The base RV32I instruction set is minimalist, containing only 40 fundamental instructions, all strictly fixed at 32 bits (4 bytes) in length and aligned on 4-byte boundaries.
A. Data Transfer / Load-Store Instructions
- LB / LH / LW – Load Byte / Halfword / Word from RAM into register.
- LBU / LHU – Load Byte / Halfword unsigned.
- SB / SH / SW – Store Byte / Halfword / Word from register to RAM.
B. Arithmetic & Logical Instructions
- ADD / ADDI – Add register or immediate value.
- SUB – Subtract registers.
- AND / ANDI – Bitwise AND (Register / Immediate).
- OR / ORI – Bitwise OR.
- XOR / XORI – Bitwise Exclusive-OR.
- SLT / SLTI / SLTU – Set on Less Than (Signed / Unsigned).
C. Shift Instructions
- SLL / SLLI – Shift Left Logical.
- SRL / SRLI – Shift Right Logical.
- SRA / SRAI – Shift Right Arithmetic (preserves sign bit).
D. Upper Immediate & Address Generation
- LUI – Load Upper Immediate (loads 20 bits into upper bits 31:12).
- AUIPC – Add Upper Immediate to PC (essential for PC-relative addressing).
E. Program Branching & Control Transfer
- JAL – Jump and Link (Unconditional jump + save return PC to ra).
- JALR – Jump and Link Register (Indirect jump via register).
- BEQ / BNE – Branch if Equal / Not Equal.
- BLT / BGE – Branch if Less Than / Greater Than or Equal (Signed).
- BLTU / BGEU – Branch if Less Than / Greater Than or Equal (Unsigned).
F. Memory Synchronization
- FENCE – Ordering fence for Memory and I/O accesses across threads/harts.
- FENCE.TSO – Total Store Ordering fence for stricter memory ordering rules.
G. System & Control Register Access (CSR)
- ECALL / EBREAK – Environment call (Syscall trap) / Breakpoint debugger entry.
All RV32I instructions use a uniform 32-bit layout categorizable into 6 standard formats (R, I, S, B, U, J). The primary opcode is always fixed in bits [6:0], simplifying hardware instruction decoding.
4. Unprivileged Architecture Extensions Overview
The modularity of the RISC-V ISA relies on fine-grained, ratified sub-extensions that complement the base integer set. Instead of monoliths, these standardized extensions allow hardware architects to tailor the processor for specific execution environments—ranging from instruction-cache management and low-power polling to fine-grained security primitives, vector cryptography, and specialized arithmetic—minimizing silicon area while maximizing domain-specific efficiency.
A. System, Control & Instruction Fetch
Zifencei (Instruction-Fetch Fence) [v2.0, Ratified]:
Domain: Instruction cache consistency and self-modifying code support.
Instructions: 1 instruction (FENCE.I).Zicsr (Control and Status Register Access) [v2.0, Ratified]:
Domain: Access and manipulation of hardware CSRs.
Instructions: 6 instructions (CSRRW, CSRRS, CSRRC, CSRRWI, CSRRSI, CSRRCI).Zicntr (Base Counters and Timers) [v2.0, Ratified]:
Domain: Unprivileged access to execution counters (cycle, time, instret).
Instructions: 0 new instructions (uses CSRRS mappings to unprivileged CSR addresses).Zihintntl (Non-Temporal Hints) [v1.0, Ratified]:
Domain: Memory hierarchy hints for non-temporal access (prevents cache pollution).
Instructions: 0 new instructions (uses NOP-encoding space).Zihintpause (Pause Hint) [v2.0, Ratified]:
Domain: Energy-efficient spin-lock loops and contention reduction.
Instructions: 0 new instructions (encoded within the FENCE opcode space).Zimop (May-Be-Operations) [v1.0, Ratified]:
Domain: Reserved opcode space for future expansion without breaking backward compatibility.
Instructions: 32 MOP instructions (encodings within unassigned spaces).Zicond (Conditional Operations) [v1.0, Ratified]:
Domain: Branchless conditional moves (czero.eqz, czero.nez) to eliminate branch mispredictions.
Instructions: 2 instructions.Zilsd & Zclsd (Load/Store Doubleword) [v1.0, Ratified]:
Domain: 64-bit load/store acceleration on 32-bit (RV32) architectures (and compressed variant Zclsd).
Instructions: 2 instructions (LSD, SSD) + compressed formats.
B. Multiplication & Division Sub-Extensions
M (Standard Multiplication and Division) [v2.0, Ratified]:
Domain: Hardware integer multiply and divide.
Instructions: 8 instructions.Zmmul (Multiply-Only Extension) [v1.0, Ratified]:
Domain: Microcontrollers needing hardware multiplication without the area overhead of division logic.
Instructions: 4 instructions (MUL, MULH, MULHU, MULHSU).
C. Atomics, Synchronization & Memory Models
A (Atomic Instructions) [v2.1, Ratified]:
Domain: Inter-processor synchronization and atomic memory operations.
Instructions: 11 instructions (Load-Reserved / Store-Conditional & AMOs).Zawrs (Wait-on-Reservation-Set) [v1.01, Ratified]:
Domain: Power-efficient polling loops using wrs.nto and wrs.sto.
Instructions: 2 instructions.Zacas (Atomic Compare-and-Swap) [v1.0, Ratified]:
Domain: Hardware-accelerated lock-free data structures.
Instructions: 3 instructions (AMOCAS.W, AMOCAS.D, AMOCAS.Q).Zabha (Byte and Halfword Atomics) [v1.0, Ratified]:
Domain: Atomic operations on 8-bit and 16-bit quantities.
Instructions: ~12 instructions (AMOADD.B, AMOSWAP.H, etc.).Zalasr (Load-Acquire and Store-Release) [v1.0, Ratified]:
Domain: Direct hardware support for C11/C++11 memory orderings.
Instructions: 2 instructions (LB.A, SB.R, etc.).RVWMO / Ztso (Memory Consistency Models) [v2.0 / v1.0, Ratified]:
Domain: Defines Weak Memory Ordering (RVWMO) or Total Store Ordering (Ztso) consistency semantics.
Instructions: 0 new instructions (defines memory architecture execution rules).CMO (Cache Management Operations) [v1.0, Ratified]:
Domain: Explicit cache block management (Zicbom, Zicboz, Zicbop for flush, zero, and prefetch).
Instructions: 5 instructions (CBO.CLEAN, CBO.FLUSH, CBO.INVAL, CBO.ZERO, PREFETCH.*).
D. Floating-Point, Vector & Alternative Register Extensions
F, D, Q (Floating-Point) [v2.2, Ratified]:
Domain: IEEE 754-2008 Single, Double, and Quad precision math.
Instructions: 26 (F) + 26 (D) + 28 (Q) instructions.Zfh & Zfhmin (Half-Precision Floating-Point) [v1.0, Ratified]:
Domain: 16-bit float (FP16) arithmetic for embedded AI/ML models.
Instructions: ~30 instructions (Zfhmin provides conversion-only subset).BF16 (Bfloat16 Extensions) [v1.0, Ratified]:
Domain: Deep learning acceleration using 16-bit Brain Floating Point format.
Instructions: ~6 instructions (VFNCVT.BF16.S, etc.).Zfa (Additional Floating-Point Instructions) [v1.0, Ratified]:
Domain: Extended FP operations (load immediate constants, min/max IEEE semantics).
Instructions: ~10 instructions.Zfinx, Zdinx, Zhinx, Zhinxmin (Floating-Point in Integer Registers) [v1.0, Ratified]:
Domain: Performs FP operations directly inside integer general-purpose registers (x0-x31), removing dedicated FP registers to reduce silicon footprint.
Instructions: Re-encodes FP instructions to reuse integer registers (0 additional registers required).
E. Code Compression & Bit Manipulation
C (Compressed Instructions) [v2.0, Ratified]:
Domain: General 16-bit instruction encodings for code size reduction.
Instructions: 38 compressed formats.Zce (Embedded Code Size Reduction) [v1.0, Ratified]:
Domain: Tailored code-density extensions for microcontrollers (Zca, Zcb, Zcmp, Zcmt).
Instructions: ~15 compressed operations (push/pop sequences, table jumps).B (Bit Manipulation) [v1.0, Ratified]:
Domain: Advanced bitwise logic (includes Zba address generation, Zbb basic bit manipulation, Zbs single-bit actions).
Instructions: ~42 instructions (CLZ, CTZ, CPOP, SH1ADD, BSET).
F. Vectors, Cryptography & Security Primitives
V (Vector Extension) [v1.0, Ratified]:
Domain: Scalable SIMD vector computing for processing large data streams.
Instructions: >200 instructions.Scalar/Vector Cryptography Sub-Extensions (Zbkb, Zbkc, Zbkx, Zk, Zks, Zvbb, Zvbc, Zvkg, Zvkned, Zvknhb, Zvksed, Zvksh, Zvkt) [v1.0, Ratified]:
Domain: Hardware acceleration for AES, SHA-2, SM3/SM4, GCM, and crossbar permutation in both scalar and vector pipelines.
Instructions: ~80 specialized cryptographic instructions across the sub-families.Zicfiss & Zicfilp (Control-Flow Integrity - Shadow Stack & Landing Pad) [v1.0, Ratified]:
Domain: Hardware-enforced Control-Flow Integrity (CFI) to prevent Return-Oriented Programming (ROP) and Jump-Oriented Programming (JOP) exploits.
Instructions: ~4 instructions (SSPUSH, SSPOP, LPAD).
5. The RISC-V Privileged Architecture Instructions
The RISC-V privileged architecture complements the base ISA by providing dedicated execution modes (User, Supervisor, Machine) and instructions required for operating system kernel operations, exception handling, memory protection, and hardware virtualization.
A. Trap & Interrupt Return Instructions
MRET – Machine-mode Trap Return (Restores PC from mepc and reverts privilege level/interrupt state).
SRET – Supervisor-mode Trap Return (Restores PC from sepc and reverts privilege level/interrupt state).
URET – User-mode Trap Return (Optional; restores state from User-level traps).
B. Hypervisor & Virtualization Instructions
HFENCE.VVMA – Hypervisor Guest Virtual-Memory Fence (Flushes guest virtual address translation TLBs).
HFENCE.GVMA – Hypervisor Guest Physical-Memory Fence (Flushes second-stage physical address translation TLBs).
HLV / HLVX – Hypervisor Load Virtual / Load Virtual Execute (Reads memory on behalf of guest VM).
HSV – Hypervisor Store Virtual (Writes memory on behalf of guest VM).
C. Memory Management & Address Translation
SFENCE.VMA – Supervisor Virtual-Memory Fence (Flushes local TLB entries for virtual memory page tables).
D. System Environment & State Management
WFI – Wait for Interrupt (Suspends processor execution to reduce power until an interrupt occurs).
MNRET – Machine Non-Maskable Interrupt Return (Restores state after handling an NMI).
6. GCC/LLVM: The C Startup Stub (crt0.s)
In bare-metal embedded software, executable ELF binaries rely on a C Runtime initialization module (crt0 or reset_handler) to bridge raw chip power-on with the execution of main().
Below is a typical bare-metal assembly startup stub generated or linked by the GCC RISC-V toolchain (crt0.s / start.S):
.section .text.init
.global _start
.type _start, @function
_start:
/* 1. Clear registers to a known state */
li x1, 0
li x2, 0
li x3, 0
/* ... [Clearing x4 to x31] ... */
/* 2. Initialize Stack Pointer (sp) and Global Pointer (gp) */
la sp, _stack_top
.option push
.option norelax
la gp, __global_pointer$
.option pop
/* 3. Copy .data section from Flash/ROM to RAM */
la a0, _sdata
la a1, _edata
la a2, _sidata
bgeu a0, a1, .L_bss_init
.L_copy_data:
lw t0, 0(a2)
sw t0, 0(a0)
addi a0, a0, 4
addi a2, a2, 4
bltu a0, a1, .L_copy_data
.L_bss_init:
/* 4. Zero-fill the .bss section in RAM */
la a0, _sbss
la a1, _ebss
bgeu a0, a1, .L_main_call
.L_zero_bss:
sw zero, 0(a0)
addi a0, a0, 4
bltu a0, a1, .L_zero_bss
.L_main_call:
/* 5. Jump to main application entry point */
call main
.L_exit_loop:
/* 6. Fallback infinite loop if main returns */
j .L_exit_loop
Analysis of the Stub Overhead
The generic CRT stub executes more than 30 to 40 instructions (120–160 bytes) before calling main().
While initializing the .data and .bss sections is essential for complex C applications, a dedicated bare-metal target where variables are explicitly initialized inside main() or held in fixed locations can drastically reduce this footprint.
For an ultra-lightweight environment, the entire startup routine can be optimized down to just 3 instructions (12 bytes):
_start:
la sp, _stack_top /* Set valid stack boundary */
call main /* Jump to user program */
1: j 1b /* Catch unexpected exit */
7. RISC-V Today
The RISC-V architecture is experiencing exponential growth across the entire spectrum of computing:
Embedded Microcontrollers & IoT: Companies like Espressif (ESP32-C3/C6), SiFive, WCH (CH32V series), and Nordic Semiconductor are deploying low-power RISC-V cores to replace legacy 8-bit and 32-bit microcontrollers.
Custom Accelerators & Coprocessors: Major tech vendors (NVIDIA, Google, Western Digital) integrate internal RISC-V cores into GPUs and storage controllers to handle power management and house security engines.
Academic & System Research: Due to its open and royalty-free nature, RISC-V has become the global standard for teaching computer architecture, compiler design, and hardware security (such as custom ISA extensions and secure sandboxes).
8. Conclusion
This article introduced what the GCC toolchain generates for a 32-bit RISC-V target. Even in this modern 32-bit architecture, default toolchains include runtime overhead and generic stubs that can be trimmed or repurposed.
n the next part of this series, we will disassemble the generated test1_rv32i.elf object file. Since pure RV32I lacks a hardware multiplier (which requires the 'M' extension), we will explore how GCC emits software library calls (__mulsi3, __divsi3) to execute basic arithmetic operations, and how it optimizes control flow and stack frame management.
Top comments (0)