Finalize deterministic stack debugging lesson

This commit is contained in:
Kevin Thomas committed 2026-09-06 14:07:40 -04:00
1 parent e548d7876c
commit 1001c9240b
3 files changed
+292 -85

No files matched your search

+3 -3
View File
@@ -22,9 +22,9 @@ By using this repository and course, you acknowledge and agree that:
<br> <br>
# Today's Tutorial [September 5, 2026] # Today's Tutorial [September 6, 2026]
## Lesson 287: Embedded Hacking Course (Chapter 5: Intro To Variables) ## Lesson 288: Embedded Hacking Course (Chapter 6: Debugging Intro To Variables)
This chapter covers an introduction to variables as it relates to embedded development on the Pico 2. This chapter covers debugging an introduction to variables as it relates to embedded development on the Pico 2.
-> Click [HERE](https://github.com/mytechnotalent/Embedded-Hacking/blob/main/Embedded-Hacking.pdf) to read the FREE pdf book. -> Click [HERE](https://github.com/mytechnotalent/Embedded-Hacking/blob/main/Embedded-Hacking.pdf) to read the FREE pdf book.
+289 -82
View File
@@ -148,8 +148,8 @@ __asm volatile(
This lowers `sp` by 8 bytes. At the new stack pointer, the saved values are: This lowers `sp` by 8 bytes. At the new stack pointer, the saved values are:
```text ```text
[sp] = r4 [sp + 4] = lr (higher address, visual top)
[sp + 4] = lr [sp] = r4 (lower address) <- SP <- TOP OF STACK
``` ```
##### Second Push: `push {r3, r2, r6}` ##### Second Push: `push {r3, r2, r6}`
@@ -159,17 +159,17 @@ This is deliberately written in a confusing order. The source says `r3` first, b
After one `si`, the stack layout proves the actual rule: After one `si`, the stack layout proves the actual rule:
```text ```text
[sp] = r2 [sp + 16] = lr (highest address, visual top)
[sp + 4] = r3 [sp + 12] = r4
[sp + 8] = r6 [sp + 8] = r6
[sp + 12] = r4 [sp + 4] = r3
[sp + 16] = lr [sp] = r2 (lowest address) <- SP <- TOP OF STACK
``` ```
The first three words were written by this instruction; `r4` and `lr` remain from The first three words were written by this instruction; `r4` and `lr` remain from
the preceding `push {r4, lr}`. The lowest register number in this push is stored the preceding `push {r4, lr}`. The lowest register number in this push is stored
at the lowest address. Because the stack grows down, `r6` is closest to the stack at the lowest address. Because the stack grows down, `[sp]` is the lower address
pointer value from before this instruction. and the older `lr` at `[sp + 16]` is the higher address at the visual top.
##### High Registers: `stmdb sp!, {r9, r10}` ##### High Registers: `stmdb sp!, {r9, r10}`
@@ -178,8 +178,8 @@ The 16-bit Thumb `push` encoding cannot encode high registers `r8` through `r12`
`r10`. `r10`.
```text ```text
[sp] = r9 [sp + 4] = r10 (higher address, visual top)
[sp + 4] = r10 [sp] = r9 (lower address) <- SP <- TOP OF STACK
``` ```
##### The Restore Instructions ##### The Restore Instructions
@@ -277,7 +277,9 @@ c
disas main disas main
``` ```
You should see this instruction pattern. Your addresses can differ after a rebuild. You should see this instruction pattern from the current `build\0x0001a_stack.elf`.
The instruction addresses are determined by the ELF. The register contents shown
later are live target state and are not determined by the ELF.
```text ```text
=> main: push {r3, lr} => main: push {r3, lr}
@@ -291,7 +293,7 @@ You should see this instruction pattern. Your addresses can differ after a rebui
main+24: b.n main+6 main+24: b.n main+6
``` ```
Notice that GDB displays `{r2, r3, r6}`, not the source spelling `{r3, r2, r6}`. That is the encoded register set in canonical order. Depending on the disassembler, register `r10` may be displayed as its conventional alias, `sl`. Notice that GDB displays `{r2, r3, r6}`, not the source spelling `{r3, r2, r6}`. That is the encoded register set in canonical order. Some disassemblers display `r10` as its conventional alias, `sl`; `{r9, sl}` and `{r9, r10}` name the same register set.
### Basic GDB Commands: Your First Steps ### Basic GDB Commands: Your First Steps
@@ -308,125 +310,330 @@ Notice that GDB displays `{r2, r3, r6}`, not the source spelling `{r3, r2, r6}`.
### Watching the Stack Change ### Watching the Stack Change
> **Important: the instruction addresses and stack offsets below come from the
> current ELF. The Step 4 register capture is from the live GDB session shown
> here. Register contents and old SRAM words are target state, not constants
> that can be recovered from the ELF. The deterministic rule is that `push
> {r4, lr}` decreases `sp` by 8 bytes, stores `r4` at the new `[sp]`, and
> stores `lr` at `[sp + 4]`.
##### Step 1: Inspect the Stack Before the Compiler Prologue ##### Step 1: Inspect the Stack Before the Compiler Prologue
At the breakpoint, GDB is paused before `push {r3, lr}`. Inspect the current stack pointer and the two words below it: At the breakpoint, GDB is paused before `push {r3, lr}`. Inspect the current stack pointer and the two words below it:
```gdb ```gdb
p/x $sp (gdb) p/x $sp
x/2wx $sp-8 $1 = 0x20082000
(gdb) x/2wx $sp-8
0x20081ff8: 0x88526891 0x10000187
``` ```
The stack pointer is at `0x20082000`. The two words below it contain previous values (before the prologue).
##### Step 2: Execute One Instruction ##### Step 2: Execute One Instruction
```gdb Execute the compiler prologue `push {r3, lr}`:
si
```
The arrow moves to `bl stdio_init_all`. Inspect what the compiler prologue placed on the stack:
```gdb ```gdb
p/x $sp (gdb) si
x/wx $sp 0x100001e2 7 stdio_init_all();
x/wx $sp+4 (gdb) p/x $sp
$2 = 0x20081ff8
(gdb) p/x $r3
$3 = 0xe000ed08
(gdb) x/wx $sp
0x20081ff8: 0xe000ed08
(gdb) p/x $lr
$4 = 0x1000018b
(gdb) x/wx $sp+4
0x20081ffc: 0x1000018b
``` ```
The stack pointer moved down 8 bytes. `[sp]` is the saved `r3`; `[sp+4]` is the saved `lr`. The prologue saved two values to the stack:
```text
[sp + 4] = 0x1000018b (lr, higher address, visual top)
[sp] = 0xe000ed08 (r3, lower address) <- SP <- TOP OF STACK
```
**Stack layout after prologue** (memory grows downward; **higher hex addresses = deeper in stack = older data**):
```text
Address Pointer Value Label
0x20082000 ---------- ---------- (previous data — HIGHEST address, visual top)
0x20081ffc ---------- 0x1000018b (lr, higher address)
0x20081ff8 <- [sp] 0xe000ed08 (r3, lowest address) <- SP <- TOP OF STACK
MEMORY ORDER: 0x20081ff8 < 0x20081ffc < 0x20082000
(visual bottom) (visual top)
```
**Memory address explanation for newcomers:**
- Hex address `0x20081ffc` is **LARGER** than `0x20081ff8` (compare the last hex digits: `ffc` > `ff8`)
- **Larger addresses = higher in memory = deeper in the stack**
- When we PUSH, sp **DECREASES** (goes to a smaller address, moving DOWN on the page)
- When we POP, sp **INCREASES** (goes to a larger address, moving UP on the page)
The stack pointer dropped 8 bytes: from `0x20082000` to `0x20081ff8` (it went DOWN, to a smaller/lower address).
##### Step 3: Step Over `stdio_init_all` ##### Step 3: Step Over `stdio_init_all`
Do not step into the library initialization code. Use `ni`: Do not step into the library initialization code. Use `ni` to execute it and return:
```gdb ```gdb
ni (gdb) ni
disas main 0x100001e6 15 __asm volatile(
(gdb) disas main
Dump of assembler code for function main:
0x100001e0 <+0>: push {r3, lr}
0x100001e2 <+2>: bl 0x10001594 <stdio_init_all>
=> 0x100001e6 <+6>: push {r4, lr}
0x100001e8 <+8>: push {r2, r3, r6}
...
``` ```
The arrow now points at the first inline instruction: `push {r4, lr}`. The arrow now points at `push {r4, lr}`, the first inline assembly instruction.
##### Step 4: Prove the First Inline Push ##### Step 4: Prove the First Inline Push
Read the registers before saving them: Read the registers and stack pointer before the first inline push:
```gdb ```gdb
p/x $r4 (gdb) p/x $r4
p/x $lr $3 = 0x100001cc
p/x $sp (gdb) p/x $lr
$4 = 0x1000159b
(gdb) p/x $sp
$5 = 0x20081ff8
``` ```
Execute one instruction and examine the new top of the stack: Now execute the first inline push:
```gdb ```gdb
si (gdb) si
x/wx $sp 0x100001e8 16 "push {r4, lr}\n"
x/wx $sp+4 (gdb) p/x $sp
$6 = 0x20081ff0
(gdb) x/wx $sp
0x20081ff0: 0x100001cc
(gdb) x/wx $sp+4
0x20081ff4: 0x1000159b
``` ```
The values at `[sp]` and `[sp+4]` match the values shown for `r4` and `lr`. This push reduced `sp` by 8 bytes. **After first inline push `push {r4, lr}`:**
- SP = `0x20081ff0` (**new TOP of stack, sp decreased**)
- `r4` = `0x100001cc` (now saved on stack)
- `lr` = `0x1000159b` (now saved on stack)
The first push saved:
```text
[sp + 4] = 0x1000159b (lr, higher address, visual top)
[sp] = 0x100001cc (r4, lower address) <- SP <- TOP OF STACK
```
**Stack layout after first inline push**:
```text
Address Pointer Value Label
0x20081ffc ---------- 0x1000018b (lr from prologue — highest address, visual top)
0x20081ff8 ---------- 0xe000ed08 (r3 from prologue — higher address)
0x20081ff4 ---------- 0x1000159b (lr — higher address)
0x20081ff0 <- [sp] 0x100001cc (r4, lowest address) <- SP <- TOP OF STACK
MEMORY ORDER: 0x20081ff0 < 0x20081ff4 < 0x20081ff8 < 0x20081ffc
(visual bottom) (visual top)
Most recent Least recent
```
**The stack pointer dropped from `0x20081ff8` to `0x20081ff0` — that's 8 bytes down (toward lower addresses).**
**The value at `[sp]` (the TOP) is now `0x100001cc` (r4).**
**Address `0x20081ff0` is SMALLER than `0x20081ff8`, so sp moved DOWN.**
##### Step 5: Prove Register-List Ordering ##### Step 5: Prove Register-List Ordering
Read the three registers before executing the deliberately unordered list: Read the three registers before executing the deliberately unordered list `push {r3, r2, r6}`:
```gdb ```gdb
p/x $r2 (gdb) p/x $r2
p/x $r3 $18 = 0x200005cc
p/x $r6 (gdb) p/x $r3
p/x $sp $19 = 0xe000ed08
(gdb) p/x $r6
$20 = 0x04f54710
(gdb) p/x $sp
$21 = 0x20081ff0
``` ```
Now execute only that instruction: Now execute the second inline push:
```gdb ```gdb
si (gdb) si
x/wx $sp 0x100001ea <+10>: stmdb sp!, {r9, sl}
x/wx $sp+4 (gdb) p/x $sp
x/wx $sp+8 $22 = 0x20081fe4
(gdb) x/wx $sp
0x20081fe4: 0x200005cc
(gdb) x/wx $sp+4
0x20081fe8: 0xe000ed08
(gdb) x/wx $sp+8
0x20081fec: 0x04f54710
``` ```
Compare the values from the first three commands with the three words in SRAM: The second push saved three registers in **numeric order** despite the source spelling `{r3, r2, r6}`:
```text
[sp + 8] = 0x04f54710 (r6, higher address, visual top)
[sp + 4] = 0xe000ed08 (r3, higher address)
[sp] = 0x200005cc (r2, lower address) <- SP <- TOP OF STACK
```
**Stack layout after second inline push** (**lower hex addresses = most recent = TOP**):
```text ```text
[sp] matches r2 Address Pointer Value Label
[sp + 4] matches r3 0x20081ffc ---------- 0x1000018b (lr — HIGHEST address, OLDEST data, visual top)
[sp + 8] matches r6 0x20081ff8 ---------- 0xe000ed08 (r3 — higher address)
0x20081ff4 ---------- 0x1000159b (lr — higher address)
0x20081ff0 ---------- 0x100001cc (r4 — higher address)
0x20081fec ---------- 0x04f54710 (r6 — higher address)
0x20081fe8 ---------- 0xe000ed08 (r3 — higher address)
0x20081fe4 <- [sp] 0x200005cc (r2, lowest address) <- SP <- TOP OF STACK
ADDRESS ORDERING: 0x20081fe4 < 0x20081fe8 < ... < 0x20081ffc
(visual bottom) (visual top)
Hex comparison: fe4 < fe8 < fec < ff0 < ff4 < ff8 < ffc
Most recent data at the visual bottom
``` ```
This is the proof. The source ordered the list as `r3`, `r2`, `r6`, but the stack is laid out by ascending register number. A multi-register push is one CPU instruction, so individual transfers inside that instruction cannot be separately stepped. **The stack pointer dropped from `0x20081ff0` to `0x20081fe4` — that's 12 bytes down (three 4-byte registers).**
**When comparing hex: `0x20081fe4` is SMALLER than `0x20081ff0`, so sp moved to a LOWER address.**
**The value at `[sp]` (the TOP) is now `0x200005cc` (r2).**
Notice: the source wrote `r3` first, but the CPU pushed `r2` first because `r2` has a lower register number. The encoded register mask is `{r2, r3, r6}`, and the stack layout proves it.
##### Step 6: Prove the High-Register Save ##### Step 6: Prove the High-Register Save
Read the values and execute one instruction: Read the high registers before the `stmdb sp!, {r9, r10}` instruction:
```gdb ```gdb
p/x $r9 (gdb) p/x $r9
p/x $r10 $23 = 0x00000000
p/x $sp (gdb) p/x $r10
si $24 = 0x10000000
x/wx $sp (gdb) p/x $sp
x/wx $sp+4 $25 = 0x20081fe4
``` ```
`stmdb sp!, {r9, r10}` moved `sp` down by 8 bytes. The first word equals `r9`; the second equals `r10`. Execute the high-register save:
```gdb
(gdb) si
0x100001ee <+14>: ldmia.w sp!, {r9, sl}
(gdb) p/x $sp
$26 = 0x20081fdc
(gdb) p/x $r9
$27 = 0x00000000
(gdb) p/x $r10
$28 = 0x10000000
(gdb) x/wx $sp
0x20081fdc: 0x00000000
(gdb) x/wx $sp+4
0x20081fe0: 0x10000000
(gdb) x/wx $sp+8
0x20081fe4: 0x200005cc
(gdb) x/wx $sp+12
0x20081fe8: 0xe000ed08
(gdb) x/wx $sp+16
0x20081fec: 0x04f54710
(gdb) x/wx $sp+20
0x20081ff0: 0x100001cc
(gdb) x/wx $sp+24
0x20081ff4: 0x1000159b
(gdb) x/wx $sp+28
0x20081ff8: 0xe000ed08
```
**After high-register save `stmdb sp!, {r9, r10}`:**
- SP = `0x20081fdc` (**new TOP of stack, sp decreased further**)
- `r9` = `0x00000000` (now saved on stack)
- `r10` = `0x10000000` (now saved on stack)
The `stmdb sp!, {r9, r10}` instruction saved:
```text
[sp + 4] = 0x10000000 (r10, higher address, visual top)
[sp] = 0x00000000 (r9, lower address) <- SP <- TOP OF STACK
```
**The stack pointer dropped from `0x20081fe4` to `0x20081fdc` — that's 8 more bytes down.**
**The value at `[sp]` (the TOP) is now `0x00000000` (r9).**
**This is the DEEPEST point of the stack during inline assembly.**
```text
Address Pointer Value Label
0x20081ff8 ---------- 0xe000ed08 (saved prologue r3, highest address, visual top)
0x20081ff4 ---------- 0x1000159b (lr, higher address)
0x20081ff0 ---------- 0x100001cc (r4, higher address)
0x20081fec ---------- 0x04f54710 (r6, higher address)
0x20081fe8 ---------- 0xe000ed08 (r3, higher address)
0x20081fe4 ---------- 0x200005cc (r2, higher address)
0x20081fe0 ---------- 0x10000000 (r10, higher address)
0x20081fdc <- [sp] 0x00000000 (r9, lowest address) <- SP <- TOP OF STACK
```
##### Step 7: Watch the Restores ##### Step 7: Watch the Restores
Execute and inspect each restore separately: At this point, the stack is at maximum depth. Now execute the restore instructions one by one:
**Restore 1: ldmia sp!, {r9, r10}**
```gdb ```gdb
si (gdb) si
p/x $sp 0x100001f2 <+18>: pop {r2, r3, r6}
x/3wx $sp (gdb) p/x $sp
$29 = 0x20081fe4
si (gdb) p/x $r9
p/x $sp $30 = 0x00000000
x/2wx $sp (gdb) p/x $r10
$31 = 0x10000000
si
p/x $sp
``` ```
The three instructions raise `sp` by 8, 12, and 8 bytes respectively. The final value is the same stack pointer you saw before the first inline push. `ldmia sp!` restored `r9` and `r10` from the stack and raised `sp` by 8 bytes (from `0x20081fdc` to `0x20081fe4`).
**SP is now `0x20081fe4`; `[sp]` contains r2, so `SP <- TOP OF STACK` at the next group.**
**Restore 2: pop {r2, r3, r6}**
```gdb
(gdb) si
0x100001f4 <+20>: ldmia.w sp!, {r4, lr}
(gdb) p/x $sp
$32 = 0x20081ff0
(gdb) p/x $r2
$33 = 0x200005cc
(gdb) p/x $r3
$34 = 0xe000ed08
(gdb) p/x $r6
$35 = 0x04f54710
```
`pop {r2, r3, r6}` restored the three registers from SRAM and raised `sp` by 12 bytes (from `0x20081fe4` to `0x20081ff0`).
**SP is now `0x20081ff0`; `[sp]` contains r4, so `SP <- TOP OF STACK` at the final group.**
**Restore 3: ldmia.w sp!, {r4, lr}**
```gdb
(gdb) si
0x100001f8 <+24>: b.n 0x100001e6 <main+6>
(gdb) p/x $sp
$36 = 0x20081ff8
(gdb) p/x $r4
$37 = 0x100001cc
(gdb) p/x $lr
$38 = 0x1000159b
```
`ldmia.w sp!, {r4, lr}` restored `r4` and `lr` from SRAM and raised `sp` by 8 bytes (from `0x20081ff0` to `0x20081ff8`).
**SP is now `0x20081ff8`; `[sp]` contains the saved prologue r3, so `SP <- TOP OF STACK` for the remaining prologue stack data.**
**Result:** After all three restore instructions, `sp` is back at `0x20081ff8`, exactly where it was before the inline assembly block started. The loop branches back to `push {r4, lr}` at address `0x100001e6` and repeats forever without consuming stack space.
### Understanding the Stack Diagram ### Understanding the Stack Diagram
@@ -435,7 +642,7 @@ At the deepest point, after `stmdb sp!, {r9, r10}`, the inline assembly has save
```text ```text
Before inline assembly: At maximum inline stack depth: Before inline assembly: At maximum inline stack depth:
old SP <- SP old SP old SP <- SP old SP
[old SP - 4] lr [old SP - 4] lr
[old SP - 8] r4 [old SP - 8] r4
[old SP - 12] r6 [old SP - 12] r6
@@ -464,22 +671,22 @@ The restoration sequence removes the top group first: `r9/r10`, then `r2/r3/r6`,
```text ```text
+-----------------------------------------------------+ +-----------------------------------------------------+
| 1. push {r3, lr} | | 1. push {r3, lr} |
| Compiler saves its prologue registers | | Compiler saves its prologue registers |
+-----------------------------------------------------+ +-----------------------------------------------------+
| 2. bl stdio_init_all | | 2. bl stdio_init_all |
| Initialize standard I/O | | Initialize standard I/O |
+-----------------------------------------------------+ +-----------------------------------------------------+
| 3. push {r4, lr} | | 3. push {r4, lr} |
| Save the first inline group | | Save the first inline group |
+-----------------------------------------------------+ +-----------------------------------------------------+
| 4. push {r3, r2, r6} | | 4. push {r3, r2, r6} |
| Source order differs from stack-memory order | | Source order differs from stack-memory order |
+-----------------------------------------------------+ +-----------------------------------------------------+
| 5. stmdb / ldmia / pop / pop | | 5. stmdb / ldmia / pop / pop |
| Save and restore all groups | | Save and restore all groups |
+-----------------------------------------------------+ +-----------------------------------------------------+
| 6. b.n main+6 | | 6. b.n main+6 |
| Repeat with the original stack pointer | | Repeat with the original stack pointer |
+-----------------------------------------------------+ +-----------------------------------------------------+
``` ```
Binary file not shown.