# Week 7: Constants in Embedded Systems: Debugging and Hacking Constants w/ 1602 LCD I2C Basics ## 🎯 What You'll Learn This Week By the end of this tutorial, you will be able to: - Understand the difference between `#define` macros and `const` variables - Know how constants are stored differently in memory (compile-time vs runtime) - Understand the I²C (Inter-Integrated Circuit) communication protocol - Configure I²C peripherals and communicate with LCD displays - Understand C structs and how the Pico SDK uses them for hardware abstraction - Use GDB to examine constants, structs, and string literals in memory - Hack constant values and string literals using a hex editor - Patch LCD display text without access to source code --- ## 📚 Part 1: Understanding Constants in C ### Two Types of Constants In C, there are two ways to create values that shouldn't change: | Type | Syntax | Where It Lives | When Resolved | | ----------- | ----------------------- | ------------------ | ------------- | | **#define** | `#define FAV_NUM 42` | Nowhere (replaced) | Compile time | | **const** | `const int NUM = 1337;` | Flash (.rodata) | Runtime | ### Preprocessor Macros (#define) A **preprocessor macro** is a text replacement that happens BEFORE your code is compiled: ```c #define FAV_NUM 42 printf("Value: %d", FAV_NUM); // Becomes: printf("Value: %d", 42); ``` Think of it like a "find and replace" in a text editor. The compiler never sees `FAV_NUM` - it only sees `42`! ``` ┌─────────────────────────────────────────────────────────────────┐ │ Preprocessor Macro Flow │ │ │ │ Source Code Preprocessor Compiler │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ #define │ │ Replace │ │ Compile │ │ │ │ FAV_NUM │ ─────► │ FAV_NUM │ ─────► │ binary │ │ │ │ 42 │ │ with 42 │ │ code │ │ │ └──────────┘ └──────────┘ └──────────┘ │ │ │ │ FAV_NUM doesn't exist in the final binary! │ │ The value 42 is embedded directly in instructions. │ │ │ └─────────────────────────────────────────────────────────────────┘ ``` ### Const Variables A **const variable** is an actual variable stored in memory, but marked as read-only: ```c const int OTHER_FAV_NUM = 1337; ``` Unlike `#define`, this creates a real memory location in the `.rodata` (read-only data) section of flash: ``` ┌─────────────────────────────────────────────────────────────────┐ │ Const Variable in Memory │ │ │ │ Flash Memory (.rodata section) │ │ ┌────────────────────────────────────────────────────────────┐ │ │ │ Address: 0x10001234 │ │ │ │ Value: 0x00000539 (1337 in hex) │ │ │ │ Name: OTHER_FAV_NUM (in debug symbols only) │ │ │ └────────────────────────────────────────────────────────────┘ │ │ │ │ The variable EXISTS in memory and can be read at runtime. │ │ │ └─────────────────────────────────────────────────────────────────┘ ``` ### Comparison: #define vs const | Feature | #define | const | | -------------------- | ------------------------ | ---------------------------- | | **Type checking** | None (just text) | Yes (compiler enforced) | | **Memory usage** | None (inlined) | Uses flash space | | **Debugger visible** | No | Yes (with symbols) | | **Can take address** | No (`&FAV_NUM` fails) | Yes (`&OTHER_FAV_NUM` works) | | **Scope** | Global (from definition) | Normal C scoping rules | --- ## 📚 Part 2: Understanding I²C Communication ### What is I²C? **I²C** (pronounced "I-squared-C" or "I-two-C") stands for **Inter-Integrated Circuit**. It's a way for chips to talk to each other using just TWO wires! ``` ┌─────────────────────────────────────────────────────────────────┐ │ I²C Bus - Two Wires, Many Devices │ │ │ │ 3.3V │ │ │ │ │ ┴ Pull-up ┴ Pull-up │ │ │ │ │ │ SDA ─┼────────────┼─────────────────────────────────────── │ │ │ │ │ │ SCL ─┼────────────┼─────────────────────────────────────── │ │ │ │ │ │ │ │ ┌───┴────┐ ┌──┴───┐ ┌────┴──┐ ┌─────┴───┐ │ │ │ Pico │ │ LCD │ │Sensor │ │ EEPROM │ │ │ │(Master)│ │ 0x27 │ │ 0x48 │ │ 0x50 │ │ │ └────────┘ └──────┘ └───────┘ └─────────┘ │ │ │ │ Each device has a unique address (0x27, 0x48, 0x50...) │ │ │ └─────────────────────────────────────────────────────────────────┘ ``` ### The Two I²C Wires | Wire | Name | Purpose | | ------- | ------------ | ------------------------------------ | | **SDA** | Serial Data | Carries the actual data bits | | **SCL** | Serial Clock | Timing signal that synchronizes data | ### Why Pull-Up Resistors? I²C uses **open-drain** signals, meaning devices can only pull the line LOW. They can't drive it HIGH! Pull-up resistors are needed to bring the lines back to HIGH when no device is pulling them down. The Pico 2 has internal pull-ups that we can enable with `gpio_pull_up()`. ### I²C Addresses Every I²C device has a unique **7-bit address**. Common addresses: | Device Type | Typical Address | | --------------------- | ---------------- | | 1602 LCD with PCF8574 | `0x27` or `0x3F` | | Temperature sensor | `0x48` | | EEPROM | `0x50` | | Real-time clock | `0x68` | ### I²C Communication Flow ``` ┌─────────────────────────────────────────────────────────────────┐ │ I²C Transaction │ │ │ │ 1. Master sends START condition │ │ 2. Master sends device address (7 bits) + R/W bit │ │ 3. Addressed device sends ACK (acknowledge) │ │ 4. Data is transferred (8 bits at a time) │ │ 5. Receiver sends ACK after each byte │ │ 6. Master sends STOP condition │ │ │ │ START ──► Address ──► ACK ──► Data ──► ACK ──► STOP │ │ │ └─────────────────────────────────────────────────────────────────┘ ``` --- ## 📚 Part 3: Understanding C Structs ### What is a Struct? A **struct** (short for "structure") is a way to group related variables together under one name. Think of it like a form with multiple fields: ```c // A struct definition - like a template struct student { char name[50]; int age; float gpa; }; // Creating a variable of this struct type struct student alice = {"Alice", 16, 3.8}; ``` ### Why Use Structs? Instead of passing many separate variables: ```c void print_student(char *name, int age, float gpa); // Messy! ``` You pass one struct: ```c void print_student(struct student s); // Clean! ``` ### The typedef Keyword Writing `struct student` everywhere is tedious. The `typedef` keyword creates an alias: ```c typedef struct student student_t; // Now you can write: student_t alice; // Instead of: struct student alice; ``` ### Forward Declaration Sometimes you need to tell the compiler "this struct exists" before defining it: ```c typedef struct i2c_inst i2c_inst_t; // Forward declaration + alias // Later, the full definition: struct i2c_inst { i2c_hw_t *hw; bool restart_on_next; }; ``` --- ## 📚 Part 4: Understanding the Pico SDK's I²C Structs ### The i2c_inst_t Struct The Pico SDK uses a struct to represent each I²C controller: ```c struct i2c_inst { i2c_hw_t *hw; // Pointer to hardware registers bool restart_on_next; // SDK internal flag }; ``` **What each member means:** | Member | Type | Purpose | | ----------------- | ------------ | ---------------------------------------- | | `hw` | `i2c_hw_t *` | Pointer to the actual hardware registers | | `restart_on_next` | `bool` | Tracks if next transfer needs a restart | ### The Macro Chain When you write `I2C_PORT` in your code, here's what happens: ``` ┌─────────────────────────────────────────────────────────────────┐ │ Macro Expansion Chain │ │ │ │ In your code: #define I2C_PORT i2c1 │ │ │ │ │ ▼ │ │ In i2c.h: #define i2c1 (&i2c1_inst) │ │ │ │ │ ▼ │ │ In i2c.c: i2c_inst_t i2c1_inst = {i2c1_hw, false}; │ │ │ │ │ ▼ │ │ In i2c.h: #define i2c1_hw ((i2c_hw_t *)I2C1_BASE) │ │ │ │ │ ▼ │ │ In addressmap.h: #define I2C1_BASE 0x40098000 │ │ │ └─────────────────────────────────────────────────────────────────┘ ``` So `I2C_PORT` eventually becomes a pointer to a struct that contains a pointer to hardware registers at address `0x40098000`! ### The Hardware Register Pointer The `i2c_hw_t *hw` member points to the actual silicon: ``` ┌─────────────────────────────────────────────────────────────────┐ │ Memory Map │ │ │ │ Address 0x40098000: I²C1 Hardware Registers │ │ ┌────────────────────────────────────────────────────────────┐ │ │ │ Offset 0x00: IC_CON (Control register) │ │ │ │ Offset 0x04: IC_TAR (Target address register) │ │ │ │ Offset 0x10: IC_DATA_CMD (Data command register) │ │ │ │ ... │ │ │ └────────────────────────────────────────────────────────────┘ │ │ │ │ The i2c_hw_t struct maps directly to these registers! │ │ │ └─────────────────────────────────────────────────────────────────┘ ``` --- ## 📚 Part 5: The ARM Calling Convention (AAPCS) ### How Arguments Are Passed On ARM Cortex-M, the **ARM Architecture Procedure Call Standard (AAPCS)** defines how functions receive arguments: | Register | Purpose | | -------- | ---------------- | | `r0` | First argument | | `r1` | Second argument | | `r2` | Third argument | | `r3` | Fourth argument | | Stack | Fifth+ arguments | | `r0` | Return value | ### Example: i2c_init(i2c1, 100000) ```c i2c_init(I2C_PORT, 100000); ``` In assembly: ```assembly ldr r0, [address of i2c1_inst] ; r0 = pointer to struct (first arg) ldr r1, =0x186A0 ; r1 = 100000 (second arg) bl i2c_init ; Call the function ``` --- ## 📚 Part 6: Setting Up Your Environment ### Prerequisites Before we start, make sure you have: 1. A Raspberry Pi Pico 2 board 2. A Raspberry Pi Pico Debug Probe 3. OpenOCD installed and configured 4. GDB (`arm-none-eabi-gdb`) installed 5. Python installed (for UF2 conversion) 6. A serial monitor (PuTTY, minicom, or screen) 7. A 1602 LCD display with I²C backpack (PCF8574) 8. A hex editor (HxD, ImHex, or similar) 9. The sample project: `0x0017_constants` ### Hardware Setup Connect your LCD like this: | LCD Pin | Pico 2 Pin | | ------- | ---------- | | VCC | 3.3V or 5V | | GND | GND | | SDA | GPIO 2 | | SCL | GPIO 3 | ``` ┌─────────────────────────────────────────────────────────────────┐ │ I²C LCD Wiring │ │ │ │ Pico 2 1602 LCD + I²C Backpack │ │ ┌──────────┐ ┌──────────────────────┐ │ │ │ │ │ │ │ │ │ GPIO 2 │─────── SDA ─────►│ SDA │ │ │ │ (SDA) │ │ │ │ │ │ │ │ ┌────────────┐ │ │ │ │ GPIO 3 │─────── SCL ─────►│ SCL│ Reverse │ │ │ │ │ (SCL) │ │ │Engineering │ │ │ │ │ │ │ └────────────┘ │ │ │ │ 3.3V │─────── VCC ─────►│ VCC │ │ │ │ │ │ │ │ │ │ GND │─────── GND ─────►│ GND │ │ │ │ │ │ │ │ │ └──────────┘ └──────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────┘ ``` ### Project Structure ``` Embedded-Hacking/ ├── 0x0017_constants/ │ ├── build/ │ │ ├── 0x0017_constants.uf2 │ │ └── 0x0017_constants.bin │ ├── 0x0017_constants.c │ └── lcd_1602.h └── uf2conv.py ``` --- ## 🔬 Part 7: Hands-On Tutorial - Constants and I²C LCD ### Step 1: Review the Source Code Let's examine the constants code: **File: `0x0017_constants.c`** ```c #include #include #include "pico/stdlib.h" #include "hardware/i2c.h" #include "lcd_1602.h" #define FAV_NUM 42 #define I2C_PORT i2c1 #define I2C_SDA_PIN 2 #define I2C_SCL_PIN 3 const int OTHER_FAV_NUM = 1337; int main(void) { stdio_init_all(); i2c_init(I2C_PORT, 100000); gpio_set_function(I2C_SDA_PIN, GPIO_FUNC_I2C); gpio_set_function(I2C_SCL_PIN, GPIO_FUNC_I2C); gpio_pull_up(I2C_SDA_PIN); gpio_pull_up(I2C_SCL_PIN); lcd_i2c_init(I2C_PORT, 0x27, 4, 0x08); lcd_set_cursor(0, 0); lcd_puts("Reverse"); lcd_set_cursor(1, 0); lcd_puts("Engineering"); while (true) { printf("FAV_NUM: %d\r\n", FAV_NUM); printf("OTHER_FAV_NUM: %d\r\n", OTHER_FAV_NUM); } } ``` **What this code does:** 1. **Lines 7-10:** Define preprocessor macros for constants and I²C configuration 2. **Line 12:** Define a `const` variable stored in flash 3. **Line 15:** Initialize UART for serial output 4. **Lines 17-21:** Initialize I²C1 at 100kHz, configure GPIO pins, enable pull-ups 5. **Lines 23-27:** Initialize LCD and display "Reverse" on line 0, "Engineering" on line 1 6. **Lines 29-32:** Infinite loop printing both constant values to serial terminal ### Step 2: Flash the Binary to Your Pico 2 1. Hold the BOOTSEL button on your Pico 2 2. Plug in the USB cable (while holding BOOTSEL) 3. Release BOOTSEL - a drive called "RPI-RP2" appears 4. Drag and drop `0x0017_constants.uf2` onto the drive 5. The Pico will reboot and start running! ### Step 3: Verify It's Working **Check the LCD:** - Line 1 should show: `Reverse` - Line 2 should show: `Engineering` **Check the serial monitor (PuTTY/screen):** ``` FAV_NUM: 42 OTHER_FAV_NUM: 1337 FAV_NUM: 42 OTHER_FAV_NUM: 1337 ... ``` --- ## 🔬 Part 8: Debugging with GDB (Dynamic Analysis) > 🔄 **REVIEW:** This setup is identical to previous weeks. If you need a refresher on OpenOCD and GDB connection, refer back to Week 3 Part 6. ### Starting 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 4: Examine Main Function Let's examine the main function. Disassemble from the entry point: ``` x/54i 0x10000234 ``` You should see output like: ``` 0x10000234
: push {r3, lr} 0x10000236 : bl 0x100037fc 0x1000023a : ldr r1, [pc, #104] @ (0x100002a4 ) 0x1000023c : ldr r0, [pc, #104] @ (0x100002a8 ) 0x1000023e : bl 0x10003cdc 0x10000242 : movs r1, #3 0x10000244 : movs r0, #2 0x10000246 : bl 0x100008f0 0x1000024a : movs r1, #3 0x1000024c : mov r0, r1 0x1000024e : bl 0x100008f0 0x10000252 : movs r2, #0 0x10000254 : movs r1, #1 0x10000256 : movs r0, #2 0x10000258 : bl 0x1000092c 0x1000025c : movs r2, #0 0x1000025e : movs r1, #1 0x10000260 : movs r0, #3 0x10000262 : bl 0x1000092c 0x10000266 : movs r3, #8 0x10000268 : movs r2, #4 0x1000026a : movs r1, #39 @ 0x27 0x1000026c : ldr r0, [pc, #56] @ (0x100002a8 ) 0x1000026e : bl 0x100002bc 0x10000272 : movs r1, #0 0x10000274 : mov r0, r1 0x10000276 : bl 0x100006f4 0x1000027a : ldr r0, [pc, #48] @ (0x100002ac ) 0x1000027c : bl 0x100007f0 0x10000280 : movs r0, #1 0x10000282 : movs r1, #0 0x10000284 : bl 0x100006f4 0x10000288 : ldr r0, [pc, #36] @ (0x100002b0 ) 0x1000028a : bl 0x100007f0 0x1000028e : movs r1, #42 @ 0x2a 0x10000290 : ldr r0, [pc, #32] @ (0x100002b4 ) 0x10000292 : bl 0x1000398c <__wrap_printf> 0x10000296 : movw r1, #1337 @ 0x539 0x1000029a : ldr r0, [pc, #28] @ (0x100002b8 ) 0x1000029c : bl 0x1000398c <__wrap_printf> 0x100002a0 : b.n 0x1000028e 0x100002a2 : nop 0x100002a4 : strh r0, [r4, #52] @ 0x34 0x100002a6 : movs r1, r0 0x100002a8 : lsls r4, r5, #24 0x100002aa : movs r0, #0 0x100002ac : subs r6, #232 @ 0xe8 0x100002ae : asrs r0, r0, #32 0x100002b0 : subs r6, #240 @ 0xf0 0x100002b2 : asrs r0, r0, #32 0x100002b4 : subs r6, #252 @ 0xfc 0x100002b6 : asrs r0, r0, #32 0x100002b8 : subs r7, #12 0x100002ba : asrs r0, r0, #32 ``` ### Step 5: Set a Breakpoint at Main ``` b *0x10000234 c ``` GDB responds: ``` Breakpoint 1 at 0x10000234: file C:/Users/flare-vm/Desktop/Embedded-Hacking-main/0x0017_constants/0x0017_constants.c, line 16. Note: automatically using hardware breakpoints for read-only addresses. (gdb) c Continuing. Thread 1 "rp2350.cm0" hit Breakpoint 1, main () at C:/Users/flare-vm/Desktop/Embedded-Hacking-main/0x0017_constants/0x0017_constants.c:16 16 stdio_init_all(); ``` > ⚠️ **Note:** If GDB says `The program is not being run.` when you type `c`, the target hasn't been started yet. Use `monitor reset halt` first, then `c` to continue to your breakpoint. ### Step 6: Find the #define Constant (FAV_NUM) Step through to the printf call and examine the registers: ``` x/20i 0x1000028e ``` Look for: ``` ... 0x1000028e : movs r1, #42 @ 0x2a ... ``` The `#define` constant is embedded directly as an immediate value in the instruction! ### Step 7: Find the const Variable (OTHER_FAV_NUM) Continue examining the loop body: ```gdb (gdb) x/5i 0x10000296 ``` Look for this instruction: ``` ... 0x10000296 : movw r1, #1337 @ 0x539 ... ``` **Surprise!** The `const` variable is ALSO embedded as an immediate value — not loaded from memory! The compiler saw that `OTHER_FAV_NUM` is never address-taken (`&OTHER_FAV_NUM` is never used), so it optimized the `const` the same way as `#define` — as a constant embedded directly in the instruction. The difference is the instruction encoding: - `FAV_NUM` (42): `movs r1, #0x2a` — 16-bit Thumb instruction (values 0-255) - `OTHER_FAV_NUM` (1337): `movw r1, #0x539` — 32-bit Thumb-2 instruction (values 0-65535) > 💡 **Why `movw` instead of `movs`?** The value 1337 doesn't fit in 8 bits (max 255), so the compiler uses `movw` (Move Wide) which can encode any 16-bit immediate (0-65535) in a 32-bit instruction. ### Step 8: Examine the Literal Pool The literal pool after the loop contains addresses and constants that are too large for regular instruction immediates. Let's examine it: ```gdb (gdb) x/6wx 0x100002a4 0x100002a4 : 0x000186a0 0x2000062c 0x10003ee8 0x10003ef0 0x100002b4 : 0x10003efc 0x10003f0c ``` These are the values that `ldr rN, [pc, #offset]` instructions load: | Literal Pool Addr | Value | Used By | | ----------------- | -------------- | ------------------------------ | | `0x100002a4` | `0x000186A0` | I2C baudrate (100000) | | `0x100002a8` | `0x2000062C` | &i2c1_inst (I2C struct in RAM) | | `0x100002ac` | `0x10003EE8` | "Reverse" string address | | `0x100002b0` | `0x10003EF0` | "Engineering" string address | | `0x100002b4` | `0x10003EFC` | "FAV_NUM: %d\r\n" format str | | `0x100002b8` | `0x10003F0C` | "OTHER_FAV_NUM: %d\r\n" fmt | > 💡 **Why does the disassembly at `0x100002a4` show `strh r0, [r4, #52]` instead of data?** Same reason as Week 6 — GDB's `x/i` tries to decode raw data as instructions. Use `x/wx` to see the actual word values. ### Step 9: Examine the I²C Struct Find the i2c1_inst struct address loaded into r0 before i2c_init: ``` x/2wx 0x2000062c ``` You should see: ``` 0x2000062c : 0x40098000 0x00000000 ``` ### Step 10: Examine the LCD String Literals Find the strings passed to lcd_puts: ``` x/s 0x10003ee8 ``` Output: ``` 0x10003ee8: "Reverse" ``` ``` x/s 0x10003ef0 ``` Output: ``` 0x10003ef0: "Engineering" ``` ### Step 11: Step Through I²C Initialization Use `si` to step through instructions and watch the I²C setup: ``` si i r r0 r1 ``` --- ## 🔬 Part 9: Understanding the Assembly Now that we've explored the binary in GDB, let's make sense of the key patterns we found. ### Step 12: Analyze #define vs const in Assembly From GDB, we discovered something interesting — **both constants ended up as instruction immediates!** **For FAV_NUM (42) — a `#define` macro:** ``` 0x1000028e: movs r1, #42 @ 0x2a ``` The value 42 is embedded directly in a 16-bit Thumb instruction. This is expected — `#define` is text replacement, so the compiler never sees `FAV_NUM`, only `42`. **For OTHER_FAV_NUM (1337) — a `const` variable:** ``` 0x10000296: movw r1, #1337 @ 0x539 ``` The value 1337 is ALSO embedded directly in an instruction — but this time a 32-bit Thumb-2 `movw` because the value doesn't fit in 8 bits. **Why wasn't `const` stored in memory?** In theory, `const int OTHER_FAV_NUM = 1337` creates a variable in the `.rodata` section. But the compiler optimized it away because: 1. We never take the address of `OTHER_FAV_NUM` (no `&OTHER_FAV_NUM`) 2. The value fits in a 16-bit `movw` immediate 3. Loading from an immediate is faster than loading from memory > 💡 **Key takeaway for reverse engineering:** Don't assume `const` variables will appear as memory loads. Modern compilers aggressively inline constant values. The C keyword `const` is a **source-level** concept — the compiler may or may not honor it in the final binary. ### Step 13: Analyze the I²C Struct Layout In GDB, we examined the `i2c1_inst` struct at `0x2000062c`: ```gdb (gdb) x/2wx 0x2000062c 0x2000062c : 0x40098000 0x00000000 ``` This maps to the `i2c_inst_t` struct: ``` ┌─────────────────────────────────────────────────────────────────┐ │ i2c_inst_t at 0x2000062c │ │ │ │ ┌────────────────────────────────────────────────────────────┐ │ │ │ Offset Type Name Value │ │ │ │ 0x00 i2c_hw_t * hw 0x40098000 │ │ │ │ 0x04 bool restart_on_next 0x00 (false) │ │ │ └────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────┘ ``` The first member (`hw`) points to `0x40098000` — the I²C1 hardware register base. This is the end of the macro chain: `I2C_PORT` → `i2c1` → `&i2c1_inst` → `hw` → `0x40098000`. ### Step 14: Locate the String Literals We found the LCD strings in flash memory: ```gdb (gdb) x/s 0x10003ee8 0x10003ee8: "Reverse" (gdb) x/s 0x10003ef0 0x10003ef0: "Engineering" ``` These are stored consecutively in the `.rodata` section. Note the addresses — we'll need them for patching. --- ## 🔬 Part 10: Hacking the Binary with a Hex Editor Now for the fun part — we'll patch the `.bin` file directly using a hex editor! > 💡 **Why a hex editor?** GDB **cannot write to flash memory** — the `0x10000000+` address range where program instructions and read-only data live. Trying `set *(char *)0x1000028e = 0x2b` in GDB gives `Writing to flash memory forbidden in this context`. To make **permanent** patches that survive a power cycle, we edit the `.bin` file directly with a hex editor and re-flash it. ### Step 15: Open the Binary in a Hex Editor 1. Open **HxD** (or your preferred hex editor: ImHex, 010 Editor, etc.) 2. Click **File** → **Open** 3. Navigate to `C:\Users\flare-vm\Desktop\Embedded-Hacking-main\0x0017_constants\build\` 4. Open `0x0017_constants.bin` ### Step 16: Calculate the File Offset The binary is loaded at base address `0x10000000`. To find the file offset of any address: ``` file_offset = address - 0x10000000 ``` For example: - Address `0x1000028e` → file offset `0x28E` (654 in decimal) - Address `0x10003ee8` → file offset `0x3EE8` (16104 in decimal) ### Step 17: Understand FAV_NUM Encoding (movs — 16-bit Thumb) From our GDB analysis, we know the instruction at `0x1000028e` is: ``` movs r1, #0x2a → bytes: 2a 21 ``` In HxD, navigate to file offset `0x28E` and verify you see the byte `2A` followed by `21`. > 🔍 **How Thumb encoding works:** In `movs r1, #imm8`, the immediate value is the first byte, and the opcode `21` is the second byte. So the bytes `2a 21` encode `movs r1, #0x2a` (42). If you wanted to change this to 43, you'd change `2A` to `2B`. ### Step 18: Understand OTHER_FAV_NUM Encoding (movw — 32-bit Thumb-2) From GDB, we found the `movw r1, #1337` instruction at `0x10000296`. Examine the exact bytes: ```gdb (gdb) x/4bx 0x10000296 0x10000296 : 0x40 0xf2 0x39 0x51 ``` This is the 32-bit Thumb-2 encoding of `movw r1, #0x539` (1337). The bytes break down as: ``` ┌─────────────────────────────────────────────────────────────────┐ │ movw r1, #0x539 → bytes: 40 F2 39 51 │ │ │ │ Byte 0: 0x40 ─┐ │ │ Byte 1: 0xF2 ─┘ First halfword (opcode + upper imm bits) │ │ Byte 2: 0x39 ──── Lower 8 bits of immediate (imm8) ← CHANGE │ │ Byte 3: 0x51 ──── Destination register (r1) + upper imm bits │ │ │ │ imm16 = 0x0539 = 1337 decimal │ │ imm8 field = 0x39 (lower 8 bits of the value) │ │ │ └─────────────────────────────────────────────────────────────────┘ ``` The file offset is `0x10000296 - 0x10000000 = 0x296`. The imm8 byte is the 3rd byte of the instruction: `0x296 + 2 = 0x298`. To change `movw r1, #1337` to `movw r1, #1344`: 1. In HxD, press **Ctrl+G** (Go to offset) 2. Enter offset: `298` (the third byte of the 4-byte instruction) 3. You should see the byte `39` at this position 4. Change `39` to `40` > 🔍 **Why offset `0x298` and not `0x296`?** The lower 8 bits of the immediate (`imm8`) are in the **third byte** of the 4-byte `movw` instruction. The instruction starts at file offset `0x296`, so imm8 is at `0x296 + 2 = 0x298`. Changing `0x39` to `0x40` changes the value from `0x539` (1337) to `0x540` (1344). ### Step 19: Hack — Change LCD Text from "Reverse" to "Exploit" **IMPORTANT:** The new string must be the **same length** as the original! "Reverse" and "Exploit" are both 7 characters — perfect! From our GDB analysis in Step 10, we found the string at `0x10003ee8`. File offset = `0x10003ee8 - 0x10000000 = 0x3EE8`. 1. In HxD, press **Ctrl+G** and enter offset: `3EE8` 2. You should see the bytes for "Reverse": `52 65 76 65 72 73 65 00` 3. Change the bytes to spell "Exploit": `45 78 70 6c 6f 69 74 00` **ASCII Reference:** | Character | Hex | | --------- | ------ | | E | `0x45` | | x | `0x78` | | p | `0x70` | | l | `0x6c` | | o | `0x6f` | | i | `0x69` | | t | `0x74` | ### Step 20: Save the Patched Binary 1. Click **File** → **Save As** 2. Save as `0x0017_constants-h.bin` in the build directory 3. Close the hex editor --- ## 🔬 Part 11: Converting and Flashing the Hacked Binary ### Step 21: Convert to UF2 Format Open a terminal and navigate to your project directory: ```powershell cd C:\Users\flare-vm\Desktop\Embedded-Hacking-main\0x0017_constants ``` Run the conversion command: ```powershell python ..\uf2conv.py build\0x0017_constants-h.bin --base 0x10000000 --family 0xe48bff59 --output build\hacked.uf2 ``` ### Step 22: Flash the Hacked Binary 1. Hold BOOTSEL and plug in your Pico 2 2. Drag and drop `hacked.uf2` onto the RPI-RP2 drive 3. Check your LCD and serial monitor ### Step 23: Verify the Hack **Check the LCD:** - Line 1 should now show: `Exploit` (instead of "Reverse") - Line 2 should still show: `Engineering` **Check the serial monitor:** ``` FAV_NUM: 42 OTHER_FAV_NUM: 1337 FAV_NUM: 42 OTHER_FAV_NUM: 1337 ... ``` The numbers are unchanged — we only patched the LCD string! 🎉 **BOOM! We successfully changed the LCD text from "Reverse" to "Exploit" without access to the source code!** --- ## 📊 Part 12: Summary and Review ### What We Accomplished 1. **Learned about constants** - `#define` macros vs `const` variables 2. **Understood I²C communication** - Two-wire protocol for peripheral communication 3. **Explored C structs** - How the Pico SDK abstracts hardware 4. **Mastered the macro chain** - From `I2C_PORT` to `0x40098000` 5. **Examined structs in GDB** - Inspected memory layout of `i2c_inst_t` 6. **Analyzed instruction encodings** - Both `movs` (8-bit) and `movw` (16-bit) immediates in the hex editor 7. **Patched a string literal** - Changed LCD display text from "Reverse" to "Exploit" ### #define vs const Summary ``` ┌─────────────────────────────────────────────────────────────────┐ │ #define FAV_NUM 42 │ │ ─────────────────── │ │ • Text replacement at compile time │ │ • No memory allocated │ │ • Cannot take address (&FAV_NUM is invalid) │ │ • In binary: value appears as immediate (movs r1, #0x2a) │ │ • To hack: patch the instruction operand │ ├─────────────────────────────────────────────────────────────────┤ │ const int OTHER_FAV_NUM = 1337 │ │ ────────────────────────────── │ │ • Theoretically in .rodata, but compiler optimized it away │ │ • Value embedded as immediate: movw r1, #0x539 (32-bit instr) │ │ • Optimization: compiler saw &OTHER_FAV_NUM is never used │ │ • In binary: immediate in instruction, same as #define! │ │ • To hack: patch instruction operand (imm8 byte at offset +2) │ └─────────────────────────────────────────────────────────────────┘ ``` ### I²C Configuration Summary ``` ┌─────────────────────────────────────────────────────────────────┐ │ I²C Setup Steps │ │ │ │ 1. i2c_init(i2c1, 100000) - Initialize at 100kHz │ │ 2. gpio_set_function(pin, I2C) - Assign pins to I²C │ │ 3. gpio_pull_up(sda_pin) - Enable SDA pull-up │ │ 4. gpio_pull_up(scl_pin) - Enable SCL pull-up │ │ 5. lcd_i2c_init(...) - Initialize the device │ │ │ └─────────────────────────────────────────────────────────────────┘ ``` ### The Struct Chain ``` ┌─────────────────────────────────────────────────────────────────┐ │ I2C_PORT → i2c1 → &i2c1_inst → i2c_inst_t │ │ │ │ │ ├── hw → i2c_hw_t * │ │ │ └── 0x40098000 │ │ │ │ │ └── restart_on_next (bool) │ │ │ └─────────────────────────────────────────────────────────────────┘ ``` ### Key Memory Addresses | Address | Description | | ------------ | ---------------------------------- | | `0x10000234` | main() entry point | | `0x1000028e` | FAV_NUM value in instruction | | `0x10000296` | OTHER_FAV_NUM value in instruction | | `0x10003ee8` | "Reverse" string literal (example) | | `0x40098000` | I²C1 hardware registers base | | `0x2000062C` | i2c1_inst struct in SRAM | --- ## ✅ Practice Exercises ### Exercise 1: Change Both LCD Lines Change "Engineering" to "Hacking!!!" (same number of characters). **Hint:** Find the second string after "Reverse" in memory. ### Exercise 2: Change the I²C Address The LCD is at address `0x27`. Find where this is passed to `lcd_i2c_init` and change it. **Warning:** If you change to an invalid address, the LCD won't work! ### Exercise 3: Find All String Literals Search the binary for all readable strings. How many can you find? What do they reveal about the program? **Hint:** In GDB, use `x/s` to search for strings in the binary, or scan through the `.bin` file in your hex editor. ### Exercise 4: Trace the Struct Pointer Follow the `i2c1_inst` pointer from the code to SRAM. What values are stored in the struct? **Hint:** The first member should point to `0x40098000`. ### Exercise 5: Add Your Own Message Can you make the LCD display your name? Remember the character limit! **Hint:** Line 1 and Line 2 each have 16 characters maximum on a 1602 LCD. --- ## 🎓 Key Takeaways 1. **#define is text replacement** - It happens before compilation, no memory used. 2. **const creates real variables** - Stored in .rodata, takes memory, has an address. 3. **I²C uses two wires** - SDA for data, SCL for clock, pull-ups required. 4. **Structs group related data** - The SDK uses them to abstract hardware. 5. **Macros can chain** - `I2C_PORT` → `i2c1` → `&i2c1_inst` → hardware pointer. 6. **ARM passes args in registers** - r0-r3 for first four arguments. 7. **GDB reveals struct layouts** - Examine memory to understand data organization. 8. **String hacking requires same length** - Or you'll corrupt adjacent data! 9. **Constants aren't constant** - With binary patching, everything can change! 10. **Compiler optimization changes code** - `gpio_pull_up` becomes `gpio_set_pulls`. --- ## 📖 Glossary | Term | Definition | | ----------------------- | --------------------------------------------------- | | **#define** | Preprocessor directive for text replacement | | **AAPCS** | ARM Architecture Procedure Call Standard | | **const** | Keyword marking a variable as read-only | | **Forward Declaration** | Telling compiler a type exists before defining it | | **I²C** | Inter-Integrated Circuit - two-wire serial protocol | | **Immediate Value** | A constant embedded directly in an instruction | | **Open-Drain** | Output that can only pull low, not drive high | | **PCF8574** | Common I²C I/O expander chip used in LCD backpacks | | **Preprocessor** | Tool that processes code before compilation | | **Pull-Up Resistor** | Resistor that holds a line HIGH by default | | **SCL** | Serial Clock - I²C timing signal | | **SDA** | Serial Data - I²C data line | | **Struct** | User-defined type grouping related variables | | **typedef** | Creates an alias for a type | --- ## 🔗 Additional Resources ### I²C Timing Reference | Speed Mode | Maximum Frequency | | ---------- | ----------------- | | Standard | 100 kHz | | Fast | 400 kHz | | Fast Plus | 1 MHz | ### Common I²C Addresses | Device | Address | | --------------------- | ------------- | | PCF8574 LCD (default) | `0x27` | | PCF8574A LCD | `0x3F` | | DS3231 RTC | `0x68` | | BMP280 Sensor | `0x76`/`0x77` | | SSD1306 OLED | `0x3C`/`0x3D` | ### Key ARM Instructions for Constants | Instruction | Description | | -------------------- | ------------------------------------------- | | `movs rN, #imm` | Load small immediate (0-255) directly | | `ldr rN, [pc, #off]` | Load larger value from literal pool | | `ldr rN, =value` | Pseudo-instruction for loading any constant | ### RP2350 I²C Memory Map | Address | Description | | ------------ | ----------------------- | | `0x40090000` | I²C0 hardware registers | | `0x40098000` | I²C1 hardware registers | --- **Remember:** When you see complex nested structures in a binary, take your time to understand the hierarchy. Use GDB to examine struct layouts in memory and trace pointer chains. And always remember — even "constants" can be hacked! Happy hacking! 🔧