8.9 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 1: Analyze the Float Binary in Ghidra
Objective
Import and analyze the 0x000e_floating-point-data-type.bin binary in Ghidra to understand how the compiler handles floating-point variables, discover float-to-double promotion, and decode the IEEE 754 double-precision encoding of 42.5 from two 32-bit registers.
Prerequisites
- Ghidra installed and configured
0x000e_floating-point-data-type.binbinary available in your build directory- Understanding of IEEE 754 encoding from Week 5 Part 2
- Basic Ghidra navigation skills from Weeks 3 and 4
Task Description
You will import the float binary into Ghidra, configure it for ARM Cortex-M33, resolve function names, discover that the compiler promotes float to double when passing to printf, and manually decode the 64-bit double 0x4045400000000000 field by field to confirm it represents 42.5.
Step-by-Step Instructions
Step 1: Start Ghidra and Create New Project
ghidraRun
- Click File ? New Project
- Select Non-Shared Project
- Click Next
- Enter Project Name:
week05-ex01-floating-point - Choose a project directory
- Click Finish
Step 2: Import the Binary
- Navigate to your file explorer
- Find
Embedded-Hacking\0x000e_floating-point-data-type\build\0x000e_floating-point-data-type.bin - Drag and drop the
.binfile into Ghidra's project window
Step 3: Configure Import Settings
When the import dialog appears:
- Click the three dots (…) next to Language
- Search for:
Cortex - Select: ARM Cortex 32 little endian default
- Click OK
Now click Options… button:
- Change Block Name to:
.text - Change Base Address to:
10000000(XIP flash base) - Click OK
Then click OK on the main import dialog.
Step 4: Analyze the Binary
- Double-click the imported file in the project window
- When prompted "Analyze now?" click Yes
- Leave all default analysis options selected
- Click Analyze
- Wait for analysis to complete (watch bottom-right progress bar)
Step 5: Locate and Rename the Main Function
Look at the Symbol Tree panel on the left. Expand Functions.
From previous weeks, we know the boot sequence leads to main():
- Click on
FUN_10000234 - Right-click ? Edit Function Signature
- Change to:
int main(void) - Click OK
Step 6: Resolve stdio_init_all and printf
Rename stdio_init_all:
- Click on
FUN_10002f5cin the decompile window - Right-click ? Edit Function Signature
- Change to:
bool stdio_init_all(void) - Click OK
Rename printf:
- Click on
FUN_100030ec - Right-click ? Edit Function Signature
- Change to:
int __wrap_printf(char *format,...) - Check the Varargs checkbox
- Click OK
Step 7: Observe the Decompiled Code
After resolving, the decompiled main should look like:
int main(void)
{
undefined4 uVar1;
undefined4 extraout_r1;
undefined4 uVar2;
undefined4 extraout_r1_00;
stdio_init_all();
uVar1 = DAT_1000024c;
uVar2 = extraout_r1;
do {
__wrap_printf(DAT_10000250,uVar2,0,uVar1);
uVar2 = extraout_r1_00;
} while( true );
}
Critical observation: Where is float fav_num = 42.5? The compiler optimized it into constants!
Step 8: Identify the Register Pair
Look at the Listing window (assembly view) for main:
1000023a 00 24 movs r4, #0x0
1000023c 03 4d ldr r5, [DAT_1000024c] = 40454000h
Two values are being passed to printf:
r2 = 0x00000000(low 32 bits)r3 = 0x40454000(high 32 bits)
Together they form a 64-bit double: 0x4045400000000000
Why a double? The C standard requires that float arguments to variadic functions like printf are promoted to double. So even though our variable is declared as float fav_num = 42.5, printf always receives a 64-bit double.
Step 9: Write Out the Binary Layout
Convert both registers to binary:
r3 (high 32 bits): 0x40454000 = 0100 0000 0100 0101 0100 0000 0000 0000
r2 (low 32 bits): 0x00000000 = 0000 0000 0000 0000 0000 0000 0000 0000
Map the 64-bit IEEE 754 fields:
Bit 63 (sign): 0
Bits 62–52 (exponent): 10000000100
Bits 51–0 (mantissa): 0101010000000000...0000
Step 10: Decode the Sign Bit
Bit 63 of the double = bit 31 of r3:
r3 = 0x40454000 = 0100 0000 0100 0101 0100 0000 0000 0000
^
bit 31 = 0 ? Positive number
IEEE 754 sign rule: 0 = Positive, 1 = Negative.
Step 11: Decode the Exponent
Extract bits 30–20 from r3:
0x40454000: 0 10000000100 01010100000000000000
^ ^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^
sign exponent mantissa (top 20)
Exponent bits: 10000000100 = 2¹° + 2² = 1024 + 4 = 1028
Subtract the double-precision bias (1023):
\text{real exponent} = 1028 - 1023 = \mathbf{5}
Step 12: Decode the Mantissa
High 20 bits of mantissa (from r3 bits 19–0):
0 1 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0
Low 32 bits of mantissa (from r2):
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
With the implied leading 1:
1.010101 00000...
Step 13: Reconstruct the Final Value
1.010101_2 \times 2^5 = 101010.1_2
Convert to decimal:
| Bit | Power | Value |
|---|---|---|
| 1 | 25 | 32 |
| 0 | 24 | 0 |
| 1 | 2³ | 8 |
| 0 | 2² | 0 |
| 1 | 2¹ | 2 |
| 0 | 2° | 0 |
| 1 | 2?¹ | 0.5 |
32 + 8 + 2 + 0.5 = \mathbf{42.5} ?
Step 14: Find the Format String
In the Listing view, click on the data reference to locate:
s_fav_num:_%f_100034a8 ds "fav_num: %f\r\n"
Note that the format specifier is %f, confirming this is a floating-point print call.
Step 15: Document Your Findings
Create a table of your observations:
| Item | Value | Notes |
|---|---|---|
| Main function address | 0x10000234 |
Entry point of program |
| Float value (original) | 42.5 |
Declared as float |
| Double hex encoding | 0x4045400000000000 |
Promoted to double for printf |
| r3 (high word) | 0x40454000 |
Contains sign + exponent + mantissa top bits |
| r2 (low word) | 0x00000000 |
All zeros — clean fractional part |
| Exponent (stored) | 1028 | Biased value |
| Exponent (real) | 5 | After subtracting bias 1023 |
| Format string | "fav_num: %f\r\n" |
Located at 0x100034a8 |
| Float-to-double promotion | Yes | C standard for variadic functions |
Expected Output
After completing this exercise, you should be able to:
- Import and configure ARM binaries in Ghidra for float analysis
- Explain why
printfreceives adoubleeven when the variable is afloat - Identify the register pair
r2:r3that holds a 64-bit double - Manually decode an IEEE 754 double from hex to decimal
- Locate format strings in the binary
Questions for Reflection
Question 1: Why does the compiler promote a float to a double when passing it to printf?
Question 2: The low word (r2) is 0x00000000. What does this tell you about the fractional part of 42.5?
Question 3: What is the purpose of the exponent bias (1023) in IEEE 754 double-precision?
Question 4: If the sign bit (bit 63) were 1 instead of 0, what value would the double represent?
Tips and Hints
- The Listing window shows raw assembly; the Decompile window shows reconstructed C
- Double-click on a
DAT_reference to jump to the data constant - Use Python to verify:
import struct; struct.pack('>d', 42.5).hex()gives4045400000000000 - Remember: r3 = high 32 bits (sign + exponent + top mantissa), r2 = low 32 bits (bottom mantissa)
- The bias for doubles is always 1023; for floats it's 127
Next Steps
- Proceed to Exercise 2 to patch this float value in Ghidra
- Try computing the IEEE 754 encoding of other values like
3.14or100.0by hand - Compare the 32-bit float encoding
0x422A0000with the 64-bit double encoding0x4045400000000000— both represent42.5
Additional Challenge
Find the data constant DAT_1000024c in the Listing view. What raw bytes are stored there? Remember that ARM is little-endian — the bytes in memory are in reverse order. Write out the byte order as it appears in memory vs. as a 32-bit value.