Files
Embedded-Hacking/WEEK07/WEEK07-03.md
2026-03-19 15:01:07 -04:00

6.9 KiB

Embedded Systems Reverse Engineering

Repository

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:

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:

  • 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?