mirror of
https://github.com/mytechnotalent/Embedded-Hacking.git
synced 2026-05-08 02:27:10 +02:00
207 lines
6.9 KiB
Markdown
207 lines
6.9 KiB
Markdown
# Embedded Systems Reverse Engineering
|
|
[Repository](https://github.com/mytechnotalent/Embedded-Hacking)
|
|
|
|
## Week 7
|
|
Constants in Embedded Systems: Debugging and Hacking Constants w/ 1602 LCD I2C Basics
|
|
|
|
### Non-Credit Practice Exercise 3: Trace the I²C Struct Pointer Chain
|
|
|
|
#### Objective
|
|
Use GDB to follow the `i2c1_inst` struct pointer chain from the code instruction that loads it, through the struct in SRAM, to the hardware registers at `0x40098000`. Document every step of the chain: `I2C_PORT` ? `i2c1` ? `&i2c1_inst` ? `hw` ? `0x40098000`, and verify each pointer and value in memory.
|
|
|
|
#### Prerequisites
|
|
- Completed Week 7 tutorial (Parts 3-4 on structs and the macro chain)
|
|
- `0x0017_constants.elf` binary available in your build directory
|
|
- GDB (`arm-none-eabi-gdb`) and OpenOCD installed
|
|
- Understanding of C pointers and structs
|
|
|
|
#### Task Description
|
|
The Pico SDK uses a chain of macros and structs to abstract hardware access. When you write `I2C_PORT` in C, it expands through multiple macro definitions to ultimately become a pointer to an `i2c_inst_t` struct in SRAM, which in turn contains a pointer to the I²C hardware registers. You will trace this entire chain in GDB, examining each link to understand how the SDK connects your code to silicon.
|
|
|
|
#### Step-by-Step Instructions
|
|
|
|
##### Step 1: Start the Debug Session
|
|
|
|
**Terminal 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"
|
|
```
|
|
|
|
**Terminal 2 - Start GDB:**
|
|
|
|
```powershell
|
|
arm-none-eabi-gdb build\0x0017_constants.elf
|
|
```
|
|
|
|
**Connect to target:**
|
|
|
|
```gdb
|
|
(gdb) target remote :3333
|
|
(gdb) monitor reset halt
|
|
```
|
|
|
|
##### Step 2: Find the i2c_init Call
|
|
|
|
Run the program to allow initialization to complete, then halt:
|
|
|
|
```gdb
|
|
(gdb) b *0x10000234
|
|
(gdb) c
|
|
```
|
|
|
|
Now step through to just before the `i2c_init` call:
|
|
|
|
```gdb
|
|
(gdb) x/10i 0x1000023c
|
|
```
|
|
|
|
Look for the instruction that loads the `i2c1_inst` pointer into `r0`:
|
|
|
|
```
|
|
0x1000023c: ldr r0, [pc, #offset] ; Load &i2c1_inst into r0
|
|
0x1000023e: ldr r1, =0x186A0 ; 100000 (baud rate)
|
|
0x10000240: bl i2c_init ; Call i2c_init(i2c1, 100000)
|
|
```
|
|
|
|
##### Step 3: Follow the PC-Relative Load
|
|
|
|
The `ldr r0, [pc, #offset]` instruction loads a value from a **literal pool** — a data area near the code. Examine what's at the literal pool:
|
|
|
|
```gdb
|
|
(gdb) x/4wx 0x100002a8
|
|
```
|
|
|
|
Look for a value in the `0x2000xxxx` range — this is the SRAM address of `i2c1_inst`. It should be `0x2000062c`.
|
|
|
|
##### Step 4: Examine the i2c1_inst Struct in SRAM
|
|
|
|
Now examine the struct at that SRAM address:
|
|
|
|
```gdb
|
|
(gdb) x/2wx 0x2000062c
|
|
```
|
|
|
|
Expected output:
|
|
```
|
|
0x2000062c: 0x40098000 0x00000000
|
|
```
|
|
|
|
This maps to the `i2c_inst_t` struct:
|
|
|
|
| Offset | Field | Value | Meaning |
|
|
| ------ | ----------------- | ------------ | -------------------------------- |
|
|
| `+0x00` | `hw` | `0x40098000` | Pointer to I²C1 hardware regs |
|
|
| `+0x04` | `restart_on_next` | `0x00000000` | `false` (no pending restart) |
|
|
|
|
##### Step 5: Follow the hw Pointer to Hardware Registers
|
|
|
|
The first member of the struct (`hw`) points to `0x40098000` — the I²C1 hardware register block. Examine it:
|
|
|
|
```gdb
|
|
(gdb) x/8wx 0x40098000
|
|
```
|
|
|
|
You should see the I²C1 control and status registers:
|
|
|
|
| Offset | Register | Description |
|
|
| ------ | -------------- | ------------------------------ |
|
|
| `+0x00` | IC_CON | I²C control register |
|
|
| `+0x04` | IC_TAR | Target address register |
|
|
| `+0x08` | IC_SAR | Slave address register |
|
|
| `+0x0C` | (reserved) | — |
|
|
| `+0x10` | IC_DATA_CMD | Data command register |
|
|
|
|
##### Step 6: Verify the I²C Target Address
|
|
|
|
After `i2c_init` and `lcd_i2c_init` have run, check the target address register:
|
|
|
|
Let the program run past initialization:
|
|
|
|
```gdb
|
|
(gdb) delete
|
|
(gdb) b *<address_after_lcd_init>
|
|
(gdb) c
|
|
```
|
|
|
|
Then examine IC_TAR:
|
|
|
|
```gdb
|
|
(gdb) x/1wx 0x40098004
|
|
```
|
|
|
|
You should see `0x27` (or a value containing 0x27) — this is the LCD's I²C address!
|
|
|
|
##### Step 7: Document the Complete Chain
|
|
|
|
Create a diagram of the complete pointer chain:
|
|
|
|
```
|
|
Your Code: I2C_PORT
|
|
¦
|
|
? (preprocessor macro)
|
|
i2c1
|
|
¦
|
|
? (macro: #define i2c1 (&i2c1_inst))
|
|
&i2c1_inst = 0x2000062c (SRAM address)
|
|
¦
|
|
? (struct member access)
|
|
i2c1_inst.hw = 0x40098000 (hardware register base)
|
|
¦
|
|
? (memory-mapped I/O)
|
|
I²C1 Hardware Registers (silicon)
|
|
¦
|
|
+-- IC_CON at 0x40098000
|
|
+-- IC_TAR at 0x40098004
|
|
+-- IC_DATA_CMD at 0x40098010
|
|
+-- ...
|
|
```
|
|
|
|
##### Step 8: Compare with I²C0
|
|
|
|
The Pico 2 has two I²C controllers. Find the `i2c0_inst` struct and compare:
|
|
|
|
```gdb
|
|
(gdb) x/2wx 0x20000628
|
|
```
|
|
|
|
If I²C0's struct is at a nearby address, you should see:
|
|
- `hw` pointing to `0x40090000` (I²C0 base, different from I²C1's `0x40098000`)
|
|
- `restart_on_next` = 0
|
|
|
|
This demonstrates how the SDK uses the same struct layout for both I²C controllers, with only the hardware pointer changing.
|
|
|
|
#### Expected Output
|
|
|
|
After completing this exercise, you should be able to:
|
|
- Trace pointer chains from high-level code to hardware registers
|
|
- Understand how the Pico SDK uses structs to abstract hardware
|
|
- Read struct members from raw memory using GDB
|
|
- Navigate from SRAM data structures to memory-mapped I/O registers
|
|
|
|
#### Questions for Reflection
|
|
|
|
###### Question 1: Why does the SDK use a struct with a pointer to hardware registers instead of accessing `0x40098000` directly? What advantage does this abstraction provide?
|
|
|
|
###### Question 2: The `hw` pointer stores `0x40098000`. In the binary, this appears as bytes `00 80 09 40`. Why is the byte order reversed from how we write the address?
|
|
|
|
###### Question 3: If you changed the `hw` pointer at `0x2000062c` from `0x40098000` to `0x40090000` using GDB (`set {int}0x2000062c = 0x40090000`), what I²C controller would the program use? What would happen to the LCD?
|
|
|
|
###### Question 4: The macro chain has 4 levels of indirection (I2C_PORT ? i2c1 ? &i2c1_inst ? hw ? registers). Is this typical for embedded SDKs? What are the trade-offs of this approach?
|
|
|
|
#### Tips and Hints
|
|
- Use `x/wx` to examine 32-bit words (pointers are 32 bits on ARM Cortex-M33)
|
|
- SRAM addresses start with `0x20xxxxxx`; hardware register addresses start with `0x40xxxxxx`
|
|
- The literal pool (where PC-relative loads get their data) is usually right after the function's code
|
|
- `i2c_inst_t` is only 8 bytes: 4-byte pointer + 4-byte bool (padded to 4 bytes for alignment)
|
|
- I²C0 base = `0x40090000`, I²C1 base = `0x40098000` — they are `0x8000` bytes apart
|
|
|
|
#### Next Steps
|
|
- Proceed to Exercise 4 to patch the LCD to display your own custom message
|
|
- Try modifying the `restart_on_next` field in GDB and observe if it changes I²C behavior
|
|
- Explore the I²C hardware registers at `0x40098000` — can you read the IC_STATUS register to see if the bus is active?
|