8.9 KiB
Embedded Systems Reverse Engineering
Week 4
Variables in Embedded Systems: Debugging and Hacking Variables w/ GPIO Output Basics
Non-Credit Practice Exercise 3: Analyze and Understand GPIO Control
Objective
Import the 0x0008_uninitialized-variables.bin binary, analyze the GPIO initialization and control sequences, understand how gpio_init(), gpio_set_dir(), and gpio_put() work at the assembly level.
Prerequisites
- Completed Exercises 1 and 2
- Understanding of GPIO basics from Week 4 Part 3
- Raspberry Pi Pico 2 with an LED connected to GPIO 16
- Basic knowledge of ARM Thumb-2 instruction set
Task Description
You will import a new binary that controls GPIO pins, identify the GPIO-related function calls, trace the initialization sequence, and understand how the Pico SDK controls hardware at the low level.
Step-by-Step Instructions
Step 1: Flash the Original Binary
Before analysis, let's see what the program does:
- Hold BOOTSEL and plug in your Pico 2
- Flash
0x0008_uninitialized-variables.uf2to the RPI-RP2 drive - Open your serial monitor
Expected output:
age: 0
age: 0
age: 0
...
Expected behavior:
- The red LED on GPIO 16 blinks on/off every 500ms
- The value
0is printed (uninitialized variable)
Step 2: Create a New Ghidra Project
- Launch Ghidra:
ghidraRun - Click File → New Project
- Select Non-Shared Project
- Project Name:
week04-ex03-gpio-analysis - Click Finish
Step 3: Import the GPIO Binary
- Drag and drop
0x0008_uninitialized-variables.bininto Ghidra - Set Language: ARM Cortex 32 little endian default
- Click Options…
- Block Name:
.text - Base Address:
10000000
- Block Name:
- Click OK on all dialogs
- Double-click the file and click Yes to analyze
Step 4: Identify the Main Function
Look for the main function (likely FUN_10000234 or similar):
In the Symbol Tree:
- Expand Functions
- Look for a function that appears to be an entry point
- Click on potential
maincandidates
Look for these patterns in the decompile:
- Call to
stdio_init_all() - Call to
gpio_init() - Infinite while loop with
gpio_put()andsleep_ms()
Step 5: Rename the Main Function
Once you identify main:
- Right-click on the function name
- Select Edit Function Signature
- Change to:
int main(void) - Click OK
Expected decompiled code structure:
int main(void)
{
// Some initial value
stdio_init_all();
gpio_init(0x10); // GPIO 16
// ... more GPIO setup
while (true) {
printf(...);
gpio_put(0x10, 1);
sleep_ms(0x1f4);
gpio_put(0x10, 0);
sleep_ms(0x1f4);
}
}
Step 6: Identify GPIO Function Calls
Look in the decompiled main for function calls. You should see several undefined functions.
Find and rename these GPIO functions:
| Auto-Generated Name | Actual Function | How to Identify |
|---|---|---|
FUN_xxxxx |
gpio_init |
Takes one parameter (pin number) |
FUN_xxxxx |
gpio_set_dir |
Takes two parameters (pin, direction) |
FUN_xxxxx |
gpio_put |
Takes two parameters (pin, value) |
FUN_xxxxx |
sleep_ms |
Takes one parameter (milliseconds) |
FUN_xxxxx |
stdio_init_all |
Takes no parameters, called first |
FUN_xxxxx |
printf |
Takes variable args, has format string |
Example renaming gpio_init:
- Click on the function call in the decompile window
- Right-click → Edit Function Signature
- Change name to:
gpio_init - Set signature to:
void gpio_init(uint gpio) - Click OK
Step 7: Analyze GPIO Initialization Sequence
After renaming, your decompiled main should look clearer:
int main(void)
{
stdio_init_all();
gpio_init(0x10); // Initialize GPIO 16
gpio_set_dir(0x10, 1); // Set as output (1 = GPIO_OUT)
while (true) {
printf("age: %d\r\n", 0);
gpio_put(0x10, 1); // LED ON
sleep_ms(0x1f4); // Wait 500ms (0x1f4 = 500)
gpio_put(0x10, 0); // LED OFF
sleep_ms(0x1f4); // Wait 500ms
}
}
Key observations:
0x10is hexadecimal for 16 (GPIO 16 - red LED)0x1f4is hexadecimal for 500 (milliseconds)1means GPIO_OUT (output direction)- The LED is controlled by toggling between 1 (on) and 0 (off)
Step 8: Examine gpio_init Assembly
Double-click on gpio_init to jump to its implementation.
Look for these key operations in the assembly:
; Load GPIO pin number into register
movs r4, r0 ; Save pin number
; Calculate pad register address
; Base address: 0x40038000 (PADS_BANK0)
; Offset: pin * 4
ldr r3, =0x40038000
lsls r5, r4, #2 ; pin * 4
add r3, r5 ; Calculate address
; Configure pad (clear OD bit, set IE bit)
ldr r2, [r3] ; Read current config
bic r2, #0x80 ; Clear output disable
orr r2, #0x40 ; Set input enable
str r2, [r3] ; Write back
; Set GPIO function to SIO (0x05)
ldr r3, =0x40028000 ; IO_BANK0 base
add r3, r5 ; Add offset
movs r2, #5 ; FUNCSEL = SIO
str r2, [r3] ; Set function
What this does:
- Configures the GPIO pad registers (physical pin properties)
- Sets the GPIO function to SIO (Software I/O)
- Prepares the pin for software control
Step 9: Examine gpio_put Assembly
Find the gpio_put function and examine its implementation.
Look for the GPIO coprocessor instruction:
gpio_put:
movs r4, r0 ; GPIO pin number
movs r5, r1 ; Value (0 or 1)
; Use ARM coprocessor to control GPIO
mcrr p0, #4, r4, r5, c0
bx lr ; Return
Critical instruction: mcrr p0, #4, r4, r5, c0
mcrr= Move to Coprocessor from two ARM Registersp0= Coprocessor 0 (GPIO coprocessor in RP2350)#4= Operation coder4, r5= Source registers (pin number, value)c0= Coprocessor register (GPIO output control)
This is a single-cycle GPIO operation - extremely fast!
Step 10: Document the GPIO Memory Map
Create a reference table of the addresses you found:
| Address | Register | Purpose |
|---|---|---|
0x40028000 |
IO_BANK0 | GPIO function selection |
0x40038000 |
PADS_BANK0 | GPIO pad configuration |
0xd0000000 |
SIO | Single-cycle I/O (coprocessor) |
GPIO 16 specific addresses:
- Pad control:
0x40038000 + (16 * 4) = 0x40038040 - Function select:
0x40028000 + (16 * 4) = 0x40028040
Step 11: Trace the Blink Timing
Calculate the actual timing:
sleep_ms(0x1f4):
- Convert: 0x1f4 = (1 × 256) + (15 × 16) + 4 = 256 + 240 + 4 = 500 decimal
- So the LED is on for 500ms, off for 500ms
- Total cycle time: 1000ms = 1 second
- Blink rate: 1 Hz
Expected Output
After completing this exercise, you should understand:
- How GPIO initialization configures hardware registers
- The role of the GPIO coprocessor in the RP2350
- How
gpio_put()uses a single ARM instruction for fast I/O - The memory-mapped addresses for GPIO control
- How timing delays are implemented with
sleep_ms()
Questions for Reflection
Question 1: Why does gpio_init() need to configure both PADS_BANK0 and IO_BANK0 registers?
Question 2: What is the advantage of using the GPIO coprocessor instruction (mcrr) instead of writing to memory-mapped registers?
Question 3: If you wanted to blink the LED at 10 Hz instead of 1 Hz, what value should sleep_ms() use?
Question 4: What would happen if you called gpio_put() on a pin that hasn't been initialized with gpio_init() first?
Tips and Hints
- Use Ghidra's References feature (right-click → Find References) to see where functions are called
- The Display → Memory Map shows all memory regions
- Look for bit manipulation instructions (
bic,orr) to understand register configuration - The ARM Architecture Reference Manual has complete documentation on coprocessor instructions
- Use hex-to-decimal converters online if you're unsure about conversions
Next Steps
- Proceed to Exercise 4 to patch the GPIO binary
- Try to identify other SDK functions like
gpio_get()if they appear - Explore the full GPIO initialization in the SDK source code
Additional Challenge
Find the gpio_set_dir() function in Ghidra. Does it also use a GPIO coprocessor instruction? What coprocessor register does it use (c0, c4, or something else)? Compare its implementation to gpio_put() and document the differences.