mirror of
https://github.com/mytechnotalent/Embedded-Hacking.git
synced 2026-06-01 12:01:37 +02:00
Updated WEEK04
This commit is contained in:
@@ -1,47 +0,0 @@
|
||||
# Embedded Systems Reverse Engineering
|
||||
[Repository](https://github.com/mytechnotalent/Embedded-Hacking)
|
||||
|
||||
## Week 3
|
||||
Embedded System Analysis: Understanding the RP2350 Architecture w/ Comprehensive Firmware Analysis
|
||||
|
||||
### Non-Credit Practice Exercise 1 Solution: Trace a Reset
|
||||
|
||||
#### Answers
|
||||
|
||||
##### Instruction Trace (First 10 Instructions from 0x1000015c)
|
||||
|
||||
| # | Address | Instruction | What It Does | Key Register Change |
|
||||
|---|-------------|----------------------------------------|------------------------------------------------------|---------------------|
|
||||
| 1 | 0x1000015c | `mov.w r0, #0xd0000000` | Loads SIO base address into r0 | r0 = 0xd0000000 |
|
||||
| 2 | 0x10000160 | `ldr r0, [r0, #0]` | Reads CPUID register at SIO base + 0 | r0 = 0 (Core 0) |
|
||||
| 3 | 0x10000162 | `cbz r0, 0x1000016a` | If CPUID == 0 (Core 0), branch to data copy section | PC = 0x1000016a |
|
||||
| 4 | 0x1000016a | `ldr r0, [pc, #...]` | Loads source address for data copy (flash) | r0 = flash addr |
|
||||
| 5 | 0x1000016c | `ldr r1, [pc, #...]` | Loads destination address for data copy (RAM) | r1 = RAM addr |
|
||||
| 6 | 0x1000016e | `ldr r2, [pc, #...]` | Loads end address for data copy | r2 = end addr |
|
||||
| 7 | 0x10000170 | `cmp r1, r2` | Checks if all data has been copied | Flags updated |
|
||||
| 8 | 0x10000172 | `bhs 0x10000178` | If done (dest >= end), skip to BSS clear | PC conditional |
|
||||
| 9 | 0x10000174 | `ldm r0!, {r3}` | Loads 4 bytes from flash source, auto-increments r0 | r3 = data, r0 += 4 |
|
||||
| 10| 0x10000176 | `stm r1!, {r3}` | Stores 4 bytes to RAM destination, auto-increments r1| r1 += 4 |
|
||||
|
||||
##### GDB Session
|
||||
|
||||
```gdb
|
||||
(gdb) b *0x1000015c
|
||||
(gdb) monitor reset halt
|
||||
(gdb) c
|
||||
Breakpoint 1, 0x1000015c in _reset_handler ()
|
||||
(gdb) si
|
||||
(gdb) disas $pc,+2
|
||||
(gdb) info registers r0
|
||||
```
|
||||
|
||||
#### Reflection Answers
|
||||
|
||||
1. **Why does the reset handler check the CPUID before doing anything else?**
|
||||
The RP2350 has two Cortex-M33 cores that share the same reset handler entry point. The CPUID check at SIO base `0xd0000000` returns 0 for Core 0 and 1 for Core 1. Only Core 0 should perform the one-time initialization (data copy, BSS clear, calling `main()`). Without this check, both cores would simultaneously try to initialize RAM, causing data corruption and race conditions.
|
||||
|
||||
2. **What would happen if Core 1 tried to run the same initialization code as Core 0?**
|
||||
Both cores would simultaneously write to the same RAM locations during the data copy and BSS clear phases. This would cause data corruption due to race conditions—values could be partially written or overwritten unpredictably. The `cbz` instruction at `0x10000162` prevents this: if CPUID != 0, the code branches to `hold_non_core0_in_bootrom`, which sends Core 1 back to the bootrom to wait until Core 0 explicitly launches it later.
|
||||
|
||||
3. **Which registers are used in the first 10 instructions, and why those specific ones?**
|
||||
The first 10 instructions use `r0`, `r1`, `r2`, and `r3`. These are the ARM "caller-saved" scratch registers (r0–r3) that don't need to be preserved across function calls. Since the reset handler is the very first code to run (no caller to preserve state for), using these low registers is both efficient (16-bit Thumb encodings) and safe. `r0` handles CPUID check and source pointer, `r1` is the destination pointer, `r2` is the end marker, and `r3` is the data transfer register.
|
||||
@@ -1,130 +0,0 @@
|
||||
# Embedded Systems Reverse Engineering
|
||||
[Repository](https://github.com/mytechnotalent/Embedded-Hacking)
|
||||
|
||||
## Week 3
|
||||
Embedded System Analysis: Understanding the RP2350 Architecture w/ Comprehensive Firmware Analysis
|
||||
|
||||
### Non-Credit Practice Exercise 1: Trace a Reset
|
||||
|
||||
#### Objective
|
||||
Single-step through the first 10 instructions of the reset handler to understand exactly what happens when the RP2350 powers on or resets.
|
||||
|
||||
#### Prerequisites
|
||||
- Raspberry Pi Pico 2 with debug probe connected
|
||||
- OpenOCD and `arm-none-eabi-gdb` available in your PATH
|
||||
- `build\0x0001_hello-world.elf` present and flashed to the board
|
||||
- Week 3 environment setup completed (OpenOCD running, GDB connected)
|
||||
|
||||
#### Task Description
|
||||
You will set a breakpoint at the reset handler (`0x1000015c`), trigger a reset, and step through each instruction one at a time while documenting what each instruction does.
|
||||
|
||||
#### Step-by-Step Instructions
|
||||
|
||||
##### Step 1: Start OpenOCD
|
||||
|
||||
```powershell
|
||||
openocd ^
|
||||
-s "C:\Users\flare-vm\.pico-sdk\openocd\0.12.0+dev\scripts" ^
|
||||
-f interface/cmsis-dap.cfg ^
|
||||
-f target/rp2350.cfg ^
|
||||
-c "adapter speed 5000"
|
||||
```
|
||||
|
||||
##### Step 2: Launch GDB
|
||||
|
||||
```powershell
|
||||
arm-none-eabi-gdb build\0x0001_hello-world.elf
|
||||
```
|
||||
|
||||
##### Step 3: Connect to Target
|
||||
|
||||
```gdb
|
||||
(gdb) target extended-remote :3333
|
||||
```
|
||||
|
||||
##### Step 4: Set Breakpoint at Reset Handler
|
||||
|
||||
```gdb
|
||||
(gdb) b *0x1000015c
|
||||
```
|
||||
|
||||
**What this does:** Places a breakpoint at the very first instruction of the reset handler (the entry point after bootrom).
|
||||
|
||||
##### Step 5: Reset and Break
|
||||
|
||||
```gdb
|
||||
(gdb) monitor reset halt
|
||||
(gdb) c
|
||||
```
|
||||
|
||||
**What this does:**
|
||||
- `monitor reset halt` resets the chip and immediately halts it
|
||||
- `c` continues execution until the breakpoint at the reset handler is hit
|
||||
|
||||
##### Step 6: Single-Step Through Instructions
|
||||
|
||||
Now step through the first 10 instructions, one at a time:
|
||||
|
||||
```gdb
|
||||
(gdb) si
|
||||
(gdb) disas $pc,+2
|
||||
(gdb) info registers r0
|
||||
```
|
||||
|
||||
Repeat `si` nine more times, examining each instruction.
|
||||
|
||||
**Example of what you'll see:**
|
||||
|
||||
**Instruction 1:**
|
||||
```
|
||||
0x1000015c <_reset_handler>: mov.w r0, #3489660928 @ 0xd0000000
|
||||
```
|
||||
**What it does:** Loads the SIO base address (0xd0000000) into r0
|
||||
|
||||
**Instruction 2:**
|
||||
```
|
||||
0x10000160 <_reset_handler+4>: ldr r0, [r0, #0]
|
||||
```
|
||||
**What it does:** Reads the CPUID register to determine which core is running
|
||||
|
||||
**Instruction 3:**
|
||||
```
|
||||
0x10000162 <_reset_handler+6>: cbz r0, 0x1000016a
|
||||
```
|
||||
**What it does:** If CPUID is 0 (Core 0), branch ahead to continue boot; otherwise handle Core 1
|
||||
|
||||
##### Step 7: Document Your Observations
|
||||
|
||||
For each of the 10 instructions:
|
||||
1. Write down the address
|
||||
2. Write down the assembly instruction
|
||||
3. Explain what it does
|
||||
4. Note any register changes using `info registers`
|
||||
|
||||
#### Expected Output
|
||||
- You should see the reset handler check which core is running
|
||||
- If you're on Core 0, you'll see it jump to the data copy section
|
||||
- Register `r0` will contain CPUID value (should be 0)
|
||||
- PC (program counter) advances with each `si` command
|
||||
|
||||
#### Questions for Reflection
|
||||
|
||||
###### Question 1: Why does the reset handler check the CPUID before doing anything else?
|
||||
|
||||
###### Question 2: What would happen if Core 1 tried to run the same initialization code as Core 0?
|
||||
|
||||
###### Question 3: Which registers are used in the first 10 instructions, and why those specific ones?
|
||||
|
||||
#### Tips and Hints
|
||||
- Use `disas $pc,+20` to see upcoming instructions without stepping through them
|
||||
- Use `info registers` to see all register values at any point
|
||||
- If you step past where you wanted to stop, just `monitor reset halt` and start over
|
||||
- Keep notes as you go—this is detective work!
|
||||
|
||||
#### Next Steps
|
||||
- Try stepping all the way through to the data copy loop
|
||||
- Set a breakpoint at `0x1000016c` (the data copy loop) and continue there directly
|
||||
- Move on to Exercise 2 to calculate the stack size from the vector table
|
||||
|
||||
#### Additional Challenge
|
||||
Set a breakpoint at `0x10000178` (the BSS clear phase) and continue execution to see how the reset handler transitions from data copying to BSS clearing.
|
||||
@@ -1,76 +0,0 @@
|
||||
# Embedded Systems Reverse Engineering
|
||||
[Repository](https://github.com/mytechnotalent/Embedded-Hacking)
|
||||
|
||||
## Week 3
|
||||
Embedded System Analysis: Understanding the RP2350 Architecture w/ Comprehensive Firmware Analysis
|
||||
|
||||
### Non-Credit Practice Exercise 2 Solution: Find the Stack Size
|
||||
|
||||
#### Answers
|
||||
|
||||
##### Initial Stack Pointer
|
||||
|
||||
```gdb
|
||||
(gdb) x/x 0x10000000
|
||||
0x10000000 <__vectors>: 0x20082000
|
||||
```
|
||||
|
||||
The initial stack pointer is **0x20082000**, stored as the first entry in the vector table.
|
||||
|
||||
##### Stack Limit
|
||||
|
||||
The stack limit is **0x20078000**, defined by the linker script.
|
||||
|
||||
```gdb
|
||||
(gdb) info symbol __StackLimit
|
||||
__StackLimit in section .stack
|
||||
```
|
||||
|
||||
##### Stack Size Calculation
|
||||
|
||||
| Value | Hex | Decimal |
|
||||
|-----------------|-------------|---------------|
|
||||
| Stack Top | 0x20082000 | 537,108,480 |
|
||||
| Stack Limit | 0x20078000 | 537,067,520 |
|
||||
| **Stack Size** | 0x0000A000 | **40,960 bytes** |
|
||||
|
||||
```
|
||||
Stack Size = 0x20082000 - 0x20078000 = 0xA000 = 40,960 bytes
|
||||
40,960 ÷ 1,024 = 40 KB
|
||||
```
|
||||
|
||||
##### Memory Region Verification
|
||||
|
||||
| Region | Start | End | Size |
|
||||
|-----------|-------------|-------------|--------|
|
||||
| RAM | 0x20000000 | 0x20080000 | 512 KB |
|
||||
| SCRATCH_X | 0x20080000 | 0x20081000 | 4 KB |
|
||||
| SCRATCH_Y | 0x20081000 | 0x20082000 | 4 KB |
|
||||
| **Stack** | 0x20078000 | 0x20082000 | **40 KB** |
|
||||
|
||||
The stack spans from the upper portion of main RAM through SCRATCH_X and into SCRATCH_Y.
|
||||
|
||||
##### Runtime Stack Usage at main()
|
||||
|
||||
```gdb
|
||||
(gdb) b main
|
||||
(gdb) c
|
||||
(gdb) info registers sp
|
||||
sp 0x20081fc8 0x20081fc8
|
||||
```
|
||||
|
||||
Stack used at `main()` entry: `0x20082000 - 0x20081fc8 = 0x38 = 56 bytes`
|
||||
|
||||
#### Reflection Answers
|
||||
|
||||
1. **Why is the stack 40 KB instead of just fitting in the 4 KB SCRATCH_Y region?**
|
||||
The stack pointer is initialized at the top of SCRATCH_Y (`0x20082000`), but the stack grows **downward**. As functions are called and local variables are allocated, the stack pointer decreases past the SCRATCH_Y boundary (`0x20081000`), through SCRATCH_X, and into the upper portion of main RAM. The linker sets the stack limit at `0x20078000` to give the stack 40 KB of room, which is sufficient for typical embedded applications with nested function calls.
|
||||
|
||||
2. **What happens if the stack grows beyond 0x20078000?**
|
||||
A **stack overflow** occurs. The stack would collide with the heap or global data stored in the lower portion of RAM. This can corrupt variables, overwrite heap metadata, or cause a HardFault if memory protection is enabled. On the RP2350 with Cortex-M33 MPU support, a MemManage fault could be triggered if stack guard regions are configured.
|
||||
|
||||
3. **How would you detect a stack overflow during runtime?**
|
||||
Several methods exist: (a) Write a known "canary" pattern (e.g., `0xDEADBEEF`) at the stack limit and periodically check if it's been overwritten. (b) Use the Cortex-M33 MPU to set a guard region at `0x20078000` that triggers a MemManage fault on access. (c) In GDB, use a watchpoint: `watch *(int*)0x20078000` to break if the stack reaches the limit. (d) Use the `stackusage` GDB macro from the exercise to monitor usage.
|
||||
|
||||
4. **Why does the stack grow downward instead of upward?**
|
||||
This is an ARM architecture convention inherited from early processor designs. Growing downward allows the stack and heap to grow toward each other from opposite ends of RAM, maximizing memory utilization without needing to know each region's size in advance. The stack starts at the highest address and grows down, while the heap starts at a lower address and grows up. If they meet, memory is exhausted—but this layout gives both regions the maximum possible space.
|
||||
@@ -1,167 +0,0 @@
|
||||
# Embedded Systems Reverse Engineering
|
||||
[Repository](https://github.com/mytechnotalent/Embedded-Hacking)
|
||||
|
||||
## Week 3
|
||||
Embedded System Analysis: Understanding the RP2350 Architecture w/ Comprehensive Firmware Analysis
|
||||
|
||||
### Non-Credit Practice Exercise 2: Find the Stack Size
|
||||
|
||||
#### Objective
|
||||
Calculate the size of the stack by examining the vector table, understanding the linker script's memory layout, and performing manual calculations.
|
||||
|
||||
#### Prerequisites
|
||||
- Raspberry Pi Pico 2 with debug probe connected
|
||||
- OpenOCD and `arm-none-eabi-gdb` available
|
||||
- `build\0x0001_hello-world.elf` flashed to the board
|
||||
- Understanding of memory regions from Week 3 Part 5 (Linker Script)
|
||||
|
||||
#### Task Description
|
||||
You will examine the initial stack pointer value from the vector table, identify the stack limit, calculate the total stack size in bytes and kilobytes, and verify your calculations.
|
||||
|
||||
#### Background Information
|
||||
|
||||
From the Week 3 lesson, we learned:
|
||||
- The initial stack pointer is stored at `0x10000000` (first entry in vector table)
|
||||
- The linker script defines: `SCRATCH_Y: ORIGIN = 0x20081000, LENGTH = 4k`
|
||||
- Stack top is calculated as: `ORIGIN + LENGTH = 0x20082000`
|
||||
- The stack grows downward from high addresses to low addresses
|
||||
|
||||
#### Step-by-Step Instructions
|
||||
|
||||
##### Step 1: Connect and Halt
|
||||
|
||||
```gdb
|
||||
(gdb) target extended-remote :3333
|
||||
(gdb) monitor reset halt
|
||||
```
|
||||
|
||||
##### Step 2: Examine the Initial Stack Pointer
|
||||
|
||||
```gdb
|
||||
(gdb) x/x 0x10000000
|
||||
```
|
||||
|
||||
**Expected output:**
|
||||
```
|
||||
0x10000000 <__vectors>: 0x20082000
|
||||
```
|
||||
|
||||
This is the **top of the stack** (where the stack starts before growing downward).
|
||||
|
||||
##### Step 3: Find the Stack Limit
|
||||
|
||||
The stack limit is defined in the linker script and can be found by examining stack-related symbols or calculating from memory regions.
|
||||
|
||||
From the Week 3 lesson, the stack limit is `0x20078000`.
|
||||
|
||||
You can verify this in GDB:
|
||||
|
||||
```gdb
|
||||
(gdb) info symbol __StackLimit
|
||||
```
|
||||
|
||||
or check registers after boot:
|
||||
|
||||
```gdb
|
||||
(gdb) info registers
|
||||
```
|
||||
|
||||
Look for stack limit values or calculate: The main RAM starts at `0x20000000`, and SCRATCH_Y starts at `0x20081000`.
|
||||
|
||||
##### Step 4: Calculate Stack Size in Bytes
|
||||
|
||||
**Formula:**
|
||||
```
|
||||
Stack Size = Stack Top - Stack Limit
|
||||
Stack Size = 0x20082000 - 0x20078000
|
||||
```
|
||||
|
||||
Let's convert to decimal:
|
||||
- `0x20082000` = 537,108,480 decimal
|
||||
- `0x20078000` = 537,067,520 decimal
|
||||
- Difference = 40,960 bytes
|
||||
|
||||
**Alternative hex calculation:**
|
||||
```
|
||||
0x20082000
|
||||
- 0x20078000
|
||||
-----------
|
||||
0x0000A000 = 40,960 bytes
|
||||
```
|
||||
|
||||
##### Step 5: Convert to Kilobytes
|
||||
|
||||
```
|
||||
Bytes to KB = 40,960 ÷ 1,024 = 40 KB
|
||||
```
|
||||
|
||||
So the stack is **40 KB** in size.
|
||||
|
||||
##### Step 6: Verify Using Memory Regions
|
||||
|
||||
Cross-check with the memory layout:
|
||||
- **RAM**: `0x20000000` - `0x20080000` (512 KB)
|
||||
- **SCRATCH_X**: `0x20080000` - `0x20081000` (4 KB)
|
||||
- **SCRATCH_Y**: `0x20081000` - `0x20082000` (4 KB) ? Stack lives here
|
||||
- **Stack range**: `0x20078000` - `0x20082000` (40 KB)
|
||||
|
||||
The stack extends from SCRATCH_Y down into the upper portion of main RAM.
|
||||
|
||||
##### Step 7: Examine Stack Usage at Runtime
|
||||
|
||||
You can see the current stack pointer value:
|
||||
|
||||
```gdb
|
||||
(gdb) b main
|
||||
(gdb) c
|
||||
(gdb) info registers sp
|
||||
```
|
||||
|
||||
**Expected output:**
|
||||
```
|
||||
sp 0x20081fc8 0x20081fc8
|
||||
```
|
||||
|
||||
This shows the stack has used:
|
||||
```
|
||||
0x20082000 - 0x20081fc8 = 0x38 = 56 bytes
|
||||
```
|
||||
|
||||
#### Expected Output
|
||||
- Initial stack pointer: `0x20082000`
|
||||
- Stack limit: `0x20078000`
|
||||
- Stack size: **40,960 bytes** or **40 KB**
|
||||
- Current stack usage (at main): approximately 56 bytes
|
||||
|
||||
#### Questions for Reflection
|
||||
|
||||
###### Question 1: Why is the stack 40 KB instead of just fitting in the 4 KB SCRATCH_Y region?
|
||||
|
||||
###### Question 2: What happens if the stack grows beyond 0x20078000?
|
||||
|
||||
###### Question 3: How would you detect a stack overflow during runtime?
|
||||
|
||||
###### Question 4: Why does the stack grow downward instead of upward?
|
||||
|
||||
#### Tips and Hints
|
||||
- Use Windows Calculator in Programmer mode to convert hex to decimal
|
||||
- Remember: 1 KB = 1,024 bytes (not 1,000)
|
||||
- The stack pointer (SP) decreases as the stack grows (push operations)
|
||||
- Use `bt` (backtrace) in GDB to see how much stack is currently in use
|
||||
|
||||
#### Next Steps
|
||||
- Monitor the stack pointer as you step through functions to see it change
|
||||
- Calculate stack usage for specific function calls
|
||||
- Move on to Exercise 3 to examine all vector table entries
|
||||
|
||||
#### Additional Challenge
|
||||
Write a GDB command to automatically calculate and display stack usage:
|
||||
|
||||
```gdb
|
||||
(gdb) define stackusage
|
||||
> set $used = 0x20082000 - $sp
|
||||
> printf "Stack used: %d bytes (%d KB)\n", $used, $used/1024
|
||||
> end
|
||||
|
||||
(gdb) stackusage
|
||||
```
|
||||
@@ -1,78 +0,0 @@
|
||||
# Embedded Systems Reverse Engineering
|
||||
[Repository](https://github.com/mytechnotalent/Embedded-Hacking)
|
||||
|
||||
## Week 3
|
||||
Embedded System Analysis: Understanding the RP2350 Architecture w/ Comprehensive Firmware Analysis
|
||||
|
||||
### Non-Credit Practice Exercise 3 Solution: Examine All Vectors
|
||||
|
||||
#### Answers
|
||||
|
||||
##### Vector Table Dump
|
||||
|
||||
```gdb
|
||||
(gdb) x/16x 0x10000000
|
||||
0x10000000 <__vectors>: 0x20082000 0x1000015d 0x1000011b 0x1000011d
|
||||
0x10000010 <__vectors+16>: 0x1000011f 0x10000121 0x10000123 0x00000000
|
||||
0x10000020 <__vectors+32>: 0x00000000 0x00000000 0x00000000 0x10000125
|
||||
0x10000030 <__vectors+48>: 0x00000000 0x00000000 0x10000127 0x10000129
|
||||
```
|
||||
|
||||
##### Complete Vector Table Map
|
||||
|
||||
| Offset | Vector # | Raw Value | Address Type | Actual Addr | Handler Name |
|
||||
|--------|----------|-------------|---------------|-------------|------------------|
|
||||
| 0x00 | — | 0x20082000 | Stack Pointer | N/A | __StackTop |
|
||||
| 0x04 | 1 | 0x1000015d | Code (Thumb) | 0x1000015c | _reset_handler |
|
||||
| 0x08 | 2 | 0x1000011b | Code (Thumb) | 0x1000011a | isr_nmi |
|
||||
| 0x0C | 3 | 0x1000011d | Code (Thumb) | 0x1000011c | isr_hardfault |
|
||||
| 0x10 | 4 | 0x1000011f | Code (Thumb) | 0x1000011e | isr_memmanage |
|
||||
| 0x14 | 5 | 0x10000121 | Code (Thumb) | 0x10000120 | isr_busfault |
|
||||
| 0x18 | 6 | 0x10000123 | Code (Thumb) | 0x10000122 | isr_usagefault |
|
||||
| 0x1C | 7 | 0x00000000 | Reserved | N/A | (Reserved) |
|
||||
| 0x20 | 8 | 0x00000000 | Reserved | N/A | (Reserved) |
|
||||
| 0x24 | 9 | 0x00000000 | Reserved | N/A | (Reserved) |
|
||||
| 0x28 | 10 | 0x00000000 | Reserved | N/A | (Reserved) |
|
||||
| 0x2C | 11 | 0x10000125 | Code (Thumb) | 0x10000124 | isr_svcall |
|
||||
| 0x30 | 12 | 0x00000000 | Reserved | N/A | (Reserved) |
|
||||
| 0x34 | 13 | 0x00000000 | Reserved | N/A | (Reserved) |
|
||||
| 0x38 | 14 | 0x10000127 | Code (Thumb) | 0x10000126 | isr_pendsv |
|
||||
| 0x3C | 15 | 0x10000129 | Code (Thumb) | 0x10000128 | isr_systick |
|
||||
|
||||
##### Handler Verification
|
||||
|
||||
```gdb
|
||||
(gdb) info symbol 0x1000015c
|
||||
_reset_handler in section .text
|
||||
(gdb) info symbol 0x1000011a
|
||||
isr_nmi in section .text
|
||||
(gdb) x/3i 0x1000011a
|
||||
0x1000011a <isr_nmi>: bkpt 0x0000
|
||||
0x1000011c <isr_hardfault>: bkpt 0x0000
|
||||
0x1000011e <isr_svcall>: bkpt 0x0000
|
||||
```
|
||||
|
||||
Most default handlers are single `bkpt` instructions—they halt the processor if triggered unexpectedly.
|
||||
|
||||
##### Summary Statistics
|
||||
|
||||
| Category | Count |
|
||||
|---------------------|-------|
|
||||
| Stack Pointer entry | 1 |
|
||||
| Valid code entries | 10 |
|
||||
| Reserved (0x0) | 5 |
|
||||
| **Total entries** | **16** |
|
||||
|
||||
#### Reflection Answers
|
||||
|
||||
1. **Why do all the code addresses end in odd numbers (LSB = 1)?**
|
||||
ARM Cortex-M processors exclusively execute in Thumb mode. The least significant bit (LSB) of vector table entries indicates the instruction set: LSB = 1 means Thumb mode. Since Cortex-M33 only supports Thumb/Thumb-2, all code addresses must have bit 0 set to 1. The actual instruction address is the value with bit 0 cleared (e.g., `0x1000015d` → instruction at `0x1000015c`). This is a hardware requirement—loading an even address into the PC on Cortex-M would cause a HardFault.
|
||||
|
||||
2. **What happens if an exception occurs for a reserved/null vector entry?**
|
||||
If the processor attempts to invoke a handler through a vector entry containing `0x00000000`, it tries to fetch instructions from address `0x00000000` (in the bootrom region). This would either execute bootrom code unexpectedly or trigger a HardFault because the address lacks the Thumb bit (LSB = 0). In practice, a HardFault would occur, and if the HardFault handler itself is invalid, the processor enters a lockup state.
|
||||
|
||||
3. **Why do most exception handlers just contain `bkpt` instructions?**
|
||||
The Pico SDK provides these as **default weak handlers**. They are intentionally minimal—a `bkpt` (breakpoint) instruction halts the processor immediately so a debugger can catch the event. This is a safety mechanism: rather than letting an unhandled exception corrupt state or silently continue, the default handlers stop execution so the developer can diagnose the problem. Application code can override any handler by defining a function with the matching name (the weak linkage gets replaced).
|
||||
|
||||
4. **How would you replace a default handler with your own custom handler?**
|
||||
Define a C function with the exact handler name (e.g., `void isr_hardfault(void)`) in your application code. Because the SDK declares these handlers as `__attribute__((weak))`, the linker will use your strong definition instead of the default `bkpt` stub. The new function's address (with Thumb bit set) will automatically appear in the vector table at the correct offset. No linker script modification is needed.
|
||||
@@ -1,209 +0,0 @@
|
||||
# Embedded Systems Reverse Engineering
|
||||
[Repository](https://github.com/mytechnotalent/Embedded-Hacking)
|
||||
|
||||
## Week 3
|
||||
Embedded System Analysis: Understanding the RP2350 Architecture w/ Comprehensive Firmware Analysis
|
||||
|
||||
### Non-Credit Practice Exercise 3: Examine All Vectors
|
||||
|
||||
#### Objective
|
||||
Examine the first 16 entries of the vector table to understand the exception handler layout, identify valid code addresses, and recognize the Thumb mode addressing convention.
|
||||
|
||||
#### Prerequisites
|
||||
- Raspberry Pi Pico 2 with debug probe connected
|
||||
- OpenOCD and `arm-none-eabi-gdb` available
|
||||
- `build\0x0001_hello-world.elf` loaded
|
||||
- Understanding of the vector table from Week 3 Part 4
|
||||
- Knowledge of Thumb mode addressing (LSB = 1 indicates Thumb code)
|
||||
|
||||
#### Task Description
|
||||
You will examine 16 consecutive 32-bit values from the vector table, decode each entry, determine if it's a valid code address, and identify which exception handler it points to.
|
||||
|
||||
#### Background Information
|
||||
|
||||
The ARM Cortex-M vector table structure:
|
||||
|
||||
| Offset | Vector # | Handler Name | Purpose |
|
||||
|--------|----------|---------------------|---------|
|
||||
| 0x00 | - | Initial SP | Stack pointer initialization |
|
||||
| 0x04 | 1 | Reset | Power-on/reset entry point |
|
||||
| 0x08 | 2 | NMI | Non-Maskable Interrupt |
|
||||
| 0x0C | 3 | HardFault | Serious errors |
|
||||
| 0x10 | 4 | MemManage | Memory protection fault |
|
||||
| 0x14 | 5 | BusFault | Bus error |
|
||||
| 0x18 | 6 | UsageFault | Undefined instruction, etc. |
|
||||
| 0x1C-0x28 | 7-10 | Reserved | Not used |
|
||||
| 0x2C | 11 | SVCall | Supervisor call |
|
||||
| 0x30 | 12 | Debug Monitor | Debug events |
|
||||
| 0x34 | 13 | Reserved | Not used |
|
||||
| 0x38 | 14 | PendSV | Pendable service call |
|
||||
| 0x3C | 15 | SysTick | System tick timer |
|
||||
|
||||
#### Step-by-Step Instructions
|
||||
|
||||
##### Step 1: Connect and Halt
|
||||
|
||||
```gdb
|
||||
(gdb) target extended-remote :3333
|
||||
(gdb) monitor reset halt
|
||||
```
|
||||
|
||||
##### Step 2: Examine 16 Vector Table Entries
|
||||
|
||||
```gdb
|
||||
(gdb) x/16x 0x10000000
|
||||
```
|
||||
|
||||
**Expected output (example):**
|
||||
```
|
||||
0x10000000 <__vectors>: 0x20082000 0x1000015d 0x1000011b 0x1000011d
|
||||
0x10000010 <__vectors+16>: 0x1000011f 0x10000121 0x10000123 0x00000000
|
||||
0x10000020 <__vectors+32>: 0x00000000 0x00000000 0x00000000 0x10000125
|
||||
0x10000030 <__vectors+48>: 0x00000000 0x00000000 0x10000127 0x10000129
|
||||
```
|
||||
|
||||
##### Step 3: Analyze Each Entry
|
||||
|
||||
Create a table documenting each entry:
|
||||
|
||||
**Entry 1 (Offset 0x00):**
|
||||
```
|
||||
Address: 0x10000000
|
||||
Value: 0x20082000
|
||||
Valid Code Address? NO - This is the stack pointer (in RAM region 0x2xxxxxxx)
|
||||
Handler: Initial Stack Pointer
|
||||
```
|
||||
|
||||
**Entry 2 (Offset 0x04):**
|
||||
```
|
||||
Address: 0x10000004
|
||||
Value: 0x1000015d
|
||||
Valid Code Address? YES (starts with 0x1000...)
|
||||
Thumb Mode? YES (LSB = 1, so actual address is 0x1000015c)
|
||||
Handler: Reset Handler (_reset_handler)
|
||||
```
|
||||
|
||||
**Entry 3 (Offset 0x08):**
|
||||
```
|
||||
Address: 0x10000008
|
||||
Value: 0x1000011b
|
||||
Valid Code Address? YES
|
||||
Thumb Mode? YES (actual address: 0x1000011a)
|
||||
Handler: NMI Handler (isr_nmi)
|
||||
```
|
||||
|
||||
Continue this analysis for all 16 entries...
|
||||
|
||||
##### Step 4: Verify Handlers with Symbols
|
||||
|
||||
For each code address, check what function it points to:
|
||||
|
||||
```gdb
|
||||
(gdb) info symbol 0x1000015c
|
||||
```
|
||||
|
||||
**Expected output:**
|
||||
```
|
||||
_reset_handler in section .text
|
||||
```
|
||||
|
||||
Repeat for other addresses:
|
||||
|
||||
```gdb
|
||||
(gdb) info symbol 0x1000011a
|
||||
(gdb) info symbol 0x1000011c
|
||||
(gdb) info symbol 0x1000011e
|
||||
```
|
||||
|
||||
##### Step 5: Examine Handler Code
|
||||
|
||||
Look at the actual code at each handler:
|
||||
|
||||
```gdb
|
||||
(gdb) x/3i 0x1000011a
|
||||
```
|
||||
|
||||
**Expected output for NMI handler:**
|
||||
```
|
||||
0x1000011a <isr_nmi>: bkpt 0x0000
|
||||
0x1000011c <isr_hardfault>: bkpt 0x0000
|
||||
0x1000011e <isr_svcall>: bkpt 0x0000
|
||||
```
|
||||
|
||||
##### Step 6: Identify Reserved Entries
|
||||
|
||||
Note any entries with value `0x00000000`:
|
||||
|
||||
```
|
||||
0x00000000 = Reserved/Unused vector
|
||||
```
|
||||
|
||||
These slots are reserved by ARM and not used on Cortex-M33.
|
||||
|
||||
##### Step 7: Create a Complete Map
|
||||
|
||||
Document all 16 entries in this format:
|
||||
|
||||
| Offset | Value | Address Type | Actual Addr | Handler Name |
|
||||
|--------|------------|--------------|-------------|------------------|
|
||||
| 0x00 | 0x20082000 | Stack Ptr | N/A | __StackTop |
|
||||
| 0x04 | 0x1000015d | Code (Thumb) | 0x1000015c | _reset_handler |
|
||||
| 0x08 | 0x1000011b | Code (Thumb) | 0x1000011a | isr_nmi |
|
||||
| 0x0C | 0x1000011d | Code (Thumb) | 0x1000011c | isr_hardfault |
|
||||
| ... | ... | ... | ... | ... |
|
||||
|
||||
#### Expected Output
|
||||
- First entry is the stack pointer in RAM (0x2xxxxxxx range)
|
||||
- Entries 2-16 are mostly code addresses in flash (0x1000xxxx range)
|
||||
- Code addresses have LSB = 1 (Thumb mode indicator)
|
||||
- Reserved entries show 0x00000000
|
||||
- Most handlers point to simple `bkpt` instructions (default handlers)
|
||||
|
||||
#### Questions for Reflection
|
||||
|
||||
###### Question 1: Why do all the code addresses end in odd numbers (LSB = 1)?
|
||||
|
||||
###### Question 2: What happens if an exception occurs for a reserved/null vector entry?
|
||||
|
||||
###### Question 3: Why do most exception handlers just contain `bkpt` instructions?
|
||||
|
||||
###### Question 4: How would you replace a default handler with your own custom handler?
|
||||
|
||||
#### Tips and Hints
|
||||
- Use `x/32x 0x10000000` to see even more vectors (up to 48)
|
||||
- Remember to subtract 1 from addresses before disassembling (remove Thumb bit)
|
||||
- Use `info functions` to see all available handler symbols
|
||||
- Compare GDB output with Ghidra's vector table view
|
||||
|
||||
#### Next Steps
|
||||
- Set breakpoints at different exception handlers to see if they're ever called
|
||||
- Trigger a fault intentionally to see which handler executes
|
||||
- Move on to Exercise 4 to analyze your main function
|
||||
|
||||
#### Additional Challenge
|
||||
Write a GDB script to automatically decode and display all vector table entries:
|
||||
|
||||
```gdb
|
||||
(gdb) define vectors
|
||||
> set $i = 0
|
||||
> while $i < 16
|
||||
> set $addr = 0x10000000 + ($i * 4)
|
||||
> set $val = *(int*)$addr
|
||||
> printf "[%2d] 0x%08x: 0x%08x", $i, $addr, $val
|
||||
> if $i == 0
|
||||
> printf " (Stack Pointer)\n"
|
||||
> else
|
||||
> if $val != 0
|
||||
> if ($val & 0x10000000) == 0x10000000
|
||||
> printf " -> 0x%08x\n", $val & 0xFFFFFFFE
|
||||
> else
|
||||
> printf " (Invalid/Reserved)\n"
|
||||
> end
|
||||
> else
|
||||
> printf " (Reserved)\n"
|
||||
> end
|
||||
> end
|
||||
> set $i = $i + 1
|
||||
> end
|
||||
> end
|
||||
```
|
||||
@@ -1,94 +0,0 @@
|
||||
# Embedded Systems Reverse Engineering
|
||||
[Repository](https://github.com/mytechnotalent/Embedded-Hacking)
|
||||
|
||||
## Week 3
|
||||
Embedded System Analysis: Understanding the RP2350 Architecture w/ Comprehensive Firmware Analysis
|
||||
|
||||
### Non-Credit Practice Exercise 4 Solution: Find Your Main Function and Trace Back
|
||||
|
||||
#### Answers
|
||||
|
||||
##### Main Function Location
|
||||
|
||||
```gdb
|
||||
(gdb) info functions main
|
||||
0x10000234 int main(void);
|
||||
```
|
||||
|
||||
`main()` is at address **0x10000234**.
|
||||
|
||||
##### Disassembly of main()
|
||||
|
||||
```gdb
|
||||
(gdb) x/10i 0x10000234
|
||||
0x10000234 <main>: push {r7, lr}
|
||||
0x10000236 <main+2>: sub sp, #8
|
||||
0x10000238 <main+4>: add r7, sp, #0
|
||||
0x1000023a <main+6>: bl 0x100012c4 <stdio_init_all>
|
||||
0x1000023e <main+10>: movw r0, #404 @ 0x194
|
||||
0x10000242 <main+14>: movt r0, #4096 @ 0x1000
|
||||
0x10000246 <main+18>: bl 0x1000023c <__wrap_puts>
|
||||
0x1000024a <main+22>: b.n 0x1000023e <main+10>
|
||||
```
|
||||
|
||||
##### First Function Call
|
||||
|
||||
The first function call is at offset +6:
|
||||
```
|
||||
0x1000023a <main+6>: bl 0x100012c4 <stdio_init_all>
|
||||
```
|
||||
|
||||
`stdio_init_all()` initializes all standard I/O systems (USB CDC, UART) so `printf()` and `puts()` can output to the serial console.
|
||||
|
||||
##### Link Register (Caller Identification)
|
||||
|
||||
```gdb
|
||||
(gdb) b main
|
||||
(gdb) c
|
||||
(gdb) info registers lr
|
||||
lr 0x1000018b 268435851
|
||||
```
|
||||
|
||||
LR = **0x1000018b** (Thumb address), actual return address = **0x1000018a**.
|
||||
|
||||
##### Caller Disassembly
|
||||
|
||||
```gdb
|
||||
(gdb) x/10i 0x10000186
|
||||
0x10000186 <platform_entry>: ldr r1, [pc, #80]
|
||||
0x10000188 <platform_entry+2>: blx r1 → calls runtime_init()
|
||||
0x1000018a <platform_entry+4>: ldr r1, [pc, #80] → LR points here (return from main)
|
||||
0x1000018c <platform_entry+6>: blx r1 → THIS called main()
|
||||
0x1000018e <platform_entry+8>: ldr r1, [pc, #80]
|
||||
0x10000190 <platform_entry+10>: blx r1 → calls exit()
|
||||
0x10000192 <platform_entry+12>: bkpt 0x0000
|
||||
```
|
||||
|
||||
##### Complete Boot Chain
|
||||
|
||||
```
|
||||
Power On
|
||||
→ Bootrom (0x00000000)
|
||||
→ Vector Table (0x10000000)
|
||||
→ _reset_handler (0x1000015c)
|
||||
→ Data Copy & BSS Clear
|
||||
→ platform_entry (0x10000186)
|
||||
→ runtime_init() (first blx)
|
||||
→ main() (second blx) ← 0x10000234
|
||||
→ stdio_init_all() (first call in main)
|
||||
→ puts() loop (infinite)
|
||||
```
|
||||
|
||||
#### Reflection Answers
|
||||
|
||||
1. **Why does the link register point 4 bytes after the `blx` instruction that called main?**
|
||||
The LR stores the **return address**—the instruction to execute after `main()` returns. The `blx r1` instruction at `0x10000188` (which calls `runtime_init`) is 2 bytes, and the `blx r1` at `0x1000018c` (which calls `main`) is also 2 bytes. The LR is set to the instruction immediately following the `blx` that called `main()`, which is `0x1000018a` (the `ldr` after `runtime_init`'s call). Actually, `platform_entry+4` at `0x1000018a` is where execution resumes, and the actual `blx` that calls main is at `+6` (`0x1000018c`), so LR = `0x1000018e` + Thumb bit. The key point: LR always points to the next instruction after the branch, so the caller can resume where it left off.
|
||||
|
||||
2. **What would happen if `main()` tried to return (instead of looping forever)?**
|
||||
Execution would return to `platform_entry` at the address stored in LR. Looking at the disassembly, `platform_entry` would proceed to execute the third `blx r1` at offset +10, which calls `exit()`. The `exit()` function would perform cleanup and ultimately halt the processor. After `exit()`, there's a `bkpt` instruction as a safety net. In practice on bare-metal embedded systems, returning from `main()` is generally avoided because there's no OS to return to.
|
||||
|
||||
3. **How can you tell from the disassembly that main contains an infinite loop?**
|
||||
The instruction at `0x1000024a` is `b.n 0x1000023e <main+10>`, which is an **unconditional branch** back to the `movw r0, #404` instruction that loads the string address. This is a `b` (branch) with no condition code, meaning it always jumps backward. There is no path that reaches the end of the function or a `pop {r7, pc}` (return). The `push {r7, lr}` at the start saves the return address but it's never restored—the function loops forever.
|
||||
|
||||
4. **Why is `stdio_init_all()` called before the printf loop?**
|
||||
`stdio_init_all()` configures the hardware interfaces (USB CDC and/or UART) that `printf()`/`puts()` uses for output. Without this initialization, the serial output drivers are not set up—writes to stdout would go nowhere or cause a fault. It must be called exactly once before any I/O operation. Calling it inside the loop would repeatedly reinitialize the hardware, wasting time and potentially disrupting active connections.
|
||||
@@ -1,238 +0,0 @@
|
||||
# Embedded Systems Reverse Engineering
|
||||
[Repository](https://github.com/mytechnotalent/Embedded-Hacking)
|
||||
|
||||
## Week 3
|
||||
Embedded System Analysis: Understanding the RP2350 Architecture w/ Comprehensive Firmware Analysis
|
||||
|
||||
### Non-Credit Practice Exercise 4: Find Your Main Function and Trace Back
|
||||
|
||||
#### Objective
|
||||
Locate the `main()` function, examine its first instructions, identify the first function call, and trace backward to discover where `main()` was called from.
|
||||
|
||||
#### Prerequisites
|
||||
- Raspberry Pi Pico 2 with debug probe connected
|
||||
- OpenOCD and `arm-none-eabi-gdb` available
|
||||
- `build\0x0001_hello-world.elf` loaded
|
||||
- Understanding of function calls and the link register (LR) from previous weeks
|
||||
|
||||
#### Task Description
|
||||
You will use GDB to find `main()`, examine its disassembly, identify the initial function call (`stdio_init_all`), and use the link register to trace backward through the boot sequence.
|
||||
|
||||
#### Background Information
|
||||
|
||||
Key concepts:
|
||||
- **Link Register (LR)**: Stores the return address when a function is called
|
||||
- **Program Counter (PC)**: Points to the currently executing instruction
|
||||
- **Function prologue**: The setup code at the start of every function
|
||||
- **bl instruction**: "Branch with Link" - calls a function and stores return address in LR
|
||||
|
||||
#### Step-by-Step Instructions
|
||||
|
||||
##### Step 1: Connect and Halt
|
||||
|
||||
```gdb
|
||||
(gdb) target extended-remote :3333
|
||||
(gdb) monitor reset halt
|
||||
```
|
||||
|
||||
##### Step 2: Find the Main Function
|
||||
|
||||
```gdb
|
||||
(gdb) info functions main
|
||||
```
|
||||
|
||||
**Expected output:**
|
||||
```
|
||||
All functions matching regular expression "main":
|
||||
|
||||
File 0x0001_hello-world.c:
|
||||
0x10000234 int main(void);
|
||||
|
||||
Non-debugging symbols:
|
||||
0x10000186 platform_entry_arm_a
|
||||
...
|
||||
```
|
||||
|
||||
Note the address of `main`: **`0x10000234`**
|
||||
|
||||
##### Step 3: Examine Instructions at Main
|
||||
|
||||
```gdb
|
||||
(gdb) x/10i 0x10000234
|
||||
```
|
||||
|
||||
**Expected output:**
|
||||
```
|
||||
0x10000234 <main>: push {r7, lr}
|
||||
0x10000236 <main+2>: sub sp, #8
|
||||
0x10000238 <main+4>: add r7, sp, #0
|
||||
0x1000023a <main+6>: bl 0x100012c4 <stdio_init_all>
|
||||
0x1000023e <main+10>: movw r0, #404 @ 0x194
|
||||
0x10000242 <main+14>: movt r0, #4096 @ 0x1000
|
||||
0x10000246 <main+18>: bl 0x1000023c <__wrap_puts>
|
||||
0x1000024a <main+22>: b.n 0x1000023e <main+10>
|
||||
0x1000024c <runtime_init>: push {r3, r4, r5, r6, r7, lr}
|
||||
```
|
||||
|
||||
##### Step 4: Identify the First Function Call
|
||||
|
||||
The first function call in `main()` is:
|
||||
```
|
||||
0x1000023a <main+6>: bl 0x100012c4 <stdio_init_all>
|
||||
```
|
||||
|
||||
**What does this function do?**
|
||||
|
||||
```gdb
|
||||
(gdb) info functions stdio_init_all
|
||||
```
|
||||
|
||||
**Answer:** `stdio_init_all()` initializes all standard I/O systems (USB, UART, etc.) so `printf()` works.
|
||||
|
||||
##### Step 5: Set a Breakpoint at Main
|
||||
|
||||
```gdb
|
||||
(gdb) b main
|
||||
(gdb) c
|
||||
```
|
||||
|
||||
**Expected output:**
|
||||
```
|
||||
Breakpoint 1, main () at 0x0001_hello-world.c:5
|
||||
5 stdio_init_all();
|
||||
```
|
||||
|
||||
##### Step 6: Examine the Link Register
|
||||
|
||||
When stopped at `main()`, check what's in the link register:
|
||||
|
||||
```gdb
|
||||
(gdb) info registers lr
|
||||
```
|
||||
|
||||
**Expected output:**
|
||||
```
|
||||
lr 0x1000018b 268435851
|
||||
```
|
||||
|
||||
The LR contains the return address - where execution will go when `main()` returns.
|
||||
|
||||
##### Step 7: Disassemble the Caller
|
||||
|
||||
Subtract 1 to remove the Thumb bit and disassemble:
|
||||
|
||||
```gdb
|
||||
(gdb) x/10i 0x1000018a
|
||||
```
|
||||
|
||||
**Expected output:**
|
||||
```
|
||||
0x10000186 <platform_entry>: ldr r1, [pc, #80]
|
||||
0x10000188 <platform_entry+2>: blx r1
|
||||
0x1000018a <platform_entry+4>: ldr r1, [pc, #80] ? LR points here
|
||||
0x1000018c <platform_entry+6>: blx r1 ? This called main
|
||||
0x1000018e <platform_entry+8>: ldr r1, [pc, #80]
|
||||
0x10000190 <platform_entry+10>: blx r1
|
||||
0x10000192 <platform_entry+12>: bkpt 0x0000
|
||||
```
|
||||
|
||||
##### Step 8: Understand the Call Chain
|
||||
|
||||
Working backward from `main()`:
|
||||
|
||||
```
|
||||
platform_entry (0x10000186)
|
||||
? calls (blx at +2)
|
||||
runtime_init() (0x1000024c)
|
||||
? calls (blx at +6)
|
||||
main() (0x10000234) ? We are here
|
||||
? will call (blx at +6)
|
||||
stdio_init_all() (0x100012c4)
|
||||
```
|
||||
|
||||
##### Step 9: Verify Platform Entry Calls Main
|
||||
|
||||
Look at what `platform_entry` loads before the `blx`:
|
||||
|
||||
```gdb
|
||||
(gdb) x/x 0x100001dc
|
||||
```
|
||||
|
||||
This is the address loaded into r1 before calling `blx`. It should point to `main()`.
|
||||
|
||||
**Expected output:**
|
||||
```
|
||||
0x100001dc <data_cpy_table+60>: 0x10000235
|
||||
```
|
||||
|
||||
Note: `0x10000235` = `0x10000234` + 1 (Thumb bit), which is the address of `main()`!
|
||||
|
||||
##### Step 10: Complete the Boot Trace
|
||||
|
||||
You've now traced the complete path:
|
||||
|
||||
```
|
||||
1. Reset (Power-on)
|
||||
?
|
||||
2. Bootrom (0x00000000)
|
||||
?
|
||||
3. Vector Table (0x10000000)
|
||||
?
|
||||
4. _reset_handler (0x1000015c)
|
||||
?
|
||||
5. Data Copy & BSS Clear
|
||||
?
|
||||
6. platform_entry (0x10000186)
|
||||
?
|
||||
7. runtime_init() (first call)
|
||||
?
|
||||
8. main() (second call) ? Exercise focus
|
||||
?
|
||||
9. stdio_init_all() (first line of main)
|
||||
```
|
||||
|
||||
#### Expected Output
|
||||
- `main()` is at address `0x10000234`
|
||||
- First function call is `stdio_init_all()` at offset +6
|
||||
- Link register points to `platform_entry+4` (0x1000018a)
|
||||
- `platform_entry` makes three function calls: runtime_init, main, and exit
|
||||
|
||||
#### Questions for Reflection
|
||||
|
||||
###### Question 1: Why does the link register point 4 bytes after the `blx` instruction that called main?
|
||||
|
||||
###### Question 2: What would happen if `main()` tried to return (instead of looping forever)?
|
||||
|
||||
###### Question 3: How can you tell from the disassembly that main contains an infinite loop?
|
||||
|
||||
###### Question 4: Why is `stdio_init_all()` called before the printf loop?
|
||||
|
||||
#### Tips and Hints
|
||||
- Use `bt` (backtrace) to see the call stack
|
||||
- Remember to account for Thumb mode when reading addresses from LR
|
||||
- Use `info frame` to see detailed information about the current stack frame
|
||||
- The `push {r7, lr}` at the start of main saves the return address
|
||||
|
||||
#### Next Steps
|
||||
- Set a breakpoint at `stdio_init_all()` and step through its initialization
|
||||
- Examine what happens after `main()` by looking at `exit()` function
|
||||
- Try Exercise 5 in Ghidra for static analysis of the boot sequence
|
||||
|
||||
#### Additional Challenge
|
||||
|
||||
Create a GDB command to automatically trace the call chain:
|
||||
|
||||
```gdb
|
||||
(gdb) define calltrace
|
||||
> set $depth = 0
|
||||
> set $addr = $pc
|
||||
> while $depth < 10
|
||||
> printf "%d: ", $depth
|
||||
> info symbol $addr
|
||||
> set $addr = *(int*)($lr - 4)
|
||||
> set $depth = $depth + 1
|
||||
> end
|
||||
> end
|
||||
```
|
||||
|
||||
Then try stepping through functions and running `calltrace` at each level to build a complete call graph.
|
||||
+198
-238
@@ -22,7 +22,7 @@ This week builds on your GDB and Ghidra skills from previous weeks:
|
||||
|
||||
---
|
||||
|
||||
## 📚 The Code We're Analyzing
|
||||
## The Code We're Analyzing
|
||||
|
||||
Throughout this week, we'll continue working with our `0x0001_hello-world.c` program:
|
||||
|
||||
@@ -42,7 +42,7 @@ But this week, we're going **deeper** - we'll understand everything that happens
|
||||
|
||||
---
|
||||
|
||||
## 📚 Part 1: Understanding the Boot Process
|
||||
## Part 1: Understanding the Boot Process
|
||||
|
||||
### What Happens When You Power On?
|
||||
|
||||
@@ -60,49 +60,49 @@ Each of these steps has a corresponding piece of code. Let's explore them all!
|
||||
### The RP2350 Boot Sequence Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ STEP 1: Power On │
|
||||
│ - The Cortex-M33 core wakes up │
|
||||
│ - Execution begins at address 0x00000000 (Bootrom) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
+-----------------------------------------------------------------+
|
||||
| STEP 1: Power On |
|
||||
| - The Cortex-M33 core wakes up |
|
||||
| - Execution begins at address 0x00000000 (Bootrom) |
|
||||
+-----------------------------------------------------------------+
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 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 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 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 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: Flash XIP Setup (bootrom-managed) │
|
||||
│ - The bootrom configures the flash interface automatically │
|
||||
│ - Sets up XIP (Execute In Place) mode │
|
||||
│ - 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") │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
+-----------------------------------------------------------------+
|
||||
| STEP 3: Flash XIP Setup (bootrom-managed) |
|
||||
| - The bootrom configures the flash interface automatically |
|
||||
| - Sets up XIP (Execute In Place) mode |
|
||||
| - 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") |
|
||||
+-----------------------------------------------------------------+
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ STEP 4: Vector Table & Reset Handler │
|
||||
│ - Bootrom reads the vector table at 0x10000000 │
|
||||
│ - Gets the initial stack pointer from offset 0x00 │
|
||||
│ - Gets the reset handler address from offset 0x04 │
|
||||
│ - Jumps to the reset handler! │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
+-----------------------------------------------------------------+
|
||||
| STEP 4: Vector Table & Reset Handler |
|
||||
| - Bootrom reads the vector table at 0x10000000 |
|
||||
| - Gets the initial stack pointer from offset 0x00 |
|
||||
| - Gets the reset handler address from offset 0x04 |
|
||||
| - Jumps to the reset handler! |
|
||||
+-----------------------------------------------------------------+
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ STEP 5: C Runtime Startup (crt0.S) │
|
||||
│ - Copies initialized data from flash to RAM │
|
||||
│ - Zeros out the BSS section │
|
||||
│ - Calls runtime_init() │
|
||||
│ - Finally calls main()! │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
+-----------------------------------------------------------------+
|
||||
| STEP 5: C Runtime Startup (crt0.S) |
|
||||
| - Copies initialized data from flash to RAM |
|
||||
| - Zeros out the BSS section |
|
||||
| - Calls runtime_init() |
|
||||
| - Finally calls main()! |
|
||||
+-----------------------------------------------------------------+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Part 2: The Bootrom - Where It All Begins
|
||||
## Part 2: The Bootrom - Where It All Begins
|
||||
|
||||
### What is the Bootrom?
|
||||
|
||||
@@ -200,7 +200,7 @@ assuming a fixed address.
|
||||
|
||||
---
|
||||
|
||||
## 📚 Part 3: Understanding XIP (Execute In Place)
|
||||
## Part 3: Understanding XIP (Execute In Place)
|
||||
|
||||
> 🔄 **REVIEW:** In Week 1, we learned that our code lives at `0x10000000` in flash memory. We used `x/1000i 0x10000000` to find our `main` function. Now we'll understand WHY code is at this address!
|
||||
|
||||
@@ -225,29 +225,29 @@ Think of it like reading a book:
|
||||
The XIP flash region starts at address `0x10000000`. This is where your compiled code lives!
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ Address: 0x10000000 (XIP Base) │
|
||||
│ ┌─────────────────────────────────────────────────┐│
|
||||
│ │ Vector Table (first thing here!) ││
|
||||
│ │ - Stack Pointer at offset 0x00 ││
|
||||
│ │ - Reset Handler at offset 0x04 ││
|
||||
│ │ - Other exception handlers... ││
|
||||
│ ├─────────────────────────────────────────────────┤│
|
||||
│ │ Your Code ││
|
||||
│ │ - Reset handler ││
|
||||
│ │ - main() function ││
|
||||
│ │ - Other functions ││
|
||||
│ ├─────────────────────────────────────────────────┤│
|
||||
│ │ Read-Only Data ││
|
||||
│ │ - Strings like "hello, world" ││
|
||||
│ │ - Constant values ││
|
||||
│ └─────────────────────────────────────────────────┘│
|
||||
└─────────────────────────────────────────────────────┘
|
||||
+-----------------------------------------------------+
|
||||
| Address: 0x10000000 (XIP Base) |
|
||||
| +-------------------------------------------------+|
|
||||
| | Vector Table (first thing here!) ||
|
||||
| | - Stack Pointer at offset 0x00 ||
|
||||
| | - Reset Handler at offset 0x04 ||
|
||||
| | - Other exception handlers... ||
|
||||
| +-------------------------------------------------+|
|
||||
| | Your Code ||
|
||||
| | - Reset handler ||
|
||||
| | - main() function ||
|
||||
| | - Other functions ||
|
||||
| +-------------------------------------------------+|
|
||||
| | Read-Only Data ||
|
||||
| | - Strings like "hello, world" ||
|
||||
| | - Constant values ||
|
||||
| +-------------------------------------------------+|
|
||||
+-----------------------------------------------------+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Part 4: The Vector Table - The CPU's Instruction Manual
|
||||
## Part 4: The Vector Table - The CPU's Instruction Manual
|
||||
|
||||
### What is the Vector Table?
|
||||
|
||||
@@ -293,7 +293,7 @@ So `0x1000015d` means:
|
||||
|
||||
---
|
||||
|
||||
## 📚 Part 5: The Linker Script - Memory Mapping
|
||||
## Part 5: The Linker Script - Memory Mapping
|
||||
|
||||
### What is a Linker Script?
|
||||
|
||||
@@ -344,7 +344,7 @@ This value (`0x20082000`) is what we see at offset `0x00` in the vector table!
|
||||
|
||||
---
|
||||
|
||||
## 📚 Part 6: Setting Up Your Environment (GDB - Dynamic Analysis)
|
||||
## Part 6: Setting Up Your Environment (GDB - Dynamic Analysis)
|
||||
|
||||
> 🔄 **REVIEW:** This setup is identical to Weeks 1-2. If you need a refresher on OpenOCD and GDB connection, refer back to Week 1 Part 4 or Week 2 Part 5.
|
||||
|
||||
@@ -363,7 +363,7 @@ Before we start, make sure you have:
|
||||
|
||||
```powershell
|
||||
openocd ^
|
||||
-s "C:\Users\flare-vm\.pico-sdk\openocd\0.12.0+dev\scripts" ^
|
||||
-s "C:\Users\assem.KEVINTHOMAS\.pico-sdk\openocd\0.12.0+dev\scripts" ^
|
||||
-f interface/cmsis-dap.cfg ^
|
||||
-f target/rp2350.cfg ^
|
||||
-c "adapter speed 5000"
|
||||
@@ -500,8 +500,8 @@ This is "Compare and Branch if Zero". If `r0` is `0` (meaning we're on Core 0),
|
||||
The RP2350 has **two cores**, but only **Core 0** should run the startup code! If both cores tried to initialize the same memory and peripherals, chaos would ensue.
|
||||
|
||||
So the reset handler checks:
|
||||
- **Core 0?** → Continue with startup
|
||||
- **Core 1?** → Go back to the bootrom and wait
|
||||
- **Core 0?** -> Continue with startup
|
||||
- **Core 1?** -> Go back to the bootrom and wait
|
||||
|
||||
---
|
||||
|
||||
@@ -557,31 +557,31 @@ Let's look at more instructions to see the full picture:
|
||||
The reset handler performs several phases:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ PHASE 1: Core Check (0x1000015c - 0x10000168) │
|
||||
│ - Check CPUID to see which core we're on │
|
||||
│ - If not Core 0, go back to bootrom │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
+-----------------------------------------------------------------+
|
||||
| PHASE 1: Core Check (0x1000015c - 0x10000168) |
|
||||
| - Check CPUID to see which core we're on |
|
||||
| - If not Core 0, go back to bootrom |
|
||||
+-----------------------------------------------------------------+
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 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 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
+-----------------------------------------------------------------+
|
||||
| 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 |
|
||||
+-----------------------------------------------------------------+
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 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 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
+-----------------------------------------------------------------+
|
||||
| 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 |
|
||||
+-----------------------------------------------------------------+
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 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 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
+-----------------------------------------------------------------+
|
||||
| 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 |
|
||||
+-----------------------------------------------------------------+
|
||||
```
|
||||
|
||||
---
|
||||
@@ -634,12 +634,12 @@ The table ends with an entry where the source address is `0x00000000` (which sig
|
||||
The data copy loop works like this:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ 1. Load source, dest, end from table │
|
||||
│ 2. If source == 0, we're done │
|
||||
│ 3. Otherwise, copy word by word │
|
||||
│ 4. Go back to step 1 for next entry │
|
||||
└─────────────────────────────────────────────┘
|
||||
+---------------------------------------------+
|
||||
| 1. Load source, dest, end from table |
|
||||
| 2. If source == 0, we're done |
|
||||
| 3. Otherwise, copy word by word |
|
||||
| 4. Go back to step 1 for next entry |
|
||||
+---------------------------------------------+
|
||||
```
|
||||
|
||||
The actual code (starting at **`0x1000016c`** in the reset handler):
|
||||
@@ -655,7 +655,7 @@ bl 0x1000019a <data_cpy>
|
||||
b.n 0x1000016c <hold_non_core0_in_bootrom+8>
|
||||
```
|
||||
|
||||
> 💡 **Note:** You can see this code in **Step 6** earlier where we examined the reset handler with `x/20i 0x1000015c`.
|
||||
> Tip: **Note:** You can see this code in **Step 6** earlier where we examined the reset handler with `x/20i 0x1000015c`.
|
||||
|
||||
---
|
||||
|
||||
@@ -699,16 +699,16 @@ The first two `ldr` instructions are still part of the **BSS clear setup**, even
|
||||
### Understanding the Loop
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ r1 = start of BSS section │
|
||||
│ r2 = end of BSS section │
|
||||
│ r0 = 0 │
|
||||
│ │
|
||||
│ LOOP: │
|
||||
│ Store 0 at address r1 │
|
||||
│ Increment r1 by 4 bytes │
|
||||
│ If r1 != r2, repeat │
|
||||
└─────────────────────────────────────────────┘
|
||||
+---------------------------------------------+
|
||||
| r1 = start of BSS section |
|
||||
| r2 = end of BSS section |
|
||||
| r0 = 0 |
|
||||
| |
|
||||
| LOOP: |
|
||||
| Store 0 at address r1 |
|
||||
| Increment r1 by 4 bytes |
|
||||
| If r1 != r2, repeat |
|
||||
+---------------------------------------------+
|
||||
```
|
||||
|
||||
---
|
||||
@@ -817,7 +817,7 @@ Let's verify we understand the boot process by setting a breakpoint at main:
|
||||
**You should see:**
|
||||
|
||||
```
|
||||
Breakpoint 1 at 0x10000234: file C:/Users/flare-vm/Desktop/Embedded-Hacking-main/0x0001_hello-world/0x0001_hello-world.c, line 5.
|
||||
Breakpoint 1 at 0x10000234: file C:/Users/assem.KEVINTHOMAS/OneDrive/Documents/Embedded-Hacking/0x0001_hello-world/0x0001_hello-world.c, line 5.
|
||||
Note: automatically using hardware breakpoints for read-only addresses.
|
||||
```
|
||||
|
||||
@@ -833,7 +833,7 @@ Note: automatically using hardware breakpoints for read-only addresses.
|
||||
Continuing.
|
||||
|
||||
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
|
||||
at C:/Users/assem.KEVINTHOMAS/OneDrive/Documents/Embedded-Hacking/0x0001_hello-world/0x0001_hello-world.c:5
|
||||
5 stdio_init_all();
|
||||
(gdb)
|
||||
```
|
||||
@@ -945,7 +945,7 @@ While GDB is excellent for dynamic analysis (watching code execute), Ghidra exce
|
||||
...
|
||||
```
|
||||
|
||||
> 💡 **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.
|
||||
> Tip: **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.
|
||||
|
||||
### Step 17: Navigate to the Reset Handler
|
||||
|
||||
@@ -1005,7 +1005,7 @@ void _reset_handler(void)
|
||||
Let's find how the boot code eventually calls `main()`:
|
||||
|
||||
1. In the Symbol Tree, find the `main` function
|
||||
2. Right-click on `main` and select **References → Show References to main**
|
||||
2. Right-click on `main` and select **References -> Show References to main**
|
||||
3. This shows everywhere `main` is called from!
|
||||
|
||||
**You should see:**
|
||||
@@ -1048,7 +1048,7 @@ In Ghidra, look at `platform_entry`:
|
||||
|
||||
Ghidra can visualize the call flow:
|
||||
|
||||
1. With `_reset_handler` selected, go to **Window → Function Call Graph**
|
||||
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 will see `_reset_handler` at the top with arrows going down to its four direct callees: `data_cpy`, `runtime_init`, `main`, and `exit`
|
||||
|
||||
@@ -1076,32 +1076,32 @@ Ghidra can visualize the call flow:
|
||||
### The Complete Boot Sequence
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 1. POWER ON │
|
||||
│ Cortex-M33 begins at 0x00000000 (bootrom) │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ 2. BOOTROM │
|
||||
│ - Initializes hardware │
|
||||
│ - 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 │
|
||||
│ - Reads Reset Handler from offset 0x04 → 0x1000015d │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ 4. RESET HANDLER (0x1000015c) │
|
||||
│ - Checks CPUID (Core 0 continues, Core 1 waits) │
|
||||
│ - Copies .data from flash to RAM │
|
||||
│ - Zeros .bss section │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ 5. PLATFORM ENTRY (0x10000186) │
|
||||
│ - Calls runtime_init() │
|
||||
│ - Calls main() │
|
||||
│ - Calls exit() when main returns │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ 6. YOUR CODE RUNS! │
|
||||
│ main() at 0x10000234 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
+-----------------------------------------------------------------+
|
||||
| 1. POWER ON |
|
||||
| Cortex-M33 begins at 0x00000000 (bootrom) |
|
||||
+-----------------------------------------------------------------+
|
||||
| 2. BOOTROM |
|
||||
| - Initializes hardware |
|
||||
| - 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 |
|
||||
| - Reads Reset Handler from offset 0x04 -> 0x1000015d |
|
||||
+-----------------------------------------------------------------+
|
||||
| 4. RESET HANDLER (0x1000015c) |
|
||||
| - Checks CPUID (Core 0 continues, Core 1 waits) |
|
||||
| - Copies .data from flash to RAM |
|
||||
| - Zeros .bss section |
|
||||
+-----------------------------------------------------------------+
|
||||
| 5. PLATFORM ENTRY (0x10000186) |
|
||||
| - Calls runtime_init() |
|
||||
| - Calls main() |
|
||||
| - Calls exit() when main returns |
|
||||
+-----------------------------------------------------------------+
|
||||
| 6. YOUR CODE RUNS! |
|
||||
| main() at 0x10000234 |
|
||||
+-----------------------------------------------------------------+
|
||||
```
|
||||
|
||||
### Key Addresses to Remember
|
||||
@@ -1157,56 +1157,14 @@ Ghidra can visualize the call flow:
|
||||
|
||||
| Action | How to Access | Purpose |
|
||||
| ------ | ------------- | ------- |
|
||||
| Go To Address | Navigation → Go To... | Jump to specific memory address |
|
||||
| Show References | Right-click → References → Show References to | Find all callers of a function |
|
||||
| Function Call Graph | Window → Function Call Graph | Visualize call flow |
|
||||
| Go To Address | Navigation -> Go To... | Jump to specific memory address |
|
||||
| Show References | Right-click -> References -> Show References to | Find all callers of a function |
|
||||
| Function Call Graph | Window -> Function Call Graph | Visualize call flow |
|
||||
| Add Comment | Press `;` | Document your analysis |
|
||||
| Rename Symbol | Right-click → Rename | Give meaningful names to functions |
|
||||
| Rename Symbol | Right-click -> Rename | Give meaningful names to functions |
|
||||
|
||||
---
|
||||
|
||||
## ✅ Practice Exercises
|
||||
|
||||
### Exercise 1: Trace a Reset
|
||||
|
||||
1. Set a breakpoint at the reset handler: `b *0x1000015c`
|
||||
2. Type `monitor reset halt` then `c`
|
||||
3. Single-step through the first 10 instructions with `si`
|
||||
4. For each instruction, explain what it does
|
||||
|
||||
### Exercise 2: Find the Stack Size
|
||||
|
||||
1. The stack starts at `0x20082000`
|
||||
2. The stack limit is `0x20078000` (from register assignments)
|
||||
3. Calculate: How many bytes is the stack?
|
||||
4. How many kilobytes is that?
|
||||
|
||||
### Exercise 3: Examine All Vectors
|
||||
|
||||
1. Use `x/16x 0x10000000` to see the first 16 vector table entries
|
||||
2. For each entry, determine:
|
||||
- Is it a valid code address (starts with `0x1000...`)?
|
||||
- What handler does it point to?
|
||||
|
||||
### 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: Ghidra Boot Analysis
|
||||
|
||||
1. In Ghidra, navigate to `_reset_handler`
|
||||
2. Use **Window → Function Call Graph** to visualize the call tree
|
||||
3. Identify the path from `_reset_handler` to `main`
|
||||
4. How many functions are called before `main` starts?
|
||||
5. Add a comment in Ghidra explaining what each function does
|
||||
|
||||
---
|
||||
|
||||
## 🎓 Key Takeaways
|
||||
@@ -1270,18 +1228,18 @@ Understanding the boot process is critical for both attackers and defenders. Kno
|
||||
#### 1. Secure Boot Implementation
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ SECURE BOOT FLOW │
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ Bootrom (immutable) │
|
||||
│ ↓ │
|
||||
│ Verify IMAGE_DEF signature │
|
||||
│ ↓ │
|
||||
│ Verify application image signature │
|
||||
│ ↓ │
|
||||
│ If all valid: Jump to reset handler │
|
||||
│ If any invalid: Refuse to boot │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
+-----------------------------------------------------+
|
||||
| SECURE BOOT FLOW |
|
||||
+-----------------------------------------------------+
|
||||
| Bootrom (immutable) |
|
||||
| ↓ |
|
||||
| Verify IMAGE_DEF signature |
|
||||
| ↓ |
|
||||
| Verify application image signature |
|
||||
| ↓ |
|
||||
| If all valid: Jump to reset handler |
|
||||
| If any invalid: Refuse to boot |
|
||||
+-----------------------------------------------------+
|
||||
```
|
||||
|
||||
**Implementation:** Use cryptographic signatures to verify each boot stage before execution.
|
||||
@@ -1386,7 +1344,7 @@ Understanding how an attacker would analyze and exploit the boot sequence is ess
|
||||
|
||||
---
|
||||
|
||||
## 📚 Additional Resources
|
||||
## Additional Resources
|
||||
|
||||
### RP2350 Datasheet
|
||||
|
||||
@@ -1411,7 +1369,7 @@ 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`.
|
||||
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
|
||||
|
||||
@@ -1429,9 +1387,9 @@ Idx Name Size VMA LMA
|
||||
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`.
|
||||
> Tip: 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
|
||||
### 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
|
||||
@@ -1465,30 +1423,30 @@ Raw output from that command:
|
||||
|
||||
Every address annotated, no skips:
|
||||
|
||||
#### 0x10000000 — Vector Table, Mandatory Entries
|
||||
#### 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`). |
|
||||
| `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)
|
||||
#### 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`).
|
||||
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
|
||||
#### 0x10000110-0x10000127 - Default IRQ Handler Code
|
||||
|
||||
```
|
||||
10000110 eff30580 103800be 00be00be 00be00be
|
||||
@@ -1498,14 +1456,14 @@ repeated pointer.
|
||||
| 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. |
|
||||
| `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. |
|
||||
| `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
|
||||
#### 0x10000128-0x1000013b - Binary Info Pointer Table
|
||||
|
||||
```
|
||||
10000120 201b0010 4c1b0010
|
||||
@@ -1517,12 +1475,12 @@ code triggers an IRQ you did not register, it crashes visibly instead of silentl
|
||||
| `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. |
|
||||
| `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)
|
||||
#### 0x10000138-0x1000014c - IMAGE_DEF Block (this build)
|
||||
|
||||
```
|
||||
10000130 a0010010 90a31ae7 d3deffff 42012110
|
||||
@@ -1531,49 +1489,49 @@ and GPIO pin map from any compiled binary without running it.
|
||||
|
||||
| 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. |
|
||||
| `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. |
|
||||
| `0x10000148` | `79 35 12 ab` | `PICOBIN_BLOCK_MARKER_END` - bootrom stops scanning here. |
|
||||
|
||||
The IMAGE_DEF sits at `0x10000138`–`0x1000014b` in this build,
|
||||
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
|
||||
#### 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)
|
||||
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 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
+-----------------------------------------------------------------+
|
||||
| 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:**
|
||||
@@ -1586,3 +1544,5 @@ well within the 4 KB scan window the bootrom uses (Datasheet §5.9.5, p. 429).
|
||||
**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