Lab: RISC-V Assembly Part 7 — printf, Hello World, and Structs¶
Overview¶
This hands-on lab session ties together everything we have learned about RISC-V assembly so far and points it directly at Project 3. We start by learning to debug assembly with GDB so you can watch your code run one instruction at a time. We then learn to call C library functions from assembly — most importantly printf — which lets us write a classic "Hello, World" entirely in assembly while still respecting the calling convention. Finally, we lay the groundwork for the next major data structure: C structs. We look at how a struct is laid out in memory, how it compares to an array and a string, and how to read individual struct fields in assembly using lw with byte offsets. These struct-access patterns are exactly what Project 3 needs for the reverse-string problem and the upcoming linked-list work.
Learning Objectives¶
- Drive GDB to debug a RISC-V assembly program: set breakpoints, step, examine registers and memory, and continue.
- Distinguish the
.dataand.textsections and place strings and instructions correctly. - Use the
la(load address) pseudo-instruction to load the address of a string into a register. - Call
printf(and other C functions) from assembly, setting upa0–a7correctly. - Apply the calling convention: preserve caller-saved registers and keep
sp16-byte aligned across acall. - Describe how a C struct is laid out sequentially in memory, and why arrays, strings, and structs all reduce to "bytes at offsets."
- Read struct fields in assembly using
lwwith the correct byte offset from the struct's base address. - Connect these techniques to Project 3 (reverse string and the introduction to structs / linked lists).
Prerequisites¶
- RISC-V Assembly Parts 1–6: instructions, registers, labels, directives, branches, loops, and functions.
- The RISC-V calling convention: caller-saved vs. callee-saved registers, the stack, and stack alignment (see the RISC-V guide).
- C strings as null-terminated byte arrays, and byte-level memory access with
lb/sb. - Pointer-based and index-based array access in assembly.
- Comfort at a Linux shell on the RISC-V VM (assembling, compiling, linking with
gcc).
1. Project 3 in Context¶
Project 3 continues the assembly work from Project 2 but shifts focus to strings and introduces composite data. Today's lab gives you the three tools you need to get started:
- GDB — so you can step through your assembly and see exactly what each instruction does to the registers and memory.
printffrom assembly — so you can print intermediate values while developing, and to satisfy any "print the result" requirement.- Structs in memory — the conceptual bridge from arrays and strings to linked lists.
The first concrete Project 3 task is reversing a string. A string is just an array of bytes, so reversing it is analogous to reversing an integer array, with two differences:
- Use
lb(load byte) andsb(store byte) instead oflw/sw, because each character is one byte. - Do not reverse the null terminator. The
\0must stay at the end. Reverse only the characters before it.
The reversal can be done in place (swap the ends and walk inward) or by copying into a destination buffer (walk the source backward, write the destination forward). Either approach is fine; in-place is the classic two-pointer swap.
// In-place string reversal in C (the algorithm you will port to assembly)
void reverse_s(char *s) {
int len = 0;
while (s[len] != '\0') { // find length, NOT counting '\0'
len++;
}
int i = 0;
int j = len - 1;
while (i < j) {
char tmp = s[i];
s[i] = s[j];
s[j] = tmp;
i++;
j--;
}
// s[len] == '\0' is never touched
}
Common mistake: including the
\0in the swap. If you swaps[0]with the terminator, the string becomes empty (the first byte is now\0). Always reverse the range[0, len-1]wherelenexcludes the terminator.
2. Debugging Assembly with GDB¶
The instructor opened the lab on GDB because debugging assembly without a debugger is painful. With GDB you can watch your program execute one instruction at a time, inspect every register, and look at raw memory. The learning curve is real, but the payoff is a precise mental model of what the machine is doing.
Starting GDB¶
Here prog is your compiled/linked executable. Once inside, you work with a small set of commands. The handwritten notes list exactly these:
| Command | Short form | What it does |
|---|---|---|
break <line> |
b <line> |
Set a breakpoint at a line or label. |
run [args] |
r [args] |
Run the program, passing optional command-line args. |
step |
s |
Execute one source step, stepping into function calls. |
next |
n |
Execute one source step, stepping over function calls. |
print <expr> |
p <expr> |
Print a value (e.g., p $a0 for a register). |
x/<fmt> <addr> |
x/... |
Examine raw memory at an address. |
continue |
c |
Resume execution until the next breakpoint. |
The notes abbreviate
runasrand addrenas a reminder thatrmeans "run." In GDB you can pass program arguments afterrun, e.g.run hello 42.
A typical debugging flow¶
flowchart TD
A["gdb prog"] --> B["break main (or a label)"]
B --> C["run args"]
C --> D{"Stopped at breakpoint"}
D --> E["step / next one instruction"]
E --> F["print registers: p $a0"]
F --> G["x/ examine memory"]
G --> H{"Bug found?"}
H -- no --> E
H -- "skip ahead" --> I["continue"]
I --> D
H -- yes --> J["Fix the .s file, rebuild"]
Examining registers and memory¶
To print a register, prefix its name with $:
(gdb) p $a0 # value in a0 (decimal)
(gdb) p/x $a0 # value in a0 (hex)
(gdb) info registers # dump all registers
The x command examines memory. Its general form is x/NFU addr where N is a count, F is a format, and U is a unit size:
(gdb) x/4xw $sp # 4 words, hex, starting at the stack pointer
(gdb) x/s $a0 # print the C string whose address is in a0
(gdb) x/8xb $a0 # 8 bytes in hex starting at a0
Examining memory is how you check that a string looks right (e.g., x/s $a0 should show your text), or that a struct's fields hold the values you expect.
A debugging strategy that scales¶
A naive approach is to step from the very first instruction, but real programs have far too many instructions for that. The instructor's strategy:
- Set a breakpoint at a meaningful point — for example, right where a particular array element is loaded, or just before a
call. - Use
continueto fly through the uninteresting code and stop only where it matters. - Step instruction-by-instruction only inside the region you care about.
The goal is not just to fix one bug; it is to build a mental model of how registers change and how the stack grows and shrinks. After a few sessions, GDB stops feeling like overhead and starts feeling like X-ray vision.
Tip: If your assembly was assembled with debug info (
as -g/gcc -g), you can break on labels by name, e.g.break reverse_s.
3. The .data and .text Sections¶
Before we can call printf, we need somewhere to put the string we want to print. Assembly source is divided into sections that tell the assembler and linker how to place things in the final program's memory image:
.textholds your instructions (the machine code). It is read-only and executable..dataholds initialized global data, such as strings and constants. It is readable and writable.
A string literal goes in .data and is given a label so we can refer to its address:
Then your code goes in .text:
The memory layout of the program separates these regions. Conceptually:
high addresses
+------------------+
| stack | grows down (locals, saved regs)
+------------------+
| ... |
+------------------+
| heap | grows up (malloc)
+------------------+
| .data / .bss | globals (our string lives here)
+------------------+
| .text | our instructions
+------------------+
low addresses
The
.stringdirective emits each character byte followed by a\0terminator automatically..ascizis a synonym..asciidoes not add the terminator — use.stringsoprintfknows where the text ends.
4. The la Pseudo-Instruction¶
To print a string with printf, we must put the address of that string into a0. The clean way to do that is the la (load address) pseudo-instruction:
la is a pseudo-instruction: it is not a single real RISC-V instruction. The assembler expands it into the real instructions needed to materialize a full 64-bit address into the register. On RV64 this is typically a lui/addi pair (PC-relative auipc/addi), and for some addresses the assembler emits a load (ld) from a constant pool. You do not have to worry about the expansion — you just write la a0, msg and the assembler does the rest.
# What you write:
la a0, msg
# What the assembler may produce (conceptually):
auipc a0, %pcrel_hi(msg) # upper bits of the PC-relative address
addi a0, a0, %pcrel_lo(msg) # lower 12 bits
This mirrors other pseudo-instructions you have seen:
| Pseudo-instruction | Real expansion | Purpose |
|---|---|---|
li a0, 99 |
addi a0, zero, 99 |
Load a small immediate. |
la a0, msg |
auipc/addi (or ld) |
Load a label's address. |
mv a0, a1 |
addi a0, a1, 0 |
Copy a register. |
j label |
jal zero, label |
Unconditional jump. |
call f |
jal ra, f |
Call a function. |
ret |
jalr zero, ra, 0 |
Return. |
Why not
li a0, msg?liis for integer constants, not for symbol addresses. Uselafor "the address of this label." Usingliwith a label will not assemble the way you want.
5. Calling C Functions from Assembly¶
A huge benefit of following the standard calling convention is that your assembly can call C library functions, and C can call your assembly. To call an external function like printf you do two things:
- Declare it global/external so the linker can find it. In practice,
printflives in the C library you link against (viagcc), so you simply reference it. If you define your own entry point, mark it.global(e.g.,.global main). - Set up the argument registers and use
call.
The RISC-V argument registers are a0–a7. For printf:
a0= pointer to the format string.a1,a2,a3, … = the additional arguments referenced by%d,%s, etc.
# printf("x = %d\n", value);
la a0, fmt # a0 = address of "x = %d\n"
mv a1, t0 # a1 = the integer to print (the first %d)
call printf
Preserving registers across a call¶
printf is a function, so it follows the calling convention — which means it is allowed to clobber all caller-saved registers: a0–a7, t0–t6, and ra. If you need any of those values after the call, you must save them first (typically on the stack) and restore them afterward.
flowchart TD
A["Need a value after the call?"] --> B{"Is it in a caller-saved reg<br/>(a0-a7, t0-t6, ra)?"}
B -- yes --> C["Save it to the stack before call"]
B -- no --> D["It is callee-saved (s0-s11) —<br/>printf preserves it for you"]
C --> E["call printf"]
E --> F["Restore from stack after call"]
D --> E
Two practical rules from the lab:
ramust be saved by any function that itself makes acall(a non-leaf function).printfwill overwriterawith its own return address. If your function calledprintfand then tried toretwithout restoringra, it would jump to the wrong place.- Keep
sp16-byte aligned at the call boundary. Allocate a frame in multiples of 16.
Worked example: register preservation¶
Suppose you are mid-computation with a running total in t0, and you want to print it without losing it:
.data
fmt: .string "total = %d\n"
.text
.global main
main:
addi sp, sp, -16 # allocate a 16-byte frame (aligned)
sd ra, 0(sp) # save ra (we are about to call printf)
sd t0, 8(sp) # save t0 — printf may clobber it (caller-saved)
la a0, fmt # a0 = format string
mv a1, t0 # a1 = the value to print
call printf # printf may trash a0-a7, t0-t6, ra
ld t0, 8(sp) # restore t0 for later use
ld ra, 0(sp) # restore ra so our ret works
addi sp, sp, 16 # free the frame
ret
Common mistake: forgetting to save
t0because "it's just a temporary." Temporaries are caller-saved, which means the caller (you) is responsible for preserving them across a call. Ifprintfoverwritest0, that is allowed by the convention — the bug is in your code, notprintf.Note: You can write an entire application in assembly, including all the
printfcalls, but in this course that is rarely the point. Write the small piece that must be assembly, and let C handle I/O and glue.
6. "Hello, World" in Assembly¶
Putting Sections 3–5 together gives the classic program. This is a complete, runnable RISC-V (RV64) assembly file:
# hello.s — Hello, World in RISC-V assembly
.data
msg: .string "Hello, World\n"
.text
.global main
main:
addi sp, sp, -16 # 16-byte aligned frame
sd ra, 0(sp) # save ra: we call printf (non-leaf)
la a0, msg # a0 = address of the string
call printf # printf(msg)
li a0, 0 # return 0 from main
ld ra, 0(sp) # restore ra
addi sp, sp, 16 # free the frame
ret
Assemble, link, and run it on the RISC-V VM:
You can also assemble separately and link:
as -o hello.o hello.s # assemble to object file
gcc -o hello hello.o # link with the C library (provides printf)
./hello
Why does this work? main is the entry point the C runtime calls. We mark it .global so the linker finds it. We save ra because call printf overwrites it. We load the string address with la, put it in a0 (the format string argument), and call printf. We return 0 by placing it in a0 before ret.
Printing a value¶
To print a number, add a format specifier and an argument:
.data
fmt: .string "answer = %d\n"
.text
.global main
main:
addi sp, sp, -16
sd ra, 0(sp)
la a0, fmt # a0 = "answer = %d\n"
li a1, 42 # a1 = first %d argument
call printf # printf("answer = %d\n", 42)
li a0, 0
ld ra, 0(sp)
addi sp, sp, 16
ret
Common mistakes when calling printf - Forgetting to save
ra→ your function returns to the wrong address and crashes. - Putting the value to print ina0instead ofa1→a0must be the format string. - Misaligningsp(frame not a multiple of 16) → undefined behavior, possible crash. - Using.asciiinstead of.string→ no null terminator, soprintfreads past your text.
7. Structs in Memory — From Arrays to Structs¶
The second half of the lab introduces C structs by first revisiting two things you already understand: arrays and strings. The big idea the instructor emphasized is that all of these are just bytes laid out sequentially in memory — the type system tells the compiler how to interpret and index those bytes, but the memory itself has no metadata.
Arrays in memory¶
An int array stores its elements sequentially, lowest address first. With 4-byte ints:
memory (low -> high)
arr -----> +--------+ offset 0
| 70 | arr[0] (4 bytes)
+--------+ offset 4
| 90 | arr[1] (4 bytes)
+--------+ offset 8
| 99 | arr[2] (4 bytes)
+--------+
The name arr is the base address — the address of arr[0]. To reach element i, add i * 4 bytes (because each int is 4 bytes). This is exactly the index-based array access you already wrote: addr = base + i * sizeof(element).
Strings in memory¶
A C string is an array of char, where each char is 1 byte, terminated by a \0:
memory (low -> high)
s ----> +------+ offset 0
| 'f' | 0x66
+------+ offset 1
| 'o' | 0x6F
+------+ offset 2
| 'o' | 0x6F
+------+ offset 3
| '\0' | 0x00 <- terminator marks the end
+------+
The pointer s holds the address of the first byte ('f'). Because each element is one byte, you advance one address at a time and access bytes with lb/sb. The \0 is how every C string function (and printf's %s) knows where the string ends — there is no separate length stored anywhere.
Structs in memory¶
A struct groups several fields. Like an array, the fields are placed sequentially in memory, lowest address first, but unlike an array the fields can have different types:
memory (low -> high)
node -----> +--------+ offset 0
| x | field x (4 bytes)
+--------+ offset 4
| y | field y (4 bytes)
+--------+
Key points the instructor stressed:
- A struct is laid out field by field, lowest address first — just like an array, but with named fields of possibly different types.
- A C struct carries no automatic metadata — there is no hidden length or type tag stored at runtime. It is just bytes. The compiler remembers the field offsets at compile time.
- The compiler may insert padding between fields so each field meets its alignment requirement (e.g., an
intis usually aligned to a 4-byte boundary, adoubleto 8). Forstruct node_stwith twoints there is no padding:xat offset 0,yat offset 4, total size 8.
int main(void) {
struct node_st node; // one node on the stack
node.x = 70;
node.y = 99;
return 0;
}
The whole point: arrays, strings, and structs are all contiguous bytes, and accessing any field or element is just base address + offset. That single idea unlocks struct access in assembly.
8. Accessing Struct Fields in Assembly¶
Because a struct is just bytes at known offsets, reading a field in assembly is lw with the byte offset of that field added to the struct's base address. Consider:
struct node_st {
int x; // offset 0
int y; // offset 4
};
int get_x(struct node_st *np) {
return np->x;
}
int get_y(struct node_st *np) {
return np->y;
}
The caller passes a pointer to the struct in a0 (its base address). The field offsets are fixed at compile time: x is at offset 0, y is at offset 4.
get_x — first field, offset 0¶
lw a0, (a0) loads the word at the address in a0 (offset 0). Since x is the first field, no offset is needed — the struct's base address is the address of x.
get_y — second field, offset 4¶
There are two equivalent ways to reach y. The first computes the address explicitly, matching the handwritten notes:
# a0 - struct node_st *np
.global get_y
get_y:
addi a0, a0, 4 # a0 = &np->y (base + 4 bytes)
lw a0, (a0) # a0 = *(np + 4) = np->y
ret
The cleaner, idiomatic form uses the load offset built into lw:
# a0 - struct node_st *np
.global get_y
get_y:
lw a0, 4(a0) # a0 = *(np + 4) = np->y (one instruction)
ret
Both produce the same result. lw a0, 4(a0) reads the word at address a0 + 4. This is exactly the offset syntax you used for arrays — accessing arr[1] is lw t0, 4(a0), and accessing the second struct field is also lw a0, 4(a0). An array index and a struct field offset are the same machine operation.
flowchart LR
A["a0 = np (base address)"] --> B["lw a0, 0(a0)<br/>reads field x"]
A --> C["lw a0, 4(a0)<br/>reads field y"]
B --> D["offset 0 = x"]
C --> E["offset 4 = y"]
Mapping C field access to assembly¶
| C expression | Field offset | Assembly (np in a0) |
|---|---|---|
np->x |
0 | lw a0, 0(a0) (or lw a0, (a0)) |
np->y |
4 | lw a0, 4(a0) |
np->x = v (v in a1) |
0 | sw a1, 0(a0) |
np->y = v (v in a1) |
4 | sw a1, 4(a0) |
These offsets feed directly into linked lists, which we cover next session: a linked-list node is just a struct whose last field is a pointer to the next node, accessed with another fixed offset (and ld for a 64-bit pointer on RV64).
Common mistake: using the wrong offset because you forgot a field's size, or because padding changed the layout. Always work out the byte offset of each field. For
intfields with no padding, the offsets are 0, 4, 8, … For a pointer field on RV64, it occupies 8 bytes and must be loaded withld, notlw.
9. Preserving Registers When Calling C Functions (Recap with FindMax)¶
The lab revisited the calling convention through a FindMax-over-linked-list discussion. The takeaways are worth pinning down because they trip people up constantly:
s0–s11(saved registers) survive calls for you. If you put a value ins1and thencallsomething,s1still holds your value afterward — but only because the callee promised to save and restore it. That promise has a cost: if you clobber ansregister, you must save it on entry and restore it on exit, because your caller is relying on the same promise.a0–a7,t0–t6,ra(caller-saved) do not survive calls. If you need them after acall, save them yourself first.
A function that calls itself (or another function) in the middle of using values illustrates the rule perfectly: any caller-saved register you still need after the recursive/other call must be spilled to the stack before the call and reloaded after.
# Sketch: a non-leaf function that must keep a running max across calls.
# Use an s-register for the value you want to survive the call, OR
# spill a caller-saved register around each call.
find_max:
addi sp, sp, -16
sd ra, 0(sp) # non-leaf: must save ra
sd s1, 8(sp) # we will use s1, which is callee-saved -> save it
# ... use s1 to hold the running max; it survives any inner call ...
call helper # ra clobbered, but we saved it; s1 preserved by callee rules
ld s1, 8(sp) # restore the caller's s1
ld ra, 0(sp)
addi sp, sp, 16
ret
The decision tree:
flowchart TD
A["I need register R to keep its value across a call"] --> B{"What class is R?"}
B -- "a0-a7 / t0-t6 / ra (caller-saved)" --> C["Save R to stack before call,<br/>restore after"]
B -- "s0-s11 (callee-saved)" --> D["The callee preserves R for me.<br/>But if I CLOBBER an s-reg,<br/>I must save/restore it on entry/exit"]
Key Concepts¶
| Concept | Definition | Example |
|---|---|---|
| GDB | Command-line debugger to run code step-by-step and inspect state | gdb prog, then b main, r, s, p $a0 |
.data section |
Region for initialized global data (strings, constants) | msg: .string "Hi\n" |
.text section |
Region for executable instructions | .global main / main: |
la (load address) |
Pseudo-instruction that loads a label's address into a register | la a0, msg |
call |
Pseudo-instruction jal ra, f — calls a function, sets ra |
call printf |
| printf args | a0 = format string, a1–a7 = subsequent arguments |
la a0, fmt; mv a1, t0; call printf |
| Caller-saved | a0–a7, t0–t6, ra — caller must save across calls |
Spill t0 before call printf |
| Callee-saved | s0–s11, sp — callee must preserve; survive calls |
Save s1 on entry, restore on exit |
| Struct layout | Fields placed sequentially, lowest address first, no metadata | x at offset 0, y at offset 4 |
| Field access | Read a field with lw offset(base) |
lw a0, 4(a0) reads np->y |
| Padding | Extra bytes inserted so fields meet alignment requirements | A double after an int may force a 4-byte gap |
Practice Problems¶
Problem 1: Hello, World, fix the bug¶
The following assembly is supposed to print Hello, World but it crashes. Find and fix the bug.
Click to reveal solution
`main` calls `printf`, which overwrites `ra`. The code never saves and restores `ra`, so the final `ret` jumps to wherever `printf` left `ra` — a crash. Allocate a 16-byte aligned frame, save `ra` before the call, and restore it before `ret`:Problem 2: Print a register without losing it¶
You have a value in t0 that you still need after printing it. Write assembly that calls printf("v = %d\n", t0) and keeps t0 intact afterward. Assume fmt is defined in .data as "v = %d\n".
Click to reveal solution
`t0` is caller-saved, so `printf` may clobber it. Save it across the call: Putting `t0` in `a1` (not `a0`) is essential — `a0` must be the format string.Problem 3: Read a struct field¶
Given struct point { int x; int y; }; and a pointer to a point in a0, write a leaf function get_y that returns p->y in a0. Give both the explicit-address form and the one-instruction form.
Click to reveal solution
`y` is the second `int`, at byte offset 4. Explicit address computation: Idiomatic one-instruction form using the load offset: Both are leaf functions (no calls), so no stack frame and no `ra` save are needed.Problem 4: Set a struct field¶
Given the same struct point and a pointer in a0, write set_x that stores the value in a1 into p->x (i.e., p->x = v). Then write set_y.
Click to reveal solution
`x` is at offset 0, `y` is at offset 4. Use `sw` (store word): Note the operand order for `sw`: the **source register** (`a1`) comes first, then the **memory address** (`offset(base)`).Problem 5: Reverse a string in place¶
Reverse the C string at the address in a0 in place, not touching the \0. Write the algorithm in C, then describe how each step maps to assembly (which load/store and which pointers).
Click to reveal solution
Mapping to assembly: - Finding the length: walk a pointer with `lb` until you read `0` (the `\0`). `lb t0, (a0)`; `beq t0, zero, done_len`; `addi a0, a0, 1`; loop. Count the bytes before the terminator. - Two indices/pointers `i` and `j`: one at the front, one at the last character (`len - 1`), **not** at the `\0`. - Swap loop: `lb` the front byte and the back byte, `sb` them swapped, advance the front pointer (`+1`), retreat the back pointer (`-1`), continue while `i < j`. - Because each character is one byte, use `lb`/`sb` (not `lw`/`sw`) and step pointers by 1. The terminator at `s[len]` is never in the `[0, len-1]` range, so it stays put.Problem 6: Which registers must be saved?¶
A function compute puts a partial result in t1, calls printf to log it, and then needs t1 again to finish. It also calls printf a second time. List every register that must be saved on entry and explain why.
Click to reveal solution
Registers to save on entry: - **`ra`** — `compute` is non-leaf (it calls `printf`). Each `call printf` overwrites `ra`, so the original return address must be preserved or the final `ret` fails. - **`t1`** — it is caller-saved, and `printf` may clobber it. Since `t1` is needed *after* the first `printf`, it must be spilled before the call and restored after. Any other caller-saved register (`a*`, other `t*`) only needs saving if its value is needed across a call. Callee-saved registers (`s0`–`s11`) do **not** need saving unless `compute` itself writes to them. A correct, aligned frame:Further Reading¶
- Course RISC-V reference: /guides/riscv/
- Course key concepts: /guides/key-concepts/
- Source notes (handwritten): /notes/CS315-01 2025-09-17 Lab RISC-V Assembly 7.pdf
- GDB Documentation
- GDB Cheat Sheet (Beej's Quick Guide)
- RISC-V Assembly Programmer's Manual
- RISC-V ELF psABI (calling convention)
- cppreference: struct (data structure)
- printf format specifiers (cppreference)
Summary¶
-
GDB lets you debug assembly precisely. Start with
gdb prog; useb/r/s/n/p/x/cto set breakpoints, step, inspect registers and memory, and continue. Break at meaningful points andcontinuebetween them rather than stepping the whole program. -
Code and data live in different sections. Put strings and constants in
.data; put instructions in.text. Use.stringso string literals get a null terminator automatically. -
laloads a label's address. It is a pseudo-instruction the assembler expands into the real instructions that build a 64-bit address. Usela a0, msgto pointa0at a string before callingprintf. -
Calling
printffollows the convention. Put the format string address ina0and additional arguments ina1–a7, thencall printf. Mark your entry point.global. -
Preserve caller-saved registers across calls.
printfmay clobbera0–a7,t0–t6, andra. Save anything you still need (especiallyrain a non-leaf function) on a 16-byte-aligned stack frame, and restore it afterward. -
"Hello, World" combines all of the above: a
.datastring, a.global mainin.text, savera,la a0, msg,call printf, restorera, return0. -
Arrays, strings, and structs are all just sequential bytes. A struct's fields are laid out lowest address first, with no runtime metadata; the compiler may add padding for alignment. For
struct node_st { int x; int y; },xis at offset 0 andyat offset 4. -
Reading a struct field is
lw offset(base). With the struct pointer ina0,lw a0, (a0)readsxandlw a0, 4(a0)readsy. This is the same base+offset mechanic as array indexing, and it generalizes directly to linked-list nodes in the next session.