6.9 KiB
Embedded Systems Reverse Engineering
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.elfbinary 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:
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:
arm-none-eabi-gdb build\0x0017_constants.elf
Connect to target:
(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) b *0x10000234
(gdb) c
Now step through to just before the i2c_init call:
(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) 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) 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) 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) delete
(gdb) b *<address_after_lcd_init>
(gdb) c
Then examine IC_TAR:
(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) x/2wx 0x20000628
If I²C0's struct is at a nearby address, you should see:
hwpointing to0x40090000(I²C0 base, different from I²C1's0x40098000)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/wxto examine 32-bit words (pointers are 32 bits on ARM Cortex-M33) - SRAM addresses start with
0x20xxxxxx; hardware register addresses start with0x40xxxxxx - The literal pool (where PC-relative loads get their data) is usually right after the function's code
i2c_inst_tis only 8 bytes: 4-byte pointer + 4-byte bool (padded to 4 bytes for alignment)- I²C0 base =
0x40090000, I²C1 base =0x40098000— they are0x8000bytes apart
Next Steps
- Proceed to Exercise 4 to patch the LCD to display your own custom message
- Try modifying the
restart_on_nextfield 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?