8.4 KiB
Embedded Systems Reverse Engineering
Week 5
Integers and Floats in Embedded Systems: Debugging and Hacking Integers and Floats w/ Intermediate GPIO Output Assembler Analysis
Non-Credit Practice Exercise 4: Patch the Double Binary — Changing 42.52525 to 99.99
Objective
Calculate the IEEE 754 double-precision encoding of 99.99, patch both register words in the double binary in Ghidra, export the patched binary, convert it to UF2 format, and flash it to the Pico 2 to verify the change.
Prerequisites
- Completed Exercise 3 (double binary imported and analyzed in Ghidra)
- Understanding of IEEE 754 double-precision encoding from Week 5 Parts 2.7 and 3.7
- Knowledge of integer-to-binary conversion and the multiply-by-2 method for fractions
- Python installed for UF2 conversion and verification
- Raspberry Pi Pico 2 connected via USB
Task Description
You will derive the IEEE 754 encoding of 99.99 step by step (integer part, fractional part, normalization, field extraction), patch both the low word and high word data constants in Ghidra, and verify on hardware that the serial output now prints 99.990000. Unlike Exercise 2 where only one word changed, this exercise requires patching both registers.
Step-by-Step Instructions
Step 1: Convert the Integer Part (99) to Binary
Use repeated division by 2:
| Division | Quotient | Remainder |
|---|---|---|
| 99 ÷ 2 | 49 | 1 |
| 49 ÷ 2 | 24 | 1 |
| 24 ÷ 2 | 12 | 0 |
| 12 ÷ 2 | 6 | 0 |
| 6 ÷ 2 | 3 | 0 |
| 3 ÷ 2 | 1 | 1 |
| 1 ÷ 2 | 0 | 1 |
Read remainders bottom-to-top: 99_{10} = 1100011_2
Step 2: Convert the Fractional Part (.99) to Binary
Use the multiply-by-2 method:
| Multiply | Result | Integer part | Remaining fraction |
|---|---|---|---|
| 0.99 × 2 | 1.98 | 1 | 0.98 |
| 0.98 × 2 | 1.96 | 1 | 0.96 |
| 0.96 × 2 | 1.92 | 1 | 0.92 |
| 0.92 × 2 | 1.84 | 1 | 0.84 |
| 0.84 × 2 | 1.68 | 1 | 0.68 |
| 0.68 × 2 | 1.36 | 1 | 0.36 |
| 0.36 × 2 | 0.72 | 0 | 0.72 |
| 0.72 × 2 | 1.44 | 1 | 0.44 |
| 0.44 × 2 | 0.88 | 0 | 0.88 |
| 0.88 × 2 | 1.76 | 1 | 0.76 |
| 0.76 × 2 | 1.52 | 1 | 0.52 |
| 0.52 × 2 | 1.04 | 1 | 0.04 |
| ... | ... | ... | (continues — repeating fraction) |
Reading the integer parts top-to-bottom: 0.99_{10} \approx 0.111111010111..._2
This is a repeating fraction — it never terminates in binary.
Step 3: Combine Integer and Fractional Parts
99.99_{10} = 1100011.111111010111..._2
Step 4: Normalize to IEEE 754 Form
Move the binary point so there is exactly one 1 before it:
1100011.111111010111..._2 = 1.100011111111010111..._2 \times 2^6
We shifted the binary point 6 places left, so the exponent is 6.
Step 5: Extract the IEEE 754 Fields
- Sign:
0(positive) - Exponent:
6 + 1023 = 1029 = 10000000101_2 - Mantissa:
1000111111110101110000101000111101011100001010001111...(52 bits after the1.) - Full double:
0x4058FF5C28F5C28F
Verify with Python:
>>> import struct
>>> struct.pack('>d', 99.99).hex()
'4058ff5c28f5c28f'
Step 6: Split into Register Words
| Register | Old Value | New Value | Changed? |
|---|---|---|---|
r2 |
0x645A1CAC |
0x28F5C28F |
Yes |
r3 |
0x4045433B |
0x4058FF5C |
Yes |
Both registers change! This is the key difference from Exercise 2 where only r3 changed.
Step 7: Locate the Data Constants in Ghidra
Open your Ghidra project from Exercise 3. In the Listing view, find the two data constants:
Low word (loaded into r2):
DAT_10000254
10000254 ac 1c 5a 64 undefined4 645A1CACh
High word (loaded into r3):
DAT_10000258
10000258 3b 43 45 40 undefined4 4045433Bh
Step 8: Patch the Low Word
- Click on Window → Bytes to open the Bytes Editor
- Click the Pencil Icon to enable editing
- Navigate to address
10000254 - The bytes read:
AC 1C 5A 64(little-endian for0x645A1CAC) - Change to:
8F C2 F5 28(little-endian for0x28F5C28F) - Press Enter
Step 9: Patch the High Word
- Navigate to address
10000258 - The bytes read:
3B 43 45 40(little-endian for0x4045433B) - Change to:
5C FF 58 40(little-endian for0x4058FF5C) - Press Enter
Verify in the Listing view:
DAT_10000254should show28F5C28FhDAT_10000258should show4058FF5Ch
Together: 0x4058FF5C28F5C28F = 99.99 as a double ✓
Step 10: Export the Patched Binary
- Click File → Export Program
- Set Format to Raw Bytes
- Navigate to your build directory
- Name the file:
0x0011_double-floating-point-data-type-h.bin - Click OK
Step 11: Convert to UF2 Format
Open a terminal:
cd Embedded-Hacking-main\0x0011_double-floating-point-data-type
python ..\uf2conv.py build\0x0011_double-floating-point-data-type-h.bin --base 0x10000000 --family 0xe48bff59 --output build\hacked.uf2
Step 12: Flash and Verify
- Hold BOOTSEL and plug in your Pico 2
- Drag and drop
hacked.uf2onto the RPI-RP2 drive - Open your serial monitor
Expected output:
fav_num: 99.990000
fav_num: 99.990000
fav_num: 99.990000
...
🎉 Success! The value changed from 42.52525 to 99.99!
Expected Output
After completing this exercise, you should:
- See
fav_num: 99.990000printing instead offav_num: 42.525250 - Have a patched binary file (
0x0011_double-floating-point-data-type-h.bin) - Have a UF2 file (
hacked.uf2) - Understand that patching doubles with repeating fractions requires modifying both register words
Questions for Reflection
Question 1: Why did both r2 and r3 change when patching 42.52525 → 99.99, but only r3 changed when patching 42.5 → 99.0?
Question 2: The multiply-by-2 method for 0.99 produces a repeating pattern. What does this mean for the precision of the stored value?
Question 3: If you wanted to patch the double to 100.0 instead of 99.99, how many data constants would need to change?
Question 4: Compare the Ghidra Listing for the float binary (Exercise 1) and the double binary (Exercise 3). How does the compiler load the double differently?
Tips and Hints
- Always verify your encoding with Python before patching
- Little-endian byte order:
0x28F5C28Fis stored as8F C2 F5 28in memory - Use Ghidra's Bytes window (Window → Bytes) for precise hex editing
- If
r2was zero before and needs to be non-zero after, you need to patch the data constant — not themovs r4, #0x0instruction - The
ldrdinstruction loads r4 and r5 from two consecutive memory addresses — both must be correct
Next Steps
- Review the complete patching workflow diagram in Week 5 Part 3.95
- Try patching to
100.0— since it has a zero low word, you'll need to changer2from non-zero to zero - Attempt the practice exercises at the end of Week 5
Additional Challenge
Patch the double to 3.14159265358979 (pi). This requires extreme precision in all 52 mantissa bits. Use Python to get the exact encoding, then patch both words. Verify the output prints at least 6 correct decimal places. What happens to the precision if you only patch the high word and leave the low word as 0x645A1CAC?
Verification Checklist
Before moving on, confirm:
- Serial output shows
fav_num: 99.990000 - Both data constants were patched (low word and high word)
- You can derive the IEEE 754 encoding of
99.99from scratch - You understand why messy fractions require patching both register words
- You can explain the difference between the float and double patching workflows
- You successfully converted and flashed the UF2
Congratulations! You've completed all Week 5 exercises and mastered floating-point analysis, IEEE 754 decoding, and double-precision binary patching!