# 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 * (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?