Floating Point, Endianness, and Bit-Packing (Verifying with Python)
TL;DR
- You’ll learn how
floatanddoubleare represented (IEEE-754 (Institute of Electrical and Electronics Engineers 754)) and how those bit patterns appear in assembly and in a hexdump. - You’ll understand why compilers sometimes emit a
doubleconstant as two 32-bit.wordvalues on RV32. - You’ll learn a reliable verification method: use Python’s
structto pack/unpack and compare against what you see in disassembly.
1. IEEE-754 basics you actually need
float (32-bit)
- 1 sign bit
- 8 exponent bits
- 23 fraction bits
double (64-bit)
- 1 sign bit
- 11 exponent bits
- 52 fraction bits
The important practical facts:
doubleis 8 bytes (64 bits).- On little-endian RV32, those 8 bytes appear with the least-significant byte at the lowest address.
2. Why a double becomes two .word values on RV32
RV32 has 32-bit general-purpose registers, and many toolchains represent constants in 32-bit chunks.
So a compiler might emit:
| |
That is simply 64 bits, split into low 32 bits and high 32 bits (because of little-endian layout).
.dword or .quad for 64-bit constants.3. Hands-on: produce a double constant in assembly
Create:
| |
Compile to assembly:
| |
Look for .LC* labels and the emitted words.
4. Verify the exact bits using Python (struct)
Pack a double into bytes
| |
'<d'means little-endian (<) and double (d).
Split into two 32-bit words (little-endian)
| |
If your assembly shows:
.word <lo>.word <hi>
…then you have a perfect match.
< little, > big) and sizes (I = 32-bit unsigned, Q = 64-bit unsigned).5. Going the other way: reconstruct the double from the words
If you only have two .word values from assembly:
| |
That yields the exact double value represented by those bits.
6. Quick endian sanity check
If you pack with big-endian by mistake:
| |
You’ll see the bytes reversed compared to the little-endian representation. This is a very common source of confusion when correlating hexdumps with numeric values.
7. What about floating-point instructions on RV32?
RISC-V floating point depends on ISA extensions:
F(single-precision float)D(double-precision)
If you compile with an ABI that expects float registers (like ilp32d), the calling convention changes for floating-point parameters/returns.
In many embedded cases you’ll be using soft-float (floating operations implemented in software), which impacts performance and disassembly shape.
F/D hardware, compiling with a hard-float ABI can produce binaries that fault or behave incorrectly.8. Practical reverse-engineering use cases
- Identifying lookup tables in
.rodata(sine tables, calibration constants) - Verifying protocol fields (fixed-point encodings)
- Confirming that “mystery constants” in firmware are actually floats/doubles
A fast heuristic:
- If you see repeating patterns that look random but stable, and 4/8 byte alignment, suspect float tables.
Exercises
- Repeat the experiment for a negative
double(e.g.,-0.125) and verify the sign bit effect. - Take a pair of
.wordconstants from your own firmware/ELF and try reconstructing thedoublewith Python. - Create a
floatconstant and verify its 32-bit representation withstruct.pack('<f', x)andstruct.unpack('<I', ...). - Dump
.rodatabytes withobjdump -s -j .rodataand try decoding the first 3doublevalues you find.
How to test your answers
- Your Python-derived words must match the assembler
.wordvalues exactly. - Reconstructed floats/doubles should match the printed value within expected rounding error.
Summary
You learned how floats/doubles become bytes, why RV32 often emits 64-bit constants as two .words, and how to verify everything with Python bit-packing.
Next: debugging with QEMU + GDB-we’ll combine what you learned so far to inspect registers, memory, and control flow in a disciplined way.