` | `x` | Examine memory |
| `continue` | `c` | Run to next breakpoint |
---
## GDB Debugging Flow
flowchart TD
A["gdb prog"] --> B["break main"]
B --> C["run args"]
C --> D{"Stopped at breakpoint"}
D --> E["step / next"]
E --> F["p $a0 / x/s $a0"]
F --> G{"Bug found?"}
G -- no --> E
G -- "skip ahead" --> H["continue"]
H --> D
G -- yes --> I["Fix .s, rebuild"]
---
## Examining Registers and Memory
```text
(gdb) p $a0 # value in a0 (decimal)
(gdb) p/x $a0 # value in a0 (hex)
(gdb) info registers # dump all registers
```
```text
(gdb) x/4xw $sp # 4 words in hex from stack pointer
(gdb) x/s $a0 # C string whose address is in a0
(gdb) x/8xb $a0 # 8 bytes in hex from a0
```
Build with gcc -g so you can break on labels by name: break reverse_s
---
## GDB Strategy: Don't Step Everything
1. Set a breakpoint at a **meaningful point** (near the bug, before a `call`)
2. Use `continue` to skip uninteresting code
3. Step instruction-by-instruction only where it matters
Goal: build a mental model of how registers change and how the stack grows/shrinks — not just fix one bug.
---
## Memory Sections: `.data` and `.text`
| Section | Contents | Permissions |
|---------|----------|-------------|
| `.text` | Instructions (machine code) | Read + Execute |
| `.data` | Initialized global data (strings, constants) | Read + Write |
```text
.data
msg: .string "Hello, World\n" # bytes + auto null terminator
.text
.global main
main:
# ... instructions here ...
```
Use .string (or .asciz), NOT .ascii — .ascii omits the \0 terminator.
---
## Program Memory Layout
```text
high addresses
+------------------+
| stack | grows down (locals, saved regs)
+------------------+
| heap | grows up (malloc)
+------------------+
| .data / .bss | globals — our strings live here
+------------------+
| .text | our instructions
+------------------+
low addresses
```
---
## The `la` Pseudo-Instruction
```text
la a0, msg # a0 = address of label 'msg'
```
`la` is a **pseudo-instruction** — the assembler expands it to real instructions:
```text
auipc a0, %pcrel_hi(msg) # upper bits, PC-relative
addi a0, a0, %pcrel_lo(msg) # lower 12 bits
```
| Pseudo | Real expansion | Purpose |
|--------|----------------|---------|
| `li a0, 99` | `addi a0, zero, 99` | Load small immediate |
| `la a0, msg` | `auipc`/`addi` | Load label address |
| `mv a0, a1` | `addi a0, a1, 0` | Copy register |
| `call f` | `jal ra, f` | Call function |
| `ret` | `jalr zero, ra, 0` | Return |
---
## Calling `printf` from Assembly
`printf` follows the standard calling convention:
- **`a0`** = pointer to the **format string**
- **`a1`, `a2`, ...** = additional arguments (`%d`, `%s`, etc.)
```text
# printf("x = %d\n", value);
.data
fmt: .string "x = %d\n"
.text
la a0, fmt # a0 = format string address
mv a1, t0 # a1 = integer to print
call printf
```
Common mistake: putting the value in a0 instead of a1 — a0 must always be the format string.
---
## Preserving Registers Across Calls
`printf` may clobber all **caller-saved** registers: `a0`–`a7`, `t0`–`t6`, `ra`.
flowchart TD
A["Need a value after the call?"] --> B{"Register class?"}
B -- "a0-a7 / t0-t6 / ra" --> C["Save to stack before call,
restore after"]
B -- "s0-s11" --> D["Callee preserves it for you"]
C --> E["call printf"]
D --> E
E --> F["Restore saved regs if needed"]
---
## Stack Alignment Rule
**Keep `sp` 16-byte aligned** at every `call` boundary.
```text
addi sp, sp, -16 # allocate 16-byte frame
sd ra, 0(sp) # save ra (non-leaf)
sd t0, 8(sp) # save t0 (caller-saved, needed after call)
la a0, fmt
mv a1, t0
call printf # may trash a0-a7, t0-t6, ra
ld t0, 8(sp) # restore t0
ld ra, 0(sp) # restore ra
addi sp, sp, 16 # free the frame
```
Why save ra? call printf overwrites ra with printf's return address. Without saving it, your ret jumps to the wrong place.
---
## Hello, World in Assembly
```text
# hello.s — Hello, World in RISC-V (RV64) 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: non-leaf (calls printf)
la a0, msg # a0 = address of 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
```
---
## Building and Running Hello World
```bash
# Option 1: compile + assemble + link in one step
gcc -o hello hello.s
./hello
# Hello, World
# Option 2: assemble separately, then link
as -o hello.o hello.s
gcc -o hello hello.o
./hello
```
gcc links against libc automatically, which provides printf. Mark your entry point .global main so the linker finds it.
---
## Printing a Value with `printf`
```text
.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
```
```bash
$ gcc -o ans ans.s && ./ans
answer = 42
```
---
## Common printf Mistakes
| Mistake | Consequence |
|---------|-------------|
| Forgetting to save `ra` | `ret` jumps to wrong address, crash |
| Value in `a0` instead of `a1` | Format string is garbage |
| `sp` not 16-byte aligned | Undefined behavior, possible crash |
| `.ascii` instead of `.string` | No `\0` — `printf` reads past your text |
---
## Arrays in Memory
```c
int arr[3] = {70, 90, 99};
```
```text
arr ----> +--------+ offset 0
| 70 | arr[0] (4 bytes)
+--------+ offset 4
| 90 | arr[1] (4 bytes)
+--------+ offset 8
| 99 | arr[2] (4 bytes)
+--------+
```
- Base address = address of `arr[0]`
- Element `i` is at `base + i * 4` bytes
- Access: `lw t0, 0(a0)` → `arr[0]`, `lw t0, 4(a0)` → `arr[1]`
---
## Strings in Memory
```c
char *s = "foo";
```
```text
s ----> +------+ offset 0 'f' 0x66
+------+ offset 1 'o' 0x6F
+------+ offset 2 'o' 0x6F
+------+ offset 3 '\0' 0x00 <- terminator
```
- Each element is **1 byte** — use `lb`/`sb`, advance by 1
- No separate length field — `\0` marks the end
---
## Structs in Memory
```c
struct node_st { int x; int y; };
```
```text
node ----> +--------+ offset 0
| x | field x (4 bytes)
+--------+ offset 4
| y | field y (4 bytes)
+--------+
```
Fields are placed sequentially, lowest address first — just like an array, but with different-typed, named fields. No runtime metadata is stored.
---
## Arrays, Strings, Structs: One Idea
flowchart LR
A["base address"] --> B["+ offset 0\narr[0] / s[0] / node.x"]
A --> C["+ offset 4\narr[1] / node.y"]
A --> D["+ offset N\n..."]
**All three reduce to: base address + byte offset**
The type system tells the compiler the offsets; the machine just sees bytes.
---
## Padding and Alignment
The compiler may insert **padding** so each field meets its alignment requirement:
| Type | Size | Alignment |
|------|------|-----------|
| `char` | 1 byte | 1 byte |
| `int` | 4 bytes | 4 bytes |
| `double` | 8 bytes | 8 bytes |
| pointer (RV64) | 8 bytes | 8 bytes |
```c
struct node_st { int x; int y; }
// x at offset 0, y at offset 4 — no padding needed
```
Always work out byte offsets explicitly. Use ld (not lw) for 64-bit pointer fields on RV64.
---
## Reading Struct Fields in Assembly
```c
struct node_st { int x; // offset 0
int y; // offset 4 };
int get_x(struct node_st *np); // np in a0
int get_y(struct node_st *np); // np in a0
```
```text
get_x:
lw a0, 0(a0) # a0 = *(np + 0) = np->x
ret
get_y:
lw a0, 4(a0) # a0 = *(np + 4) = np->y
ret
```
Both are **leaf functions** — no calls, no stack frame needed.
---
## Struct Field Access: Visual
flowchart LR
A["a0 = np (base address)"] --> B["lw a0, 0(a0)\nreads x"]
A --> C["lw a0, 4(a0)\nreads y"]
B --> D["offset 0 = x"]
C --> E["offset 4 = y"]
---
## C Field Access to Assembly Mapping
| C expression | Offset | Assembly (np in `a0`) |
|--------------|--------|------------------------|
| `np->x` | 0 | `lw a0, 0(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)` |
For sw: source register first, then offset(base). Same pattern extends to linked-list nodes — the next pointer is just another field at a fixed offset.
---
## Callee-Saved vs Caller-Saved (Recap)
| Register class | Registers | Who preserves? |
|----------------|-----------|----------------|
| Caller-saved | `a0`–`a7`, `t0`–`t6`, `ra` | **You** save before a call |
| Callee-saved | `s0`–`s11`, `sp` | **Callee** saves on entry |
```text
find_max:
addi sp, sp, -16
sd ra, 8(sp) # non-leaf: save ra
sd s1, 0(sp) # using s1 for running max — save it
call helper # s1 survives (callee promise)
ld s1, 0(sp)
ld ra, 8(sp)
addi sp, sp, 16
ret
```
---
## Which Registers Need Saving?
flowchart TD
A["Need R to survive a call?"] --> B{"Register class?"}
B -- "a0-a7 / t0-t6 / ra" --> C["Save to stack before call,\nrestore after"]
B -- "s0-s11" --> D["Callee preserves it.\nBut if YOU write to an s-reg,\nyou must save/restore it."]
---
## Key Concepts Summary Table
| Concept | Example |
|---------|---------|
| `.data` string | `msg: .string "Hi\n"` |
| `.text` code | `.global main` / `main:` |
| `la` pseudo | `la a0, msg` |
| `call printf` | `a0`=fmt, `a1`=first arg |
| Save `ra` | `sd ra, 0(sp)` before `call` |
| Struct field read | `lw a0, 4(a0)` → `np->y` |
| Struct field write | `sw a1, 4(a0)` → `np->y = v` |
---
## Summary
1. **GDB** lets you step through assembly, inspect registers (`p $a0`, `x/s $a0`), and break at labels.
2. **`.data`** holds strings and constants; **`.text`** holds instructions. Use `.string` for null-terminated strings.
3. **`la a0, msg`** loads a label's address — use it to point `a0` at a format string before `call printf`.
4. **`printf` convention**: `a0` = format string, `a1`–`a7` = arguments. Always a 16-byte-aligned frame.
5. **Save `ra` and caller-saved regs** you need after any `call`. Use `s`-registers for values that must survive multiple calls.
6. **Arrays, strings, structs** all reduce to base address + byte offset. Read fields with `lw offset(base)` — same mechanic as array indexing.