mirror of
https://github.com/mytechnotalent/Embedded-Hacking.git
synced 2026-08-30 23:00:44 +02:00
Updated WEEK03
This commit is contained in:
+312
-82
@@ -70,14 +70,17 @@ Each of these steps has a corresponding piece of code. Let's explore them all!
|
||||
│ STEP 2: Bootrom Executes (32KB on-chip ROM) │
|
||||
│ - This code is burned into the chip - can't be changed! │
|
||||
│ - It looks for valid firmware in flash memory │
|
||||
│ - It checks for the IMAGE_DEF structure at 0x10000000 │
|
||||
│ - It scans the first 4 kB of the image for a valid IMAGE_DEF │
|
||||
│ (Datasheet §4.1, p. 338: 32KB ROM; §5.9.5, p. 429: IMAGE_DEF) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ STEP 3: Boot Stage 2 (boot2) │
|
||||
│ - Configures the flash interface for fast reading │
|
||||
│ STEP 3: Flash XIP Setup (bootrom-managed) │
|
||||
│ - The bootrom configures the flash interface automatically │
|
||||
│ - Sets up XIP (Execute In Place) mode │
|
||||
│ - Returns control to the bootrom │
|
||||
│ - NOTE: Unlike RP2040, there is NO separate boot2 in flash! │
|
||||
│ (Datasheet §5.2, p. 375: "removal of a boot2 in the first │
|
||||
│ 256 bytes of the image") │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
@@ -119,8 +122,8 @@ Think of the bootrom like the BIOS in your computer - it's the first thing that
|
||||
### What Does the Bootrom Do?
|
||||
|
||||
1. **Initialize Hardware**: Sets up clocks, resets peripherals
|
||||
2. **Check Boot Sources**: Looks for valid firmware in flash
|
||||
3. **Validate Firmware**: Checks for magic markers (IMAGE_DEF)
|
||||
2. **Check Boot Sources (Discovery)**: Scans configured boot sources (for this course: flash) to find a candidate firmware image region.
|
||||
3. **Validate Firmware (Validation)**: Verifies that candidate by finding IMAGE_DEF start/end markers and parsing the block.
|
||||
4. **Configure Flash**: Sets up the XIP interface
|
||||
5. **Jump to Your Code**: Reads the vector table and jumps to your reset handler
|
||||
|
||||
@@ -131,22 +134,70 @@ The bootrom looks for a special marker in your firmware called **IMAGE_DEF**. Th
|
||||
Here's what it looks like in the Pico SDK:
|
||||
|
||||
```assembly
|
||||
.section .picobin_block, "a" // placed in flash
|
||||
.word 0xffffded3 // PICOBIN_BLOCK_MARKER_START ← ROM looks for this!
|
||||
.byte 0x42 // PICOBIN_BLOCK_ITEM_1BS_IMAGE_TYPE
|
||||
.byte 0x1 // item is 1 word in size
|
||||
.hword 0b0001000000100001 // SECURE mode (0x1021)
|
||||
.byte 0xff // PICOBIN_BLOCK_ITEM_2BS_LAST
|
||||
.hword 0x0001 // item is 1 word in size
|
||||
.byte 0x0 // pad
|
||||
.word 0x0 // relative pointer to next block (0 = loop to self)
|
||||
.word 0xab123579 // PICOBIN_BLOCK_MARKER_END
|
||||
.section .picobin_block, "a" // placed in flash
|
||||
.word 0xffffded3 // PICOBIN_BLOCK_MARKER_START ← ROM looks for this!
|
||||
.byte 0x42 // PICOBIN_BLOCK_ITEM_1BS_IMAGE_TYPE
|
||||
.byte 0x1 // item is 1 word in size
|
||||
.hword 0b0001000000100001 // SECURE mode (0x1021)
|
||||
.byte 0xff // PICOBIN_BLOCK_ITEM_2BS_LAST
|
||||
.hword 0x0001 // item is 1 word in size
|
||||
.byte 0x0 // pad
|
||||
.word 0x0 // relative pointer to next block (0 = loop to self)
|
||||
.word 0xab123579 // PICOBIN_BLOCK_MARKER_END
|
||||
```
|
||||
|
||||
**The magic numbers:**
|
||||
- `0xffffded3` = Start marker ("I'm a valid Pico binary!")
|
||||
- `0xab123579` = End marker ("End of the header block")
|
||||
|
||||
### See This Exact Block in Your ELF (Commands + Real Output)
|
||||
|
||||
Use these commands to view the IMAGE_DEF bytes directly in the ELF:
|
||||
|
||||
```powershell
|
||||
arm-none-eabi-objdump -s --start-address=0x1000013c --stop-address=0x10000150 build/0x0001_hello-world.elf
|
||||
arm-none-eabi-objdump -s --start-address=0x10000130 --stop-address=0x10000154 build/0x0001_hello-world.elf
|
||||
arm-none-eabi-gdb build/0x0001_hello-world.elf -ex "x/20bx 0x1000013c" -ex quit
|
||||
```
|
||||
|
||||
Actual output from this lesson build:
|
||||
|
||||
```text
|
||||
build/0x0001_hello-world.elf: file format elf32-littlearm
|
||||
|
||||
Contents of section .text:
|
||||
1000013c 42012110 ff010000 b01b0000 793512ab B.!.........y5..
|
||||
1000014c 4ff00000 O...
|
||||
|
||||
build/0x0001_hello-world.elf: file format elf32-littlearm
|
||||
|
||||
Contents of section .text:
|
||||
10000130 a0010010 90a31ae7 d3deffff 42012110 ............B.!.
|
||||
10000140 ff010000 b01b0000 793512ab 4ff00000 ........y5..O...
|
||||
10000150 1e490860 .I.`
|
||||
```
|
||||
|
||||
Command 1 explained (`--start-address=0x1000013c --stop-address=0x10000150`):
|
||||
- Starts at `0x1000013c`, so it does **not** include the start marker at `0x10000138` (`d3deffff`).
|
||||
- Shows IMAGE_DEF body fields and the end marker:
|
||||
- `42012110` = `42 01 21 10` (item type/size + secure mode field)
|
||||
- `ff010000` = last-item marker + size + pad
|
||||
- `b01b0000` = next word in the block payload for this build
|
||||
- `793512ab` = `PICOBIN_BLOCK_MARKER_END` (`0xab123579` in little-endian)
|
||||
- `4ff00000` at `0x1000014c` is already the next instruction word after IMAGE_DEF.
|
||||
|
||||
Command 2 explained (`--start-address=0x10000130 --stop-address=0x10000154`):
|
||||
- Starts earlier, so it captures context **and** both IMAGE_DEF markers.
|
||||
- `a0010010 90a31ae7` = binary-info context before IMAGE_DEF.
|
||||
- `d3deffff` at `0x10000138` = `PICOBIN_BLOCK_MARKER_START`.
|
||||
- `793512ab` at `0x10000148` = `PICOBIN_BLOCK_MARKER_END`.
|
||||
- `4ff00000 1e490860` = code words after the IMAGE_DEF block.
|
||||
- This command proves the full block location for this build: `0x10000138` to `0x1000014b`.
|
||||
|
||||
Important: IMAGE_DEF offset can vary by build. In this build, the start marker `d3deffff`
|
||||
is at `0x10000138` (not `0x1000013c`), so always search for the marker bytes instead of
|
||||
assuming a fixed address.
|
||||
|
||||
---
|
||||
|
||||
## 📚 Part 3: Understanding XIP (Execute In Place)
|
||||
@@ -327,7 +378,7 @@ arm-none-eabi-gdb build\0x0001_hello-world.elf
|
||||
**Connect to target:**
|
||||
|
||||
```gdb
|
||||
(gdb) target remote :3333
|
||||
(gdb) target extended-remote :3333
|
||||
(gdb) monitor reset halt
|
||||
```
|
||||
|
||||
@@ -360,7 +411,7 @@ Let's look at the first 4 entries of the vector table at `0x10000000`:
|
||||
|
||||
### Step 2: Understanding What We See
|
||||
|
||||
> 🔄 **REVIEW:** In Week 1, we saw `sp = 0x20081fc8` when stopped at `main`. That's *after* some stack was used during boot. Here we see the *initial* stack pointer before any code runs!
|
||||
> 🔄 **REVIEW:** In Weeks 1-2, we saw both `sp = 0x20082000` at the clean breakpoint at `main` and lower values like `0x20081fc8` or `0x20081ff8` after additional stack activity. Here we are looking at the *initial* stack pointer from the vector table before any code runs.
|
||||
|
||||
Let's decode each value:
|
||||
|
||||
@@ -466,39 +517,39 @@ Let's look at more instructions to see the full picture:
|
||||
(gdb) x/20i 0x1000015c
|
||||
```
|
||||
|
||||
**You should see something like:**
|
||||
**You should see:**
|
||||
|
||||
```
|
||||
0x1000015c <_reset_handler>: mov.w r0, #3489660928 @ 0xd0000000
|
||||
0x10000160 <_reset_handler+4>: ldr r0, [r0, #0]
|
||||
0x10000162 <_reset_handler+6>:
|
||||
cbz r0, 0x1000016a <hold_non_core0_in_bootrom+6>
|
||||
0x10000164 <hold_non_core0_in_bootrom>: mov.w r0, #0
|
||||
0x10000168 <hold_non_core0_in_bootrom+4>:
|
||||
b.n 0x10000150 <_enter_vtable_in_r0>
|
||||
0x1000016a <hold_non_core0_in_bootrom+6>:
|
||||
add r4, pc, #52 @ (adr r4, 0x100001a0 <data_cpy_table>)
|
||||
0x1000016c <hold_non_core0_in_bootrom+8>: ldmia r4!, {r1, r2, r3}
|
||||
0x1000016e <hold_non_core0_in_bootrom+10>: cmp r1, #0
|
||||
0x10000170 <hold_non_core0_in_bootrom+12>:
|
||||
beq.n 0x10000178 <hold_non_core0_in_bootrom+20>
|
||||
0x10000172 <hold_non_core0_in_bootrom+14>:
|
||||
bl 0x1000019a <data_cpy>
|
||||
0x10000176 <hold_non_core0_in_bootrom+18>:
|
||||
b.n 0x1000016c <hold_non_core0_in_bootrom+8>
|
||||
0x10000178 <hold_non_core0_in_bootrom+20>:
|
||||
ldr r1, [pc, #84] @ (0x100001d0 <data_cpy_table+48>)
|
||||
0x1000017a <hold_non_core0_in_bootrom+22>:
|
||||
ldr r2, [pc, #88] @ (0x100001d4 <data_cpy_table+52>)
|
||||
0x1000017c <hold_non_core0_in_bootrom+24>: movs r0, #0
|
||||
0x1000017e <hold_non_core0_in_bootrom+26>:
|
||||
b.n 0x10000182 <bss_fill_test>
|
||||
0x10000180 <bss_fill_loop>: stmia r1!, {r0}
|
||||
0x10000182 <bss_fill_test>: cmp r1, r2
|
||||
0x10000184 <bss_fill_test+2>: bne.n 0x10000180 <bss_fill_loop>
|
||||
0x10000186 <platform_entry>:
|
||||
ldr r1, [pc, #80] @ (0x100001d8 <data_cpy_table+56>)
|
||||
0x10000188 <platform_entry+2>: blx r1
|
||||
0x1000015c <_reset_handler>: mov.w r0, #3489660928 @ 0xd0000000
|
||||
0x10000160 <_reset_handler+4>: ldr r0, [r0, #0]
|
||||
0x10000162 <_reset_handler+6>:
|
||||
cbz r0, 0x1000016a <hold_non_core0_in_bootrom+6>
|
||||
0x10000164 <hold_non_core0_in_bootrom>: mov.w r0, #0
|
||||
0x10000168 <hold_non_core0_in_bootrom+4>:
|
||||
b.n 0x10000150 <_enter_vtable_in_r0>
|
||||
0x1000016a <hold_non_core0_in_bootrom+6>:
|
||||
add r4, pc, #52 @ (adr r4, 0x100001a0 <data_cpy_table>)
|
||||
0x1000016c <hold_non_core0_in_bootrom+8>: ldmia r4!, {r1, r2, r3}
|
||||
0x1000016e <hold_non_core0_in_bootrom+10>: cmp r1, #0
|
||||
0x10000170 <hold_non_core0_in_bootrom+12>:
|
||||
beq.n 0x10000178 <hold_non_core0_in_bootrom+20>
|
||||
0x10000172 <hold_non_core0_in_bootrom+14>:
|
||||
bl 0x1000019a <data_cpy>
|
||||
0x10000176 <hold_non_core0_in_bootrom+18>:
|
||||
b.n 0x1000016c <hold_non_core0_in_bootrom+8>
|
||||
0x10000178 <hold_non_core0_in_bootrom+20>:
|
||||
ldr r1, [pc, #84] @ (0x100001d0 <data_cpy_table+48>)
|
||||
0x1000017a <hold_non_core0_in_bootrom+22>:
|
||||
ldr r2, [pc, #88] @ (0x100001d4 <data_cpy_table+52>)
|
||||
0x1000017c <hold_non_core0_in_bootrom+24>: movs r0, #0
|
||||
0x1000017e <hold_non_core0_in_bootrom+26>:
|
||||
b.n 0x10000182 <bss_fill_test>
|
||||
0x10000180 <bss_fill_loop>: stmia r1!, {r0}
|
||||
0x10000182 <bss_fill_test>: cmp r1, r2
|
||||
0x10000184 <bss_fill_test+2>: bne.n 0x10000180 <bss_fill_loop>
|
||||
0x10000186 <platform_entry>:
|
||||
ldr r1, [pc, #80] @ (0x100001d8 <data_cpy_table+56>)
|
||||
0x10000188 <platform_entry+2>: blx r1
|
||||
```
|
||||
|
||||
### Step 7: Understanding the Startup Phases
|
||||
@@ -513,22 +564,23 @@ The reset handler performs several phases:
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ PHASE 2: Data Copy (0x1000016a - 0x10000176) │
|
||||
│ PHASE 2: Data Copy Setup & Loop (0x1000016a - 0x10000176) │
|
||||
│ - Set up the data_cpy_table pointer and load each copy triplet │
|
||||
│ - Copy initialized variables from flash to RAM │
|
||||
│ - Uses data_cpy_table for source/destination info │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ PHASE 3: BSS Clear (0x10000178 - 0x10000184) │
|
||||
│ PHASE 3: BSS Setup & Clear (0x10000178 - 0x10000184) │
|
||||
│ - Load the BSS start/end addresses into r1 and r2 │
|
||||
│ - GDB labels those literals as `data_cpy_table+48/+52` │
|
||||
│ - Zero out all uninitialized global variables │
|
||||
│ - C standard requires BSS to start at zero │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ PHASE 4: Runtime Init & Main (0x10000186+) │
|
||||
│ - Call runtime_init() for SDK setup │
|
||||
│ - Call __libc_init_array() for C++ constructors │
|
||||
│ - Finally call main()! │
|
||||
│ PHASE 4: Platform Entry Begins (0x10000186 - 0x10000188 shown) │
|
||||
│ - Load the runtime_init() pointer from the table │
|
||||
│ - Branch to runtime_init() with `blx r1` │
|
||||
│ - `main()` and `exit()` appear a few instructions later │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
@@ -562,7 +614,8 @@ The data copy table contains entries that describe what to copy where. Let's exa
|
||||
|
||||
```
|
||||
0x100001a0 <data_cpy_table>: 0x10001b4c 0x20000110 0x200002ac 0x10001ce8
|
||||
...
|
||||
0x100001b0 <data_cpy_table+16>: 0x20080000 0x20080000 0x10001ce8 0x20081000
|
||||
0x100001c0 <data_cpy_table+32>: 0x20081000 0x00000000 0x00004770 0xe000ed08
|
||||
```
|
||||
|
||||
The data_cpy_table contains multiple entries. Each entry has three values:
|
||||
@@ -641,6 +694,8 @@ b.n 0x10000182 <bss_fill_test>
|
||||
0x10000180 <bss_fill_loop>: stmia r1!, {r0}
|
||||
```
|
||||
|
||||
The first two `ldr` instructions are still part of the **BSS clear setup**, even though GDB shows the source words as `data_cpy_table+48` and `data_cpy_table+52`. That label means the two literal words live in the same nearby constant block as the copy-table entries; it does **not** mean the code is still performing `.data` copies. At this point, `r1` becomes the BSS start address, `r2` becomes the BSS end address, and the loop beginning at `0x10000180` zeros that range.
|
||||
|
||||
### Understanding the Loop
|
||||
|
||||
```
|
||||
@@ -762,7 +817,8 @@ Let's verify we understand the boot process by setting a breakpoint at main:
|
||||
**You should see:**
|
||||
|
||||
```
|
||||
Breakpoint 1 at 0x10000234: file 0x0001_hello-world.c, line 5.
|
||||
Breakpoint 1 at 0x10000234: file C:/Users/flare-vm/Desktop/Embedded-Hacking-main/0x0001_hello-world/0x0001_hello-world.c, line 5.
|
||||
Note: automatically using hardware breakpoints for read-only addresses.
|
||||
```
|
||||
|
||||
**Now continue:**
|
||||
@@ -776,8 +832,10 @@ Breakpoint 1 at 0x10000234: file 0x0001_hello-world.c, line 5.
|
||||
```
|
||||
Continuing.
|
||||
|
||||
Breakpoint 1, main () at 0x0001_hello-world.c:5
|
||||
5 stdio_init_all();
|
||||
Thread 1 "rp2350.cm0" hit Breakpoint 1, main ()
|
||||
at C:/Users/flare-vm/Desktop/Embedded-Hacking-main/0x0001_hello-world/0x0001_hello-world.c:5
|
||||
5 stdio_init_all();
|
||||
(gdb)
|
||||
```
|
||||
|
||||
🎉 We've traced the entire boot process from power-on to `main()`!
|
||||
@@ -822,7 +880,7 @@ GDB doesn't know this is data, not code! It tries to disassemble it as Thumb ins
|
||||
```
|
||||
|
||||
```
|
||||
0x10000138 <__binary_info_header_end>: udf #211 @ 0xd3
|
||||
0x10000138 <__binary_info_header_end>: udf #211 @ 0xd3
|
||||
```
|
||||
|
||||
That's not real code - it's the magic number `0xffffded3` being misinterpreted!
|
||||
@@ -884,6 +942,7 @@ While GDB is excellent for dynamic analysis (watching code execute), Ghidra exce
|
||||
1000000d 01 ?? 01h
|
||||
1000000e 00 ?? 00h
|
||||
1000000f 10 ?? 10h
|
||||
...
|
||||
```
|
||||
|
||||
> 💡 **Notice:** Ghidra shows the vector table data as individual bytes by default. You can see it has labeled the start as `__vectors`, `__flash_binary_start`, `__VECTOR_TABLE`, and `__logical_binary_start`. The arrows (like `? -> 1000015d`) show that Ghidra recognizes these bytes as pointers to code addresses! To see the data formatted as 32-bit addresses instead of bytes, you can right-click and retype the data.
|
||||
@@ -953,9 +1012,9 @@ Let's find how the boot code eventually calls `main()`:
|
||||
|
||||
| Location | Type | Label |
|
||||
| ------------------------- | ---- | ------------------ |
|
||||
| `platform_entry+6` | CALL | `blx r1` (to main) |
|
||||
| `1000018c` | CALL | `blx r1` (to main) |
|
||||
|
||||
4. Double-click on the reference to jump to `platform_entry`
|
||||
4. Double-click on the reference to jump to `1000018c`
|
||||
|
||||
### Step 19: Examine Platform Entry
|
||||
|
||||
@@ -991,7 +1050,7 @@ Ghidra can visualize the call flow:
|
||||
|
||||
1. With `_reset_handler` selected, go to **Window → Function Call Graph**
|
||||
2. This shows a visual graph of all function calls from the reset handler
|
||||
3. You can see the path: `_reset_handler` → `platform_entry` → `main`
|
||||
3. You will see `_reset_handler` at the top with arrows going down to its four direct callees: `data_cpy`, `runtime_init`, `main`, and `exit`
|
||||
|
||||
### Comparing GDB and Ghidra for Boot Analysis
|
||||
|
||||
@@ -1023,8 +1082,8 @@ Ghidra can visualize the call flow:
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ 2. BOOTROM │
|
||||
│ - Initializes hardware │
|
||||
│ - Finds IMAGE_DEF at 0x10000000 │
|
||||
│ - Runs boot2 to configure flash │
|
||||
│ - Configures flash XIP (no separate boot2 on RP2350) │
|
||||
│ - Finds IMAGE_DEF within first 4 kB of flash image │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ 3. VECTOR TABLE (0x10000000) │
|
||||
│ - Reads SP from offset 0x00 → 0x20082000 │
|
||||
@@ -1129,21 +1188,18 @@ Ghidra can visualize the call flow:
|
||||
- Is it a valid code address (starts with `0x1000...`)?
|
||||
- What handler does it point to?
|
||||
|
||||
### Exercise 4: Find Your Main Function
|
||||
### Exercise 4: Find Your Main Function and Trace Back
|
||||
|
||||
1. Use `info functions main` to find main
|
||||
2. Examine 10 instructions at that address
|
||||
3. Identify the first function call in main
|
||||
4. What does that function do?
|
||||
5. When stopped at main, examine `$lr` (link register)
|
||||
6. What address is stored there?
|
||||
7. Disassemble that address - what function is it?
|
||||
8. This shows you where main was called from!
|
||||
|
||||
### Exercise 5: Trace Back from Main
|
||||
|
||||
1. When stopped at main, examine `$lr` (link register)
|
||||
2. What address is stored there?
|
||||
3. Disassemble that address - what function is it?
|
||||
4. This shows you where main was called from!
|
||||
|
||||
### Exercise 6: Ghidra Boot Analysis
|
||||
### Exercise 5: Ghidra Boot Analysis
|
||||
|
||||
1. In Ghidra, navigate to `_reset_handler`
|
||||
2. Use **Window → Function Call Graph** to visualize the call tree
|
||||
@@ -1221,9 +1277,7 @@ Understanding the boot process is critical for both attackers and defenders. Kno
|
||||
│ ↓ │
|
||||
│ Verify IMAGE_DEF signature │
|
||||
│ ↓ │
|
||||
│ Verify boot2 signature │
|
||||
│ ↓ │
|
||||
│ Verify application signature │
|
||||
│ Verify application image signature │
|
||||
│ ↓ │
|
||||
│ If all valid: Jump to reset handler │
|
||||
│ If any invalid: Refuse to boot │
|
||||
@@ -1342,9 +1396,11 @@ https://datasheets.raspberrypi.com/rp2350/rp2350-datasheet.pdf
|
||||
### Pico SDK Source Code
|
||||
|
||||
The startup code lives in:
|
||||
- `crt0.S` - Main startup assembly
|
||||
- `memmap_default.ld` - Default linker script
|
||||
- `boot2_generic_03h.S` - Second stage bootloader
|
||||
- `crt0.S` - Main startup assembly (vector table at `.section .vectors`, reset handler, data copy, BSS clear, platform_entry)
|
||||
- `memmap_default.ld` - Default linker script (section ordering: `.vectors` -> `.binary_info_header` -> `.embedded_block` -> `.reset`)
|
||||
- `embedded_start_block.inc.S` - IMAGE_DEF block (replaces RP2040's `boot2_generic_03h.S`)
|
||||
|
||||
> ⚠️ **Note:** The RP2040 used a `boot2_generic_03h.S` second-stage bootloader occupying the first 256 bytes of flash. The RP2350 eliminated this; the bootrom handles flash XIP setup directly. The SDK still includes a `boot2` mechanism for compatibility, but it is **not** placed at flash address 0 - it is embedded in the data copy table and executed from the stack during startup.
|
||||
|
||||
### Bootrom Source
|
||||
|
||||
@@ -1353,6 +1409,180 @@ https://github.com/raspberrypi/pico-bootrom-rp2350
|
||||
|
||||
---
|
||||
|
||||
## 🔬 Part 17: Proving the Boot Sequence with objdump
|
||||
|
||||
Everything we have learned about the boot sequence can be proven directly from the compiled ELF binary using `arm-none-eabi-objdump`. The bootrom is not in your ELF (it is mask ROM burned into the chip at `0x00000000`), but everything your firmware provides — the vector table, the IMAGE_DEF, and the reset handler — lives in your ELF starting at `0x10000000`.
|
||||
|
||||
### Step 1: List All Sections
|
||||
|
||||
```bash
|
||||
arm-none-eabi-objdump -h build/0x0001_hello-world.elf
|
||||
```
|
||||
|
||||
Expected output (key sections):
|
||||
|
||||
```
|
||||
Idx Name Size VMA LMA
|
||||
0 .text 000019cc 10000000 10000000
|
||||
3 .binary_info 0000002c 10001b20 10001b20
|
||||
4 .ram_vector_table 00000110 20000000 20000000
|
||||
6 .data 0000019c 20000110 10001b4c
|
||||
```
|
||||
|
||||
> 💡 The Pico SDK merges `.vectors`, `.embedded_block`, and `.reset` all into `.text` at `0x10000000`. They are not separate named ELF sections — they are sub-regions inside `.text`.
|
||||
|
||||
### Step 2: Dump the First 0x150 Bytes of Flash — One Command, Zero Skips
|
||||
|
||||
```bash
|
||||
arm-none-eabi-objdump -s --start-address=0x10000000 --stop-address=0x10000150 build/0x0001_hello-world.elf
|
||||
```
|
||||
|
||||
Raw output from that command:
|
||||
|
||||
```
|
||||
10000000 00200820 5d010010 1b010010 1d010010 . . ]...........
|
||||
10000010 11010010 11010010 11010010 11010010 ................
|
||||
10000020 11010010 11010010 11010010 11010010 ................
|
||||
10000030 11010010 11010010 11010010 11010010 ................
|
||||
10000040 11010010 11010010 11010010 11010010 ................
|
||||
10000050 11010010 11010010 11010010 11010010 ................
|
||||
10000060 11010010 11010010 11010010 11010010 ................
|
||||
10000070 11010010 11010010 11010010 11010010 ................
|
||||
10000080 11010010 11010010 11010010 11010010 ................
|
||||
10000090 11010010 11010010 11010010 11010010 ................
|
||||
100000a0 11010010 11010010 11010010 11010010 ................
|
||||
100000b0 11010010 11010010 11010010 11010010 ................
|
||||
100000c0 11010010 11010010 11010010 11010010 ................
|
||||
100000d0 11010010 11010010 11010010 11010010 ................
|
||||
100000e0 11010010 11010010 11010010 11010010 ................
|
||||
100000f0 11010010 11010010 11010010 11010010 ................
|
||||
10000100 11010010 11010010 11010010 11010010 ................
|
||||
10000110 eff30580 103800be 00be00be 00be00be .....8..........
|
||||
10000120 00be00be f2eb8871 201b0010 4c1b0010 .......q ...L...
|
||||
10000130 a0010010 90a31ae7 d3deffff 42012110 ............B.!.
|
||||
10000140 ff010000 b01b0000 793512ab ........y5..
|
||||
```
|
||||
|
||||
Every address annotated, no skips:
|
||||
|
||||
#### 0x10000000 — Vector Table, Mandatory Entries
|
||||
|
||||
| Address | Raw Bytes (LE) | Decoded | What it is |
|
||||
|---------|----------------|---------|------------|
|
||||
| `0x10000000` | `00 20 08 20` | `0x20082000` | **Initial SP** — top of SCRATCH_Y RAM. Bootrom loads MSP from here before doing anything else. |
|
||||
| `0x10000004` | `5d 01 00 10` | `0x1000015d` | **Reset_Handler** address with Thumb bit set. Strip bit 0 → real address `0x1000015c`. Bootrom jumps here. |
|
||||
| `0x10000008` | `1b 01 00 10` | `0x1000011b` | **NMI** handler address (Thumb, → `0x1000011a`). |
|
||||
| `0x1000000c` | `1d 01 00 10` | `0x1000011d` | **HardFault** handler address (Thumb, → `0x1000011c`). |
|
||||
|
||||
#### 0x10000010–0x1000010f — Vector Table, IRQ Slots (all 52 external IRQs)
|
||||
|
||||
```
|
||||
10000010 11010010 11010010 ...(repeats through 0x1000010f)...
|
||||
```
|
||||
|
||||
Every 4-byte word here is `11 01 00 10` = pointer `0x10000111`.
|
||||
That is the **default IRQ handler** address with Thumb bit set (→ `0x10000110`).
|
||||
The RP2350 Cortex-M33 has 16 system vectors (offsets 0x00–0x3f) plus up to 52
|
||||
external IRQ vectors (offsets 0x40–0xff = addresses `0x10000040`–`0x1000010f`).
|
||||
Every IRQ the application does not register gets this default handler pointer.
|
||||
This block is 240 bytes (`0x10000010` to `0x1000010f`) of nothing but that one
|
||||
repeated pointer.
|
||||
|
||||
#### 0x10000110–0x10000127 — Default IRQ Handler Code
|
||||
|
||||
```
|
||||
10000110 eff30580 103800be 00be00be 00be00be
|
||||
10000120 00be00be f2eb8871
|
||||
```
|
||||
|
||||
| Address | Bytes | ARM Thumb-2 Instruction | What it does |
|
||||
|---------|-------|-------------------------|--------------|
|
||||
| `0x10000110` | `ef f3 05 80` | `MRS r0, IPSR` | Read the Interrupt Program Status Register into r0. The low 9 bits = the active vector number. |
|
||||
| `0x10000114` | `10 38` | `SUBS r0, #16` | Vector 16 = IRQ0, so subtract 16 to convert vector number → IRQ index. |
|
||||
| `0x10000116` | `00 be` | `BKPT #0` | Software breakpoint. If a debugger is attached, it stops here and you can inspect r0 to see which IRQ fired. If no debugger is attached, the CPU enters a fault loop and the chip hangs. |
|
||||
| `0x10000118`–`0x10000127` | `00 be` ×12 | `BKPT #0` repeating | Alignment padding to the next 4-byte boundary. |
|
||||
|
||||
This is the **entire** default IRQ handler. It is intentionally minimal: if your
|
||||
code triggers an IRQ you did not register, it crashes visibly instead of silently.
|
||||
|
||||
#### 0x10000128–0x1000013b — Binary Info Pointer Table
|
||||
|
||||
```
|
||||
10000120 201b0010 4c1b0010
|
||||
10000130 a0010010 90a31ae7
|
||||
```
|
||||
|
||||
| Address | Bytes (LE) | Decoded Value | What it is |
|
||||
|---------|------------|---------------|------------|
|
||||
| `0x10000128` | `20 1b 00 10` | `0x10001b20` | Pointer to **start** of `.binary_info` data section in flash. |
|
||||
| `0x1000012c` | `4c 1b 00 10` | `0x10001b4c` | Pointer to **end** of `.binary_info` data section in flash. |
|
||||
| `0x10000130` | `a0 01 00 10` | `0x100001a0` | Pointer to `binary_info_callback` function. |
|
||||
| `0x10000134` | `90 a3 1a e7` | (magic marker) | `BINARY_INFO_MARKER_END` — marks the end of this pointer table. |
|
||||
|
||||
`picotool` reads this table to extract the program name, version string, URL,
|
||||
and GPIO pin map from any compiled binary without running it.
|
||||
|
||||
#### 0x10000138–0x1000014c — IMAGE_DEF Block (this build)
|
||||
|
||||
```
|
||||
10000130 a0010010 90a31ae7 d3deffff 42012110
|
||||
10000140 ff010000 b01b0000 793512ab 4ff00000
|
||||
```
|
||||
|
||||
| Address | Bytes | What it is |
|
||||
|---------|-------|------------|
|
||||
| `0x10000138` | `d3 de ff ff` | `PICOBIN_BLOCK_MARKER_START` — the bootrom scans flash for this exact 4-byte sequence to locate the IMAGE_DEF. |
|
||||
| `0x1000013c` | `42 01 21 10` | IMAGE_DEF content (image type, flags, version). |
|
||||
| `0x10000140` | `ff 01 00 00` | IMAGE_DEF content (continuation). |
|
||||
| `0x10000144` | `b0 1b 00 00` | IMAGE_DEF content (continuation). |
|
||||
| `0x10000148` | `79 35 12 ab` | `PICOBIN_BLOCK_MARKER_END` — bootrom stops scanning here. |
|
||||
|
||||
The IMAGE_DEF sits at `0x10000138`–`0x1000014b` in this build,
|
||||
well within the 4 KB scan window the bootrom uses (Datasheet §5.9.5, p. 429).
|
||||
|
||||
#### Full Flash Map: 0x10000000–0x1000015c
|
||||
|
||||
```
|
||||
0x10000000–0x1000000f Vector Table: mandatory entries (SP, Reset, NMI, HardFault)
|
||||
0x10000010–0x1000010f Vector Table: 52 external IRQ slots → all point to default handler
|
||||
0x10000110–0x10000127 Default IRQ handler code (MRS / SUBS / BKPT)
|
||||
0x10000128–0x10000137 Binary info pointer table (start / end / callback / magic end)
|
||||
0x10000138–0x1000014b IMAGE_DEF block (d3 de ff ff ... 79 35 12 ab)
|
||||
0x10000150–0x1000015b (padding / alignment)
|
||||
0x1000015c Reset_Handler (_reset_handler in crt0.S) ← bootrom jumps here
|
||||
```
|
||||
|
||||
### Step 4: Confirmed Boot Sequence (proven from ELF)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ PROVEN BOOT SEQUENCE (0x0001_hello-world) │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ 1. Bootrom reads 0x10000000 │
|
||||
│ → SP = 0x20082000 (offset +0x00 of vector table) │
|
||||
│ → RST = 0x1000015d (offset +0x04, Thumb → 0x1000015c) │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ 2. Bootrom scans first 4 kB for IMAGE_DEF │
|
||||
│ → Found at 0x10000138 (this build) │
|
||||
│ → Start marker: d3 de ff ff │
|
||||
│ → End marker: 79 35 12 ab │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ 3. Bootrom jumps to reset handler at 0x1000015c │
|
||||
│ → _reset_handler (crt0.S) runs │
|
||||
│ → Checks CPUID — Core 1 sent back to bootrom │
|
||||
│ → Core 0: .data copied, .bss zeroed, platform_entry called │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ 4. platform_entry calls runtime_init → main → exit │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
> 📖 **Datasheet References:**
|
||||
> - §5.1.5.1 (p. 357): Block markers `0xffffded3` (start) and `0xab123579` (end)
|
||||
> - §5.9.5 (p. 429): IMAGE_DEF must appear within first 4 kB of flash image
|
||||
> - §5.9.5.1 (p. 429): Bootrom enters via reset handler at vector table offset +4
|
||||
|
||||
---
|
||||
|
||||
**Remember:** Understanding the boot process is fundamental to embedded systems work. Whether you're debugging a system that won't start, reverse engineering firmware, or building secure boot chains, this knowledge is essential!
|
||||
|
||||
Happy exploring! 🔍
|
||||
|
||||
Reference in New Issue
Block a user