46 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/54i 0x10000234
You should see output like:
0x10000234 <main>: push {r3, lr}
0x10000236 <main+2>: bl 0x100037fc <stdio_init_all>
0x1000023a <main+6>: ldr r1, [pc, #104] @ (0x100002a4 <main+112>)
0x1000023c <main+8>: ldr r0, [pc, #104] @ (0x100002a8 <main+116>)
0x1000023e <main+10>: bl 0x10003cdc <i2c_init>
0x10000242 <main+14>: movs r1, #3
0x10000244 <main+16>: movs r0, #2
0x10000246 <main+18>: bl 0x100008f0 <gpio_set_function>
0x1000024a <main+22>: movs r1, #3
0x1000024c <main+24>: mov r0, r1
0x1000024e <main+26>: bl 0x100008f0 <gpio_set_function>
0x10000252 <main+30>: movs r2, #0
0x10000254 <main+32>: movs r1, #1
0x10000256 <main+34>: movs r0, #2
0x10000258 <main+36>: bl 0x1000092c <gpio_set_pulls>
0x1000025c <main+40>: movs r2, #0
0x1000025e <main+42>: movs r1, #1
0x10000260 <main+44>: movs r0, #3
0x10000262 <main+46>: bl 0x1000092c <gpio_set_pulls>
0x10000266 <main+50>: movs r3, #8
0x10000268 <main+52>: movs r2, #4
0x1000026a <main+54>: movs r1, #39 @ 0x27
0x1000026c <main+56>:
ldr r0, [pc, #56] @ (0x100002a8 <main+116>)
0x1000026e <main+58>: bl 0x100002bc <lcd_i2c_init>
0x10000272 <main+62>: movs r1, #0
0x10000274 <main+64>: mov r0, r1
0x10000276 <main+66>: bl 0x100006f4 <lcd_set_cursor>
0x1000027a <main+70>:
ldr r0, [pc, #48] @ (0x100002ac <main+120>)
0x1000027c <main+72>: bl 0x100007f0 <lcd_puts>
0x10000280 <main+76>: movs r0, #1
0x10000282 <main+78>: movs r1, #0
0x10000284 <main+80>: bl 0x100006f4 <lcd_set_cursor>
0x10000288 <main+84>:
ldr r0, [pc, #36] @ (0x100002b0 <main+124>)
0x1000028a <main+86>: bl 0x100007f0 <lcd_puts>
0x1000028e <main+90>: movs r1, #42 @ 0x2a
0x10000290 <main+92>:
ldr r0, [pc, #32] @ (0x100002b4 <main+128>)
0x10000292 <main+94>: bl 0x1000398c <__wrap_printf>
0x10000296 <main+98>: movw r1, #1337 @ 0x539
0x1000029a <main+102>:
ldr r0, [pc, #28] @ (0x100002b8 <main+132>)
0x1000029c <main+104>: bl 0x1000398c <__wrap_printf>
0x100002a0 <main+108>: b.n 0x1000028e <main+90>
0x100002a2 <main+110>: nop
0x100002a4 <main+112>: strh r0, [r4, #52] @ 0x34
0x100002a6 <main+114>: movs r1, r0
0x100002a8 <main+116>: lsls r4, r5, #24
0x100002aa <main+118>: movs r0, #0
0x100002ac <main+120>: subs r6, #232 @ 0xe8
0x100002ae <main+122>: asrs r0, r0, #32
0x100002b0 <main+124>: subs r6, #240 @ 0xf0
0x100002b2 <main+126>: asrs r0, r0, #32
0x100002b4 <main+128>: subs r6, #252 @ 0xfc
0x100002b6 <main+130>: asrs r0, r0, #32
0x100002b8 <main+132>: subs r7, #12
0x100002ba <main+134>: 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 typec, the target hasn't been started yet. Usemonitor reset haltfirst, thencto 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 <main+90>: 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) x/5i 0x10000296
Look for this instruction:
...
0x10000296 <main+98>: 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
movwinstead ofmovs? The value 1337 doesn't fit in 8 bits (max 255), so the compiler usesmovw(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) x/6wx 0x100002a4
0x100002a4 <main+112>: 0x000186a0 0x2000062c 0x10003ee8 0x10003ef0
0x100002b4 <main+128>: 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
0x100002a4showstrh r0, [r4, #52]instead of data? Same reason as Week 6 — GDB'sx/itries to decode raw data as instructions. Usex/wxto 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 <i2c1_inst>: 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:
- We never take the address of
OTHER_FAV_NUM(no&OTHER_FAV_NUM) - The value fits in a 16-bit
movwimmediate - Loading from an immediate is faster than loading from memory
💡 Key takeaway for reverse engineering: Don't assume
constvariables will appear as memory loads. Modern compilers aggressively inline constant values. The C keywordconstis 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) x/2wx 0x2000062c
0x2000062c <i2c1_inst>: 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 cannot write to flash memory — the
0x10000000+address range where program instructions and read-only data live. Tryingset *(char *)0x1000028e = 0x2bin GDB givesWriting to flash memory forbidden in this context. To make permanent patches that survive a power cycle, we edit the.binfile directly with a hex editor 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
C:\Users\flare-vm\Desktop\Embedded-Hacking-main\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: 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 opcode21is the second byte. So the bytes2a 21encodemovs r1, #0x2a(42). If you wanted to change this to 43, you'd change2Ato2B.
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) x/4bx 0x10000296
0x10000296 <main+98>: 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:
- In HxD, press Ctrl+G (Go to offset)
- Enter offset:
298(the third byte of the 4-byte instruction) - You should see the byte
39at this position - Change
39to40
🔍 Why offset
0x298and not0x296? The lower 8 bits of the immediate (imm8) are in the third byte of the 4-bytemovwinstruction. The instruction starts at file offset0x296, so imm8 is at0x296 + 2 = 0x298. Changing0x39to0x40changes the value from0x539(1337) to0x540(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.
- 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 C:\Users\flare-vm\Desktop\Embedded-Hacking-main\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 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
- 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 - Analyzed instruction encodings - Both
movs(8-bit) andmovw(16-bit) immediates in the hex editor - 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
-
#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! 🔧