39 KiB
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
#definemacros andconstvariables - 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:
#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:
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:
// 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:
void print_student(char *name, int age, float gpa); // Messy!
You pass one struct:
void print_student(struct student s); // Clean!
The typedef Keyword
Writing struct student everywhere is tedious. The typedef keyword creates an alias:
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:
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:
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)
i2c_init(I2C_PORT, 100000);
In 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:
- A Raspberry Pi Pico 2 board
- A Raspberry Pi Pico Debug Probe
- OpenOCD installed and configured
- GDB (
arm-none-eabi-gdb) installed - Python installed (for UF2 conversion)
- A serial monitor (PuTTY, minicom, or screen)
- A 1602 LCD display with I²C backpack (PCF8574)
- A hex editor (HxD, ImHex, or similar)
- 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
#include <stdio.h>
#include <string.h>
#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:
- Lines 7-10: Define preprocessor macros for constants and I²C configuration
- Line 12: Define a
constvariable stored in flash - Line 15: Initialize UART for serial output
- Lines 17-21: Initialize I²C1 at 100kHz, configure GPIO pins, enable pull-ups
- Lines 23-27: Initialize LCD and display "Reverse" on line 0, "Engineering" on line 1
- Lines 29-32: Infinite loop printing both constant values to serial terminal
Step 2: Flash the Binary to Your Pico 2
- Hold the BOOTSEL button on your Pico 2
- Plug in the USB cable (while holding BOOTSEL)
- Release BOOTSEL - a drive called "RPI-RP2" appears
- Drag and drop
0x0017_constants.uf2onto the drive - 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:
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 4: Examine Main Function
Let's examine the main function. Disassemble from the entry point:
x/60i 0x10000234
You should see output like:
0x10000234: push {r4, r5, r6, r7, lr}
0x10000236: sub sp, #12
0x10000238: bl 0x10003014 ; stdio_init_all
0x1000023c: ldr r0, [pc, #108] ; Load i2c1_inst pointer
0x1000023e: ldr r1, =0x186A0 ; 100000 (baud rate)
0x10000240: bl 0x100002b4 ; i2c_init
0x10000244: movs r0, #2 ; GPIO 2 (SDA)
0x10000246: movs r1, #3 ; GPIO_FUNC_I2C
0x10000248: bl 0x100002c8 ; gpio_set_function
0x1000024c: movs r0, #3 ; GPIO 3 (SCL)
0x1000024e: movs r1, #3 ; GPIO_FUNC_I2C
0x10000250: bl 0x100002c8 ; gpio_set_function
...
Step 5: Set a Breakpoint at Main
b *0x10000234
c
GDB responds:
Breakpoint 1 at 0x10000234
Continuing.
Breakpoint 1, 0x10000234 in ?? ()
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,#0x2a ; 0x2a = 42 decimal (FAV_NUM)
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 code:
x/10i 0x10000296
Look for a load instruction:
0x10000296: ldr r1, [pc, #offset] ; Load OTHER_FAV_NUM from memory
The const variable is loaded from a memory address because it actually exists in flash!
Step 8: Examine the const Value in Memory
Find where OTHER_FAV_NUM is stored:
x/1wx 0x100002ac
This should show the address pointing to the value. Then examine that address:
x/1wd 0x10003xxx
You should see 1337 (or 0x539 in hex).
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 ; hw pointer (I2C1 hardware base)
0x20000630: 0x00000000 ; restart_on_next = false
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
Observe how:
r0gets the i2c_inst pointerr1gets the baud rate (100000)- The function call happens via
bl
🔬 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 a critical difference:
For FAV_NUM (42) — a #define macro:
0x1000028e: movs r1, #0x2a ; 0x2a = 42 decimal
The value is embedded directly as an immediate in the instruction. There is no memory location — the preprocessor replaced FAV_NUM with 42 before compilation.
For OTHER_FAV_NUM (1337) — a const variable:
0x10000296: ldr r1, [pc, #offset] ; Load value from flash
The value is loaded from a memory address because const creates a real variable stored in the .rodata section of flash.
Step 13: Analyze the I²C Struct Layout
In GDB, we examined the i2c1_inst struct at 0x2000062c:
(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) 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 can modify values in RAM at runtime, but those changes are lost when the device reboots. To make permanent changes, we edit the
.binfile on disk and re-flash it.
Step 15: Open the Binary in a Hex Editor
- Open HxD (or your preferred hex editor: ImHex, 010 Editor, etc.)
- Click File → Open
- Navigate to
0x0017_constants/build/ - 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 offset0x28E(654 in decimal) - Address
0x10003ee8→ file offset0x3EE8(16104 in decimal)
Step 17: Hack #1 — Change FAV_NUM from 42 to 43
From GDB, we know the instruction at 0x1000028e is:
movs r1, #0x2a → bytes: 2a 21
To change the value from 42 (0x2a) to 43 (0x2b):
- In HxD, press Ctrl+G (Go to offset)
- Enter offset:
28E - You should see the byte
2Aat this position - Change
2Ato2B - The instruction is now
movs r1, #0x2b(43 in decimal)
🔍 How Thumb encoding works: In
movs r1, #imm8, the immediate value is the first byte, and the opcode21is the second byte. So the bytes2a 21encodemovs r1, #0x2a.
Step 18: Hack #2 — Change OTHER_FAV_NUM from 1337 to 1344
The const value 1337 (0x539) is stored in the data section, not as an instruction immediate. From GDB, we found its memory address.
Examine the literal pool to find where 0x539 is stored:
(gdb) x/4wx 0x100002a8
Look for the word 0x00000539. Calculate its file offset and patch it:
- In HxD, go to the offset where
0x539is stored - The bytes will be
39 05 00 00(little-endian) - Change to
40 05 00 00(0x540= 1344 in decimal)
🔍 Little-endian byte order: The RP2350 stores multi-byte values with the least significant byte first. So
0x00000539appears as39 05 00 00in memory.
Step 19: Hack #3 — 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 GDB, we know "Reverse" is at address 0x10003ee8:
- In HxD, press Ctrl+G and enter offset:
3EE8 - You should see the bytes for "Reverse":
52 65 76 65 72 73 65 00 - 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
- Click File → Save As
- Save as
0x0017_constants-h.binin the build directory - 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:
cd Embedded-Hacking/0x0017_constants
Run the conversion command:
python ../uf2conv.py build/0x0017_constants-h.bin --base 0x10000000 --family 0xe48bff59 --output build/hacked.uf2
Step 22: Flash the Hacked Binary
- Hold BOOTSEL and plug in your Pico 2
- Drag and drop
hacked.uf2onto the RPI-RP2 drive - Check your LCD and serial monitor
Step 23: Verify the Hacks
Check the LCD:
- Line 1 should now show:
Exploit(instead of "Reverse") - Line 2 should still show:
Engineering
Check the serial monitor:
FAV_NUM: 43
OTHER_FAV_NUM: 1344
FAV_NUM: 43
OTHER_FAV_NUM: 1344
...
🎉 BOOM! We successfully:
- Changed FAV_NUM from 42 to 43
- Changed OTHER_FAV_NUM from 1337 to 1344
- Changed the LCD text from "Reverse" to "Exploit"
📊 Part 12: Summary and Review
What We Accomplished
- Learned about constants -
#definemacros vsconstvariables - Understood I²C communication - Two-wire protocol for peripheral communication
- Explored C structs - How the Pico SDK abstracts hardware
- Mastered the macro chain - From
I2C_PORTto0x40098000 - Examined structs in GDB - Inspected memory layout of
i2c_inst_t - Hacked constant values - Both immediate and memory-stored using a hex editor
- Patched string literals - Changed LCD display text
#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 │
│ ────────────────────────────── │
│ • Real variable in .rodata section │
│ • Takes memory (4 bytes for int) │
│ • Can take address (&OTHER_FAV_NUM is valid) │
│ • In binary: value loaded from memory (ldr r1, [address]) │
│ • To hack: patch the value in the data section │
└─────────────────────────────────────────────────────────────────┘
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 load 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
-
#define is text replacement - It happens before compilation, no memory used.
-
const creates real variables - Stored in .rodata, takes memory, has an address.
-
I²C uses two wires - SDA for data, SCL for clock, pull-ups required.
-
Structs group related data - The SDK uses them to abstract hardware.
-
Macros can chain -
I2C_PORT→i2c1→&i2c1_inst→ hardware pointer. -
ARM passes args in registers - r0-r3 for first four arguments.
-
GDB reveals struct layouts - Examine memory to understand data organization.
-
String hacking requires same length - Or you'll corrupt adjacent data!
-
Constants aren't constant - With binary patching, everything can change!
-
Compiler optimization changes code -
gpio_pull_upbecomesgpio_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! 🔧