RV32 ABI and C Data Types: Sizes, Alignment, and Layout
TL;DR
- You’ll learn the RV32 ILP32 ABI (Application Binary Interface) rules that make C code and assembly “agree” about:
- register usage,
- parameter passing,
- return values,
- stack alignment,
- and data layout.
- You’ll measure and verify type sizes, struct padding, and endianness on RV32.
- You’ll produce small experiments you can inspect in both C and assembly.
The ABI is the contract. If you violate it (even accidentally), you get “weird bugs” that look like stack corruption, bad pointers, or random crashes.
1. RV32 in one sentence
- RV32: registers and addresses are 32-bit.
- Most common teaching ABI: ILP32 (Integer/Long/Pointer are 32-bit).
Typical compiler flags
-march=rv32im(RV32I + multiply/divide)-mabi=ilp32
2. Register roles (the part you must memorize)
RISC-V has 32 integer registers: x0..x31.
x0: always zerox1:ra(Return Address)x2:sp(Stack Pointer)x3:gp(Global Pointer)x4:tp(Thread Pointer)x5..x7:t0..t2(Temporaries)x8:s0/fp(Saved / Frame Pointer)x9:s1(Saved)x10..x17:a0..a7(Arguments / returns)x18..x27:s2..s11(Saved)x28..x31:t3..t6(Temporaries)
2.1. Who preserves what? (The “Responsibility” Rule)
Caller-saved (Temp registers
t*, Argsa*): These are like “scratch paper”. If you (the Caller) have something important in them and you call another function, you must save them first. The function you call is free to overwrite them without asking.Callee-saved (Saved registers
s*): These are like “borrowed tools”. If a function (the Callee) wants to use them, it must put them back exactly how it found them before returning.Return value: By convention, the result is placed in
a0(anda1if it’s 64-bit).
3. Stack rules (the second part you must memorize)
3.1. Stack grows downward
| |
3.2. Alignment
The ABI requires the stack pointer (sp) to be aligned to 16 bytes whenever you call a function.
Why 16 bytes? Why not just 4? Think of the stack like a delivery truck.
- Efficiency: The CPU often moves data in 128-bit (16-byte) chunks (for SIMD - Single Instruction, Multiple Data - vector operations or
long doubletypes). If you park the truck at a weird angle (misaligned), the forklift can’t load the pallet in one go; it has to do two partial loads. - Crash Prevention: Some hardware instructions crash if the address isn’t a multiple of 16.
“At call boundaries” means:
Before you jump to a new function, you must ensure sp is a multiple of 16. If you push 1 word (4 bytes), you must add 12 bytes of padding so the next function starts on a clean 16-byte line.
4. C type sizes on RV32 (ILP32)
These are the typical sizes (verify in your toolchain):
| C type | Typical bytes (RV32 ILP32) |
|---|---|
char | 1 |
short | 2 |
int | 4 |
long | 4 |
long long | 8 |
void* | 4 |
size_t | 4 |
float | 4 |
double | 8 |
5. Hands-on: measure sizes and alignment
Create:
| |
Build and run in QEMU:
| |
5.1. What you should observe
- The
sizetable should match ILP32 expectations. struct Ausually has padding betweenaandbso thatbis aligned to 4 bytes.- Reordering fields (
struct B) typically reduces padding.
5.2. Deep Dive: Type Alignment vs. Stack Alignment
You observed double align=8, but the ABI requires sp to be 16-byte aligned. Confusion is common here. Let’s distinguish the Content from the Container.
5.2.1. The Content Rule (Type Alignment)
Every variable has a “natural alignment”.
char(1 byte) can live anywhere (address divisible by 1).int(4 bytes) must live at an address divisible by 4 (0x1000, 0x1004…).double(8 bytes) must live at an address divisible by 8 (0x1000, 0x1008…).
If you violate this, the CPU generates a Misaligned Access Exception (or does a simpler, slower two-part read).
5.2.2. The Container Rule (Stack Alignment)
The Stack Frame is the container for all these local variables. To be a “universal container”, the stack must be aligned to the strictest requirement of any variable it might hold.
- If the stack were only 4-byte aligned, it could validly start at
0x1004. - If you then tried to place a
double(needs 8-byte align) on the stack, you might be forced to put it at0x1004relative to memory 0, which is illegal for a double.
The Solution:
The RISC-V ABI forces the stack to be 16-byte aligned (divisible by 16).
Since 16 is divisible by 1, 2, 4, and 8, a fresh stack frame is guaranteed to be a safe starting point for any standard data type, including 128-bit SIMD vectors (float128 or v128), without needing complex adjustments.
6. Struct padding explained (with a diagram)
6.1. Example: struct A
| |
One common RV32 layout:
Think of memory as a grid of 4-byte (32-bit) words.
| Offset | Byte 0 | Byte 1 | Byte 2 | Byte 3 | Content |
|---|---|---|---|---|---|
| +0 | a | pad | pad | pad | a takes 1 byte. We skip 3 bytes so the next row starts fresh. |
| +4 | b | b | b | b | b (4 bytes) fits perfectly in a new word. |
| +8 | c | c | pad | pad | c (2 bytes) sits here. We pad the end to align the whole struct size. |
6.2. Complex Example: Mixing char, int, long, long long, double
Let’s look at a struct using all the types you asked about, specifically distinguishing long (32-bit) from long long (64-bit).
| |
Layout Analysis:
csits at +0.ineeds 4-byte alignment. Next available slot is +1, so we skip 3 bytes.istarts at +4.lneeds 4-byte alignment. It fits perfectly at +8. Ends at +12.llneeds 8-byte alignment. +12 is not divisible by 8 (12 % 8 = 4). We need 4 bytes of padding.llstarts at +16.dneeds 8-byte alignment.llended at +24. 24 is divisible by 8.dstarts at +24 immediately.
Memory Grid:
| Offset | Byte 0 | Byte 1 | Byte 2 | Byte 3 | Content |
|---|---|---|---|---|---|
| +0 | c | pad | pad | pad | Aligning for i |
| +4 | i | i | i | i | |
| +8 | l | l | l | l | long is 4 bytes on RV32 |
| +12 | pad | pad | pad | pad | Aligning for ll (must be % 8) |
| +16 | ll (lo) | ll | ll | ll | long long (first half) |
| +20 | ll (hi) | ll | ll | ll | long long (second half) |
| +24 | d (lo) | d | d | d | double (first half) |
| +28 | d (hi) | d | d | d | double (second half) |
Total Size: 32 bytes.
Why padding exists:
- Many CPUs load/store more efficiently (or only correctly) when aligned.
- The ABI chooses rules that balance performance and compatibility.
7. Endianness and what it means for C
Most RV32 targets are little-endian.
If you store 0x11223344 in memory, bytes appear as:
| address | +0 | +1 | +2 | +3 |
| bytes | 44 | 33 | 22 | 11 |
7.1. Hands-on: confirm endianness
| |
Compile/run:
| |
8. ABI meets assembly: parameters and return values
Consider:
| |
At the ABI level:
aarrives ina0barrives ina1- return value goes back in
a0
In disassembly you’ll often see:
- compute in a register
- ensure result ends in
a0 - return via
jalrusingra
9. Exercises
- Change
struct Aby adding auint8_t d;at the end. Predict the new size before compiling. - Create a packed version:
1struct __attribute__((packed)) P { u8 a; u32 b; }; - Compare
sizeof(struct P)with the unpacked version. - Write a function that returns a
u64. Observe which registers carry the return value.
__attribute__((packed)) can cause misaligned accesses. Some CPUs handle them slowly; others can fault. Use packed structs only when you control every access and you need an exact layout (e.g., wire formats).9.1. How to test your answers
- Verify sizes and offsets using
sizeofand theOFFSETOFmacro pattern. - Use
objdump -d -M numeric,no-aliasesto confirm which registers are used.
10. Summary
You learned the RV32 ILP32 ABI “contract”: register roles, stack rules, and how C types map to bytes.
Next: C → assembly + optimizations - you’ll learn how -O changes what you see in disassembly, and how volatile really affects generated code.