mirror of
https://github.com/mytechnotalent/Embedded-Hacking.git
synced 2026-06-02 20:41:39 +02:00
Updated WEEK04
This commit is contained in:
@@ -1,63 +0,0 @@
|
||||
# Embedded Systems Reverse Engineering
|
||||
[Repository](https://github.com/mytechnotalent/Embedded-Hacking)
|
||||
|
||||
## 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 Solution: Analyze the Float Binary in Ghidra
|
||||
|
||||
#### Answers
|
||||
|
||||
##### Main Function Analysis
|
||||
|
||||
| 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 | Sign + exponent + top mantissa |
|
||||
| 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 |
|
||||
| stdio_init_all address | 0x10002f5c | I/O initialization |
|
||||
| printf address | 0x100030ec | Standard library function |
|
||||
|
||||
##### IEEE 754 Decoding of 0x4045400000000000
|
||||
|
||||
```
|
||||
r3 = 0x40454000 = 0100 0000 0100 0101 0100 0000 0000 0000
|
||||
r2 = 0x00000000 = 0000 0000 0000 0000 0000 0000 0000 0000
|
||||
|
||||
Sign bit (bit 63): 0 → Positive
|
||||
Exponent (bits 62-52): 10000000100 = 1028 → 1028 - 1023 = 5
|
||||
Mantissa (bits 51-0): 0101010000...0 → 1.010101 (with implied 1)
|
||||
|
||||
Value = 1.010101₂ × 2⁵ = 101010.1₂ = 32 + 8 + 2 + 0.5 = 42.5 ✓
|
||||
```
|
||||
|
||||
##### Decompiled main() After Renaming
|
||||
|
||||
```c
|
||||
int main(void)
|
||||
{
|
||||
stdio_init_all();
|
||||
do {
|
||||
__wrap_printf("fav_num: %f\r\n", /* r2:r3 = 0x4045400000000000 = 42.5 */);
|
||||
} while (true);
|
||||
}
|
||||
```
|
||||
|
||||
#### Reflection Answers
|
||||
|
||||
1. **Why does the compiler promote a `float` to a `double` when passing it to `printf`?**
|
||||
The C standard (§6.5.2.2) specifies **default argument promotions** for variadic functions like `printf`. When a `float` is passed to a variadic parameter (the `...` part), it is automatically promoted to `double`. This is because historically, floating-point hardware and calling conventions operated more efficiently with double precision. The `printf` function with `%f` always expects a 64-bit `double` on the stack or in the register pair `r2:r3`, never a 32-bit `float`.
|
||||
|
||||
2. **The low word (`r2`) is `0x00000000`. What does this tell you about the fractional part of `42.5`?**
|
||||
It means the fractional part of 42.5 can be represented exactly with very few mantissa bits. The value 0.5 is exactly 2⁻¹ in binary—a single bit. After normalization, the mantissa is `010101000...` which only needs 6 significant bits. All remaining 46 bits (including the entire low 32-bit word) are zero. Values like 0.5, 0.25, 0.125 (negative powers of 2) and their sums always produce clean low words, while values like 0.1 or 0.3 produce repeating binary fractions that fill both words.
|
||||
|
||||
3. **What is the purpose of the exponent bias (1023) in IEEE 754 double-precision?**
|
||||
The bias allows the exponent field to represent both positive and negative exponents using only unsigned integers. The 11-bit exponent field stores values 0–2047. By subtracting the bias (1023), the actual exponent range is −1022 to +1023. This avoids needing a separate sign bit for the exponent and simplifies hardware comparison—doubles can be compared as unsigned integers (for positive values) because larger exponents produce larger bit patterns. The bias value 1023 = 2¹⁰ − 1 is chosen to center the range symmetrically.
|
||||
|
||||
4. **If the sign bit (bit 63) were `1` instead of `0`, what value would the double represent?**
|
||||
The value would be **−42.5**. The sign bit in IEEE 754 is independent of all other fields: flipping bit 63 from 0 to 1 simply negates the value. The hex encoding would change from `0x4045400000000000` to `0xC045400000000000`—only the most significant nibble changes from `4` (`0100`) to `C` (`1100`), with bit 31 of r3 changing from 0 to 1.
|
||||
@@ -1,271 +0,0 @@
|
||||
# Embedded Systems Reverse Engineering
|
||||
[Repository](https://github.com/mytechnotalent/Embedded-Hacking)
|
||||
|
||||
## 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.bin` binary 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
|
||||
|
||||
```powershell
|
||||
ghidraRun
|
||||
```
|
||||
|
||||
1. Click **File** ? **New Project**
|
||||
2. Select **Non-Shared Project**
|
||||
3. Click **Next**
|
||||
4. Enter Project Name: `week05-ex01-floating-point`
|
||||
5. Choose a project directory
|
||||
6. Click **Finish**
|
||||
|
||||
##### Step 2: Import the Binary
|
||||
|
||||
1. Navigate to your file explorer
|
||||
2. Find `Embedded-Hacking\0x000e_floating-point-data-type\build\0x000e_floating-point-data-type.bin`
|
||||
3. **Drag and drop** the `.bin` file into Ghidra's project window
|
||||
|
||||
##### Step 3: Configure Import Settings
|
||||
|
||||
When the import dialog appears:
|
||||
|
||||
1. Click the three dots (**…**) next to **Language**
|
||||
2. Search for: `Cortex`
|
||||
3. Select: **ARM Cortex 32 little endian default**
|
||||
4. Click **OK**
|
||||
|
||||
Now click **Options…** button:
|
||||
1. Change **Block Name** to: `.text`
|
||||
2. Change **Base Address** to: `10000000` (XIP flash base)
|
||||
3. Click **OK**
|
||||
|
||||
Then click **OK** on the main import dialog.
|
||||
|
||||
##### Step 4: Analyze the Binary
|
||||
|
||||
1. Double-click the imported file in the project window
|
||||
2. When prompted "Analyze now?" click **Yes**
|
||||
3. Leave all default analysis options selected
|
||||
4. Click **Analyze**
|
||||
5. 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()`:
|
||||
|
||||
1. Click on `FUN_10000234`
|
||||
2. Right-click ? **Edit Function Signature**
|
||||
3. Change to: `int main(void)`
|
||||
4. Click **OK**
|
||||
|
||||
##### Step 6: Resolve stdio_init_all and printf
|
||||
|
||||
**Rename stdio_init_all:**
|
||||
1. Click on `FUN_10002f5c` in the decompile window
|
||||
2. Right-click ? **Edit Function Signature**
|
||||
3. Change to: `bool stdio_init_all(void)`
|
||||
4. Click **OK**
|
||||
|
||||
**Rename printf:**
|
||||
1. Click on `FUN_100030ec`
|
||||
2. Right-click ? **Edit Function Signature**
|
||||
3. Change to: `int __wrap_printf(char *format,...)`
|
||||
4. Check the **Varargs** checkbox
|
||||
5. Click **OK**
|
||||
|
||||
##### Step 7: Observe the Decompiled Code
|
||||
|
||||
After resolving, the decompiled `main` should look like:
|
||||
|
||||
```c
|
||||
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`:
|
||||
|
||||
```assembly
|
||||
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 `printf` receives a `double` even when the variable is a `float`
|
||||
- Identify the register pair `r2:r3` that 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()` gives `4045400000000000`
|
||||
- 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.14` or `100.0` by hand
|
||||
- Compare the 32-bit float encoding `0x422A0000` with the 64-bit double encoding `0x4045400000000000` — both represent `42.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.
|
||||
@@ -1,73 +0,0 @@
|
||||
# Embedded Systems Reverse Engineering
|
||||
[Repository](https://github.com/mytechnotalent/Embedded-Hacking)
|
||||
|
||||
## Week 5
|
||||
Integers and Floats in Embedded Systems: Debugging and Hacking Integers and Floats w/ Intermediate GPIO Output Assembler Analysis
|
||||
|
||||
### Non-Credit Practice Exercise 2 Solution: Patch the Float Binary — Changing 42.5 to 99.0
|
||||
|
||||
#### Answers
|
||||
|
||||
##### IEEE 754 Encoding of 99.0
|
||||
|
||||
```
|
||||
Integer: 99 = 1100011₂
|
||||
Fractional: .0 = .0₂
|
||||
Combined: 1100011.0₂
|
||||
Normalized: 1.100011₂ × 2⁶
|
||||
|
||||
Sign: 0 (positive)
|
||||
Exponent: 6 + 1023 = 1029 = 10000000101₂
|
||||
Mantissa: 100011 followed by 46 zeros
|
||||
|
||||
Full double: 0x4058C00000000000
|
||||
```
|
||||
|
||||
##### Patch Summary
|
||||
|
||||
| Register | Old Value (42.5) | New Value (99.0) | Changed? |
|
||||
|----------|-----------------|------------------|----------|
|
||||
| r2 | 0x00000000 | 0x00000000 | No |
|
||||
| r3 | 0x40454000 | 0x4058C000 | **Yes** |
|
||||
|
||||
##### Ghidra Patch
|
||||
|
||||
```
|
||||
DAT_1000024c:
|
||||
Before (little-endian): 00 40 45 40 → 0x40454000
|
||||
After (little-endian): 00 C0 58 40 → 0x4058C000
|
||||
```
|
||||
|
||||
##### Serial Output
|
||||
|
||||
```
|
||||
fav_num: 99.000000
|
||||
fav_num: 99.000000
|
||||
fav_num: 99.000000
|
||||
...
|
||||
```
|
||||
|
||||
#### Reflection Answers
|
||||
|
||||
1. **Why did we only need to patch r3 (the high word) and not r2 (the low word)?**
|
||||
Both 42.5 and 99.0 have "clean" fractional parts that can be exactly represented with few mantissa bits. For 42.5, the mantissa is `010101000...0`; for 99.0, it's `100011000...0`. In both cases, all significant mantissa bits fit within the top 20 bits (stored in r3 bits 19–0), leaving the bottom 32 bits (r2) as all zeros. Only values with complex or repeating binary fractions (like 42.52525 or 99.99) need non-zero low words.
|
||||
|
||||
2. **What would the high word be if we wanted to patch the value to `-99.0` instead?**
|
||||
Flip bit 31 of r3 (the sign bit). The current r3 = `0x4058C000` = `0100 0000 0101 1000 1100...`. Setting bit 31 to 1: `0xC058C000` = `1100 0000 0101 1000 1100...`. The full double encoding of −99.0 is `0xC058C00000000000`. Only the most significant nibble changes from `4` to `C`.
|
||||
|
||||
3. **Walk through the encoding of `100.0` as a double. What are the high and low words?**
|
||||
```
|
||||
100 = 1100100₂
|
||||
100.0 = 1100100.0₂ = 1.1001₂ × 2⁶
|
||||
Sign: 0
|
||||
Exponent: 6 + 1023 = 1029 = 10000000101₂
|
||||
Mantissa: 1001 followed by 48 zeros
|
||||
|
||||
Full 64-bit: 0 10000000101 1001000000...0
|
||||
High word (r3): 0x40590000
|
||||
Low word (r2): 0x00000000
|
||||
```
|
||||
Verification: `struct.pack('>d', 100.0).hex()` → `4059000000000000` ✓
|
||||
|
||||
4. **Why do we need the `--family 0xe48bff59` flag when converting to UF2?**
|
||||
The `--family` flag specifies the target chip family in the UF2 file header. `0xe48bff59` is the registered family ID for the RP2350. The bootloader reads this field to verify the firmware is intended for the correct chip before flashing. If the family ID doesn't match (e.g., using the RP2040 ID `0xe48bff56`), the bootloader may reject the firmware or write it incorrectly. This prevents accidentally flashing RP2040 firmware onto an RP2350 (or vice versa), which could cause undefined behavior since the chips have different architectures (Cortex-M0+ vs Cortex-M33).
|
||||
@@ -1,173 +0,0 @@
|
||||
# Embedded Systems Reverse Engineering
|
||||
[Repository](https://github.com/mytechnotalent/Embedded-Hacking)
|
||||
|
||||
## Week 5
|
||||
Integers and Floats in Embedded Systems: Debugging and Hacking Integers and Floats w/ Intermediate GPIO Output Assembler Analysis
|
||||
|
||||
### Non-Credit Practice Exercise 2: Patch the Float Binary — Changing 42.5 to 99.0
|
||||
|
||||
#### Objective
|
||||
Calculate the IEEE 754 double-precision encoding of `99.0`, patch the float binary in Ghidra to change the printed value from `42.5` to `99.0`, export the patched binary, convert it to UF2 format, and flash it to the Pico 2 to verify the change.
|
||||
|
||||
#### Prerequisites
|
||||
- Completed Exercise 1 (float binary imported and analyzed in Ghidra)
|
||||
- Understanding of IEEE 754 double-precision encoding from Week 5 Part 2.7
|
||||
- Python installed for UF2 conversion
|
||||
- `uf2conv.py` script available in your project directory
|
||||
- Raspberry Pi Pico 2 connected via USB
|
||||
- Serial monitor software (PuTTY, minicom, or screen)
|
||||
|
||||
#### Task Description
|
||||
You will convert `99.0` to its IEEE 754 double-precision encoding by hand (integer-to-binary conversion, normalization, field extraction), determine which register words need to change, patch the data constant in Ghidra, and verify on hardware that the serial output now prints `99.000000`.
|
||||
|
||||
#### Step-by-Step Instructions
|
||||
|
||||
##### Step 1: Convert 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: Handle the Fractional Part
|
||||
|
||||
The fractional part of `99.0` is `.0` — exactly zero. There are no fractional bits.
|
||||
|
||||
$$99.0_{10} = 1100011.0_2$$
|
||||
|
||||
##### Step 3: Normalize to IEEE 754 Form
|
||||
|
||||
Move the binary point so there is exactly one `1` before it:
|
||||
|
||||
$$1100011.0_2 = 1.100011_2 \times 2^6$$
|
||||
|
||||
We shifted the binary point 6 places left, so the exponent is **6**.
|
||||
|
||||
##### Step 4: Extract the IEEE 754 Fields
|
||||
|
||||
1. **Sign:** `0` (positive)
|
||||
2. **Exponent:** $6 + 1023 = 1029 = 10000000101_2$
|
||||
3. **Mantissa:** `100011` followed by 46 zeros (everything after the `1.`, padded to 52 bits)
|
||||
4. **Full double:** `0x4058C00000000000`
|
||||
|
||||
Split into register words:
|
||||
- **r3 (high word):** `0x4058C000`
|
||||
- **r2 (low word):** `0x00000000`
|
||||
|
||||
##### Step 5: Determine What to Patch
|
||||
|
||||
Compare old vs. new:
|
||||
|
||||
| Register | Old Value | New Value | Changed? |
|
||||
| -------- | ------------ | ------------ | -------- |
|
||||
| `r2` | `0x00000000` | `0x00000000` | No |
|
||||
| `r3` | `0x40454000` | `0x4058C000` | **Yes** |
|
||||
|
||||
Since `r2` stays all zeros, we only need to patch the high word in `r3`.
|
||||
|
||||
##### Step 6: Locate the Data Constant in Ghidra
|
||||
|
||||
Open your Ghidra project from Exercise 1. In the Listing view, find the data constant that loads into `r3`:
|
||||
|
||||
```
|
||||
DAT_1000024c
|
||||
10000248 00 40 45 40 undefined4 40454000h
|
||||
```
|
||||
|
||||
This is the 32-bit constant `0x40454000` — the high word of the double `42.5`.
|
||||
|
||||
##### Step 7: Patch the Constant
|
||||
|
||||
1. Click on **Window** → **Bytes** to open the Bytes Editor
|
||||
2. Click the **Pencil Icon** to enable editing
|
||||
3. Navigate to the data at `DAT_1000024c`
|
||||
4. The bytes in memory (little-endian) read: `00 40 45 40`
|
||||
5. Change them to: `00 C0 58 40` (which is `0x4058C000` in little-endian)
|
||||
6. Press **Enter**
|
||||
|
||||
**Verify** in the Listing view — the data should now show `4058C000h`.
|
||||
|
||||
##### Step 8: Verify the Patch in the Decompile Window
|
||||
|
||||
The decompiled code should now reference the new constant. The value loaded into `r3` should be `0x4058C000`.
|
||||
|
||||
##### Step 9: Export the Patched Binary
|
||||
|
||||
1. Click **File** → **Export Program**
|
||||
2. Set **Format** to **Raw Bytes**
|
||||
3. Navigate to your build directory
|
||||
4. Name the file: `0x000e_floating-point-data-type-h.bin`
|
||||
5. Click **OK**
|
||||
|
||||
##### Step 10: Convert to UF2 Format
|
||||
|
||||
Open a terminal and navigate to your project directory:
|
||||
|
||||
```powershell
|
||||
cd Embedded-Hacking-main\0x000e_floating-point-data-type
|
||||
```
|
||||
|
||||
Run the conversion:
|
||||
|
||||
```powershell
|
||||
python ..\uf2conv.py build\0x000e_floating-point-data-type-h.bin --base 0x10000000 --family 0xe48bff59 --output build\hacked.uf2
|
||||
```
|
||||
|
||||
##### Step 11: Flash and Verify
|
||||
|
||||
1. Hold **BOOTSEL** and plug in your Pico 2
|
||||
2. Drag and drop `hacked.uf2` onto the RPI-RP2 drive
|
||||
3. Open your serial monitor
|
||||
|
||||
**Expected output:**
|
||||
|
||||
```
|
||||
fav_num: 99.000000
|
||||
fav_num: 99.000000
|
||||
fav_num: 99.000000
|
||||
...
|
||||
```
|
||||
|
||||
🎉 **Success!** The value changed from `42.5` to `99.0`!
|
||||
|
||||
#### Expected Output
|
||||
|
||||
After completing this exercise, you should:
|
||||
- See `fav_num: 99.000000` printing instead of `fav_num: 42.500000`
|
||||
- Have a patched binary file (`0x000e_floating-point-data-type-h.bin`)
|
||||
- Have a UF2 file (`hacked.uf2`)
|
||||
- Understand the complete IEEE 754 encoding and patching workflow for floats
|
||||
|
||||
#### Questions for Reflection
|
||||
|
||||
###### Question 1: Why did we only need to patch r3 (the high word) and not r2 (the low word)?
|
||||
|
||||
###### Question 2: What would the high word be if we wanted to patch the value to `-99.0` instead? (Hint: which bit controls the sign?)
|
||||
|
||||
###### Question 3: Walk through the encoding of `100.0` as a double. What are the high and low words?
|
||||
|
||||
###### Question 4: Why do we need the `--family 0xe48bff59` flag when converting to UF2?
|
||||
|
||||
#### Tips and Hints
|
||||
- Always verify your encoding with Python: `import struct; struct.pack('>d', 99.0).hex()`
|
||||
- Little-endian means the bytes in memory are reversed: `0x4058C000` is stored as `00 C0 58 40`
|
||||
- If you patch the wrong bytes, use **File** → **Undo** in Ghidra (or re-import the original binary)
|
||||
- The bias for double-precision is always 1023; for single-precision it's 127
|
||||
- A clean fractional part (like `.0` or `.5`) means the low word (`r2`) is all zeros
|
||||
|
||||
#### Next Steps
|
||||
- Proceed to Exercise 3 to analyze the double-precision binary
|
||||
- Try patching to different values: `3.14`, `100.0`, `255.0`
|
||||
- Compare how many register words change for clean vs. messy fractions
|
||||
|
||||
#### Additional Challenge
|
||||
Patch the float to `3.14` instead of `99.0`. Since `3.14` has a repeating binary fraction, the low word will **not** be zero. This means you need to modify the assembly to load a non-zero value into `r2` as well. Can you figure out where `r2` gets its zero value (`movs r4, #0x0`) and change it?
|
||||
@@ -1,70 +0,0 @@
|
||||
# Embedded Systems Reverse Engineering
|
||||
[Repository](https://github.com/mytechnotalent/Embedded-Hacking)
|
||||
|
||||
## Week 5
|
||||
Integers and Floats in Embedded Systems: Debugging and Hacking Integers and Floats w/ Intermediate GPIO Output Assembler Analysis
|
||||
|
||||
### Non-Credit Practice Exercise 3 Solution: Analyze the Double Binary in Ghidra
|
||||
|
||||
#### Answers
|
||||
|
||||
##### Register Pair for 42.52525
|
||||
|
||||
| Register | Value | Role |
|
||||
|----------|-------------|---------------|
|
||||
| r2 | 0x645A1CAC | Low 32 bits |
|
||||
| r3 | 0x4045433B | High 32 bits |
|
||||
|
||||
Full double: **0x4045433B645A1CAC**
|
||||
|
||||
##### IEEE 754 Decoding
|
||||
|
||||
```
|
||||
r3 = 0x4045433B = 0100 0000 0100 0101 0100 0011 0011 1011
|
||||
r2 = 0x645A1CAC = 0110 0100 0101 1010 0001 1100 1010 1100
|
||||
|
||||
Sign bit (bit 63): 0 → Positive
|
||||
Exponent (bits 62-52): 10000000100 = 1028 → 1028 - 1023 = 5
|
||||
Mantissa (bits 51-0): 0101010000110011101101100100010110100001110010101100
|
||||
|
||||
Value = 1.0101010000110011...₂ × 2⁵ = 101010.10000110011...₂
|
||||
Integer part: 101010₂ = 32 + 8 + 2 = 42
|
||||
Fractional part: .10000110011... ≈ 0.52525
|
||||
Result: 42.52525 ✓
|
||||
```
|
||||
|
||||
##### Float vs Double Comparison
|
||||
|
||||
| Item | Float (42.5) | Double (42.52525) |
|
||||
|--------------------------|---------------------|------------------------|
|
||||
| r2 (low word) | 0x00000000 | 0x645A1CAC |
|
||||
| r3 (high word) | 0x40454000 | 0x4045433B |
|
||||
| Low word is zero? | Yes | **No** |
|
||||
| Words to patch | 1 (r3 only) | **2 (both r2 and r3)** |
|
||||
| Format specifier | %f | %lf |
|
||||
| Assembly load instruction| movs + ldr | ldrd (load double) |
|
||||
|
||||
##### Key Assembly
|
||||
|
||||
```assembly
|
||||
10000238 push {r3,r4,r5,lr}
|
||||
1000023a adr r5, [0x10000254]
|
||||
1000023c ldrd r4, r5, [r5, #0x0] ; r4 = 0x645A1CAC, r5 = 0x4045433B
|
||||
10000240 bl stdio_init_all
|
||||
10000244 mov r2, r4 ; r2 = low word
|
||||
10000246 mov r3, r5 ; r3 = high word
|
||||
```
|
||||
|
||||
#### Reflection Answers
|
||||
|
||||
1. **Why does `42.5` have a zero low word but `42.52525` does not?**
|
||||
The fractional part determines whether the low word is zero. 0.5 in binary is exactly 2⁻¹ = `0.1₂`—a single bit. After normalization, the mantissa for 42.5 is `010101000...0`, needing only 6 significant bits, all fitting in the top 20 mantissa bits within r3. In contrast, 0.52525 is a **repeating binary fraction** that cannot be represented exactly—it requires all 52 mantissa bits to approximate as closely as possible. The lower 32 bits in r2 (`0x645A1CAC`) carry the additional precision needed for this approximation.
|
||||
|
||||
2. **The assembly uses `ldrd r4, r5, [r5, #0x0]` instead of two separate `ldr` instructions. What is the advantage?**
|
||||
`ldrd` (Load Register Double) loads two consecutive 32-bit words from memory in a single instruction, completing in one memory access cycle (or two back-to-back aligned accesses on the bus). Using two separate `ldr` instructions would require two instruction fetches, two decode cycles, and two memory accesses. `ldrd` reduces code size by 4 bytes (one 4-byte instruction vs. two) and improves performance by allowing the memory controller to pipeline both loads. For 64-bit doubles that are always loaded in pairs, `ldrd` is the optimal choice.
|
||||
|
||||
3. **Both the float and double programs have the same exponent (stored as 1028, real exponent 5). Why?**
|
||||
Both 42.5 and 42.52525 fall in the same range: between 32 (2⁵) and 64 (2⁶). Normalization produces `1.xxx × 2⁵` for both values. The exponent is determined solely by the magnitude (which power of 2 the number falls between), not by the fractional precision. Any number from 32.0 to 63.999... would have real exponent 5 (stored as 1028). The mantissa captures the differences—42.5 has mantissa `010101000...` while 42.52525 has `0101010000110011...`.
|
||||
|
||||
4. **If you were patching this double, how many data constants would you need to modify compared to the float exercise?**
|
||||
**Two** data constants—both `DAT_10000254` (low word, r2) and `DAT_10000258` (high word, r3). In the float exercise (42.5), only one constant needed patching because r2 was zero and stayed zero when patching to 99.0. For the double 42.52525, since the low word is already non-zero (`0x645A1CAC`), any new value with a different repeating fraction will require changing both words. The only exception would be patching to a value whose low word happens to also be `0x645A1CAC` (virtually impossible for an arbitrary target value).
|
||||
@@ -1,277 +0,0 @@
|
||||
# Embedded Systems Reverse Engineering
|
||||
[Repository](https://github.com/mytechnotalent/Embedded-Hacking)
|
||||
|
||||
## Week 5
|
||||
Integers and Floats in Embedded Systems: Debugging and Hacking Integers and Floats w/ Intermediate GPIO Output Assembler Analysis
|
||||
|
||||
### Non-Credit Practice Exercise 3: Analyze the Double Binary in Ghidra
|
||||
|
||||
#### Objective
|
||||
Import and analyze the `0x0011_double-floating-point-data-type.bin` binary in Ghidra to understand how doubles differ from floats at the binary level, observe that **both** register words carry non-zero data when the fractional part is complex, and decode the IEEE 754 double-precision encoding of `42.52525` from two 32-bit registers.
|
||||
|
||||
#### Prerequisites
|
||||
- Completed Exercises 1 and 2 (float analysis and patching)
|
||||
- Understanding of IEEE 754 double-precision format from Week 5 Part 3
|
||||
- Understanding of register pairs (`r2:r3`) from Exercise 1
|
||||
- Basic Ghidra navigation skills
|
||||
|
||||
#### Task Description
|
||||
You will import the double binary into Ghidra, resolve function names, identify that `42.52525` requires non-zero data in **both** r2 and r3 (unlike `42.5` which had r2 = 0), and decode the full 64-bit double `0x4045433B645A1CAC` field by field to confirm it represents `42.52525`.
|
||||
|
||||
#### Step-by-Step Instructions
|
||||
|
||||
##### Step 1: Flash the Original Binary
|
||||
|
||||
Before analysis, verify the program works:
|
||||
|
||||
1. Hold **BOOTSEL** and plug in your Pico 2
|
||||
2. Flash `0x0011_double-floating-point-data-type.uf2` to the RPI-RP2 drive
|
||||
3. Open your serial monitor
|
||||
|
||||
**Expected output:**
|
||||
```
|
||||
fav_num: 42.525250
|
||||
fav_num: 42.525250
|
||||
fav_num: 42.525250
|
||||
...
|
||||
```
|
||||
|
||||
##### Step 2: Create a New Ghidra Project
|
||||
|
||||
1. Launch Ghidra: `ghidraRun`
|
||||
2. Click **File** → **New Project**
|
||||
3. Select **Non-Shared Project**
|
||||
4. Project Name: `week05-ex03-double-analysis`
|
||||
5. Click **Finish**
|
||||
|
||||
##### Step 3: Import and Configure the Binary
|
||||
|
||||
1. Drag and drop `0x0011_double-floating-point-data-type.bin` into Ghidra
|
||||
2. Set Language: **ARM Cortex 32 little endian default**
|
||||
3. Click **Options…**
|
||||
- Block Name: `.text`
|
||||
- Base Address: `10000000`
|
||||
4. Click **OK** on all dialogs
|
||||
5. Double-click the file and click **Yes** to analyze
|
||||
|
||||
##### Step 4: Locate and Rename Functions
|
||||
|
||||
Identify the main function and standard library calls:
|
||||
|
||||
**Rename main:**
|
||||
1. Click on `FUN_10000238`
|
||||
2. Right-click → **Edit Function Signature**
|
||||
3. Change to: `int main(void)`
|
||||
4. Click **OK**
|
||||
|
||||
**Rename stdio_init_all:**
|
||||
1. Click on `FUN_10002f64`
|
||||
2. Right-click → **Edit Function Signature**
|
||||
3. Change to: `bool stdio_init_all(void)`
|
||||
4. Click **OK**
|
||||
|
||||
**Rename printf:**
|
||||
1. Click on `FUN_100030f4`
|
||||
2. Right-click → **Edit Function Signature**
|
||||
3. Change to: `int printf(char *format,...)`
|
||||
4. Check the **Varargs** checkbox
|
||||
5. Click **OK**
|
||||
|
||||
##### Step 5: Observe the Decompiled Code
|
||||
|
||||
After renaming, the decompiled `main` should look like:
|
||||
|
||||
```c
|
||||
int main(void)
|
||||
{
|
||||
undefined4 uVar1;
|
||||
undefined4 uVar2;
|
||||
undefined4 extraout_r1;
|
||||
undefined4 uVar3;
|
||||
undefined4 extraout_r1_00;
|
||||
|
||||
uVar2 = DAT_10000258;
|
||||
uVar1 = DAT_10000254;
|
||||
stdio_init_all();
|
||||
uVar3 = extraout_r1;
|
||||
do {
|
||||
printf(DAT_10000250,uVar3,uVar1,uVar2);
|
||||
uVar3 = extraout_r1_00;
|
||||
} while( true );
|
||||
}
|
||||
```
|
||||
|
||||
**Critical observation:** There are now **two** non-zero data constants — `DAT_10000254` and `DAT_10000258`. This is the key difference from the float exercise!
|
||||
|
||||
##### Step 6: Examine the Assembly Listing
|
||||
|
||||
Click on the **Listing** window and find the main function assembly:
|
||||
|
||||
```assembly
|
||||
10000238 push {r3,r4,r5,lr}
|
||||
1000023a adr r5, [0x10000254]
|
||||
1000023c ldrd r4, r5, [r5, #0x0] = 645A1CACh / 4045433Bh
|
||||
10000240 bl stdio_init_all
|
||||
```
|
||||
|
||||
**Key instruction: `ldrd r4, r5, [r5, #0x0]`**
|
||||
|
||||
This is a **load register double** instruction — it loads two consecutive 32-bit words in a single instruction:
|
||||
- `r4` gets the low word: `0x645A1CAC`
|
||||
- `r5` gets the high word: `0x4045433B`
|
||||
|
||||
Later in the loop:
|
||||
```assembly
|
||||
10000244 mov r2, r4 ; r2 = 0x645A1CAC (low)
|
||||
10000246 mov r3, r5 ; r3 = 0x4045433B (high)
|
||||
```
|
||||
|
||||
##### Step 7: Identify the Register Pair
|
||||
|
||||
| Register | Value | Role |
|
||||
| -------- | ------------ | ------------ |
|
||||
| `r2` | `0x645A1CAC` | Low 32 bits |
|
||||
| `r3` | `0x4045433B` | High 32 bits |
|
||||
|
||||
Together: `0x4045433B645A1CAC`
|
||||
|
||||
**Compare to the float exercise:**
|
||||
- Float `42.5`: r2 = `0x00000000`, r3 = `0x40454000` — low word is all zeros
|
||||
- Double `42.52525`: r2 = `0x645A1CAC`, r3 = `0x4045433B` — **both words are non-zero!**
|
||||
|
||||
This happens because `42.52525` has a repeating binary fraction that needs all 52 mantissa bits.
|
||||
|
||||
##### Step 8: Decode the Sign Bit
|
||||
|
||||
Convert r3 to binary and check bit 31:
|
||||
|
||||
```
|
||||
r3 = 0x4045433B = 0100 0000 0100 0101 0100 0011 0011 1011
|
||||
^
|
||||
bit 31 = 0 → Positive number ✓
|
||||
```
|
||||
|
||||
##### Step 9: Decode the Exponent
|
||||
|
||||
Extract bits 30–20 from r3:
|
||||
|
||||
```
|
||||
0x4045433B: 0 10000000100 01010100001100111011
|
||||
^ ^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^
|
||||
sign exponent mantissa (top 20)
|
||||
```
|
||||
|
||||
Exponent bits: `10000000100` = 2¹⁰ + 2² = 1024 + 4 = **1028**
|
||||
|
||||
$$\text{real exponent} = 1028 - 1023 = \mathbf{5}$$
|
||||
|
||||
##### Step 10: Decode the Mantissa
|
||||
|
||||
**High 20 bits** (r3 bits 19–0):
|
||||
```
|
||||
0 1 0 1 0 1 0 0 0 0 1 1 0 0 1 1 1 0 1 1
|
||||
```
|
||||
|
||||
**Low 32 bits** (all of r2 = `0x645A1CAC`):
|
||||
```
|
||||
0 1 1 0 0 1 0 0 0 1 0 1 1 0 1 0 0 0 0 1 1 1 0 0 1 0 1 0 1 1 0 0
|
||||
```
|
||||
|
||||
With the implied leading `1`:
|
||||
```
|
||||
1.0101010000110011101101100100010110100001110010101100
|
||||
```
|
||||
|
||||
##### Step 11: Reconstruct the Integer Part
|
||||
|
||||
$$1.0101010000110011..._2 \times 2^5$$
|
||||
|
||||
Shift the binary point 5 places right → `101010.1000011001110110...`
|
||||
|
||||
Integer part `101010`:
|
||||
|
||||
| Bit | Power | Value |
|
||||
|-----|-------|-------|
|
||||
| 1 | 2⁵ | 32 |
|
||||
| 0 | 2⁴ | 0 |
|
||||
| 1 | 2³ | 8 |
|
||||
| 0 | 2² | 0 |
|
||||
| 1 | 2¹ | 2 |
|
||||
| 0 | 2⁰ | 0 |
|
||||
|
||||
$$32 + 8 + 2 = \mathbf{42}$$
|
||||
|
||||
##### Step 12: Approximate the Fractional Part
|
||||
|
||||
The first few fractional bits `.10000110011...`:
|
||||
|
||||
| Bit | Power | Decimal |
|
||||
|------|--------|---------------|
|
||||
| 1 | 2⁻¹ | 0.5 |
|
||||
| 0 | 2⁻² | 0 |
|
||||
| 0 | 2⁻³ | 0 |
|
||||
| 0 | 2⁻⁴ | 0 |
|
||||
| 0 | 2⁻⁵ | 0 |
|
||||
| 1 | 2⁻⁶ | 0.015625 |
|
||||
| 1 | 2⁻⁷ | 0.0078125 |
|
||||
| 0 | 2⁻⁸ | 0 |
|
||||
| 0 | 2⁻⁹ | 0 |
|
||||
| 1 | 2⁻¹⁰ | 0.0009765625 |
|
||||
| 1 | 2⁻¹¹ | 0.00048828125 |
|
||||
|
||||
First 11 bits sum ≈ 0.5249. The remaining 36 fractional bits refine this to ≈ 0.52525.
|
||||
|
||||
$$42 + 0.52525 = \mathbf{42.52525} ✓$$
|
||||
|
||||
##### Step 13: Find the Format String
|
||||
|
||||
In the Listing view, locate:
|
||||
|
||||
```
|
||||
s_fav_num:_%lf_100034b0 ds "fav_num: %lf\r\n"
|
||||
```
|
||||
|
||||
Note the `%lf` format specifier — this program explicitly uses `double`, unlike the float program which used `%f`.
|
||||
|
||||
##### Step 14: Document Your Findings
|
||||
|
||||
| Item | Float (`42.5`) | Double (`42.52525`) |
|
||||
| ------------------------- | -------------------- | ------------------------ |
|
||||
| r2 (low word) | `0x00000000` | `0x645A1CAC` |
|
||||
| r3 (high word) | `0x40454000` | `0x4045433B` |
|
||||
| Low word is zero? | Yes | **No** |
|
||||
| Words to patch | 1 (r3 only) | **2 (both r2 and r3)** |
|
||||
| Format specifier | `%f` | `%lf` |
|
||||
| Assembly load instruction | `movs` + `ldr` | `ldrd` (load double) |
|
||||
|
||||
#### Expected Output
|
||||
|
||||
After completing this exercise, you should understand:
|
||||
- How the compiler loads a 64-bit double using `ldrd` (load register double)
|
||||
- Why `42.52525` requires non-zero data in both registers while `42.5` does not
|
||||
- How to decode a complex IEEE 754 double with a repeating binary fraction
|
||||
- The differences between float and double handling at the assembly level
|
||||
|
||||
#### Questions for Reflection
|
||||
|
||||
###### Question 1: Why does `42.5` have a zero low word but `42.52525` does not?
|
||||
|
||||
###### Question 2: The assembly uses `ldrd r4, r5, [r5, #0x0]` instead of two separate `ldr` instructions. What is the advantage?
|
||||
|
||||
###### Question 3: Both the float and double programs have the same exponent (stored as 1028, real exponent 5). Why?
|
||||
|
||||
###### Question 4: If you were patching this double, how many data constants would you need to modify compared to the float exercise?
|
||||
|
||||
#### Tips and Hints
|
||||
- Use Python to verify: `import struct; struct.pack('>d', 42.52525).hex()` gives `4045433b645a1cac`
|
||||
- The `ldrd` instruction always loads the lower-addressed word into the first register
|
||||
- A repeating binary fraction (like `0.52525`) can never be represented exactly — double precision uses the closest 52-bit approximation
|
||||
- Compare data addresses: the float binary has one `DAT_` constant; the double binary has two consecutive ones
|
||||
|
||||
#### Next Steps
|
||||
- Proceed to Exercise 4 to patch both register words
|
||||
- Compare the mantissa of `42.5` (clean: `010101 000...`) vs. `42.52525` (complex: `0101010000110011...`)
|
||||
- Think about what values would have a zero low word (hint: powers of 2, halves, quarters)
|
||||
|
||||
#### Additional Challenge
|
||||
Using the 52-bit mantissa `0101010000110011101101100100010110100001110010101100`, manually sum the first 20 fractional bits to see how close you get to `0.52525`. How many bits of precision does it take to get within 0.001 of the true value?
|
||||
@@ -1,70 +0,0 @@
|
||||
# Embedded Systems Reverse Engineering
|
||||
[Repository](https://github.com/mytechnotalent/Embedded-Hacking)
|
||||
|
||||
## 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 Solution: Patch the Double Binary — Changing 42.52525 to 99.99
|
||||
|
||||
#### Answers
|
||||
|
||||
##### IEEE 754 Encoding of 99.99
|
||||
|
||||
```
|
||||
Integer: 99 = 1100011₂
|
||||
Fractional: .99 ≈ .111111010111...₂ (repeating)
|
||||
Combined: 1100011.111111010111...₂
|
||||
Normalized: 1.100011111111010111...₂ × 2⁶
|
||||
|
||||
Sign: 0 (positive)
|
||||
Exponent: 6 + 1023 = 1029 = 10000000101₂
|
||||
Mantissa: 1000111111110101 11000010100011110101 11000010100011110...₂ (52 bits)
|
||||
|
||||
Full double: 0x4058FF5C28F5C28F
|
||||
```
|
||||
|
||||
Python verification: `struct.pack('>d', 99.99).hex()` → `4058ff5c28f5c28f` ✓
|
||||
|
||||
##### Patch Summary
|
||||
|
||||
| Register | Old Value (42.52525) | New Value (99.99) | Changed? |
|
||||
|----------|---------------------|-------------------|----------|
|
||||
| r2 | 0x645A1CAC | 0x28F5C28F | **Yes** |
|
||||
| r3 | 0x4045433B | 0x4058FF5C | **Yes** |
|
||||
|
||||
##### Ghidra Patches
|
||||
|
||||
**Low word (DAT_10000254):**
|
||||
```
|
||||
Before (little-endian): AC 1C 5A 64 → 0x645A1CAC
|
||||
After (little-endian): 8F C2 F5 28 → 0x28F5C28F
|
||||
```
|
||||
|
||||
**High word (DAT_10000258):**
|
||||
```
|
||||
Before (little-endian): 3B 43 45 40 → 0x4045433B
|
||||
After (little-endian): 5C FF 58 40 → 0x4058FF5C
|
||||
```
|
||||
|
||||
##### Serial Output
|
||||
|
||||
```
|
||||
fav_num: 99.990000
|
||||
fav_num: 99.990000
|
||||
fav_num: 99.990000
|
||||
...
|
||||
```
|
||||
|
||||
#### Reflection Answers
|
||||
|
||||
1. **Why did both r2 and r3 change when patching 42.52525 → 99.99, but only r3 changed when patching 42.5 → 99.0?**
|
||||
Both 42.5 and 99.0 have "clean" fractional parts (0.5 and 0.0 respectively) that are exact in binary—they need very few mantissa bits, all fitting in the top 20 bits of r3. The low word (r2) remains `0x00000000` for both. In contrast, 42.52525 and 99.99 both have repeating binary fractions (0.52525 and 0.99 respectively) that require all 52 mantissa bits to approximate. Since the low 32 bits of the mantissa live in r2, changing from one repeating fraction to another necessarily changes both r2 and r3.
|
||||
|
||||
2. **The multiply-by-2 method for 0.99 produces a repeating pattern. What does this mean for the precision of the stored value?**
|
||||
It means 99.99 **cannot** be represented exactly as an IEEE 754 double. The binary fraction 0.111111010111... repeats indefinitely, but the mantissa only has 52 bits. The stored value is the closest 52-bit approximation, which is 99.98999999999999... (off by approximately 10⁻¹⁴). This is a fundamental limitation of binary floating-point: decimal fractions that aren't sums of negative powers of 2 always produce repeating binary expansions. The `printf` output rounds to `99.990000` because the default `%lf` precision (6 decimal places) hides the tiny error.
|
||||
|
||||
3. **If you wanted to patch the double to `100.0` instead of `99.99`, how many data constants would need to change?**
|
||||
**Both** would need to change—but for the opposite reason. Currently r2 = `0x645A1CAC` (non-zero). For 100.0: `struct.pack('>d', 100.0).hex()` = `4059000000000000`, so r3 = `0x40590000` and r2 = `0x00000000`. The r2 constant must be patched from `0x645A1CAC` to `0x00000000`, and r3 from `0x4045433B` to `0x40590000`. Even though the low word becomes zero, you still need to patch it because it was previously non-zero.
|
||||
|
||||
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?**
|
||||
The float binary uses separate instructions: `movs r4, #0x0` (loads zero into r4 for the low word) and `ldr r5, [DAT_1000024c]` (loads the high word from a literal pool). The double binary uses a single `ldrd r4, r5, [r5, #0x0]` instruction that loads both words from consecutive memory addresses in one operation. The `ldrd` approach is more efficient (fewer instructions, single memory transaction) and is preferred when both words carry meaningful data. The float's approach works fine because one word is a trivially loaded zero.
|
||||
@@ -1,217 +0,0 @@
|
||||
# Embedded Systems Reverse Engineering
|
||||
[Repository](https://github.com/mytechnotalent/Embedded-Hacking)
|
||||
|
||||
## 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
|
||||
|
||||
1. **Sign:** `0` (positive)
|
||||
2. **Exponent:** $6 + 1023 = 1029 = 10000000101_2$
|
||||
3. **Mantissa:** `1000111111110101110000101000111101011100001010001111...` (52 bits after the `1.`)
|
||||
4. **Full double:** `0x4058FF5C28F5C28F`
|
||||
|
||||
**Verify with Python:**
|
||||
```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
|
||||
|
||||
1. Click on **Window** → **Bytes** to open the Bytes Editor
|
||||
2. Click the **Pencil Icon** to enable editing
|
||||
3. Navigate to address `10000254`
|
||||
4. The bytes read: `AC 1C 5A 64` (little-endian for `0x645A1CAC`)
|
||||
5. Change to: `8F C2 F5 28` (little-endian for `0x28F5C28F`)
|
||||
6. Press **Enter**
|
||||
|
||||
##### Step 9: Patch the High Word
|
||||
|
||||
1. Navigate to address `10000258`
|
||||
2. The bytes read: `3B 43 45 40` (little-endian for `0x4045433B`)
|
||||
3. Change to: `5C FF 58 40` (little-endian for `0x4058FF5C`)
|
||||
4. Press **Enter**
|
||||
|
||||
**Verify** in the Listing view:
|
||||
- `DAT_10000254` should show `28F5C28Fh`
|
||||
- `DAT_10000258` should show `4058FF5Ch`
|
||||
|
||||
Together: `0x4058FF5C28F5C28F` = `99.99` as a double ✓
|
||||
|
||||
##### Step 10: Export the Patched Binary
|
||||
|
||||
1. Click **File** → **Export Program**
|
||||
2. Set **Format** to **Raw Bytes**
|
||||
3. Navigate to your build directory
|
||||
4. Name the file: `0x0011_double-floating-point-data-type-h.bin`
|
||||
5. Click **OK**
|
||||
|
||||
##### Step 11: Convert to UF2 Format
|
||||
|
||||
Open a terminal:
|
||||
|
||||
```powershell
|
||||
cd Embedded-Hacking-main\0x0011_double-floating-point-data-type
|
||||
```
|
||||
|
||||
```powershell
|
||||
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
|
||||
|
||||
1. Hold **BOOTSEL** and plug in your Pico 2
|
||||
2. Drag and drop `hacked.uf2` onto the RPI-RP2 drive
|
||||
3. 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.990000` printing instead of `fav_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: `0x28F5C28F` is stored as `8F C2 F5 28` in memory
|
||||
- Use Ghidra's **Bytes** window (Window → Bytes) for precise hex editing
|
||||
- If `r2` was zero before and needs to be non-zero after, you need to patch the data constant — not the `movs r4, #0x0` instruction
|
||||
- The `ldrd` instruction 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 change `r2` from 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.99` from 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!
|
||||
+203
-223
@@ -1,6 +1,6 @@
|
||||
# Week 5: Integers and Floats in Embedded Systems: Debugging and Hacking Integers and Floats w/ Intermediate GPIO Output Assembler Analysis
|
||||
?# Week 5: Integers and Floats in Embedded Systems: Debugging and Hacking Integers and Floats w/ Intermediate GPIO Output Assembler Analysis
|
||||
|
||||
## 🎯 What You'll Learn This Week
|
||||
## ? What You'll Learn This Week
|
||||
|
||||
By the end of this tutorial, you will be able to:
|
||||
- Understand how integers and floating-point numbers are stored in memory
|
||||
@@ -13,7 +13,7 @@ By the end of this tutorial, you will be able to:
|
||||
- Reconstruct 64-bit doubles from two 32-bit registers
|
||||
---
|
||||
|
||||
## 📚 Part 1: Understanding Integer Data Types
|
||||
## Part 1: Understanding Integer Data Types
|
||||
|
||||
### What is an Integer?
|
||||
|
||||
@@ -22,15 +22,15 @@ An **integer** is a whole number without any decimal point. Think of it like cou
|
||||
In C programming for embedded systems, we have special integer types that tell the compiler exactly how much memory to use:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Integer Types - Different Sizes for Different Needs │
|
||||
│ │
|
||||
│ uint8_t: 1 byte (0 to 255) - like a small box │
|
||||
│ int8_t: 1 byte (-128 to 127) - can hold negatives! │
|
||||
│ uint16_t: 2 bytes (0 to 65,535) - medium box │
|
||||
│ uint32_t: 4 bytes (0 to 4 billion) - big box │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
+-----------------------------------------------------------------+
|
||||
| Integer Types - Different Sizes for Different Needs |
|
||||
| |
|
||||
| uint8_t: 1 byte (0 to 255) - like a small box |
|
||||
| int8_t: 1 byte (-128 to 127) - can hold negatives! |
|
||||
| uint16_t: 2 bytes (0 to 65,535) - medium box |
|
||||
| uint32_t: 4 bytes (0 to 4 billion) - big box |
|
||||
| |
|
||||
+-----------------------------------------------------------------+
|
||||
```
|
||||
|
||||
### Signed vs Unsigned Integers
|
||||
@@ -125,7 +125,7 @@ uint8_t age = 43;
|
||||
int8_t range = -42;
|
||||
```
|
||||
|
||||
The variable `age` is a `uint8_t` — an **unsigned** 8-bit integer that can only hold values from `0` to `255`. Since age is always a positive number, unsigned is the right choice. The variable `range` is an `int8_t` — a **signed** 8-bit integer that can hold values from `-128` to `127`. The signed type allows it to represent negative numbers like `-42`. Under the hood, negative values are stored using **two's complement** encoding: the CPU flips all the bits of `42` (`0x2A`) and adds `1`, producing `0xD6`, which is how `-42` lives in a single byte of memory.
|
||||
The variable `age` is a `uint8_t` - an **unsigned** 8-bit integer that can only hold values from `0` to `255`. Since age is always a positive number, unsigned is the right choice. The variable `range` is an `int8_t` - a **signed** 8-bit integer that can hold values from `-128` to `127`. The signed type allows it to represent negative numbers like `-42`. Under the hood, negative values are stored using **two's complement** encoding: the CPU flips all the bits of `42` (`0x2A`) and adds `1`, producing `0xD6`, which is how `-42` lives in a single byte of memory.
|
||||
|
||||
#### GPIO Initialization with Inline Assembly
|
||||
|
||||
@@ -133,11 +133,11 @@ Instead of using the Pico SDK's `gpio_init()`, `gpio_set_dir()`, and `gpio_set_f
|
||||
|
||||
The initialization loop configures GPIO pins 16 through 19 (our red, green, blue, and yellow LEDs) in three steps per pin:
|
||||
|
||||
**Step 1 — Configure the pad.** Each GPIO pin has a pad control register in `PADS_BANK0` starting at base address `0x40038000`. The code calculates the offset as `pin * 4`, loads the current register value, clears the **OD** (output disable) and **ISO** (isolation) bits with `bic r5, r5, #0x180`, and sets the **IE** (input enable) bit with `orr r5, r5, #0x40`. This ensures the pad is electrically active and ready to drive output.
|
||||
**Step 1 - Configure the pad.** Each GPIO pin has a pad control register in `PADS_BANK0` starting at base address `0x40038000`. The code calculates the offset as `pin * 4`, loads the current register value, clears the **OD** (output disable) and **ISO** (isolation) bits with `bic r5, r5, #0x180`, and sets the **IE** (input enable) bit with `orr r5, r5, #0x40`. This ensures the pad is electrically active and ready to drive output.
|
||||
|
||||
**Step 2 — Set the pin function.** Each GPIO pin has a control register in `IO_BANK0` starting at `0x40028004`. The offset is `pin * 8` because each pin's control block is 8 bytes wide. The code clears the `FUNCSEL` field (bits `[4:0]`) and sets it to `5`, which selects the **SIO** (Single-cycle I/O) function. SIO is the mode that lets software directly control pin state through the GPIO coprocessor.
|
||||
**Step 2 - Set the pin function.** Each GPIO pin has a control register in `IO_BANK0` starting at `0x40028004`. The offset is `pin * 8` because each pin's control block is 8 bytes wide. The code clears the `FUNCSEL` field (bits `[4:0]`) and sets it to `5`, which selects the **SIO** (Single-cycle I/O) function. SIO is the mode that lets software directly control pin state through the GPIO coprocessor.
|
||||
|
||||
**Step 3 — Enable the output driver.** The instruction `mcrr p0, #4, r4, r5, c4` writes to the RP2350's GPIO coprocessor. Coprocessor register `c4` controls the **output enable** — with `r4` holding the pin number and `r5` set to `1`, this tells the hardware "this pin is an output." The `mcrr` (Move to Coprocessor from two ARM Registers) instruction is how the Cortex-M33 on the RP2350 talks to its dedicated GPIO coprocessor, bypassing the normal memory-mapped I/O path for single-cycle pin control.
|
||||
**Step 3 - Enable the output driver.** The instruction `mcrr p0, #4, r4, r5, c4` writes to the RP2350's GPIO coprocessor. Coprocessor register `c4` controls the **output enable** - with `r4` holding the pin number and `r5` set to `1`, this tells the hardware "this pin is an output." The `mcrr` (Move to Coprocessor from two ARM Registers) instruction is how the Cortex-M33 on the RP2350 talks to its dedicated GPIO coprocessor, bypassing the normal memory-mapped I/O path for single-cycle pin control.
|
||||
|
||||
#### The Blink Loop with Inline Assembly
|
||||
|
||||
@@ -147,7 +147,7 @@ Inside the `while (1)` loop, the program uses two inline assembly blocks to togg
|
||||
"mcrr p0, #4, r4, r5, c0\n" // gpioc_bit_out_put(pin, 1)
|
||||
```
|
||||
|
||||
This time the coprocessor register is `c0` instead of `c4`. Register `c0` controls the **output value** — setting `r5 = 1` drives the pin HIGH (LED on), and `r5 = 0` drives it LOW (LED off). Each toggle is followed by `sleep_ms(500)` for a half-second delay, creating a visible blink.
|
||||
This time the coprocessor register is `c0` instead of `c4`. Register `c0` controls the **output value** - setting `r5 = 1` drives the pin HIGH (LED on), and `r5 = 0` drives it LOW (LED off). Each toggle is followed by `sleep_ms(500)` for a half-second delay, creating a visible blink.
|
||||
|
||||
The GCC extended assembly syntax `"r"(pin)` tells the compiler to load the C variable `pin` into a general-purpose register and make it available as `%0` inside the assembly block. The clobber list `"r4","r5"` warns the compiler that those registers are modified, so it won't store anything important there.
|
||||
|
||||
@@ -160,42 +160,42 @@ pin++;
|
||||
if (pin > 18) pin = 16;
|
||||
```
|
||||
|
||||
This cycles through GPIO 16, 17, and 18 — red, green, and blue LEDs — creating a rotating blink pattern. Finally, `printf` prints both integer variables over UART so we can observe their values on the serial terminal:
|
||||
This cycles through GPIO 16, 17, and 18 - red, green, and blue LEDs - creating a rotating blink pattern. Finally, `printf` prints both integer variables over UART so we can observe their values on the serial terminal:
|
||||
|
||||
```
|
||||
age: 43
|
||||
range: -42
|
||||
```
|
||||
|
||||
> 💡 **Why use inline assembly instead of the SDK?** This program is designed to teach you what happens *beneath* the SDK. When you call `gpio_put(16, 1)` in normal Pico code, the SDK ultimately does the same coprocessor write — `mcrr p0, #4, r4, r5, c0`. By writing the assembly directly, you can see exactly how the RP2350 hardware is controlled, which is essential knowledge for reverse engineering and binary patching.
|
||||
> Tip: **Why use inline assembly instead of the SDK?** This program is designed to teach you what happens *beneath* the SDK. When you call `gpio_put(16, 1)` in normal Pico code, the SDK ultimately does the same coprocessor write - `mcrr p0, #4, r4, r5, c0`. By writing the assembly directly, you can see exactly how the RP2350 hardware is controlled, which is essential knowledge for reverse engineering and binary patching.
|
||||
|
||||
---
|
||||
|
||||
## 📚 Part 2: Understanding Floating-Point Data Types
|
||||
## Part 2: Understanding Floating-Point Data Types
|
||||
|
||||
### What is a Float?
|
||||
|
||||
A **float** is a number that can have a decimal point. Unlike integers which can only hold whole numbers like `42`, a float can hold values like `42.5`, `3.14`, or `-0.001`. In C, the `float` type uses **32 bits (4 bytes)** to store a number using the **IEEE 754** standard.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ IEEE 754 Single-Precision (32-bit float) │
|
||||
│ │
|
||||
│ ┌──────┬──────────┬───────────────────────────┐ │
|
||||
│ │ Sign │ Exponent │ Mantissa (Fraction) │ │
|
||||
│ │ 1bit │ 8 bits │ 23 bits │ │
|
||||
│ └──────┴──────────┴───────────────────────────┘ │
|
||||
│ │
|
||||
│ Value = (-1)^sign × 2^(exponent-127) × 1.mantissa │
|
||||
│ │
|
||||
│ Example: 42.5 │
|
||||
│ Sign: 0 (positive) │
|
||||
│ Exponent: 10000100 (132 - 127 = 5) │
|
||||
│ Mantissa: 01010100000000000000000 │
|
||||
│ Full: 0 10000100 01010100000000000000000 │
|
||||
│ Hex: 0x422A0000 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
+-----------------------------------------------------------------+
|
||||
| IEEE 754 Single-Precision (32-bit float) |
|
||||
| |
|
||||
| +------+----------+---------------------------+ |
|
||||
| | Sign | Exponent | Mantissa (Fraction) | |
|
||||
| | 1bit | 8 bits | 23 bits | |
|
||||
| +------+----------+---------------------------+ |
|
||||
| |
|
||||
| Value = (-1)^sign * 2^(exponent-127) * 1.mantissa |
|
||||
| |
|
||||
| Example: 42.5 |
|
||||
| Sign: 0 (positive) |
|
||||
| Exponent: 10000100 (132 - 127 = 5) |
|
||||
| Mantissa: 01010100000000000000000 |
|
||||
| Full: 0 10000100 01010100000000000000000 |
|
||||
| Hex: 0x422A0000 |
|
||||
| |
|
||||
+-----------------------------------------------------------------+
|
||||
```
|
||||
|
||||
### Float vs Integer - Key Differences
|
||||
@@ -204,7 +204,7 @@ A **float** is a number that can have a decimal point. Unlike integers which can
|
||||
| -------------- | ---------------------- | --------------------------- |
|
||||
| **Size** | 1 byte | 4 bytes |
|
||||
| **Precision** | Exact | ~7 decimal digits |
|
||||
| **Range** | 0 to 255 | ±3.4 × 10³⁸ |
|
||||
| **Range** | 0 to 255 | ?3.4 * 10++ |
|
||||
| **Encoding** | Direct binary | IEEE 754 (sign/exp/mantissa)|
|
||||
| **printf** | `%d` | `%f` |
|
||||
|
||||
@@ -233,7 +233,7 @@ int main(void) {
|
||||
2. Initializes the serial output
|
||||
3. Prints `fav_num` forever in a loop using the `%f` format specifier
|
||||
|
||||
> 💡 **Why `%f` instead of `%d`?** The `%d` format specifier tells `printf` to expect an integer. The `%f` specifier tells it to expect a floating-point number. Using the wrong one would print garbage!
|
||||
> Tip: **Why `%f` instead of `%d`?** The `%d` format specifier tells `printf` to expect an integer. The `%f` specifier tells it to expect a floating-point number. Using the wrong one would print garbage!
|
||||
|
||||
### Step 1: Flash the Binary to Your Pico 2
|
||||
|
||||
@@ -260,7 +260,7 @@ The program is printing `42.500000` because `printf` with `%f` defaults to 6 dec
|
||||
|
||||
---
|
||||
|
||||
## 🔬 Part 2.5: Setting Up Ghidra for Float Analysis
|
||||
## ? Part 2.5: Setting Up Ghidra for Float Analysis
|
||||
|
||||
### Step 3: Start Ghidra
|
||||
|
||||
@@ -274,7 +274,7 @@ Ghidra will open. Now we need to create a new project.
|
||||
|
||||
### Step 4: Create a New Project
|
||||
|
||||
1. Click **File** → **New Project**
|
||||
1. Click **File** -> **New Project**
|
||||
2. Select **Non-Shared Project**
|
||||
3. Click **Next**
|
||||
4. Enter Project Name: `0x000e_floating-point-data-type`
|
||||
@@ -293,12 +293,12 @@ Ghidra will open. Now we need to create a new project.
|
||||
|
||||
A dialog appears. The file is identified as a "BIN" (raw binary without debug symbols).
|
||||
|
||||
**Click the three dots (…) next to "Language" and:**
|
||||
**Click the three dots (...) next to "Language" and:**
|
||||
1. Search for "Cortex"
|
||||
2. Select **ARM Cortex 32 little endian default**
|
||||
3. Click **OK**
|
||||
|
||||
**Click the "Options…" button and:**
|
||||
**Click the "Options..." button and:**
|
||||
1. Change **Block Name** to `.text`
|
||||
2. Change **Base Address** to `10000000` (the XIP address!)
|
||||
3. Click **OK**
|
||||
@@ -313,7 +313,7 @@ Wait for analysis to complete (watch the progress bar in the bottom right).
|
||||
|
||||
---
|
||||
|
||||
## 🔬 Part 2.6: Navigating and Resolving Functions
|
||||
## ? Part 2.6: Navigating and Resolving Functions
|
||||
|
||||
### Step 8: Find the Functions
|
||||
|
||||
@@ -347,7 +347,7 @@ For `main`, let's also fix the return type:
|
||||
|
||||
---
|
||||
|
||||
## 🔬 Part 2.7: Analyzing the Main Function
|
||||
## ? Part 2.7: Analyzing the Main Function
|
||||
|
||||
### Step 11: Examine Main in Ghidra
|
||||
|
||||
@@ -376,14 +376,14 @@ int main(void)
|
||||
### Step 12: Resolve stdio_init_all
|
||||
|
||||
1. Click on `FUN_10002f5c`
|
||||
2. Right-click → **Edit Function Signature**
|
||||
2. Right-click -> **Edit Function Signature**
|
||||
3. Change to: `bool stdio_init_all(void)`
|
||||
4. Click **OK**
|
||||
|
||||
### Step 13: Resolve printf
|
||||
|
||||
1. Click on `FUN_100030ec`
|
||||
2. Right-click → **Edit Function Signature**
|
||||
2. Right-click -> **Edit Function Signature**
|
||||
3. Change to: `int __wrap_printf(char *format,...)`
|
||||
4. Check the **Varargs** checkbox (printf takes variable arguments!)
|
||||
5. Click **OK**
|
||||
@@ -412,7 +412,7 @@ int main(void)
|
||||
|
||||
**Where's `float fav_num = 42.5`?** It's been optimized into an immediate value!
|
||||
|
||||
The compiler replaced our float variable with constants passed directly to `printf`. But wait — we see **two** values: `0x0`, in `r2` and `DAT_1000024c` or `0x40454000`, in `r3`. That's because `printf` with `%f` always receives a **double** (64-bit), not a `float` (32-bit). The C standard requires that `float` arguments to variadic functions like `printf` are **promoted to `double`**.
|
||||
The compiler replaced our float variable with constants passed directly to `printf`. But wait - we see **two** values: `0x0`, in `r2` and `DAT_1000024c` or `0x40454000`, in `r3`. That's because `printf` with `%f` always receives a **double** (64-bit), not a `float` (32-bit). The C standard requires that `float` arguments to variadic functions like `printf` are **promoted to `double`**.
|
||||
|
||||
A 64-bit double is passed in two 32-bit registers:
|
||||
|
||||
@@ -421,7 +421,7 @@ A 64-bit double is passed in two 32-bit registers:
|
||||
| `r2` | `0x00000000` | Low 32 bits |
|
||||
| `r3` | `0x40454000` | High 32 bits |
|
||||
|
||||
Together they form `0x40454000_00000000` — the IEEE 754 **double-precision** encoding of `42.5`.
|
||||
Together they form `0x40454000_00000000` - the IEEE 754 **double-precision** encoding of `42.5`.
|
||||
|
||||
### Step 15: Verify the Double Encoding
|
||||
|
||||
@@ -436,11 +436,11 @@ Laid out as a single 64-bit value with every bit numbered:
|
||||
|
||||
```
|
||||
Bit: 63 62-52 (11 bits) 51-32 (20 bits) 31-0 (32 bits)
|
||||
┌───┬───────────────────────┬──────────────────────────────────────────┬──────────────────────────────────┐
|
||||
│ 0 │ 1 0 0 0 0 0 0 0 1 0 0 │ 0 1 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 │ 00000000000000000000000000000000 │
|
||||
└───┴───────────────────────┴──────────────────────────────────────────┴──────────────────────────────────┘
|
||||
+---+-----------------------+------------------------------------------+----------------------------------+
|
||||
| 0 | 1 0 0 0 0 0 0 0 1 0 0 | 0 1 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 | 00000000000000000000000000000000 |
|
||||
+---+-----------------------+------------------------------------------+----------------------------------+
|
||||
Sign Exponent (11) Mantissa high 20 bits Mantissa low 32 bits
|
||||
(from r3 bits 19–0) (from r2, all zero)
|
||||
(from r3 bits 19-0) (from r2, all zero)
|
||||
```
|
||||
|
||||
**Step-by-step field extraction:**
|
||||
@@ -455,11 +455,11 @@ In IEEE 754, the **sign bit** is the very first (leftmost) bit of the 64-bit dou
|
||||
sign bit
|
||||
```
|
||||
|
||||
But we don't have a single 64-bit register — we have **two** 32-bit registers. The high register `r3` holds bits 63–32 of the double. So bit 63 of the double is the same physical bit as **bit 31 of r3** (the topmost bit of r3):
|
||||
But we don't have a single 64-bit register - we have **two** 32-bit registers. The high register `r3` holds bits 63-32 of the double. So bit 63 of the double is the same physical bit as **bit 31 of r3** (the topmost bit of r3):
|
||||
|
||||
```
|
||||
r3 holds bits 63–32 of the double
|
||||
r2 holds bits 31–0 of the double
|
||||
r3 holds bits 63-32 of the double
|
||||
r2 holds bits 31-0 of the double
|
||||
```
|
||||
|
||||
Now let's check it. IEEE 754 uses a simple rule for the sign bit:
|
||||
@@ -472,14 +472,14 @@ Now let's check it. IEEE 754 uses a simple rule for the sign bit:
|
||||
```
|
||||
r3 = 0x40454000 = 0100 0000 0100 0101 0100 0000 0000 0000
|
||||
^
|
||||
r3 bit 31 = 0 → sign = 0 → Positive number
|
||||
r3 bit 31 = 0 -> sign = 0 -> Positive number
|
||||
```
|
||||
|
||||
The topmost bit of r3 is `0`, so the number is **positive**. If that bit were `1` instead (e.g. `0xC0454000`), the number would be negative (`-42.5`).
|
||||
|
||||
**2. Exponent — bits 62–52 of the 64-bit value = bits 30–20 of r3**
|
||||
**2. Exponent - bits 62-52 of the 64-bit value = bits 30-20 of r3**
|
||||
|
||||
Extract bits 30–20 from `0x40454000`:
|
||||
Extract bits 30-20 from `0x40454000`:
|
||||
|
||||
```
|
||||
0x40454000 in binary: 0 10000000100 01010100000000000000
|
||||
@@ -490,9 +490,9 @@ Exponent bits: `10000000100`
|
||||
|
||||
Convert to decimal: $2^{10} + 2^{2} = 1024 + 4 = 1028$
|
||||
|
||||
But `1028` is **not** the actual power of 2 yet. IEEE 754 stores exponents with a **bias** — a fixed number that gets added during encoding so that the stored value is always positive (no sign bit needed for the exponent). For doubles, the bias is **1023**.
|
||||
But `1028` is **not** the actual power of 2 yet. IEEE 754 stores exponents with a **bias** - a fixed number that gets added during encoding so that the stored value is always positive (no sign bit needed for the exponent). For doubles, the bias is **1023**.
|
||||
|
||||
> 💡 **Why 1023?** The exponent field is 11 bits wide, giving $2^{11} = 2048$ total values. Half of that range should represent negative exponents and half positive. The midpoint is $(2^{11} / 2) - 1 = 1023$. So a stored exponent of `1023` means a real exponent of **0**, values below `1023` are negative exponents, and values above `1023` are positive exponents.
|
||||
> Tip: **Why 1023?** The exponent field is 11 bits wide, giving $2^{11} = 2048$ total values. Half of that range should represent negative exponents and half positive. The midpoint is $(2^{11} / 2) - 1 = 1023$. So a stored exponent of `1023` means a real exponent of **0**, values below `1023` are negative exponents, and values above `1023` are positive exponents.
|
||||
|
||||
To recover the real exponent, we subtract the bias:
|
||||
|
||||
@@ -502,25 +502,25 @@ $$\text{real exponent} = 1028 - 1023 = \mathbf{5}$$
|
||||
|
||||
This means the number is scaled by $2^5 = 32$. In other words, the mantissa gets shifted left by 5 binary places.
|
||||
|
||||
**3. Mantissa — bits 51–0 of the 64-bit value**
|
||||
**3. Mantissa - bits 51-0 of the 64-bit value**
|
||||
|
||||
- **High 20 bits of mantissa** (bits 51–32) = bits 19–0 of r3:
|
||||
- **High 20 bits of mantissa** (bits 51-32) = bits 19-0 of r3:
|
||||
|
||||
```
|
||||
r3 bits 19–0: 0 1 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
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** (bits 31–0) = all of r2:
|
||||
- **Low 32 bits of mantissa** (bits 31-0) = all of r2:
|
||||
|
||||
```
|
||||
r2 = 0x00000000 → 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
|
||||
r2 = 0x00000000 -> 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
|
||||
```
|
||||
|
||||
Full 52-bit mantissa:
|
||||
|
||||
```
|
||||
0 1 0 1 0 1 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 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
← top 20 bits from r3 → ← bottom 32 bits from r2 (all zero) →
|
||||
?? top 20 bits from r3 -> ?? bottom 32 bits from r2 (all zero) ->
|
||||
```
|
||||
|
||||
IEEE 754 always prepends an **implied leading `1`**, so the actual value represented is:
|
||||
@@ -547,9 +547,9 @@ Now convert each bit position to decimal:
|
||||
| `0` (bit 2) | $2^2$ | 0 |
|
||||
| `1` (bit 1) | $2^1$ | 2 |
|
||||
| `0` (bit 0) | $2^0$ | 0 |
|
||||
| `1` (bit −1) | $2^{-1}$ | 0.5 |
|
||||
| `1` (bit ?1) | $2^{-1}$ | 0.5 |
|
||||
|
||||
$$32 + 8 + 2 + 0.5 = \mathbf{42.5} ✓$$
|
||||
$$32 + 8 + 2 + 0.5 = \mathbf{42.5} ?$$
|
||||
|
||||
### Step 16: Examine the Assembly
|
||||
|
||||
@@ -583,7 +583,7 @@ Look at the **Listing** window (assembly view). Find the main function:
|
||||
|
||||
```
|
||||
|
||||
> 🎯 **Key Insight:** The `mov.w r2, #0x0` loads the low 32 bits (all zeros) and `ldr r3, [DAT_...]` loads the high 32 bits (`0x40454000`) of the double. Together, `r2:r3` = `0x40454000_00000000` = `42.5` as a double.
|
||||
> ? **Key Insight:** The `mov.w r2, #0x0` loads the low 32 bits (all zeros) and `ldr r3, [DAT_...]` loads the high 32 bits (`0x40454000`) of the double. Together, `r2:r3` = `0x40454000_00000000` = `42.5` as a double.
|
||||
|
||||
### Step 17: Find the Format String
|
||||
|
||||
@@ -601,41 +601,41 @@ This confirms `printf` is called with the format string `"fav_num: %f\r\n"` and
|
||||
|
||||
---
|
||||
|
||||
## 🔬 Part 2.8: Patching the Float - Changing 42.5 to 99.0
|
||||
## ? Part 2.8: Patching the Float - Changing 42.5 to 99.0
|
||||
|
||||
### Step 18: Calculate the New IEEE 754 Encoding
|
||||
|
||||
We want to change `42.5` to `99.0`. First, we need to figure out the double-precision encoding of `99.0`:
|
||||
|
||||
**Step A — Convert the integer part (99) to binary:**
|
||||
**Step A - Convert the integer part (99) to binary:**
|
||||
|
||||
| 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** |
|
||||
| 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 B — Convert the fractional part (.0) to binary:**
|
||||
**Step B - Convert the fractional part (.0) to binary:**
|
||||
|
||||
There is no fractional part — `.0` is exactly zero, so the fractional binary is just `0`.
|
||||
There is no fractional part - `.0` is exactly zero, so the fractional binary is just `0`.
|
||||
|
||||
**Step C — Combine:**
|
||||
**Step C - Combine:**
|
||||
|
||||
$$99.0_{10} = 1100011.0_2$$
|
||||
|
||||
**Step D — Normalize to IEEE 754 form** (move the binary point so there's exactly one `1` before it):
|
||||
**Step D - Normalize to IEEE 754 form** (move the binary point so there's exactly one `1` before it):
|
||||
|
||||
$$1100011.0_2 = 1.100011_2 \times 2^6$$
|
||||
|
||||
We shifted the binary point 6 places left, so the exponent is **6**.
|
||||
|
||||
**Step E — Extract the IEEE 754 fields:**
|
||||
**Step E - Extract the IEEE 754 fields:**
|
||||
|
||||
1. **Sign:** `0` (positive)
|
||||
2. **Exponent:** $6 + 1023 = 1029 = 10000000101_2$
|
||||
@@ -657,7 +657,7 @@ Look in the Listing view for the data that loads the high word of the double:
|
||||
10000248 00 40 45 40 undefined4 40454000h
|
||||
```
|
||||
|
||||
This is the 32-bit constant that gets loaded into `r3` — the high word of our double `42.5`.
|
||||
This is the 32-bit constant that gets loaded into `r3` - the high word of our double `42.5`.
|
||||
|
||||
### Step 20: Patch the Constant
|
||||
|
||||
@@ -671,11 +671,11 @@ This changes the high word from `0x40454000` (42.5 as double) to `0x4058C000` (9
|
||||
|
||||
---
|
||||
|
||||
## 🔬 Part 2.9: Export and Test the Hacked Binary
|
||||
## ? Part 2.9: Export and Test the Hacked Binary
|
||||
|
||||
### Step 21: Export the Patched Binary
|
||||
|
||||
1. Click **File** → **Export Program**
|
||||
1. Click **File** -> **Export Program**
|
||||
2. Set **Format** to **Raw Bytes**
|
||||
3. Navigate to your build directory
|
||||
4. Name the file `0x000e_floating-point-data-type-h.bin`
|
||||
@@ -686,7 +686,7 @@ This changes the high word from `0x40454000` (42.5 as double) to `0x4058C000` (9
|
||||
**Open a terminal and navigate to your project directory:**
|
||||
|
||||
```powershell
|
||||
cd C:\Users\flare-vm\Desktop\Embedded-Hacking-main\0x000e_floating-point-data-type
|
||||
cd C:\Users\assem.KEVINTHOMAS\OneDrive\Documents\Embedded-Hacking\0x000e_floating-point-data-type
|
||||
```
|
||||
|
||||
**Run the conversion command:**
|
||||
@@ -710,34 +710,34 @@ fav_num: 99.000000
|
||||
...
|
||||
```
|
||||
|
||||
🎉 **BOOM! We hacked the float!** The value changed from `42.5` to `99.0`!
|
||||
? **BOOM! We hacked the float!** The value changed from `42.5` to `99.0`!
|
||||
|
||||
---
|
||||
|
||||
## 📚 Part 3: Understanding Double-Precision Floating-Point Data Types
|
||||
## Part 3: Understanding Double-Precision Floating-Point Data Types
|
||||
|
||||
### What is a Double?
|
||||
|
||||
A **double** (short for "double-precision floating-point") is like a `float` but with **twice the precision**. While a `float` uses 32 bits, a `double` uses **64 bits (8 bytes)**, giving it roughly **15–16 significant decimal digits** of precision compared to a float's ~7.
|
||||
A **double** (short for "double-precision floating-point") is like a `float` but with **twice the precision**. While a `float` uses 32 bits, a `double` uses **64 bits (8 bytes)**, giving it roughly **15-16 significant decimal digits** of precision compared to a float's ~7.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ IEEE 754 Double-Precision (64-bit double) │
|
||||
│ │
|
||||
│ ┌──────┬───────────┬──────────────────────────────────────┐ │
|
||||
│ │ Sign │ Exponent │ Mantissa (Fraction) │ │
|
||||
│ │ 1bit │ 11 bits │ 52 bits │ │
|
||||
│ └──────┴───────────┴──────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Value = (-1)^sign × 2^(exponent-1023) × 1.mantissa │
|
||||
│ │
|
||||
│ Example: 42.52525 │
|
||||
│ Sign: 0 (positive) │
|
||||
│ Exponent: 10000000100 (1028 - 1023 = 5) │
|
||||
│ Mantissa: 0101010000110011101101100100010110100001110010101100 │
|
||||
│ Hex: 0x4045433B645A1CAC │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
+-----------------------------------------------------------------+
|
||||
| IEEE 754 Double-Precision (64-bit double) |
|
||||
| |
|
||||
| +------+-----------+--------------------------------------+ |
|
||||
| | Sign | Exponent | Mantissa (Fraction) | |
|
||||
| | 1bit | 11 bits | 52 bits | |
|
||||
| +------+-----------+--------------------------------------+ |
|
||||
| |
|
||||
| Value = (-1)^sign * 2^(exponent-1023) * 1.mantissa |
|
||||
| |
|
||||
| Example: 42.52525 |
|
||||
| Sign: 0 (positive) |
|
||||
| Exponent: 10000000100 (1028 - 1023 = 5) |
|
||||
| Mantissa: 0101010000110011101101100100010110100001110010101100 |
|
||||
| Hex: 0x4045433B645A1CAC |
|
||||
| |
|
||||
+-----------------------------------------------------------------+
|
||||
```
|
||||
|
||||
### Float vs Double - Key Differences
|
||||
@@ -748,11 +748,11 @@ A **double** (short for "double-precision floating-point") is like a `float` but
|
||||
| **Precision** | ~7 decimal digits | ~15 decimal digits |
|
||||
| **Exponent** | 8 bits (bias 127) | 11 bits (bias 1023) |
|
||||
| **Mantissa** | 23 bits | 52 bits |
|
||||
| **Range** | ±3.4 × 10³⁸ | ±1.8 × 10³⁰⁸ |
|
||||
| **Range** | ?3.4 * 10++ | ?1.8 * 10+++? |
|
||||
| **printf** | `%f` | `%lf` |
|
||||
| **ARM passing** | Promoted to double | Native in `r2:r3` |
|
||||
|
||||
> 💡 **Why does precision matter?** With a `float`, the value `42.52525` might be stored as `42.525249` due to rounding. A `double` can represent it as `42.525250` with much higher fidelity. For scientific or financial applications, that extra precision is critical!
|
||||
> Tip: **Why does precision matter?** With a `float`, the value `42.52525` might be stored as `42.525249` due to rounding. A `double` can represent it as `42.525250` with much higher fidelity. For scientific or financial applications, that extra precision is critical!
|
||||
|
||||
### Our Double-Precision Program
|
||||
|
||||
@@ -779,7 +779,7 @@ int main(void) {
|
||||
2. Initializes the serial output
|
||||
3. Prints `fav_num` forever in a loop using the `%lf` format specifier
|
||||
|
||||
> 💡 **`%lf` vs `%f`:** While `printf` actually treats `%f` and `%lf` identically (both expect a `double`), using `%lf` makes your intent clear — you're explicitly working with a `double`, not a `float`. It's good practice to match the format specifier to your variable type.
|
||||
> Tip: **`%lf` vs `%f`:** While `printf` actually treats `%f` and `%lf` identically (both expect a `double`), using `%lf` makes your intent clear - you're explicitly working with a `double`, not a `float`. It's good practice to match the format specifier to your variable type.
|
||||
|
||||
### Step 1: Flash the Binary to Your Pico 2
|
||||
|
||||
@@ -806,7 +806,7 @@ The program is printing `42.525250` because `printf` with `%lf` defaults to 6 de
|
||||
|
||||
---
|
||||
|
||||
## 🔬 Part 3.5: Setting Up Ghidra for Double Analysis
|
||||
## ? Part 3.5: Setting Up Ghidra for Double Analysis
|
||||
|
||||
### Step 3: Start Ghidra
|
||||
|
||||
@@ -820,7 +820,7 @@ Ghidra will open. Now we need to create a new project.
|
||||
|
||||
### Step 4: Create a New Project
|
||||
|
||||
1. Click **File** → **New Project**
|
||||
1. Click **File** -> **New Project**
|
||||
2. Select **Non-Shared Project**
|
||||
3. Click **Next**
|
||||
4. Enter Project Name: `0x000A_intro-to-doubles`
|
||||
@@ -839,12 +839,12 @@ Ghidra will open. Now we need to create a new project.
|
||||
|
||||
A dialog appears. The file is identified as a "BIN" (raw binary without debug symbols).
|
||||
|
||||
**Click the three dots (…) next to "Language" and:**
|
||||
**Click the three dots (...) next to "Language" and:**
|
||||
1. Search for "Cortex"
|
||||
2. Select **ARM Cortex 32 little endian default**
|
||||
3. Click **OK**
|
||||
|
||||
**Click the "Options…" button and:**
|
||||
**Click the "Options..." button and:**
|
||||
1. Change **Block Name** to `.text`
|
||||
2. Change **Base Address** to `10000000` (the XIP address!)
|
||||
3. Click **OK**
|
||||
@@ -859,7 +859,7 @@ Wait for analysis to complete (watch the progress bar in the bottom right).
|
||||
|
||||
---
|
||||
|
||||
## 🔬 Part 3.6: Navigating and Resolving Functions
|
||||
## ? Part 3.6: Navigating and Resolving Functions
|
||||
|
||||
### Step 8: Find the Functions
|
||||
|
||||
@@ -893,7 +893,7 @@ For `main`, let's also fix the return type:
|
||||
|
||||
---
|
||||
|
||||
## 🔬 Part 3.7: Analyzing the Main Function
|
||||
## ? Part 3.7: Analyzing the Main Function
|
||||
|
||||
### Step 11: Examine Main in Ghidra
|
||||
|
||||
@@ -925,14 +925,14 @@ void FUN_10000238(void)
|
||||
### Step 12: Resolve stdio_init_all
|
||||
|
||||
1. Click on `FUN_10002f64`
|
||||
2. Right-click → **Edit Function Signature**
|
||||
2. Right-click -> **Edit Function Signature**
|
||||
3. Change to: `bool stdio_init_all(void)`
|
||||
4. Click **OK**
|
||||
|
||||
### Step 13: Resolve printf
|
||||
|
||||
1. Click on `FUN_100030f4`
|
||||
2. Right-click → **Edit Function Signature**
|
||||
2. Right-click -> **Edit Function Signature**
|
||||
3. Change the name to `printf`
|
||||
4. Check the **Varargs** checkbox (printf takes variable arguments!)
|
||||
5. Click **OK**
|
||||
@@ -963,7 +963,7 @@ int main(void)
|
||||
|
||||
**Where's `double fav_num = 42.52525`?** It's been optimized into immediate values!
|
||||
|
||||
This time we see **two** non-zero values: `0x645a1cac` and `0x4045433b`. Unlike the float example where the low word was `0x0`, a double with a fractional part like `42.52525` needs **all 52 mantissa bits** — so both halves carry data.
|
||||
This time we see **two** non-zero values: `0x645a1cac` and `0x4045433b`. Unlike the float example where the low word was `0x0`, a double with a fractional part like `42.52525` needs **all 52 mantissa bits** - so both halves carry data.
|
||||
|
||||
A 64-bit double is passed in two 32-bit registers:
|
||||
|
||||
@@ -972,9 +972,9 @@ A 64-bit double is passed in two 32-bit registers:
|
||||
| `r2` | `0x645A1CAC` | Low 32 bits |
|
||||
| `r3` | `0x4045433B` | High 32 bits |
|
||||
|
||||
Together they form `0x4045433B645A1CAC` — the IEEE 754 **double-precision** encoding of `42.52525`.
|
||||
Together they form `0x4045433B645A1CAC` - the IEEE 754 **double-precision** encoding of `42.52525`.
|
||||
|
||||
> 🎯 **Key Difference from Float:** In the float example, `r2` was `0x00000000` because `42.5` has a clean fractional part. But `42.52525` has a repeating binary fraction, so the low 32 bits are non-zero (`0x645A1CAC`). This means **both** registers matter when patching doubles with complex fractional values!
|
||||
> ? **Key Difference from Float:** In the float example, `r2` was `0x00000000` because `42.5` has a clean fractional part. But `42.52525` has a repeating binary fraction, so the low 32 bits are non-zero (`0x645A1CAC`). This means **both** registers matter when patching doubles with complex fractional values!
|
||||
|
||||
### Step 15: Verify the Double Encoding
|
||||
|
||||
@@ -989,30 +989,30 @@ Laid out as a single 64-bit value with every bit numbered:
|
||||
|
||||
```
|
||||
Bit: 63 62-52 (11 bits) 51-32 (20 bits) 31-0 (32 bits)
|
||||
┌───┬───────────────────────┬──────────────────────────────────────────┬──────────────────────────────────────────┐
|
||||
│ 0 │ 1 0 0 0 0 0 0 0 1 0 0 │ 0 1 0 1 0 1 0 0 0 0 1 1 0 0 1 1 1 0 1 1 │ 01100100010110100001110010101100 │
|
||||
└───┴───────────────────────┴──────────────────────────────────────────┴──────────────────────────────────────────┘
|
||||
+---+-----------------------+------------------------------------------+------------------------------------------+
|
||||
| 0 | 1 0 0 0 0 0 0 0 1 0 0 | 0 1 0 1 0 1 0 0 0 0 1 1 0 0 1 1 1 0 1 1 | 01100100010110100001110010101100 |
|
||||
+---+-----------------------+------------------------------------------+------------------------------------------+
|
||||
Sign Exponent (11) Mantissa high 20 bits Mantissa low 32 bits
|
||||
(from r3 bits 19–0) (from r2)
|
||||
(from r3 bits 19-0) (from r2)
|
||||
```
|
||||
|
||||
> 🎯 **Key Difference from 42.5:** In the `42.5` example, r2 was `0x00000000` because `42.5` has a clean fractional part (`.5` = exactly one binary digit). But `42.52525` has a repeating binary fraction, so the low 32 bits are **non-zero** (`0x645A1CAC`). Every bit of both registers matters here!
|
||||
> ? **Key Difference from 42.5:** In the `42.5` example, r2 was `0x00000000` because `42.5` has a clean fractional part (`.5` = exactly one binary digit). But `42.52525` has a repeating binary fraction, so the low 32 bits are **non-zero** (`0x645A1CAC`). Every bit of both registers matters here!
|
||||
|
||||
**Step-by-step field extraction:**
|
||||
|
||||
**1. Sign bit**
|
||||
|
||||
The sign bit is bit 63 of the 64-bit double, which is bit 31 of r3 (the high register holds bits 63–32):
|
||||
The sign bit is bit 63 of the 64-bit double, which is bit 31 of r3 (the high register holds bits 63-32):
|
||||
|
||||
```
|
||||
r3 = 0x4045433B = 0100 0000 0100 0101 0100 0011 0011 1011
|
||||
^
|
||||
r3 bit 31 = 0 → sign = 0 → Positive number ✓
|
||||
r3 bit 31 = 0 -> sign = 0 -> Positive number ?
|
||||
```
|
||||
|
||||
**2. Exponent — bits 62–52 = bits 30–20 of r3**
|
||||
**2. Exponent - bits 62-52 = bits 30-20 of r3**
|
||||
|
||||
Extract bits 30–20 from `0x4045433B`:
|
||||
Extract bits 30-20 from `0x4045433B`:
|
||||
|
||||
```
|
||||
0x4045433B in binary: 0 10000000100 01010100001100111011
|
||||
@@ -1023,33 +1023,33 @@ Exponent bits: `10000000100`
|
||||
|
||||
Convert to decimal: $2^{10} + 2^{2} = 1024 + 4 = 1028$
|
||||
|
||||
Subtract the bias (same formula as Part 2 — the bias is 1023 for all doubles):
|
||||
Subtract the bias (same formula as Part 2 - the bias is 1023 for all doubles):
|
||||
|
||||
$$\text{real exponent} = 1028 - 1023 = \mathbf{5}$$
|
||||
|
||||
This means the mantissa gets shifted left by 5 binary places (i.e. multiplied by $2^5 = 32$).
|
||||
|
||||
**3. Mantissa — bits 51–0**
|
||||
**3. Mantissa - bits 51-0**
|
||||
|
||||
Unlike the `42.5` example where r2 was all zeros, **both registers contribute non-zero bits** here:
|
||||
|
||||
- **High 20 bits of mantissa** (bits 51–32) = bits 19–0 of r3:
|
||||
- **High 20 bits of mantissa** (bits 51-32) = bits 19-0 of r3:
|
||||
|
||||
```
|
||||
r3 bits 19–0: 0 1 0 1 0 1 0 0 0 0 1 1 0 0 1 1 1 0 1 1
|
||||
r3 bits 19-0: 0 1 0 1 0 1 0 0 0 0 1 1 0 0 1 1 1 0 1 1
|
||||
```
|
||||
|
||||
- **Low 32 bits of mantissa** (bits 31–0) = all of r2:
|
||||
- **Low 32 bits of mantissa** (bits 31-0) = all of r2:
|
||||
|
||||
```
|
||||
r2 = 0x645A1CAC → 0 1 1 0 0 1 0 0 0 1 0 1 1 0 1 0 0 0 0 1 1 1 0 0 1 0 1 0 1 1 0 0
|
||||
r2 = 0x645A1CAC -> 0 1 1 0 0 1 0 0 0 1 0 1 1 0 1 0 0 0 0 1 1 1 0 0 1 0 1 0 1 1 0 0
|
||||
```
|
||||
|
||||
Full 52-bit mantissa:
|
||||
|
||||
```
|
||||
0 1 0 1 0 1 0 0 0 0 1 1 0 0 1 1 1 0 1 1 | 0 1 1 0 0 1 0 0 0 1 0 1 1 0 1 0 0 0 0 1 1 1 0 0 1 0 1 0 1 1 0 0
|
||||
← top 20 bits from r3 → ← bottom 32 bits from r2 →
|
||||
?? top 20 bits from r3 -> ?? bottom 32 bits from r2 ->
|
||||
```
|
||||
|
||||
IEEE 754 always prepends an **implied leading `1`**, so the actual value represented is:
|
||||
@@ -1083,25 +1083,25 @@ $$32 + 8 + 2 = \mathbf{42}$$
|
||||
|
||||
| Bit position | Power of 2 | Decimal value |
|
||||
|---|---|---|
|
||||
| `1` (bit −1) | $2^{-1}$ | 0.5 |
|
||||
| `0` (bit −2) | $2^{-2}$ | 0 |
|
||||
| `0` (bit −3) | $2^{-3}$ | 0 |
|
||||
| `0` (bit −4) | $2^{-4}$ | 0 |
|
||||
| `0` (bit −5) | $2^{-5}$ | 0 |
|
||||
| `1` (bit −6) | $2^{-6}$ | 0.015625 |
|
||||
| `1` (bit −7) | $2^{-7}$ | 0.0078125 |
|
||||
| `0` (bit −8) | $2^{-8}$ | 0 |
|
||||
| `0` (bit −9) | $2^{-9}$ | 0 |
|
||||
| `1` (bit −10) | $2^{-10}$ | 0.0009765625 |
|
||||
| `1` (bit −11) | $2^{-11}$ | 0.00048828125 |
|
||||
| `1` (bit −12) | $2^{-12}$ | 0.000244140625 |
|
||||
| `1` (bit ?1) | $2^{-1}$ | 0.5 |
|
||||
| `0` (bit ?2) | $2^{-2}$ | 0 |
|
||||
| `0` (bit ?3) | $2^{-3}$ | 0 |
|
||||
| `0` (bit ?4) | $2^{-4}$ | 0 |
|
||||
| `0` (bit ?5) | $2^{-5}$ | 0 |
|
||||
| `1` (bit ?6) | $2^{-6}$ | 0.015625 |
|
||||
| `1` (bit ?7) | $2^{-7}$ | 0.0078125 |
|
||||
| `0` (bit ?8) | $2^{-8}$ | 0 |
|
||||
| `0` (bit ?9) | $2^{-9}$ | 0 |
|
||||
| `1` (bit ?10) | $2^{-10}$ | 0.0009765625 |
|
||||
| `1` (bit ?11) | $2^{-11}$ | 0.00048828125 |
|
||||
| `1` (bit ?12) | $2^{-12}$ | 0.000244140625 |
|
||||
| ... | ... | *(remaining 35 bits add smaller and smaller fractions)* |
|
||||
|
||||
First 12 fractional bits sum: $0.5 + 0.015625 + 0.0078125 + 0.0009765625 + 0.00048828125 + 0.000244140625 \approx 0.5251$
|
||||
|
||||
The remaining 35 fractional bits refine this to $\approx 0.52525$. This is because `0.52525` is a **repeating fraction** in binary — it can never be represented with a finite number of bits, so double precision stores the closest possible 52-bit approximation.
|
||||
The remaining 35 fractional bits refine this to $\approx 0.52525$. This is because `0.52525` is a **repeating fraction** in binary - it can never be represented with a finite number of bits, so double precision stores the closest possible 52-bit approximation.
|
||||
|
||||
$$42 + 0.52525 = \mathbf{42.52525} ✓$$
|
||||
$$42 + 0.52525 = \mathbf{42.52525} ?$$
|
||||
|
||||
### Step 16: Examine the Assembly
|
||||
|
||||
@@ -1135,7 +1135,7 @@ Look at the **Listing** window (assembly view). Find the main function:
|
||||
10000258 3b 43 45 40 undefine 4045433Bh
|
||||
```
|
||||
|
||||
> 🎯 **Key Insight:** Notice that **both** `r2` and `r3` are loaded from data constants using `ldr`. Compare this to the float example where `r2` was loaded with `mov.w r2, #0x0`. Because `42.52525` requires all 52 mantissa bits, neither word can be zero — the compiler must store both halves as separate data constants.
|
||||
> ? **Key Insight:** Notice that **both** `r2` and `r3` are loaded from data constants using `ldr`. Compare this to the float example where `r2` was loaded with `mov.w r2, #0x0`. Because `42.52525` requires all 52 mantissa bits, neither word can be zero - the compiler must store both halves as separate data constants.
|
||||
|
||||
### Step 17: Find the Format String
|
||||
|
||||
@@ -1153,7 +1153,7 @@ This confirms `printf` is called with the format string `"fav_num: %lf\r\n"` and
|
||||
|
||||
---
|
||||
|
||||
## 🔬 Part 3.8: Patching the Double - Changing 42.52525 to 99.99
|
||||
## ? Part 3.8: Patching the Double - Changing 42.52525 to 99.99
|
||||
|
||||
### Step 18: Calculate the New IEEE 754 Encoding
|
||||
|
||||
@@ -1202,15 +1202,15 @@ Look in the Listing view for the two data constants:
|
||||
|
||||
This changes the full 64-bit double from `0x4045433B645A1CAC` (42.52525) to `0x4058FF5C28F5C28F` (99.99).
|
||||
|
||||
> 🎯 **Key Difference from Float Patching:** When we patched the float `42.5`, we only needed to change one word (the high word in `r3`) because the low word was all zeros. With `42.52525 → 99.99`, **both** words change. Always check whether the low word is non-zero before patching!
|
||||
> ? **Key Difference from Float Patching:** When we patched the float `42.5`, we only needed to change one word (the high word in `r3`) because the low word was all zeros. With `42.52525 -> 99.99`, **both** words change. Always check whether the low word is non-zero before patching!
|
||||
|
||||
---
|
||||
|
||||
## 🔬 Part 3.9: Export and Test the Hacked Binary
|
||||
## ? Part 3.9: Export and Test the Hacked Binary
|
||||
|
||||
### Step 21: Export the Patched Binary
|
||||
|
||||
1. Click **File** → **Export Program**
|
||||
1. Click **File** -> **Export Program**
|
||||
2. Set **Format** to **Raw Bytes**
|
||||
3. Navigate to your build directory
|
||||
4. Name the file `0x0011_double-floating-point-data-type-h.bin`
|
||||
@@ -1221,7 +1221,7 @@ This changes the full 64-bit double from `0x4045433B645A1CAC` (42.52525) to `0x4
|
||||
**Open a terminal and navigate to your project directory:**
|
||||
|
||||
```powershell
|
||||
cd C:\Users\flare-vm\Desktop\Embedded-Hacking-main\0x0011_double-floating-point-data-type
|
||||
cd C:\Users\assem.KEVINTHOMAS\OneDrive\Documents\Embedded-Hacking\0x0011_double-floating-point-data-type
|
||||
```
|
||||
|
||||
**Run the conversion command:**
|
||||
@@ -1245,11 +1245,11 @@ fav_num: 99.990000
|
||||
...
|
||||
```
|
||||
|
||||
🎉 **BOOM! We hacked the double!** The value changed from `42.52525` to `99.99`!
|
||||
? **BOOM! We hacked the double!** The value changed from `42.52525` to `99.99`!
|
||||
|
||||
---
|
||||
|
||||
## 📊 Part 3.95: Summary - Float and Double Analysis
|
||||
## ? Part 3.95: Summary - Float and Double Analysis
|
||||
|
||||
### What We Accomplished
|
||||
|
||||
@@ -1276,62 +1276,40 @@ fav_num: 99.990000
|
||||
### The Float/Double Patching Workflow
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 1. Identify the float/double value in the decompiled view │
|
||||
│ - Look for hex constants like 0x40454000 or 0x4045433B │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ 2. Determine if it's float (32-bit) or double (64-bit) │
|
||||
│ - printf promotes floats to doubles! │
|
||||
│ - Check if value spans r2:r3 (double) or just r0 (float) │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ 3. Check if the low word (r2) is zero or non-zero │
|
||||
│ - Zero low word = only patch the high word │
|
||||
│ - Non-zero low word = patch BOTH words │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ 4. Calculate the new IEEE 754 encoding │
|
||||
│ - Convert your desired value to IEEE 754 │
|
||||
│ - Split into high/low words │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ 5. Patch the constant(s) in Ghidra │
|
||||
│ - Right-click → Patch Data │
|
||||
│ - Replace the old encoding with the new one │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ 6. Export → Convert to UF2 → Flash → Verify │
|
||||
│ - Same workflow as integer patching │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
+-----------------------------------------------------------------+
|
||||
| 1. Identify the float/double value in the decompiled view |
|
||||
| - Look for hex constants like 0x40454000 or 0x4045433B |
|
||||
+-----------------------------------------------------------------+
|
||||
| 2. Determine if it's float (32-bit) or double (64-bit) |
|
||||
| - printf promotes floats to doubles! |
|
||||
| - Check if value spans r2:r3 (double) or just r0 (float) |
|
||||
+-----------------------------------------------------------------+
|
||||
| 3. Check if the low word (r2) is zero or non-zero |
|
||||
| - Zero low word = only patch the high word |
|
||||
| - Non-zero low word = patch BOTH words |
|
||||
+-----------------------------------------------------------------+
|
||||
| 4. Calculate the new IEEE 754 encoding |
|
||||
| - Convert your desired value to IEEE 754 |
|
||||
| - Split into high/low words |
|
||||
+-----------------------------------------------------------------+
|
||||
| 5. Patch the constant(s) in Ghidra |
|
||||
| - Right-click -> Patch Data |
|
||||
| - Replace the old encoding with the new one |
|
||||
+-----------------------------------------------------------------+
|
||||
| 6. Export -> Convert to UF2 -> Flash -> Verify |
|
||||
| - Same workflow as integer patching |
|
||||
+-----------------------------------------------------------------+
|
||||
```
|
||||
|
||||
> 💡 **Key takeaway:** Hacking doubles is the same process as hacking floats — find the IEEE 754 constant, calculate the new encoding, patch it. The only extra step is checking whether the **low word** (`r2`) is also non-zero. Clean values like `42.5` only need one patch; messy fractions like `42.52525` need two!
|
||||
> Tip: **Key takeaway:** Hacking doubles is the same process as hacking floats - find the IEEE 754 constant, calculate the new encoding, patch it. The only extra step is checking whether the **low word** (`r2`) is also non-zero. Clean values like `42.5` only need one patch; messy fractions like `42.52525` need two!
|
||||
|
||||
---
|
||||
|
||||
## ✅ Practice Exercises
|
||||
|
||||
### Exercise 1: Patch the Float to Pi
|
||||
The float program stores `42.5`. Patch it in Ghidra so the serial output prints `3.14` instead.
|
||||
|
||||
**Hint:** `3.14` as a double is `0x40091EB851EB851F` — the high word is `0x40091EB8` and the low word is `0x51EB851F`. You'll need to patch **both** words since the low word is non-zero!
|
||||
|
||||
### Exercise 2: Patch the Double to a Whole Number
|
||||
The double program stores `42.52525`. Instead of patching it to `99.99` (as we did in the chapter), patch it to `100.0`.
|
||||
|
||||
**Hint:** `100.0` as a double is `0x4059000000000000` — high word `0x40590000`, low word `0x00000000`. Notice the low word is all zeros this time — but the original low word (`0x645A1CAC`) is non-zero, so you still need to patch it to `0x00000000`!
|
||||
|
||||
### Exercise 3: Change the Blink Speed
|
||||
The LED blinks every 500ms. Find the `sleep_ms(500)` calls in the binary and change them to `sleep_ms(100)` for faster blinking.
|
||||
|
||||
**Hint:** Look for the value `0x1F4` (500 in hex) being loaded into a register before the delay call.
|
||||
|
||||
### Exercise 4: Find the Format Strings
|
||||
Both programs use format strings (`"fav_num: %f\r\n"` and `"fav_num: %lf\r\n"`). Can you find these strings in Ghidra and determine where they're stored?
|
||||
|
||||
**Hint:** Look in the `.rodata` section. Try pressing `S` in Ghidra to search for strings, or navigate to addresses near `0x10003xxx`.
|
||||
|
||||
---
|
||||
|
||||
## 🎓 Key Takeaways
|
||||
## ? Key Takeaways
|
||||
|
||||
1. **Integers have fixed sizes** - `uint8_t` is 1 byte (0–255), `int8_t` is 1 byte (-128 to 127). The `u` prefix means unsigned.
|
||||
1. **Integers have fixed sizes** - `uint8_t` is 1 byte (0-255), `int8_t` is 1 byte (-128 to 127). The `u` prefix means unsigned.
|
||||
|
||||
2. **IEEE 754 encodes floats in binary** - Sign bit, exponent (with bias), and mantissa form the encoding for both 32-bit floats and 64-bit doubles.
|
||||
|
||||
@@ -1347,7 +1325,7 @@ Both programs use format strings (`"fav_num: %f\r\n"` and `"fav_num: %lf\r\n"`).
|
||||
|
||||
---
|
||||
|
||||
## 📖 Glossary
|
||||
## ? Glossary
|
||||
|
||||
| Term | Definition |
|
||||
| ----------------------- | ------------------------------------------------------------------------------ |
|
||||
@@ -1364,14 +1342,14 @@ Both programs use format strings (`"fav_num: %f\r\n"` and `"fav_num: %lf\r\n"`).
|
||||
| **Mantissa** | Fractional part of IEEE 754 encoding (23 bits for float, 52 bits for double) |
|
||||
| **mcrr** | ARM coprocessor register transfer instruction used for GPIO control |
|
||||
| **PADS_BANK0** | Register block at `0x40038000` that controls GPIO pad electrical properties |
|
||||
| **Promotion** | Automatic conversion of a smaller type to a larger type (float → double) |
|
||||
| **Promotion** | Automatic conversion of a smaller type to a larger type (float -> double) |
|
||||
| **Register Pair** | Two 32-bit registers (r2:r3) used together to hold a 64-bit value |
|
||||
| **UF2** | USB Flashing Format - file format for Pico 2 firmware |
|
||||
| **uint8_t** | Unsigned 8-bit integer type (0 to 255) |
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Additional Resources
|
||||
## ? Additional Resources
|
||||
|
||||
### GPIO Coprocessor Reference
|
||||
|
||||
@@ -1395,19 +1373,21 @@ The RP2350 GPIO coprocessor instructions:
|
||||
### IEEE 754 Encoding Formula
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Float (32-bit): [1 sign] [8 exponent] [23 mantissa] │
|
||||
│ Double (64-bit): [1 sign] [11 exponent] [52 mantissa] │
|
||||
│ │
|
||||
│ Value = (-1)^sign × 2^(exponent - bias) × (1 + mantissa) │
|
||||
│ │
|
||||
│ Float bias: 127 │
|
||||
│ Double bias: 1023 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
+-----------------------------------------------------------------+
|
||||
| Float (32-bit): [1 sign] [8 exponent] [23 mantissa] |
|
||||
| Double (64-bit): [1 sign] [11 exponent] [52 mantissa] |
|
||||
| |
|
||||
| Value = (-1)^sign * 2^(exponent - bias) * (1 + mantissa) |
|
||||
| |
|
||||
| Float bias: 127 |
|
||||
| Double bias: 1023 |
|
||||
+-----------------------------------------------------------------+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Remember:** Every binary you encounter in the real world can be analyzed and understood using these same techniques. Whether it's an integer, a float, or a double — it's all just bits waiting to be decoded. Practice makes perfect!
|
||||
**Remember:** Every binary you encounter in the real world can be analyzed and understood using these same techniques. Whether it's an integer, a float, or a double - it's all just bits waiting to be decoded. Practice makes perfect!
|
||||
|
||||
Happy hacking! ?
|
||||
|
||||
|
||||
Happy hacking! 🔧
|
||||
|
||||
Reference in New Issue
Block a user