Refactor E and S

This commit is contained in:
Kevin Thomas
2026-03-19 15:01:07 -04:00
parent 60506f99bd
commit 1457a4bb8e
81 changed files with 2986 additions and 247 deletions
+63
View File
@@ -0,0 +1,63 @@
# 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 02047. 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.
+25 -25
View File
@@ -1,10 +1,10 @@
# Embedded Systems Reverse Engineering
# 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
### Exercise 1: Analyze the Float Binary in Ghidra
### 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.
@@ -26,7 +26,7 @@ You will import the float binary into Ghidra, configure it for ARM Cortex-M33, r
ghidraRun
```
1. Click **File** **New Project**
1. Click **File** ? **New Project**
2. Select **Non-Shared Project**
3. Click **Next**
4. Enter Project Name: `week05-ex01-floating-point`
@@ -43,12 +43,12 @@ ghidraRun
When the import dialog appears:
1. Click the three dots (**…**) next to **Language**
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:
Now click **Options…** button:
1. Change **Block Name** to: `.text`
2. Change **Base Address** to: `10000000` (XIP flash base)
3. Click **OK**
@@ -70,7 +70,7 @@ 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**
2. Right-click ? **Edit Function Signature**
3. Change to: `int main(void)`
4. Click **OK**
@@ -78,13 +78,13 @@ From previous weeks, we know the boot sequence leads to `main()`:
**Rename stdio_init_all:**
1. Click on `FUN_10002f5c` in the decompile window
2. Right-click **Edit Function Signature**
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**
2. Right-click ? **Edit Function Signature**
3. Change to: `int __wrap_printf(char *format,...)`
4. Check the **Varargs** checkbox
5. Click **OK**
@@ -143,8 +143,8 @@ Map the 64-bit IEEE 754 fields:
```
Bit 63 (sign): 0
Bits 6252 (exponent): 10000000100
Bits 510 (mantissa): 0101010000000000...0000
Bits 6252 (exponent): 10000000100
Bits 510 (mantissa): 0101010000000000...0000
```
##### Step 10: Decode the Sign Bit
@@ -154,14 +154,14 @@ Bit 63 of the double = bit 31 of r3:
```
r3 = 0x40454000 = 0100 0000 0100 0101 0100 0000 0000 0000
^
bit 31 = 0 Positive number
bit 31 = 0 ? Positive number
```
IEEE 754 sign rule: `0` = Positive, `1` = Negative.
##### Step 11: Decode the Exponent
Extract bits 3020 from r3:
Extract bits 3020 from r3:
```
0x40454000: 0 10000000100 01010100000000000000
@@ -169,7 +169,7 @@ Extract bits 3020 from r3:
sign exponent mantissa (top 20)
```
Exponent bits: `10000000100` = 2¹ + 2² = 1024 + 4 = **1028**
Exponent bits: `10000000100` = 2¹° + 2² = 1024 + 4 = **1028**
Subtract the double-precision bias (1023):
@@ -177,7 +177,7 @@ $$\text{real exponent} = 1028 - 1023 = \mathbf{5}$$
##### Step 12: Decode the Mantissa
High 20 bits of mantissa (from r3 bits 190):
High 20 bits of mantissa (from r3 bits 190):
```
0 1 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0
```
@@ -200,15 +200,15 @@ Convert to decimal:
| Bit | Power | Value |
|-----|-------|-------|
| 1 | 2 | 32 |
| 0 | 2 | 0 |
| 1 | 2³ | 8 |
| 0 | 2² | 0 |
| 1 | 2¹ | 2 |
| 0 | 2 | 0 |
| 1 | 2¹ | 0.5 |
| 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} $$
$$32 + 8 + 2 + 0.5 = \mathbf{42.5} ?$$
##### Step 14: Find the Format String
@@ -230,7 +230,7 @@ Create a table of your observations:
| 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 |
| 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` |
@@ -265,7 +265,7 @@ After completing this exercise, you should be able to:
#### 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`
- 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.
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.
+73
View File
@@ -0,0 +1,73 @@
# 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 190), 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 -1
View File
@@ -4,7 +4,7 @@
## Week 5
Integers and Floats in Embedded Systems: Debugging and Hacking Integers and Floats w/ Intermediate GPIO Output Assembler Analysis
### Exercise 2: Patch the Float Binary — Changing 42.5 to 99.0
### 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.
+70
View File
@@ -0,0 +1,70 @@
# 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 -1
View File
@@ -4,7 +4,7 @@
## Week 5
Integers and Floats in Embedded Systems: Debugging and Hacking Integers and Floats w/ Intermediate GPIO Output Assembler Analysis
### Exercise 3: Analyze the Double Binary in Ghidra
### 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.
+70
View File
@@ -0,0 +1,70 @@
# 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 -1
View File
@@ -4,7 +4,7 @@
## Week 5
Integers and Floats in Embedded Systems: Debugging and Hacking Integers and Floats w/ Intermediate GPIO Output Assembler Analysis
### Exercise 4: Patch the Double Binary — Changing 42.52525 to 99.99
### 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.