Ponto flutuante, endianness e bit-packing (verificando com Python)

TL;DR


1. O básico de IEEE-754 que você realmente precisa

float (32-bit)

double (64-bit)

Fatos práticos importantes:


2. Por que um double vira dois .word no RV32

RV32 tem registradores de propósito geral de 32 bits, e muitas toolchains representam constantes em blocos de 32 bits.

Então o compilador pode emitir:

1
2
3
.LC0:
  .word  962072674
  .word  1083394740

Isso é simplesmente 64 bits, divididos em 32 bits baixos e 32 bits altos (por causa do layout little-endian).


3. Na prática: produzir uma constante double em assembly

Crie:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// src/double_const.c
#include "types.h"
#include "uart.h"

static const double g = 1234.676;

__attribute__((noinline))
double f(void) {
  return g;
}

int main(void) {
  union {
    double d;
    u32 w[2];
  } u;

  u.d = f();
  uart_puts("lo=");
  uart_puthex32(u.w[0]);
  uart_puts(" hi=");
  uart_puthex32(u.w[1]);
  uart_putc('\n');
  return 0;
}

Compile para assembly:

1
2
3
4
5
6
7
8
9
riscv64-unknown-elf-gcc -S -O0 -ffreestanding -nostdlib -march=rv32im -mabi=ilp32 \
  -o build/double_const.s src/double_const.c

sed -n '1,200p' build/double_const.s

riscv64-unknown-elf-gcc -O0 -g -ffreestanding -nostdlib -march=rv32im -mabi=ilp32 \
  -T src/link.ld src/start.s src/uart.c src/double_const.c -o build/double_const.elf

qemu-system-riscv32 -M virt -nographic -bios none -kernel build/double_const.elf

Procure rótulos .LC* e os words emitidos.


4. Verifique os bits exatos com Python (struct)

Empacote um double em bytes

1
2
3
4
import struct
x = 1234.676
b = struct.pack('<d', x)  # little-endian double
print(b.hex())

Divida em duas words de 32 bits (little-endian)

1
2
3
4
5
6
import struct
x = 1234.676
b = struct.pack('<d', x)
lo, hi = struct.unpack('<II', b)   # dois ints unsigned de 32 bits
print('lo =', lo)
print('hi =', hi)

Se o seu assembly mostra:

…então bate exatamente.


5. O caminho inverso: reconstruir o double a partir das words

Se você só tem dois valores .word no assembly:

1
2
3
4
5
6
import struct
lo = 962072674
hi = 1083394740
b = struct.pack('<II', lo, hi)
x = struct.unpack('<d', b)[0]
print(x)

Isso retorna o double exato representado por aqueles bits.


6. Sanity check rápido de endian

Se você empacotar com big-endian por engano:

1
2
3
import struct
x = 1234.676
print(struct.pack('>d', x).hex())

Você verá os bytes invertidos em relação à representação little-endian. Essa é uma fonte de confusão muito comum ao correlacionar hexdumps com valores numéricos.


7. E as instruções de ponto flutuante no RV32?

Ponto flutuante no RISC-V depende de extensões da ISA:

Se você compila com um ABI que espera registradores de float (como ilp32d), a convenção de chamada muda para parâmetros/retornos de ponto flutuante.

Em muitos casos embarcados você usará soft-float (operações de ponto flutuante em software), o que afeta desempenho e o formato do disassembly.


8. Casos práticos de engenharia reversa

Uma heurística rápida:


Exercícios

  1. Repita o experimento com um double negativo (ex.: -0.125) e verifique o efeito do bit de sinal.
  2. Pegue um par de constantes .word do seu próprio firmware/ELF e tente reconstruir o double com Python.
  3. Crie uma constante float e verifique sua representação de 32 bits com struct.pack('<f', x) e struct.unpack('<I', ...).
  4. Faça dump de .rodata com objdump -s -j .rodata e tente decodificar os primeiros 3 double que encontrar.

Como testar suas respostas


Resumo

Você aprendeu como floats/doubles viram bytes, por que RV32 costuma emitir constantes de 64 bits como dois .word, e como verificar tudo com Python usando bit-packing.

A seguir: debug com QEMU + GDB, vamos combinar o que você aprendeu para inspecionar registradores, memória e fluxo de controle de forma disciplinada.