Updated WEEK04

This commit is contained in:
Kevin Thomas
2026-05-09 11:42:33 -04:00
parent 005fd08646
commit ee664b6733
165 changed files with 3952 additions and 13308 deletions
+203 -223
View File
@@ -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 190) (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 6332 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 6332 of the double
r2 holds bits 310 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 6252 of the 64-bit value = bits 3020 of r3**
**2. Exponent - bits 62-52 of the 64-bit value = bits 30-20 of r3**
Extract bits 3020 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 510 of the 64-bit value**
**3. Mantissa - bits 51-0 of the 64-bit value**
- **High 20 bits of mantissa** (bits 5132) = bits 190 of r3:
- **High 20 bits of mantissa** (bits 51-32) = bits 19-0 of r3:
```
r3 bits 190: 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 310) = 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 **1516 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 190) (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 6332):
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 6252 = bits 3020 of r3**
**2. Exponent - bits 62-52 = bits 30-20 of r3**
Extract bits 3020 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 510**
**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 5132) = bits 190 of r3:
- **High 20 bits of mantissa** (bits 51-32) = bits 19-0 of r3:
```
r3 bits 190: 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 310) = 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 (0255), `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! 🔧