tinymux/mux/modules/engine/dbt_x64_sysv.cpp
Stephen Dennis aa4b632988 fix(dbt): refuse the warm superblock when the loop body calls (#2019)
A self-loop whose body contains a function call was admitted to the
warm_entry fast path.  The back edge re-enters *past* the preload, so
the second and later iterations read host registers that the callee has
since reused — silently wrong guest values, no crash, no log line.

The scan already guards this hazard for ordinary slot pressure via
rc_loop_overcommits(), but it walks over a JAL ra without counting the
callee at all, and try_emit_inline_call() ends in rc_invalidate_reload().
A call is an eviction of everything, and slot pressure cannot model it.

Reproduced deterministically on x86-64 with tests/codiff (#2020):
40/40 wrong before, 0/40 after.  The corrupted register in that case is
count_words' loop counter itself, so the word count runs away and the
result saturates at the LBUF_SIZE cap.

Block chaining is the enabling condition, not the defect: without it the
callee exits to the dispatcher, the inline call takes its cold side exit,
and the back edge is never reached with a wrecked cache.  That is also
why AArch64 saw it intermittently — an unflushed patch left the safe path
in place.

All three backends carried the identical scan and the identical hole.

Fixes #2019.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 20:37:54 -06:00

3177 lines
130 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*! \file dbt_x64_sysv.cpp
* \brief RV64IMD dynamic binary translator — x86-64 System V ABI backend.
*
* Block-at-a-time JIT translation from RV64IMD to x86-64.
* This file implements the platform-specific parts of the DBT:
* - Register cache (host register assignment)
* - Trampoline (callee-saved setup, block dispatch)
* - Instruction translation (RV64 → x86-64)
* - Intrinsic stubs (native host function calls)
* - Block chaining (x86-64 JMP rel32 backpatching)
*
* Reference: ~/riscv/dbt/dbt.c (RV32IMFD version).
*
* See docs/DBT-PORTABILITY.md for the multi-platform design.
*/
#include "dbt.h"
#include "dbt_decoder.h"
#include "dbt_emit_x64.h"
#include "dbt_internal.h"
#include <cstdio>
#include <cstdlib>
#include <cstring>
// ---------------------------------------------------------------
// Register cache — 8-slot LRU for guest registers in host registers
// ---------------------------------------------------------------
static const int rc_host_regs[RC_NUM_SLOTS] = {
X64_RSI, X64_RDI, X64_R8, X64_R9,
X64_R10, X64_R11, X64_R14, X64_R15
};
// Pinned guest registers: a0-a3 (x10-x13) in slots 0-3.
// Pre-loaded by the trampoline, persist across chained block exits.
//
static const int rc_pinned_guest[RC_NUM_PINNED] = { 10, 11, 12, 13 };
// FP register cache — 6-slot LRU for guest FP registers in XMM2-XMM7.
// XMM0 and XMM1 are scratch (like RAX for integers).
// ---------------------------------------------------------------
static const int fc_host_xmm[FC_NUM_SLOTS] = {
2, 3, 4, 5, 6, 7 // XMM2-XMM7
};
static void fc_init(fp_cache_t *fc) {
for (int i = 0; i < FC_NUM_SLOTS; i++) {
fc->slots[i].guest_freg = -1;
fc->slots[i].dirty = 0;
fc->slots[i].last_use = 0;
}
fc->clock = 0;
}
static int fc_find(fp_cache_t *fc, int guest_freg) {
for (int i = 0; i < FC_NUM_SLOTS; i++)
if (fc->slots[i].guest_freg == guest_freg) return i;
return -1;
}
static int fc_alloc(emit_t *e, fp_cache_t *fc) {
// Prefer free slots.
for (int i = 0; i < FC_NUM_SLOTS; i++)
if (fc->slots[i].guest_freg == -1) return i;
// Evict LRU.
int lru = 0;
for (int i = 1; i < FC_NUM_SLOTS; i++)
if (fc->slots[i].last_use < fc->slots[lru].last_use) lru = i;
if (fc->slots[lru].dirty)
emit_store_fp_d(e, fc->slots[lru].guest_freg, fc_host_xmm[lru]);
fc->slots[lru].guest_freg = -1;
fc->slots[lru].dirty = 0;
return lru;
}
// Return XMM register holding guest_freg's value. Loads on miss.
static int fc_read(emit_t *e, fp_cache_t *fc, int guest_freg) {
int slot = fc_find(fc, guest_freg);
if (slot >= 0) {
fc->slots[slot].last_use = ++fc->clock;
return fc_host_xmm[slot];
}
slot = fc_alloc(e, fc);
emit_load_fp_d(e, fc_host_xmm[slot], guest_freg);
fc->slots[slot].guest_freg = guest_freg;
fc->slots[slot].dirty = 0;
fc->slots[slot].last_use = ++fc->clock;
return fc_host_xmm[slot];
}
// Return XMM register allocated for writing guest_freg. Marks dirty.
static int fc_write(emit_t *e, fp_cache_t *fc, int guest_freg) {
int slot = fc_find(fc, guest_freg);
if (slot < 0) slot = fc_alloc(e, fc);
fc->slots[slot].guest_freg = guest_freg;
fc->slots[slot].dirty = 1;
fc->slots[slot].last_use = ++fc->clock;
return fc_host_xmm[slot];
}
// Flush all dirty cached FP registers to memory.
static void fc_flush(emit_t *e, fp_cache_t *fc) {
for (int i = 0; i < FC_NUM_SLOTS; i++)
if (fc->slots[i].guest_freg >= 0 && fc->slots[i].dirty)
emit_store_fp_d(e, fc->slots[i].guest_freg, fc_host_xmm[i]);
}
// Invalidate all FP cache slots after a native CALL. The callee may have
// modified any guest FP register, and the FP intrinsic stubs (rv64_strtod,
// rv64_fval, rv64_nearest_pretty, rv64_ftoa_round) write their double result
// directly to ctx.f[], so any cached value is now stale. Unlike the integer
// cache, no FP registers are pinned across blocks, so we simply drop every
// slot; subsequent fc_read() reloads from ctx on demand. Mirrors
// rc_invalidate_reload() for the integer side — its absence here left the
// caller reading pre-call XMM values instead of the intrinsic's result.
static void fc_invalidate(fp_cache_t *fc) {
for (int i = 0; i < FC_NUM_SLOTS; i++) {
fc->slots[i].guest_freg = -1;
fc->slots[i].dirty = 0;
fc->slots[i].last_use = 0;
}
fc->clock = 0;
}
static void rc_init(reg_cache_t *rc) {
for (int i = 0; i < RC_NUM_SLOTS; i++) {
rc->slots[i].guest_reg = -1;
rc->slots[i].dirty = 0;
rc->slots[i].last_use = 0;
rc->slots[i].pinned = 0;
}
rc->clock = 0;
}
// Initialize with pinned guest registers pre-populated.
// The trampoline pre-loads these into the corresponding host registers,
// and they persist across chained block transitions.
//
static void rc_init_pinned(reg_cache_t *rc) {
rc_init(rc);
for (int i = 0; i < RC_NUM_PINNED; i++) {
rc->slots[i].guest_reg = rc_pinned_guest[i];
rc->slots[i].dirty = 0;
rc->slots[i].last_use = 0;
rc->slots[i].pinned = 1;
}
}
static int rc_find(reg_cache_t *rc, int guest_reg) {
for (int i = 0; i < RC_NUM_SLOTS; i++)
if (rc->slots[i].guest_reg == guest_reg) return i;
return -1;
}
static int rc_alloc(reg_cache_t *rc, emit_t *e) {
// Prefer free (non-pinned) slots.
for (int i = 0; i < RC_NUM_SLOTS; i++)
if (rc->slots[i].guest_reg == -1 && !rc->slots[i].pinned)
return i;
// Evict LRU among non-pinned slots.
int lru = -1;
for (int i = 0; i < RC_NUM_SLOTS; i++) {
if (rc->slots[i].pinned) continue;
if (lru < 0 || rc->slots[i].last_use < rc->slots[lru].last_use)
lru = i;
}
if (lru < 0) {
// All slots pinned — shouldn't happen with RC_NUM_PINNED < RC_NUM_SLOTS.
// Fall back to slot 0 as last resort.
lru = 0;
}
if (rc->slots[lru].dirty)
emit_store_guest(e, rc->slots[lru].guest_reg, rc_host_regs[lru]);
rc->slots[lru].guest_reg = -1;
rc->slots[lru].dirty = 0;
return lru;
}
// Return host register holding guest_reg's value. Loads from memory on miss.
//
static int rc_read(emit_t *e, reg_cache_t *rc, int guest_reg) {
if (guest_reg == 0) {
// x0 is always zero — use RAX as scratch.
emit_xor_r64(e, X64_RAX, X64_RAX);
return X64_RAX;
}
int slot = rc_find(rc, guest_reg);
if (slot >= 0) {
rc->slots[slot].last_use = ++rc->clock;
return rc_host_regs[slot];
}
slot = rc_alloc(rc, e);
emit_load_guest(e, rc_host_regs[slot], guest_reg);
rc->slots[slot].guest_reg = guest_reg;
rc->slots[slot].dirty = 0;
rc->slots[slot].last_use = ++rc->clock;
return rc_host_regs[slot];
}
// Return host register allocated for writing guest_reg. Marks dirty.
//
static int rc_write(emit_t *e, reg_cache_t *rc, int guest_reg) {
if (guest_reg == 0) return X64_RAX;
int slot = rc_find(rc, guest_reg);
if (slot < 0) slot = rc_alloc(rc, e);
rc->slots[slot].guest_reg = guest_reg;
rc->slots[slot].dirty = 1;
rc->slots[slot].last_use = ++rc->clock;
return rc_host_regs[slot];
}
// Load a guest register into a specific host register.
//
static void rc_load(emit_t *e, reg_cache_t *rc, int host_dst, int guest_reg) {
int hr = rc_read(e, rc, guest_reg);
emit_mov_r64(e, host_dst, hr);
}
// Store a specific host register into a guest register's cache slot.
//
static void rc_store(emit_t *e, reg_cache_t *rc, int guest_reg, int host_src) {
if (guest_reg == 0) return;
int hr = rc_write(e, rc, guest_reg);
emit_mov_r64(e, hr, host_src);
}
// Flush all dirty cached registers to memory.
//
static void rc_flush(emit_t *e, reg_cache_t *rc) {
for (int i = 0; i < RC_NUM_SLOTS; i++)
if (rc->slots[i].guest_reg >= 0 && rc->slots[i].dirty)
emit_store_guest(e, rc->slots[i].guest_reg, rc_host_regs[i]);
}
// Invalidate all register cache slots and reload pinned registers.
// Used after a native CALL where the callee may have modified any guest
// register — the cached values are stale and must be reloaded from ctx.
//
// The trampoline unconditionally stores pinned host registers (RSI, RDI,
// R8, R9) back to ctx after the block RETs. If we clear the pinned
// mapping, the translator may reassign those host registers to other
// guest registers, and the trampoline post-store will overwrite ctx
// with wrong values. Re-establishing the pinned slots + emitting
// reload instructions keeps the convention intact.
//
static void rc_invalidate_reload(emit_t *e, reg_cache_t *rc) {
for (int i = 0; i < RC_NUM_SLOTS; i++) {
rc->slots[i].guest_reg = -1;
rc->slots[i].dirty = 0;
rc->slots[i].last_use = 0;
rc->slots[i].pinned = 0;
}
rc->clock = 0;
// Restore pinned slots and reload from ctx.
for (int i = 0; i < RC_NUM_PINNED; i++) {
rc->slots[i].guest_reg = rc_pinned_guest[i];
rc->slots[i].dirty = 0;
rc->slots[i].last_use = 0;
rc->slots[i].pinned = 1;
emit_load_guest(e, rc_host_regs[i], rc_pinned_guest[i]);
}
}
// Forward declaration for block exit helper (defined below).
static void emit_exit_chained(emit_t *e, dbt_state_t *dbt, uint64_t target_pc);
// ---------------------------------------------------------------
// Block-level intrinsic stubs.
//
// When translate_block() is asked to translate a block at a known
// intrinsic address, it emits a complete native stub instead of
// translating the RV64 byte loops. The stub:
// 1. Loads guest registers from ctx (via RBX)
// 2. Converts guest pointers to host pointers (add R12)
// 3. Calls the host-native function
// 4. Stores the result back to guest a0
// 5. Sets next_pc = ra (guest return address)
// 6. Returns to the dispatch loop
//
// This is the same architecture as ~/slow-32/tools/dbt: the caller's
// JAL gets translated normally, block chaining finds the native stub,
// and the RAS-predicted return goes back to the caller. The RV64
// fallback implementations in softlib.c are never translated.
// ---------------------------------------------------------------
// Helper: emit "load guest reg x[n] into host_reg" from context.
// mov host_reg, [rbx + n*8]
//
static void emit_load_ctx_reg(emit_t *e, int host_reg, int guest_reg) {
emit_load_guest(e, host_reg, guest_reg);
}
// Helper: emit "store host_reg into guest reg x[n]" in context.
// mov [rbx + n*8], host_reg
//
static void emit_store_ctx_reg(emit_t *e, int guest_reg, int host_reg) {
emit_store_guest(e, guest_reg, host_reg);
}
static void emit_load_next_pc(emit_t *e, int host_reg) {
emit_byte(e, rex(1, reg_hi(host_reg), 0, 0));
emit_byte(e, 0x8B);
emit_byte(e, modrm(0x02, host_reg, X64_RBX));
emit_u32(e, CTX_NEXT_PC_OFF);
}
static void emit_store_next_pc(emit_t *e, int host_reg) {
emit_byte(e, rex(1, reg_hi(host_reg), 0, 0));
emit_byte(e, 0x89);
emit_byte(e, modrm(0x02, host_reg, X64_RBX));
emit_u32(e, CTX_NEXT_PC_OFF);
}
// Load/store a 64-bit ctx field at an arbitrary offset (the guest-register
// helpers only reach x[0..31]).
//
static void emit_load_ctx_u64(emit_t *e, int host_reg, int32_t off) {
emit_byte(e, rex(1, reg_hi(host_reg), 0, 0));
emit_byte(e, 0x8B);
emit_byte(e, modrm(0x02, host_reg, X64_RBX));
emit_u32(e, static_cast<uint32_t>(off));
}
static void emit_store_ctx_u64(emit_t *e, int host_reg, int32_t off) {
emit_byte(e, rex(1, reg_hi(host_reg), 0, 0));
emit_byte(e, 0x89);
emit_byte(e, modrm(0x02, host_reg, X64_RBX));
emit_u32(e, static_cast<uint32_t>(off));
}
// Convert a guest offset in host_reg to a host pointer, bounds-checked
// against ctx.mem_size (#1151).
//
// Every intrinsic stub reaches host pointers through here — both the
// hand-written ones and the generic ptr_mask marshaller — so this one
// function covers the whole intrinsic surface.
//
// The compare is against the raw guest offset, not against the converted
// pointer: base+offset can wrap for a large offset and then compare below
// the limit, which is exactly the adversarial case. x86 ADD writes flags
// and there is no LEA helper here, so the offset is saved first and the
// compare happens after the add.
//
// Scratch: RAX, R10, R11. RAX is the return register and dead before the
// host call; R10/R11 hold pinned guest registers, but the host call is
// about to clobber them anyway and emit_intrinsic_return reloads all four
// pinned registers from ctx before returning.
//
static void emit_guest_to_host(emit_t *e, int host_reg) {
emit_mov_r64(e, X64_RAX, host_reg); // save raw guest offset
emit_add_r64(e, host_reg, X64_R12); // base + offset
emit_load_ctx_u64(e, X64_R10, CTX_MEM_CLAMPS_OFF);
emit_add_r64_imm(e, X64_R10, 1); // clamps + 1 (flags dead)
emit_load_ctx_u64(e, X64_R11, CTX_MEM_SIZE_OFF);
emit_cmp_r64(e, X64_RAX, X64_R11); // offset vs bound
// Everything below is MOV/CMOV, which leave the flags from the CMP.
emit_mov_r64_imm64(e, X64_RAX,
reinterpret_cast<uint64_t>(g_dbt_safe_page));
emit_cmovcc(e, CMOV_AE, host_reg, X64_RAX); // out of range -> sink
emit_load_ctx_u64(e, X64_RAX, CTX_MEM_CLAMPS_OFF);
emit_cmovcc(e, CMOV_AE, X64_RAX, X64_R10); // count only when clamped
emit_store_ctx_u64(e, X64_RAX, CTX_MEM_CLAMPS_OFF);
}
// Emit intrinsic return: reload pinned host registers from ctx
// (so the trampoline's post-store writes correct values), set
// next_pc = guest x1 (ra), then RET.
//
static void emit_intrinsic_return(emit_t *e) {
// Reload pinned registers from ctx. The stubs clobber RSI/RDI/R8/R9
// with host function arguments/results. The trampoline unconditionally
// stores these back to ctx->x[10..13] after every block, so they must
// reflect the current guest state.
//
for (int i = 0; i < RC_NUM_PINNED; i++) {
emit_load_guest(e, rc_host_regs[i], rc_pinned_guest[i]);
}
// Load ra from guest context → rcx
emit_load_ctx_reg(e, X64_RCX, 1);
// Set next_pc = ra
emit_exit_indirect(e, X64_RCX);
}
// Emit the prologue that every intrinsic stub needs: align the
// stack to 16 bytes (required by System V ABI before CALL).
// push rbp; mov rbp, rsp; and rsp, -16
//
static void emit_stub_prologue(emit_t *e) {
emit_byte(e, 0x55); // push rbp
emit_mov_r64(e, X64_RBP, X64_RSP);
// and rsp, -16
emit_byte(e, rex(1, 0, 0, 0));
emit_byte(e, 0x83);
emit_byte(e, modrm(0x03, 4, X64_RSP)); // and /4
emit_byte(e, 0xF0); // -16
}
static void emit_stub_epilogue(emit_t *e) {
emit_mov_r64(e, X64_RSP, X64_RBP);
emit_byte(e, 0x5D); // pop rbp
}
// emit_call_host: mov rax, imm64(fn_ptr); call rax
//
static void emit_call_host(emit_t *e, void *fn) {
emit_mov_r64_imm64(e, X64_RAX, reinterpret_cast<uint64_t>(fn));
// call rax: FF D0
emit_byte(e, 0xFF);
emit_byte(e, modrm(0x03, 2, X64_RAX));
}
// ---- Individual intrinsic stubs ----
// rv64_slen: a0=string_ptr → a0=length
// Host: size_t strlen(const char *s)
//
static void emit_stub_slen(emit_t *e) {
emit_stub_prologue(e);
// rdi = host pointer to string
emit_load_ctx_reg(e, X64_RDI, 10); // a0
emit_guest_to_host(e, X64_RDI);
emit_call_host(e, reinterpret_cast<void *>(strlen));
// Store result (rax) to guest a0 (x10)
emit_store_ctx_reg(e, 10, X64_RAX);
emit_stub_epilogue(e);
emit_intrinsic_return(e);
}
// rv64_scopy: a0=dst, a1=src → a0=pointer AT NUL
// Host: strcpy then strlen to find NUL position.
// Actually simpler: inline byte loop since strcpy returns dst not end.
// Or use stpcpy if available. For now, call strcpy and then strlen.
//
// Guest dst is saved in R14 (callee-saved). A push after the 16-byte
// aligned prologue would leave RSP ≡ 8 (mod 16) at CALL and violate
// System V (#1148). R14 is restored by the trampoline on block return.
//
static void emit_stub_scopy(emit_t *e) {
emit_stub_prologue(e);
// Save guest dst in R14 across host CALLs (stack stays 16-aligned).
emit_load_ctx_reg(e, X64_R14, 10); // guest a0 (dst)
// rdi = host dst, rsi = host src
emit_load_ctx_reg(e, X64_RDI, 10); // a0
emit_guest_to_host(e, X64_RDI);
emit_load_ctx_reg(e, X64_RSI, 11); // a1
emit_guest_to_host(e, X64_RSI);
// call strcpy(host_dst, host_src) → returns host_dst in rax
emit_call_host(e, reinterpret_cast<void *>(strcpy));
// Now find the NUL: rdi = rax (host_dst returned by strcpy)
emit_mov_r64(e, X64_RDI, X64_RAX);
emit_call_host(e, reinterpret_cast<void *>(strlen));
// rax = length of string at dst
// result = guest_dst (R14) + length → points at the NUL
emit_add_r64(e, X64_RAX, X64_R14);
emit_store_ctx_reg(e, 10, X64_RAX);
emit_stub_epilogue(e);
emit_intrinsic_return(e);
}
// memcpy: a0=dst, a1=src, a2=len → a0=dst
// Host: void *memcpy(void *dst, const void *src, size_t n)
//
static void emit_stub_memcpy(emit_t *e) {
emit_stub_prologue(e);
// Save guest dst in R14 — keeps RSP 16-byte aligned for CALL (#1148).
emit_load_ctx_reg(e, X64_R14, 10);
emit_load_ctx_reg(e, X64_RDI, 10); // dst
emit_guest_to_host(e, X64_RDI);
emit_load_ctx_reg(e, X64_RSI, 11); // src
emit_guest_to_host(e, X64_RSI);
emit_load_ctx_reg(e, X64_RDX, 12); // len
emit_call_host(e, reinterpret_cast<void *>(memcpy));
emit_store_ctx_reg(e, 10, X64_R14); // return original guest dst
emit_stub_epilogue(e);
emit_intrinsic_return(e);
}
// memcmp: a0=ptr_a, a1=ptr_b, a2=len → a0=result
// Host: int memcmp(const void *s1, const void *s2, size_t n)
//
static void emit_stub_memcmp(emit_t *e) {
emit_stub_prologue(e);
emit_load_ctx_reg(e, X64_RDI, 10); // ptr_a
emit_guest_to_host(e, X64_RDI);
emit_load_ctx_reg(e, X64_RSI, 11); // ptr_b
emit_guest_to_host(e, X64_RSI);
emit_load_ctx_reg(e, X64_RDX, 12); // len
emit_call_host(e, reinterpret_cast<void *>(memcmp));
// sign-extend eax → rax for 64-bit guest register
// cdqe: REX.W + 0x98
emit_byte(e, rex(1, 0, 0, 0));
emit_byte(e, 0x98);
emit_store_ctx_reg(e, 10, X64_RAX);
emit_stub_epilogue(e);
emit_intrinsic_return(e);
}
// memset: a0=dst, a1=byte_val, a2=len → a0=dst
// Host: void *memset(void *s, int c, size_t n)
//
static void emit_stub_memset(emit_t *e) {
emit_stub_prologue(e);
// Save guest dst in R14 — keeps RSP 16-byte aligned for CALL (#1148).
emit_load_ctx_reg(e, X64_R14, 10);
emit_load_ctx_reg(e, X64_RDI, 10); // dst
emit_guest_to_host(e, X64_RDI);
emit_load_ctx_reg(e, X64_RSI, 11); // byte value (int)
emit_load_ctx_reg(e, X64_RDX, 12); // len
emit_call_host(e, reinterpret_cast<void *>(memset));
emit_store_ctx_reg(e, 10, X64_R14);
emit_stub_epilogue(e);
emit_intrinsic_return(e);
}
// memswap: a0=ptr_a, a1=ptr_b, a2=len → void
// No libc equivalent — we implement the 3-phase qword-optimized
// swap inline. This is the same algorithm as ~/slow-32/runtime
// intrinsics.s memswap but targeting x86-64.
//
// Host native: swap 8 bytes at a time when aligned, byte fallback.
//
static void emit_stub_memswap(emit_t *e) {
emit_stub_prologue(e);
// rax = host ptr_a, rcx = host ptr_b, rdx = len
emit_load_ctx_reg(e, X64_RAX, 10);
emit_guest_to_host(e, X64_RAX);
emit_load_ctx_reg(e, X64_RCX, 11);
emit_guest_to_host(e, X64_RCX);
emit_load_ctx_reg(e, X64_RDX, 12);
// test rdx, rdx; jz done
emit_byte(e, rex(1, 0, 0, 0));
emit_byte(e, 0x85);
emit_byte(e, modrm(0x03, X64_RDX, X64_RDX));
uint32_t jz_done = emit_pos(e);
emit_byte(e, 0x0F);
emit_byte(e, 0x84);
emit_u32(e, 0);
// Check alignment: (a ^ b) & 7 — if nonzero, byte-only path
emit_mov_r64(e, X64_RSI, X64_RAX);
emit_xor_r64(e, X64_RSI, X64_RCX);
emit_and_r64_imm(e, X64_RSI, 7);
uint32_t jnz_byte = emit_pos(e);
emit_byte(e, 0x0F);
emit_byte(e, 0x85);
emit_u32(e, 0);
// Phase 1: Align to 8-byte boundary
uint32_t align_loop = emit_pos(e);
emit_byte(e, rex(1, 0, 0, 0)); emit_byte(e, 0x85);
emit_byte(e, modrm(0x03, X64_RDX, X64_RDX));
uint32_t jz_tail = emit_pos(e);
emit_byte(e, 0x0F); emit_byte(e, 0x84); emit_u32(e, 0);
// test al, 7
emit_byte(e, 0xA8); emit_byte(e, 0x07);
uint32_t jz_qword = emit_pos(e);
emit_byte(e, 0x0F); emit_byte(e, 0x84); emit_u32(e, 0);
// byte swap: rsi = [rax], rdi = [rcx], [rax] = rdi, [rcx] = rsi
emit_byte(e, 0x0F); emit_byte(e, 0xB6); emit_byte(e, modrm(0x00, X64_RSI, X64_RAX));
emit_byte(e, 0x0F); emit_byte(e, 0xB6); emit_byte(e, modrm(0x00, X64_RDI, X64_RCX));
emit_byte(e, 0x40); emit_byte(e, 0x88); emit_byte(e, modrm(0x00, X64_RDI, X64_RAX));
emit_byte(e, 0x40); emit_byte(e, 0x88); emit_byte(e, modrm(0x00, X64_RSI, X64_RCX));
// inc rax; inc rcx; dec rdx
emit_byte(e, rex(1, 0, 0, 0)); emit_byte(e, 0xFF); emit_byte(e, modrm(0x03, 0, X64_RAX));
emit_byte(e, rex(1, 0, 0, 0)); emit_byte(e, 0xFF); emit_byte(e, modrm(0x03, 0, X64_RCX));
emit_byte(e, rex(1, 0, 0, 0)); emit_byte(e, 0xFF); emit_byte(e, modrm(0x03, 1, X64_RDX));
int32_t ja = static_cast<int32_t>(align_loop) - static_cast<int32_t>(emit_pos(e) + 2);
emit_byte(e, 0xEB); emit_byte(e, static_cast<uint8_t>(ja));
// Phase 2: Qword swap
uint32_t qword_loop = emit_pos(e);
emit_patch_rel32(e, jz_qword + 2, qword_loop);
// cmp rdx, 8; jb tail
emit_byte(e, rex(1, 0, 0, 0)); emit_byte(e, 0x83);
emit_byte(e, modrm(0x03, 7, X64_RDX)); emit_byte(e, 0x08);
uint32_t jb_tail = emit_pos(e);
emit_byte(e, 0x0F); emit_byte(e, 0x82); emit_u32(e, 0);
// rsi = [rax]; rdi = [rcx]; [rax] = rdi; [rcx] = rsi
emit_byte(e, rex(1, 0, 0, 0)); emit_byte(e, 0x8B); emit_byte(e, modrm(0x00, X64_RSI, X64_RAX));
emit_byte(e, rex(1, 0, 0, 0)); emit_byte(e, 0x8B); emit_byte(e, modrm(0x00, X64_RDI, X64_RCX));
emit_byte(e, rex(1, 0, 0, 0)); emit_byte(e, 0x89); emit_byte(e, modrm(0x00, X64_RDI, X64_RAX));
emit_byte(e, rex(1, 0, 0, 0)); emit_byte(e, 0x89); emit_byte(e, modrm(0x00, X64_RSI, X64_RCX));
// add rax, 8; add rcx, 8; sub rdx, 8
emit_byte(e, rex(1, 0, 0, 0)); emit_byte(e, 0x83); emit_byte(e, modrm(0x03, 0, X64_RAX)); emit_byte(e, 0x08);
emit_byte(e, rex(1, 0, 0, 0)); emit_byte(e, 0x83); emit_byte(e, modrm(0x03, 0, X64_RCX)); emit_byte(e, 0x08);
emit_byte(e, rex(1, 0, 0, 0)); emit_byte(e, 0x83); emit_byte(e, modrm(0x03, 5, X64_RDX)); emit_byte(e, 0x08);
int32_t jq = static_cast<int32_t>(qword_loop) - static_cast<int32_t>(emit_pos(e) + 2);
emit_byte(e, 0xEB); emit_byte(e, static_cast<uint8_t>(jq));
// Phase 3: Tail bytes (also byte-only fallback for misaligned)
uint32_t byte_loop = emit_pos(e);
emit_patch_rel32(e, jnz_byte + 2, byte_loop);
emit_patch_rel32(e, jb_tail + 2, byte_loop);
emit_byte(e, rex(1, 0, 0, 0)); emit_byte(e, 0x85);
emit_byte(e, modrm(0x03, X64_RDX, X64_RDX));
uint32_t jz_done2 = emit_pos(e);
emit_byte(e, 0x0F); emit_byte(e, 0x84); emit_u32(e, 0);
emit_byte(e, 0x0F); emit_byte(e, 0xB6); emit_byte(e, modrm(0x00, X64_RSI, X64_RAX));
emit_byte(e, 0x0F); emit_byte(e, 0xB6); emit_byte(e, modrm(0x00, X64_RDI, X64_RCX));
emit_byte(e, 0x40); emit_byte(e, 0x88); emit_byte(e, modrm(0x00, X64_RDI, X64_RAX));
emit_byte(e, 0x40); emit_byte(e, 0x88); emit_byte(e, modrm(0x00, X64_RSI, X64_RCX));
emit_byte(e, rex(1, 0, 0, 0)); emit_byte(e, 0xFF); emit_byte(e, modrm(0x03, 0, X64_RAX));
emit_byte(e, rex(1, 0, 0, 0)); emit_byte(e, 0xFF); emit_byte(e, modrm(0x03, 0, X64_RCX));
emit_byte(e, rex(1, 0, 0, 0)); emit_byte(e, 0xFF); emit_byte(e, modrm(0x03, 1, X64_RDX));
int32_t jb = static_cast<int32_t>(byte_loop) - static_cast<int32_t>(emit_pos(e) + 2);
emit_byte(e, 0xEB); emit_byte(e, static_cast<uint8_t>(jb));
// .done:
uint32_t done_pos = emit_pos(e);
emit_patch_rel32(e, jz_done + 2, done_pos);
emit_patch_rel32(e, jz_tail + 2, done_pos);
emit_patch_rel32(e, jz_done2 + 2, done_pos);
emit_stub_epilogue(e);
emit_intrinsic_return(e);
}
// ---- FP intrinsic stubs ----
//
// For double→double functions (sin, cos, etc.):
// Load guest fa0 (f[10]) into host xmm0.
// Call the host libm function.
// Store xmm0 result back to guest fa0.
//
// For (double,double)→double functions (pow, atan2, fmod):
// Load guest fa0 (f[10]) into host xmm0.
// Load guest fa1 (f[11]) into host xmm1.
// Call the host libm function.
// Store xmm0 result back to guest fa0.
//
// Guest FP register fa0 = f[10], offset = CTX_FP_OFF + 10*8 = 608.
// Guest FP register fa1 = f[11], offset = CTX_FP_OFF + 11*8 = 616.
//
static constexpr int CTX_FA0_OFF = CTX_FP_OFF + 10 * 8; // 608
static constexpr int CTX_FA1_OFF = CTX_FP_OFF + 11 * 8; // 616
// Helper: movsd xmm_reg, [rbx + offset]
//
static void emit_load_ctx_fp(emit_t *e, int xmm_reg, int ctx_off) {
// F2 0F 10 /r (movsd xmm, m64)
// With RBX as base: mod=10 (disp32), rm=011 (rbx)
emit_byte(e, 0xF2);
if (xmm_reg >= 8) {
emit_byte(e, rex(0, (xmm_reg >> 3) & 1, 0, 0));
}
emit_byte(e, 0x0F);
emit_byte(e, 0x10);
emit_byte(e, modrm(0x02, xmm_reg & 7, X64_RBX));
emit_u32(e, ctx_off);
}
// Helper: movsd [rbx + offset], xmm_reg
//
static void emit_store_ctx_fp(emit_t *e, int ctx_off, int xmm_reg) {
// F2 0F 11 /r (movsd m64, xmm)
emit_byte(e, 0xF2);
if (xmm_reg >= 8) {
emit_byte(e, rex(0, (xmm_reg >> 3) & 1, 0, 0));
}
emit_byte(e, 0x0F);
emit_byte(e, 0x11);
emit_byte(e, modrm(0x02, xmm_reg & 7, X64_RBX));
emit_u32(e, ctx_off);
}
// double fn(double): sin, cos, tan, asin, acos, atan, exp, log, etc.
//
static void emit_stub_fp_d_d(void *ev, void *fn) {
emit_t *e = static_cast<emit_t *>(ev);
emit_stub_prologue(e);
emit_load_ctx_fp(e, 0, CTX_FA0_OFF); // movsd xmm0, [rbx+608]
emit_call_host(e, fn);
emit_store_ctx_fp(e, CTX_FA0_OFF, 0); // movsd [rbx+608], xmm0
emit_stub_epilogue(e);
emit_intrinsic_return(e);
}
// double fn(double, double): pow, atan2, fmod
//
static void emit_stub_fp_dd_d(void *ev, void *fn) {
emit_t *e = static_cast<emit_t *>(ev);
emit_stub_prologue(e);
emit_load_ctx_fp(e, 0, CTX_FA0_OFF); // movsd xmm0, [rbx+608]
emit_load_ctx_fp(e, 1, CTX_FA1_OFF); // movsd xmm1, [rbx+616]
emit_call_host(e, fn);
emit_store_ctx_fp(e, CTX_FA0_OFF, 0); // movsd [rbx+608], xmm0
emit_stub_epilogue(e);
emit_intrinsic_return(e);
}
// ---- String↔double conversion intrinsic stubs ----
// rv64_strtod: guest a0=string_ptr → guest fa0=double
// Host: double host_strtod(const char *s)
// SysV: rdi=s → xmm0=result
//
static void emit_stub_strtod(void *ev, void *fn) {
emit_t *e = static_cast<emit_t *>(ev);
emit_stub_prologue(e);
// rdi = host pointer to string
emit_load_ctx_reg(e, X64_RDI, 10); // guest a0
emit_guest_to_host(e, X64_RDI);
emit_call_host(e, fn);
// Result in xmm0 → store to guest fa0
emit_store_ctx_fp(e, CTX_FA0_OFF, 0);
emit_stub_epilogue(e);
emit_intrinsic_return(e);
}
// rv64_fval: guest a0=buf_ptr, guest fa0=double → guest a0=length
// Host: int host_fval(char *buf, double val)
// SysV: rdi=buf, xmm0=val → rax=len
//
static void emit_stub_fval(void *ev, void *fn) {
emit_t *e = static_cast<emit_t *>(ev);
emit_stub_prologue(e);
// rdi = host pointer to output buffer
emit_load_ctx_reg(e, X64_RDI, 10); // guest a0
emit_guest_to_host(e, X64_RDI);
// xmm0 = double value from guest fa0
emit_load_ctx_fp(e, 0, CTX_FA0_OFF);
emit_call_host(e, fn);
// Result (length) in rax → store to guest a0
emit_store_ctx_reg(e, 10, X64_RAX);
emit_stub_epilogue(e);
emit_intrinsic_return(e);
}
// rv64_ftoa_round: guest a0=buf_ptr, guest fa0=double, guest a1=frac → guest a0=length
// Host: int host_ftoa_round(char *buf, double val, int frac)
// SysV: rdi=buf, xmm0=val, esi=frac → rax=len
//
static void emit_stub_ftoa_round(void *ev, void *fn) {
emit_t *e = static_cast<emit_t *>(ev);
emit_stub_prologue(e);
// rdi = host pointer to output buffer
emit_load_ctx_reg(e, X64_RDI, 10); // guest a0
emit_guest_to_host(e, X64_RDI);
// xmm0 = double value from guest fa0
emit_load_ctx_fp(e, 0, CTX_FA0_OFF);
// esi = frac from guest a1
emit_load_ctx_reg(e, X64_RSI, 11); // guest a1
emit_call_host(e, fn);
// Result (length) in rax → store to guest a0
emit_store_ctx_reg(e, 10, X64_RAX);
emit_stub_epilogue(e);
emit_intrinsic_return(e);
}
// double rv64_add_doubles(double *vals, int n): a0=ptr (guest→host), a1=n,
// result in xmm0 → guest fa0.
static void emit_stub_add_doubles(void *ev, void *fn) {
emit_t *e = static_cast<emit_t *>(ev);
emit_stub_prologue(e);
// rdi = host pointer to vals
emit_load_ctx_reg(e, X64_RDI, 10); // guest a0
emit_guest_to_host(e, X64_RDI);
// esi = n
emit_load_ctx_reg(e, X64_RSI, 11); // guest a1
emit_call_host(e, fn);
// Result in xmm0 → store to guest fa0
emit_store_ctx_fp(e, CTX_FA0_OFF, 0);
emit_stub_epilogue(e);
emit_intrinsic_return(e);
}
// ---- Generic co_* intrinsic stub emitter ----
//
// Emits a native CALL to the host co_* function. Arguments are in
// guest a0-a5 (x10-x15), mapped to System V: RDI, RSI, RDX, RCX, R8, R9.
// Pointer arguments (guest addresses) are converted to host via +R12.
// The host function returns size_t in RAX; the stub stores it to guest a0.
//
// The `fn` parameter is the host function pointer (cast to void*),
// passed through the intrinsic_slot_t.host_fn field.
//
// Argument descriptor bits: 1 bit per arg, 0=integer, 1=guest pointer.
// Packed into a uint8_t: bit 0 = arg 0 (a0), bit 1 = arg 1 (a1), etc.
//
static constexpr int x64_arg_regs[6] = {
X64_RDI, X64_RSI, X64_RDX, X64_RCX, X64_R8, X64_R9
};
// Generic emitter for co_* functions, result in a0.
// `fn` is the host function pointer (from the intrinsic_slot_t).
// The nargs and ptr_mask are encoded in the emitter function (one
// emitter per signature pattern).
//
// For nargs <= 6: all args in SysV registers (RDI,RSI,RDX,RCX,R8,R9).
// For nargs > 6: first 6 in registers, overflow pushed onto x86-64 stack.
// The prologue aligns RSP to 16; we sub additional space for stack args
// (padded to 16) and store via MOV [RSP+disp]. The epilogue's
// `mov rsp, rbp` cleans it all up.
//
static void emit_stub_co_generic(void *ev, void *fn,
int nargs, uint8_t ptr_mask) {
emit_t *e = static_cast<emit_t *>(ev);
emit_stub_prologue(e);
int n_reg = (nargs <= 6) ? nargs : 6;
int n_stack = (nargs > 6) ? (nargs - 6) : 0;
// Allocate stack space for overflow arguments (16-byte aligned).
//
if (n_stack > 0) {
int stack_bytes = ((n_stack * 8 + 15) & ~15);
emit_sub_r64_imm(e, X64_RSP, stack_bytes);
// Load overflow args via RAX scratch, store to [RSP + offset].
//
for (int i = nargs - 1; i >= 6; i--) {
emit_load_ctx_reg(e, X64_RAX, 10 + i);
if (ptr_mask & (1 << i)) {
emit_guest_to_host(e, X64_RAX);
}
emit_store_rsp(e, X64_RAX, (i - 6) * 8);
}
}
// Load register args (first 6) in reverse order to avoid clobbering
// RDI/RSI before reading them as ctx pointers.
//
for (int i = n_reg - 1; i >= 0; i--) {
emit_load_ctx_reg(e, x64_arg_regs[i], 10 + i); // guest a0+i
if (ptr_mask & (1 << i)) {
emit_guest_to_host(e, x64_arg_regs[i]);
}
}
emit_call_host(e, fn);
// Store return value (rax) to guest a0.
emit_store_ctx_reg(e, 10, X64_RAX);
emit_stub_epilogue(e);
emit_intrinsic_return(e);
}
// Macro to define a thin emitter wrapper for each co_* signature.
// The wrapper captures the nargs and ptr_mask at compile time.
//
#define DEFINE_CO_EMITTER(name, nargs, ptr_mask) \
static void emit_stub_##name(void *e, void *fn) { \
emit_stub_co_generic(e, fn, nargs, ptr_mask); \
}
// Signature patterns for co_* functions:
// PP_II: 2 pointers, 2 integers → co_first(out,p,len,delim)
// P_II: 1 pointer, 2 integers → co_words_count(p,len,delim)
// PP_III: 2 ptrs, 3 ints → co_mid(out,p,len,start,count)
// co_trim(out,p,len,char,flags)
// co_member(tgt,tlen,list,llen,delim)
// PP_I: 2 ptrs, 1 int → (no current use)
// PPII: 2 ptrs, 2 ints (alt pos) → co_pos(h,hlen,n,nlen)
// co_repeat(out,p,len,count)
// PP_IIII: 2 ptrs, 4 ints → co_delete(out,list,llen,pos,delim,osep)
//
// ptr_mask bits: bit 0=a0, bit 1=a1, etc.
// PP_II: a0=ptr, a1=ptr → 0x03 (bits 0,1)
// P_II: a0=ptr → 0x01
// PPPP_II: a0,a1=ptr → 0x03 (same as PP_II, other args are ints)
// PPP: a0=ptr, a1,a2=int, a3=int, a4=ptr, a5=int → need per-function
//
// Since ptr_mask is per-arg, each function needs its own mask.
// 2 args: co_cluster_count(p, len)
// a0=ptr, a1=int → mask=0x01
DEFINE_CO_EMITTER(co_2p, 2, 0x01)
// 3 args: co_tolower/co_toupper/co_reverse/co_escape(out, p, len)
// a0=ptr, a1=ptr, a2=int → mask=0x03
DEFINE_CO_EMITTER(co_3pp, 3, 0x03)
// 4 args: co_first/co_rest/co_last(out, p, len, delim)
// a0=ptr, a1=ptr, a2=int, a3=int → mask=0x03
DEFINE_CO_EMITTER(co_4pp, 4, 0x03)
// 4 args: co_repeat(out, p, len, count)
// a0=ptr, a1=ptr, a2=int, a3=int → mask=0x03 (same as above)
// (reuse co_4pp)
// 4 args: co_pos(haystack, hlen, needle, nlen)
// a0=ptr, a1=int, a2=ptr, a3=int → mask=0x05
DEFINE_CO_EMITTER(co_pos, 4, 0x05)
// 3 args: co_words_count(p, len, delim)
// a0=ptr, a1=int, a2=int → mask=0x01
DEFINE_CO_EMITTER(co_3p, 3, 0x01)
// 5 args: co_mid(out, p, len, start, count)
// a0=ptr, a1=ptr, a2-a4=int → mask=0x03
DEFINE_CO_EMITTER(co_5pp, 5, 0x03)
// 5 args: co_member(target, tlen, list, llen, delim)
// a0=ptr, a1=int, a2=ptr, a3-a4=int → mask=0x05
DEFINE_CO_EMITTER(co_member, 5, 0x05)
// 5 args: co_trim(out, p, len, trim_char, trim_flags)
// a0=ptr, a1=ptr, a2-a4=int → mask=0x03
// (reuse co_5pp)
// 6 args: co_sort_words(out, list, llen, delim, osep, sort_type)
// a0=ptr, a1=ptr, a2-a5=int → mask=0x03
DEFINE_CO_EMITTER(co_6pp, 6, 0x03)
// 6 args: co_delete(out, list, llen, pos, delim, osep)
// same as co_6pp → mask=0x03
// 7 args: co_extract(out, p, len, iFirst, nWords, delim, osep)
// a0=ptr, a1=ptr, a2-a6=int → mask=0x03
DEFINE_CO_EMITTER(co_7pp, 7, 0x03)
// 8 args: co_setunion(out, list1, len1, list2, len2, delim, osep, sort_type)
// a0=ptr, a1=ptr, a2=int, a3=ptr, a4-a7=int → mask=0x0B (bits 0,1,3)
DEFINE_CO_EMITTER(co_8ppp, 8, 0x0B)
// rv64_alloc(size): 1 integer arg, integer (guest offset) return.
// ptr_mask=0 so neither the arg nor the return is host<->guest converted.
DEFINE_CO_EMITTER(alloc, 1, 0x00)
// Wrapper emitters for the old-style stubs (no host_fn parameter).
//
static void emit_stub_slen_w(void *ev, void *) { emit_stub_slen(static_cast<emit_t *>(ev)); }
static void emit_stub_scopy_w(void *ev, void *) { emit_stub_scopy(static_cast<emit_t *>(ev)); }
static void emit_stub_memcpy_w(void *ev, void *) { emit_stub_memcpy(static_cast<emit_t *>(ev)); }
static void emit_stub_memcmp_w(void *ev, void *) { emit_stub_memcmp(static_cast<emit_t *>(ev)); }
static void emit_stub_memset_w(void *ev, void *) { emit_stub_memset(static_cast<emit_t *>(ev)); }
static void emit_stub_memswap_w(void *ev, void *) { emit_stub_memswap(static_cast<emit_t *>(ev)); }
// try_emit_intrinsic: check if guest_pc matches a known intrinsic.
// If so, emit a complete native stub block and return the code pointer.
// If not, return nullptr (normal translation proceeds).
//
static uint8_t *try_emit_intrinsic(dbt_state_t *dbt, uint64_t guest_pc) {
for (int i = 0; i < dbt->num_intrinsics; i++) {
if (dbt->intrinsics[i].guest_addr == guest_pc) {
// Emit into the code buffer.
uint8_t *block_start = dbt->code_buf + dbt->code_used;
emit_t e;
e.buf = block_start;
e.offset = 0;
e.capacity = CODE_BUF_SIZE - dbt->code_used;
dbt->intrinsics[i].emitter(&e, dbt->intrinsics[i].host_fn);
if (e.offset > e.capacity) return dbt_xlate_full(dbt);
dbt->code_used += e.offset;
dbt->intrinsic_hits++;
dbt_cache_insert(dbt, guest_pc, block_start);
dbt_trace_translate_pc(dbt, guest_pc,
"intrinsic guest_pc=0x%llX bytes=%u slot=%d",
static_cast<unsigned long long>(guest_pc),
e.offset, i);
return block_start;
}
}
return nullptr; // not an intrinsic — not a translate failure
}
// ---------------------------------------------------------------
// Intrinsic registration
// ---------------------------------------------------------------
// Maps dbt_emitter_id → function pointer. The old-style stubs
// (slen, scopy, etc.) ignore the host_fn parameter; the generic
// co_* emitters use it as the CALL target.
//
typedef void (*generic_emitter_fn)(void *e, void *fn);
static generic_emitter_fn s_emitter_table[] = {
emit_stub_slen_w, // DBT_EMIT_SLEN
emit_stub_scopy_w, // DBT_EMIT_SCOPY
emit_stub_memcpy_w, // DBT_EMIT_MEMCPY
emit_stub_memcmp_w, // DBT_EMIT_MEMCMP
emit_stub_memset_w, // DBT_EMIT_MEMSET
emit_stub_memswap_w, // DBT_EMIT_MEMSWAP
emit_stub_co_3p, // DBT_EMIT_CO_3P
emit_stub_co_4pp, // DBT_EMIT_CO_4PP
emit_stub_co_pos, // DBT_EMIT_CO_POS
emit_stub_co_5pp, // DBT_EMIT_CO_5PP
emit_stub_co_member, // DBT_EMIT_CO_MEMBER
emit_stub_co_6pp, // DBT_EMIT_CO_6PP
emit_stub_co_2p, // DBT_EMIT_CO_2P
emit_stub_co_3pp, // DBT_EMIT_CO_3PP
emit_stub_co_7pp, // DBT_EMIT_CO_7PP
emit_stub_co_8ppp, // DBT_EMIT_CO_8PPP
emit_stub_fp_d_d, // DBT_EMIT_FP_D_D
emit_stub_fp_dd_d, // DBT_EMIT_FP_DD_D
emit_stub_strtod, // DBT_EMIT_STRTOD
emit_stub_fval, // DBT_EMIT_FVAL
emit_stub_ftoa_round, // DBT_EMIT_FTOA_ROUND
emit_stub_alloc, // DBT_EMIT_ALLOC
emit_stub_add_doubles, // DBT_EMIT_ADD_DOUBLES
};
void dbt_register_intrinsic(dbt_state_t *dbt, uint64_t guest_addr,
dbt_emitter_id emitter_id, void *host_fn) {
if (dbt->num_intrinsics >= dbt_state_t::MAX_INTRINSICS) {
// A silent drop here disabled the FP-conversion intrinsics and
// surfaced as "all float math returns empty" (#778). The
// registration count is fixed at build time (pretranslate_tier2),
// so overflow is a developer error: fail loudly at startup
// instead of miscompiling every affected function.
fprintf(stderr, "dbt_register_intrinsic: intrinsic table overflow "
"(MAX_INTRINSICS=%d); raise the cap in dbt.h\n",
dbt_state_t::MAX_INTRINSICS);
abort();
}
if (!guest_addr) return;
auto &slot = dbt->intrinsics[dbt->num_intrinsics++];
slot.guest_addr = guest_addr;
slot.emitter = s_emitter_table[emitter_id];
slot.host_fn = host_fn;
}
// ---------------------------------------------------------------
// Return Address Stack (RAS) helpers
// ---------------------------------------------------------------
// Emit inline RAS push: ras[ras_top++ & MASK] = return_addr.
// Clobbers RAX, RCX, RDX.
//
static void emit_ras_push(emit_t *e, uint64_t return_addr) {
// mov eax, [rbx + CTX_RAS_TOP_OFF] — load ras_top (32-bit)
emit_byte(e, 0x8B);
emit_byte(e, modrm(0x02, X64_RAX, X64_RBX));
emit_u32(e, CTX_RAS_TOP_OFF);
// mov ecx, eax ; and ecx, RAS_MASK — index
emit_mov_r64(e, X64_RCX, X64_RAX);
emit_and_r64_imm(e, X64_RCX, RAS_MASK);
// mov qword [rbx + CTX_RAS_OFF + rcx*8], return_addr
// Use: lea rdx, [rbx + CTX_RAS_OFF]; mov [rdx + rcx*8], imm
// Actually, simpler: compute offset = CTX_RAS_OFF + rcx*8 in rdx
emit_shl_r64_imm(e, X64_RCX, 3); // rcx *= 8
emit_add_r64_imm(e, X64_RCX, CTX_RAS_OFF);
// mov rdx, return_addr
emit_mov_r64_imm32(e, X64_RDX, static_cast<int32_t>(return_addr));
// mov [rbx + rcx], rdx
emit_byte(e, rex(1, reg_hi(X64_RDX), 0, 0));
emit_byte(e, 0x89);
emit_byte(e, modrm(0x01, X64_RDX, 0x04)); // SIB follows
emit_byte(e, static_cast<uint8_t>((reg_lo(X64_RCX) << 3) | reg_lo(X64_RBX)));
emit_byte(e, 0x00); // disp8 = 0
// inc dword [rbx + CTX_RAS_TOP_OFF]
emit_add_r64_imm(e, X64_RAX, 1);
emit_byte(e, 0x89); // mov [rbx + CTX_RAS_TOP_OFF], eax (32-bit)
emit_byte(e, modrm(0x02, X64_RAX, X64_RBX));
emit_u32(e, CTX_RAS_TOP_OFF);
}
// Emit inline RAS pop + block cache probe for JALR returns.
// RCX holds the actual computed target. Clobbers RAX, RDX.
// On RAS hit + cache hit: jumps directly to native code (no ret).
// On miss: falls through to emit_exit_indirect.
//
static void emit_ras_pop_and_probe(emit_t *e, dbt_state_t *dbt) {
// dec dword [rbx + CTX_RAS_TOP_OFF]
emit_byte(e, 0x8B); // mov eax, [rbx + CTX_RAS_TOP_OFF]
emit_byte(e, modrm(0x02, X64_RAX, X64_RBX));
emit_u32(e, CTX_RAS_TOP_OFF);
emit_add_r64_imm(e, X64_RAX, -1);
emit_byte(e, 0x89); // mov [rbx + CTX_RAS_TOP_OFF], eax
emit_byte(e, modrm(0x02, X64_RAX, X64_RBX));
emit_u32(e, CTX_RAS_TOP_OFF);
// Load predicted: rdx = ras[eax & RAS_MASK]
emit_and_r64_imm(e, X64_RAX, RAS_MASK);
emit_shl_r64_imm(e, X64_RAX, 3); // * 8
emit_add_r64_imm(e, X64_RAX, CTX_RAS_OFF);
// mov rdx, [rbx + rax]
emit_byte(e, rex(1, reg_hi(X64_RDX), 0, 0));
emit_byte(e, 0x8B);
emit_byte(e, modrm(0x01, X64_RDX, 0x04)); // SIB
emit_byte(e, static_cast<uint8_t>((reg_lo(X64_RAX) << 3) | reg_lo(X64_RBX)));
emit_byte(e, 0x00); // disp8 = 0
// Compare actual (RCX) vs predicted (RDX)
emit_cmp_r64(e, X64_RCX, X64_RDX);
uint32_t jne_miss = emit_jcc_rel32(e, JCC_NE);
// RAS hit: inline block cache probe (4-way set-associative).
// set = ((rcx >> 2) ^ ((rcx >> 2) >> 10)) & BLOCK_CACHE_MASK
// base_offset = set * WAYS * 16 = set * 64
emit_mov_r64(e, X64_RAX, X64_RCX);
emit_shr_r64_imm(e, X64_RAX, 2);
emit_mov_r64(e, X64_RDX, X64_RAX);
emit_shr_r64_imm(e, X64_RDX, 10);
// xor rax, rdx
emit_byte(e, rex(1, reg_hi(X64_RAX), 0, 0));
emit_byte(e, 0x33);
emit_byte(e, modrm(0x03, X64_RAX, X64_RDX));
emit_and_r64_imm(e, X64_RAX, static_cast<int32_t>(BLOCK_CACHE_MASK));
// rax = set * 64 (4 ways × 16 bytes each)
emit_shl_r64_imm(e, X64_RAX, 6);
// Probe 4 ways: check [r13 + rax + way*16].guest_pc == rcx
uint32_t jmp_hits[BLOCK_CACHE_WAYS];
uint32_t jne_misses[BLOCK_CACHE_WAYS];
for (size_t w = 0; w < BLOCK_CACHE_WAYS; w++) {
int32_t disp = static_cast<int32_t>(w * 16);
// cmp [r13 + rax + disp], rcx
emit_byte(e, rex(1, reg_hi(X64_RCX), reg_hi(X64_RAX), 1));
emit_byte(e, 0x3B);
if (disp == 0) {
emit_byte(e, modrm(0x01, X64_RCX, 0x04)); // SIB, disp8
emit_byte(e, static_cast<uint8_t>((reg_lo(X64_RAX) << 3) | reg_lo(X64_R13)));
emit_byte(e, 0x00);
} else {
emit_byte(e, modrm(0x01, X64_RCX, 0x04)); // SIB, disp8
emit_byte(e, static_cast<uint8_t>((reg_lo(X64_RAX) << 3) | reg_lo(X64_R13)));
emit_byte(e, static_cast<uint8_t>(disp));
}
jne_misses[w] = emit_jcc_rel32(e, JCC_NE);
// Hit: load native_code from [r13 + rax + disp + 8]
emit_byte(e, rex(1, reg_hi(X64_RDX), reg_hi(X64_RAX), 1));
emit_byte(e, 0x8B);
emit_byte(e, modrm(0x01, X64_RDX, 0x04)); // SIB, disp8
emit_byte(e, static_cast<uint8_t>((reg_lo(X64_RAX) << 3) | reg_lo(X64_R13)));
emit_byte(e, static_cast<uint8_t>(disp + 8));
emit_test_r64(e, X64_RDX, X64_RDX);
uint32_t jz_skip = emit_jcc_rel32(e, JCC_E);
// jmp rdx — direct to native code!
emit_byte(e, 0xFF);
emit_byte(e, modrm(0x03, 4, X64_RDX)); // jmp rdx
dbt->ras_hits++;
// Null native_code: fall through to next way.
emit_patch_rel32(e, jz_skip, emit_pos(e));
// Patch miss to next way's check.
emit_patch_rel32(e, jne_misses[w], emit_pos(e));
}
// All 4 ways missed: fall through to indirect exit.
emit_patch_rel32(e, jne_miss, emit_pos(e));
dbt->ras_misses++;
}
static bool try_emit_inline_call(emit_t *e, reg_cache_t *rc, fp_cache_t *fc,
dbt_state_t *dbt,
uint64_t target_pc, block_entry_t *callee,
uint64_t return_pc,
side_exit_t *side_exits,
int *num_side_exits) {
(void)target_pc;
if (!callee || *num_side_exits >= MAX_SIDE_EXITS) return false;
// Flush cached registers — callee reads from ctx.
rc_flush(e, rc);
fc_flush(e, fc);
// Store ra = return_pc in ctx (callee's JALR reads this).
emit_mov_r64_imm32(e, X64_RAX, static_cast<int32_t>(return_pc));
emit_store_guest(e, 1, X64_RAX);
// Poison the RAS so the callee's pop_and_probe mismatches and falls
// through to RET, returning to this native CALL site.
emit_ras_push(e, 1ULL);
// Native CALL to the translated target.
uint32_t call_patch = emit_call_rel32(e);
uint32_t target_off = static_cast<uint32_t>(callee->native_code - e->buf);
emit_patch_rel32(e, call_patch, target_off);
// If ctx.next_pc != return_pc, the callee exited early and we must
// fall back to the dispatch loop through a cold side-exit stub.
emit_cmp_ctx_imm32(e, CTX_NEXT_PC_OFF, static_cast<int32_t>(return_pc));
uint32_t jne_cold = emit_jcc_rel32(e, JCC_NE);
// Hot path: callee returned normally. Reload register cache because
// the callee may have modified any guest register. The FP cache must
// likewise be invalidated so the caller reloads FP results (e.g. an
// intrinsic's double return in fa0) from ctx instead of stale XMM regs.
rc_invalidate_reload(e, rc);
fc_invalidate(fc);
side_exits[*num_side_exits].jcc_patch = jne_cold;
side_exits[*num_side_exits].target_pc = 0; // sentinel
side_exits[*num_side_exits].expected_next_pc = return_pc;
(*num_side_exits)++;
dbt->inline_calls++;
return true;
}
static direct_jalr_flow_t emit_direct_jalr_flow(
emit_t *e, reg_cache_t *rc, fp_cache_t *fc, dbt_state_t *dbt,
uint64_t guest_pc, uint64_t pc, uint64_t target_pc, uint64_t return_pc,
const rv64_insn_t &next, side_exit_t *side_exits, int *num_side_exits,
bool can_tail_inline, const char *call_trace, const char *tail_trace,
const char *exit_trace) {
if (next.rd == 1) {
block_entry_t *be = dbt_cache_lookup(dbt, target_pc);
if (try_emit_inline_call(e, rc, fc, dbt, target_pc, be, return_pc,
side_exits, num_side_exits)) {
dbt_trace_fusion(dbt, pc, call_trace);
return direct_jalr_flow_t::inline_call_done;
}
emit_ras_push(e, return_pc);
}
if (next.rd == 0 && can_tail_inline) {
dbt_trace_fusion(dbt, pc, tail_trace);
return direct_jalr_flow_t::tail_call;
}
rc_flush(e, rc);
fc_flush(e, fc);
emit_exit_chained(e, dbt, target_pc);
dbt_trace_fusion(dbt, pc, exit_trace);
return direct_jalr_flow_t::chained_exit;
}
// ---------------------------------------------------------------
// Block chaining helpers
// ---------------------------------------------------------------
// Backpatch a JMP rel32 in the code buffer to point to a new target.
//
void dbt_backend_backpatch_jmp(uint8_t *code_buf, uint32_t jmp_disp_offset,
uint8_t *target) {
// Refuse OOB patches from stale/failed translate sites (#1147).
if (static_cast<size_t>(jmp_disp_offset) + 4 > CODE_BUF_SIZE) {
return;
}
int32_t disp = static_cast<int32_t>(
target - (code_buf + jmp_disp_offset + 4));
memcpy(code_buf + jmp_disp_offset, &disp, 4);
}
uint32_t dbt_backend_decode_jmp_target(const uint8_t *code_buf,
uint32_t jmp_disp_offset) {
// rel32: the displacement is in bytes and is measured from the end
// of the displacement field.
//
int32_t disp;
memcpy(&disp, code_buf + jmp_disp_offset, 4);
return jmp_disp_offset + 4 + static_cast<uint32_t>(disp);
}
// ---------------------------------------------------------------
// Block exit helpers
// ---------------------------------------------------------------
// Emit a chained exit: if target block is already translated, emit a
// direct JMP. Otherwise emit JMP to a slow-path stub and record a
// patch site for backpatching when the target is later translated.
//
static void emit_exit_chained(emit_t *e, dbt_state_t *dbt,
uint64_t target_pc) {
// A back-edge chains only through the loop-budget countdown (#1571,
// #1741). Chaining it bare would send the exit straight into native
// code with max_dispatch / alarm_flag -- polled only at the top of
// dbt_run's dispatch loop -- watched by nothing. The countdown is
// the same bargain the self-loop warm path already made:
// DBT_LOOP_BUDGET iterations in native code, then one dispatcher
// visit to poll and refill. Bailing to the trampoline on EVERY
// back edge instead is what priced compiled loops at ~7x the Lua
// VM's marginal per-iteration cost (#1741's first measurement).
if ( dbt->translating_pc != DBT_NO_TRANSLATION
&& target_pc <= dbt->translating_pc) {
emit_loop_budget_check(e, target_pc);
// Budget remains: fall through and chain like any forward exit.
}
// Check if target is already translated (4-way lookup).
block_entry_t *be = dbt_cache_lookup(dbt, target_pc);
bool known = (be != nullptr);
// Emit JMP rel32.
emit_byte(e, 0xE9);
uint32_t jmp_patch = emit_pos(e);
emit_u32(e, 0); // placeholder
if (known) {
// Target already translated — patch JMP to go directly there.
// Compute target offset relative to e->buf for emit_patch_rel32.
uint32_t target_off = static_cast<uint32_t>(
be->native_code - e->buf);
emit_patch_rel32(e, jmp_patch, target_off);
} else {
// Record patch site for backpatching. Store offset relative to
// code_buf (not e->buf) since backpatch_jmp uses code_buf base.
uint32_t abs_offset = static_cast<uint32_t>(
e->buf - dbt->code_buf) + jmp_patch;
// Slow-path stub: store next_pc and return to trampoline.
uint32_t stub_pos = emit_pos(e);
emit_patch_rel32(e, jmp_patch, stub_pos);
// Record patch site (after stub_pos is known).
uint32_t stub_abs = static_cast<uint32_t>(
e->buf - dbt->code_buf) + stub_pos;
size_t patch_index = dbt->patches.size();
dbt->patches.push_back(
patch_site_t{abs_offset, stub_abs, target_pc});
dbt->pending_patch_targets[target_pc].push_back(patch_index);
dbt->chain_misses++;
emit_exit_with_pc(e, target_pc);
}
}
// ---------------------------------------------------------------
// Translate a single block
// ---------------------------------------------------------------
uint8_t *dbt_backend_translate_block(dbt_state_t *dbt, uint64_t guest_pc) {
// Check for intrinsic recognition before normal translation.
// If guest_pc is a known intrinsic, emit a native stub block.
//
uint8_t *intrinsic = try_emit_intrinsic(dbt, guest_pc);
if (intrinsic) return intrinsic;
// Snapshot patch table so emit_exit_chained sites from a failed
// emit can be rolled back (#1147).
const size_t patches_before = dbt->patches.size();
uint8_t *block_start = dbt->code_buf + dbt->code_used;
emit_t e;
e.buf = block_start;
e.offset = 0;
e.capacity = CODE_BUF_SIZE - dbt->code_used;
reg_cache_t rc;
rc_init_pinned(&rc);
fp_cache_t fc;
fc_init(&fc);
// Self-loop detection: pre-scan to find if any branch targets start_pc.
// If so, pre-warm the register cache and record a warm_entry point.
// The back-edge jumps to warm_entry (inside this block), keeping the
// entire loop in one contiguous x86-64 block.
//
uint32_t warm_entry = 0;
bool self_loop = false;
{
uint64_t scan_pc = guest_pc;
int used[32] = {0}; // sources read early — preload candidates
int referenced[32] = {0}; // sources + destinations — slot pressure
bool past_first_branch = false;
bool body_has_call = false; // a call evicts the whole cache (#2019)
for (int i = 0; i < MAX_BLOCK_INSNS && dbt_guest_range_ok(scan_pc, 4, dbt->memory_size); i++) {
uint32_t w;
memcpy(&w, dbt->memory + scan_pc, 4);
rv64_insn_t si;
rv64_decode(w, &si);
// Preload candidates are the registers read before the first
// branch. Slot pressure is every register the body touches:
// the back edge re-enters at warm_entry, which is *past* the
// preload, so any eviction of a preloaded slot inside the body
// leaves the second and later iterations reading a host
// register that now holds some other guest register. Counting
// only the pre-branch prefix under-reports that pressure and
// admits exactly the loops that go wrong.
if (!past_first_branch) rc_mark_used(si, used);
rc_mark_referenced(si, referenced);
if (si.opcode == OP_BRANCH) {
uint64_t target = scan_pc + static_cast<int64_t>(si.imm);
if (target == guest_pc) {
self_loop = true;
break;
}
if (si.imm < 0) break; // backward branch elsewhere
past_first_branch = true;
// Follow the fall-through, not the target: for a forward
// branch inside a self-loop the emitter records the taken
// path as a cold side exit and continues translating the
// fall-through inline, so the fall-through is what lands
// in the loop body and what determines slot pressure.
scan_pc += 4;
continue;
}
if (si.opcode == OP_JAL) {
if (si.rd != 0) {
// JAL ra, target — function call. The call returns to
// pc+4, so for loop detection purposes, skip past it.
// The callee's register usage is not scanned, so the
// slot-pressure count below cannot see it (#2019).
body_has_call = true;
past_first_branch = true;
scan_pc += 4;
continue;
}
uint64_t target = scan_pc + static_cast<int64_t>(si.imm);
if (target == guest_pc) {
self_loop = true;
break;
}
if (si.imm < 0) break; // backward jump elsewhere
// Forward unconditional jump: follow target.
if (si.imm > 0 && dbt_guest_range_ok(target, 4, dbt->memory_size)) {
past_first_branch = true;
scan_pc = target;
continue;
}
break;
}
if (si.opcode == OP_JALR) {
// Skip past returns (JALR x0, ra, 0) — they return
// from Tier 2 calls back to the next instruction.
if (si.rd == 0 && si.rs1 == 1 && si.imm == 0) {
scan_pc += 4;
continue;
}
break; // other indirect jumps — stop
}
if (si.opcode == OP_SYSTEM)
break;
scan_pc += 4;
}
// If the loop over-commits the register cache, fall back to ordinary
// per-iteration dispatch (see rc_loop_overcommits in dbt_internal.h).
if (self_loop && rc_loop_overcommits(referenced, rc_pinned_guest, RC_NUM_PINNED)) {
self_loop = false;
}
// A call in the body is an eviction of everything (#2019). The
// scan walks over a JAL ra without counting the callee, and
// try_emit_inline_call ends in rc_invalidate_reload, so the
// non-pinned host registers the preload wrote hold the callee's
// values by the time the back edge re-enters at warm_entry —
// which is past the preload. The second iteration then reads a
// host register believing it still holds the preloaded guest
// register. Slot pressure cannot model that; refuse the warm
// superblock and take ordinary per-iteration dispatch.
if (self_loop && body_has_call) {
self_loop = false;
}
if (self_loop) {
// Pre-load frequently used registers. Since we flush at the
// back-edge (register mapping may diverge through complex loop
// bodies), warm_entry only avoids loading from cold context on
// the first entry. Still beneficial: the x86-64 loads at
// warm_entry become the reload targets for subsequent iterations.
int loaded = 0;
for (int r = 1; r < 32 && loaded < RC_NUM_SLOTS; r++) {
if (used[r]) { rc_read(&e, &rc, r); loaded++; }
}
// Align warm_entry to 32-byte boundary (absolute address).
// The backward branch targets this point every iteration —
// good alignment avoids instruction decode stalls from
// cache line crossings.
uintptr_t abs_cur = reinterpret_cast<uintptr_t>(e.buf) + emit_pos(&e);
uint32_t pad_needed = ((abs_cur + 31) & ~(uintptr_t)31) - abs_cur;
uint32_t target_pos = emit_pos(&e) + pad_needed;
// Use multi-byte NOPs for padding.
while (emit_pos(&e) < target_pos) {
uint32_t pad = target_pos - emit_pos(&e);
if (pad >= 8) {
// 8-byte NOP: 0F 1F 84 00 00 00 00 00
emit_byte(&e, 0x0F); emit_byte(&e, 0x1F);
emit_byte(&e, 0x84); emit_byte(&e, 0x00);
emit_byte(&e, 0x00); emit_byte(&e, 0x00);
emit_byte(&e, 0x00); emit_byte(&e, 0x00);
} else if (pad >= 4) {
// 4-byte NOP: 0F 1F 40 00
emit_byte(&e, 0x0F); emit_byte(&e, 0x1F);
emit_byte(&e, 0x40); emit_byte(&e, 0x00);
} else if (pad >= 2) {
// 2-byte NOP: 66 90
emit_byte(&e, 0x66); emit_byte(&e, 0x90);
} else {
// 1-byte NOP: 90
emit_byte(&e, 0x90);
}
}
warm_entry = emit_pos(&e);
// Bound the loop (#1571). Every back-edge targets warm_entry, so
// one check here covers them all; without it the block spins in
// native code and dbt_run's dispatch loop -- the only place
// max_dispatch and alarm_flag are polled -- is never reached again.
emit_loop_budget_check(&e, guest_pc);
}
}
side_exit_t side_exits[MAX_SIDE_EXITS];
int num_side_exits = 0;
uint64_t pc = guest_pc;
int count = 0;
uint64_t fused_before = dbt->insns_fused;
uint64_t inline_calls_before = dbt->inline_calls;
while (count < MAX_BLOCK_INSNS) {
// #1864: overflow-safe fetch bound (not `pc + 4 > memory_size`).
//
if (!dbt_guest_range_ok(pc, 4, dbt->memory_size)) {
rc_flush(&e, &rc); fc_flush(&e, &fc);
emit_exit_chained(&e, dbt, pc);
break;
}
uint32_t word;
memcpy(&word, dbt->memory + pc, 4);
rv64_insn_t insn;
rv64_decode(word, &insn);
count++;
// -- Peek-ahead for instruction fusion --
//
// Decode the next instruction if available, for LUI+ADDI,
// AUIPC+ADDI, and AUIPC+JALR fusion patterns.
//
rv64_insn_t next;
bool have_next = false;
if (dbt_guest_range_ok(pc, 8, dbt->memory_size)) {
uint32_t next_word;
memcpy(&next_word, dbt->memory + pc + 4, 4);
rv64_decode(next_word, &next);
have_next = true;
}
if (dbt_trace_translate_enabled(dbt, guest_pc)) {
const char *n = "?";
switch(insn.opcode) {
case OP_LUI:n="LUI";break; case OP_AUIPC:n="AUI";break;
case OP_JAL:n="JAL";break; case OP_JALR:n="JLR";break;
case OP_BRANCH:n="BRN";break; case OP_LOAD:n="LD";break;
case OP_STORE:n="SD";break; case OP_IMM:n="IMM";break;
case OP_REG:n="REG";break; case OP_SYSTEM:n="SYS";break;
}
dbt_trace_translate_pc(dbt, guest_pc,
"insn #%d pc=0x%llX %s rd=%d rs1=%d rs2=%d imm=%d",
count, (unsigned long long)pc, n, insn.rd, insn.rs1, insn.rs2, insn.imm);
}
// Fusion: SLT/SLTI/SLTU/SLTIU + BEQ/BNE against x0.
// This preserves rd (if nonzero) but reuses the original compare
// flags to branch directly, avoiding a redundant test of rd.
if (have_next
&& ((insn.opcode == OP_REG && insn.funct7 == 0
&& (insn.funct3 == ALU_SLT || insn.funct3 == ALU_SLTU))
|| (insn.opcode == OP_IMM
&& (insn.funct3 == ALU_SLTI || insn.funct3 == ALU_SLTIU)))
&& next.opcode == OP_BRANCH
&& (next.funct3 == 0 || next.funct3 == 1)
&& ((next.rs1 == insn.rd && next.rs2 == 0)
|| (next.rs2 == insn.rd && next.rs1 == 0))) {
uint64_t branch_pc = pc + 4;
uint64_t target = branch_pc + static_cast<int64_t>(next.imm);
bool is_unsigned = (insn.opcode == OP_REG)
? (insn.funct3 == ALU_SLTU)
: (insn.funct3 == ALU_SLTIU);
if (insn.opcode == OP_REG) {
int rs1 = rc_read(&e, &rc, insn.rs1);
int rs2 = rc_read(&e, &rc, insn.rs2);
emit_cmp_r64(&e, rs1, rs2);
} else {
int rs1 = rc_read(&e, &rc, insn.rs1);
emit_cmp_r64_imm(&e, rs1, insn.imm);
}
if (insn.rd) {
int rd = rc_write(&e, &rc, insn.rd);
emit_setcc(&e, is_unsigned ? SETCC_B : SETCC_L, rd);
emit_movzx_r64_r8(&e, rd, rd);
}
uint8_t cc;
if (next.funct3 == 1) {
cc = is_unsigned ? JCC_B : JCC_L;
} else {
cc = is_unsigned ? JCC_AE : JCC_GE;
}
// Diamond merge: fuse the following branch-over-one pattern
// into a conditional move while preserving the SLT result in rd.
if (next.imm == 8 && dbt_guest_range_ok(pc, 12, dbt->memory_size)) {
uint32_t skip_word;
memcpy(&skip_word, dbt->memory + pc + 8, 4);
rv64_insn_t skip;
rv64_decode(skip_word, &skip);
uint8_t cmov_cc;
if (next.funct3 == 1) {
cmov_cc = is_unsigned ? CMOV_AE : CMOV_GE;
} else {
cmov_cc = is_unsigned ? CMOV_B : CMOV_L;
}
bool can_predicate = false;
if (skip.opcode == OP_IMM && skip.rd != 0
&& (skip.funct3 == ALU_ADDI || skip.funct3 == ALU_XORI
|| skip.funct3 == ALU_ORI || skip.funct3 == ALU_ANDI)) {
can_predicate = true;
}
if (skip.opcode == OP_REG && skip.rd != 0
&& skip.funct7 != 0x01
&& (skip.funct3 == ALU_ADD || skip.funct3 == ALU_XOR
|| skip.funct3 == ALU_OR || skip.funct3 == ALU_AND)) {
can_predicate = true;
}
if (skip.opcode == OP_LUI && skip.rd != 0) {
can_predicate = true;
}
if (can_predicate) {
// Compute the skipped instruction result first; it may
// clobber flags, so the SLT compare comes afterward.
if (skip.opcode == OP_LUI) {
emit_mov_r64_imm32(&e, X64_RCX, skip.imm);
} else if (skip.opcode == OP_IMM) {
int hr_src = rc_read(&e, &rc, skip.rs1);
emit_mov_r64(&e, X64_RCX, hr_src);
switch (skip.funct3) {
case ALU_ADDI: emit_add_r64_imm(&e, X64_RCX, skip.imm); break;
case ALU_XORI: emit_xor_r64_imm(&e, X64_RCX, skip.imm); break;
case ALU_ORI: emit_or_r64_imm(&e, X64_RCX, skip.imm); break;
case ALU_ANDI: emit_and_r64_imm(&e, X64_RCX, skip.imm); break;
}
} else {
int hr_s1 = rc_read(&e, &rc, skip.rs1);
int hr_s2 = rc_read(&e, &rc, skip.rs2);
emit_mov_r64(&e, X64_RCX, hr_s1);
switch (skip.funct3) {
case ALU_ADD:
if (skip.funct7 == 0x20) {
emit_sub_r64(&e, X64_RCX, hr_s2);
} else {
emit_add_r64(&e, X64_RCX, hr_s2);
}
break;
case ALU_XOR: emit_xor_r64_op(&e, X64_RCX, hr_s2); break;
case ALU_OR: emit_or_r64(&e, X64_RCX, hr_s2); break;
case ALU_AND: emit_and_r64(&e, X64_RCX, hr_s2); break;
}
}
if (insn.opcode == OP_REG) {
int rs1 = rc_read(&e, &rc, insn.rs1);
int rs2 = rc_read(&e, &rc, insn.rs2);
emit_cmp_r64(&e, rs1, rs2);
} else {
int rs1 = rc_read(&e, &rc, insn.rs1);
emit_cmp_r64_imm(&e, rs1, insn.imm);
}
if (insn.rd) {
int rd = rc_write(&e, &rc, insn.rd);
emit_setcc(&e, is_unsigned ? SETCC_B : SETCC_L, rd);
emit_movzx_r64_r8(&e, rd, rd);
}
int hr_rd = rc_read(&e, &rc, skip.rd);
emit_cmovcc(&e, cmov_cc, hr_rd, X64_RCX);
int slot = rc_find(&rc, skip.rd);
if (slot >= 0) {
rc.slots[slot].dirty = 1;
rc.slots[slot].last_use = ++rc.clock;
}
dbt_trace_fusion(dbt, pc, "slt_branch_diamond");
pc += 12;
count += 2;
dbt->insns_fused++;
continue;
}
}
if (self_loop && target == guest_pc) {
rc_flush(&e, &rc); fc_flush(&e, &fc);
uint32_t jcc_patch = emit_jcc_rel32(&e, cc);
emit_patch_rel32(&e, jcc_patch, warm_entry);
emit_exit_chained(&e, dbt, branch_pc + 4);
dbt->insns_fused++;
count++;
goto done;
}
// Superblock SLT+branch forward side exit. FP flush: #1338.
//
if (self_loop && next.imm > 0
&& num_side_exits < MAX_SIDE_EXITS
&& count < MAX_BLOCK_INSNS - 5) {
fc_flush(&e, &fc);
uint32_t jcc_patch = emit_jcc_rel32(&e, cc);
side_exits[num_side_exits].jcc_patch = jcc_patch;
side_exits[num_side_exits].target_pc = target;
side_exits[num_side_exits].expected_next_pc = 0;
memcpy(side_exits[num_side_exits].snapshot, rc.slots,
sizeof(rc.slots));
num_side_exits++;
pc = branch_pc + 4;
count++;
dbt->insns_fused++;
continue;
}
uint32_t jcc_patch = emit_jcc_rel32(&e, cc);
rc_flush(&e, &rc); fc_flush(&e, &fc);
emit_exit_chained(&e, dbt, branch_pc + 4);
emit_patch_rel32(&e, jcc_patch, emit_pos(&e));
rc_flush(&e, &rc); fc_flush(&e, &fc);
emit_exit_chained(&e, dbt, target);
dbt_trace_fusion(dbt, pc, "slt_branch");
dbt->insns_fused++;
count++;
goto done;
}
// Fusion: LUI/AUIPC + LOAD/STORE with statically known address.
if (have_next
&& (insn.opcode == OP_LUI || insn.opcode == OP_AUIPC)
&& insn.rd) {
int64_t base = (insn.opcode == OP_AUIPC)
? static_cast<int64_t>(pc)
: 0;
int64_t addr64 = base + static_cast<int64_t>(insn.imm)
+ static_cast<int64_t>(next.imm);
if (addr64 >= INT32_MIN && addr64 <= INT32_MAX) {
int32_t addr = static_cast<int32_t>(addr64);
if (next.opcode == OP_LOAD && next.rs1 == insn.rd) {
// Preserve the address in rd when the load writes elsewhere.
if (insn.rd != next.rd) {
int au_rd = rc_write(&e, &rc, insn.rd);
if (base == 0 && insn.imm >= INT32_MIN && insn.imm <= INT32_MAX) {
emit_mov_r64_imm32(&e, au_rd, insn.imm);
} else {
emit_mov_r64_imm64(&e, au_rd,
static_cast<uint64_t>(base + static_cast<int64_t>(insn.imm)));
}
}
emit_xor_r64(&e, X64_RAX, X64_RAX);
int rd = next.rd ? rc_write(&e, &rc, next.rd) : X64_RAX;
switch (next.funct3) {
case 0: emit_load_mem8s(&e, rd, X64_RAX, addr); break; // LB
case 1: emit_load_mem16s(&e, rd, X64_RAX, addr); break; // LH
case 2: emit_load_mem32s(&e, rd, X64_RAX, addr); break; // LW
case 3: emit_load_mem64(&e, rd, X64_RAX, addr); break; // LD
case 4: emit_load_mem8u(&e, rd, X64_RAX, addr); break; // LBU
case 5: emit_load_mem16u(&e, rd, X64_RAX, addr); break; // LHU
case 6: emit_load_mem32(&e, rd, X64_RAX, addr); break; // LWU
default: goto no_addr_fusion;
}
dbt_trace_fusion(dbt, pc,
insn.opcode == OP_AUIPC ? "auipc_load" : "lui_load");
pc += 8;
count++;
dbt->insns_fused++;
continue;
}
if (next.opcode == OP_STORE && next.rs1 == insn.rd) {
// Store does not overwrite rd, so preserve the computed address.
int au_rd = rc_write(&e, &rc, insn.rd);
if (base == 0 && insn.imm >= INT32_MIN && insn.imm <= INT32_MAX) {
emit_mov_r64_imm32(&e, au_rd, insn.imm);
} else {
emit_mov_r64_imm64(&e, au_rd,
static_cast<uint64_t>(base + static_cast<int64_t>(insn.imm)));
}
int rs2 = next.rs2 ? rc_read(&e, &rc, next.rs2) : X64_RAX;
if (next.rs2 == 0) {
emit_xor_r64(&e, X64_RDX, X64_RDX);
rs2 = X64_RDX;
}
emit_xor_r64(&e, X64_RAX, X64_RAX);
switch (next.funct3) {
case 0: emit_store_mem8(&e, X64_RAX, rs2, addr); break; // SB
case 1: emit_store_mem16(&e, X64_RAX, rs2, addr); break; // SH
case 2: emit_store_mem32(&e, X64_RAX, rs2, addr); break; // SW
case 3: emit_store_mem64(&e, X64_RAX, rs2, addr); break; // SD
default: goto no_addr_fusion;
}
dbt_trace_fusion(dbt, pc,
insn.opcode == OP_AUIPC ? "auipc_store" : "lui_store");
pc += 8;
count++;
dbt->insns_fused++;
continue;
}
}
}
no_addr_fusion:
switch (insn.opcode) {
// -- LUI (with LUI+ADDI fusion) --
//
case OP_LUI: {
// Fusion: LUI rd, upper + JALR rs1=rd → direct jump/call
uint64_t target_u64;
uint64_t return_pc;
if (have_next
&& dbt_resolve_direct_jalr_target(pc, insn, next, &target_u64,
&return_pc)) {
int64_t target = static_cast<int64_t>(target_u64);
// Materialize the LUI result if JALR writes a different rd.
if (insn.rd != next.rd && insn.rd != 0) {
int rd = rc_write(&e, &rc, insn.rd);
emit_mov_r64_imm32(&e, rd, insn.imm);
}
if (next.rd) {
int rd = rc_write(&e, &rc, next.rd);
emit_mov_r64_imm32(&e, rd,
static_cast<int32_t>(return_pc));
}
bool can_tail_inline =
next.rd == 0
&& !(target >= static_cast<int64_t>(guest_pc)
&& target <= static_cast<int64_t>(pc + 4))
&& count < MAX_BLOCK_INSNS - 4;
switch (emit_direct_jalr_flow(&e, &rc, &fc, dbt, guest_pc, pc,
target_u64, return_pc, next,
side_exits, &num_side_exits,
can_tail_inline,
"lui_jalr_call",
"lui_jalr_tail",
"lui_jalr")) {
case direct_jalr_flow_t::tail_call:
pc = static_cast<uint64_t>(target);
count++;
dbt->insns_fused++;
continue;
case direct_jalr_flow_t::inline_call_done:
case direct_jalr_flow_t::chained_exit:
count++;
dbt->insns_fused++;
goto done;
}
}
// Fusion: LUI rd, upper + ADDI rd, rd, lower → MOV rd, imm32
if (have_next && insn.rd
&& next.opcode == OP_IMM && next.funct3 == ALU_ADDI
&& next.rd == insn.rd && next.rs1 == insn.rd) {
int64_t val = static_cast<int64_t>(insn.imm)
+ static_cast<int64_t>(next.imm);
int rd = rc_write(&e, &rc, insn.rd);
if (val >= INT32_MIN && val <= INT32_MAX) {
emit_mov_r64_imm32(&e, rd, static_cast<int32_t>(val));
} else {
emit_mov_r64_imm64(&e, rd, static_cast<uint64_t>(val));
}
dbt_trace_fusion(dbt, pc, "lui_addi");
pc += 8;
count++;
dbt->insns_fused++;
continue;
}
if (insn.rd) {
int rd = rc_write(&e, &rc, insn.rd);
emit_mov_r64_imm32(&e, rd, insn.imm);
}
pc += 4;
continue;
}
// -- AUIPC (with AUIPC+ADDI and AUIPC+JALR fusion) --
//
case OP_AUIPC: {
// Fusion: AUIPC rd, upper + JALR rs1=rd → direct jump/call
uint64_t target_u64;
uint64_t return_pc;
if (have_next
&& dbt_resolve_direct_jalr_target(pc, insn, next, &target_u64,
&return_pc)) {
int64_t target = static_cast<int64_t>(target_u64);
// Preserve the AUIPC result when JALR writes a different rd.
if (insn.rd != next.rd && insn.rd != 0) {
int rd = rc_write(&e, &rc, insn.rd);
int64_t val = static_cast<int64_t>(pc)
+ static_cast<int64_t>(insn.imm);
if (val >= INT32_MIN && val <= INT32_MAX) {
emit_mov_r64_imm32(&e, rd, static_cast<int32_t>(val));
} else {
emit_mov_r64_imm64(&e, rd, static_cast<uint64_t>(val));
}
}
if (next.rd) {
int rd = rc_write(&e, &rc, next.rd);
emit_mov_r64_imm32(&e, rd,
static_cast<int32_t>(return_pc));
}
bool can_tail_inline =
next.rd == 0
&& !(target >= static_cast<int64_t>(guest_pc)
&& target <= static_cast<int64_t>(pc + 4))
&& count < MAX_BLOCK_INSNS - 4;
switch (emit_direct_jalr_flow(&e, &rc, &fc, dbt, guest_pc, pc,
target_u64, return_pc, next,
side_exits, &num_side_exits,
can_tail_inline,
"auipc_jalr_call",
"auipc_jalr_tail",
"auipc_jalr")) {
case direct_jalr_flow_t::tail_call:
pc = static_cast<uint64_t>(target);
count++;
dbt->insns_fused++;
continue;
case direct_jalr_flow_t::inline_call_done:
case direct_jalr_flow_t::chained_exit:
count++;
dbt->insns_fused++;
goto done;
}
}
// Fusion: AUIPC rd, upper + ADDI rd, rd, lower → MOV rd, pc+imm
if (have_next && insn.rd
&& next.opcode == OP_IMM && next.funct3 == ALU_ADDI
&& next.rd == insn.rd && next.rs1 == insn.rd) {
int64_t val = static_cast<int64_t>(pc)
+ static_cast<int64_t>(insn.imm)
+ static_cast<int64_t>(next.imm);
int rd = rc_write(&e, &rc, insn.rd);
if (val >= INT32_MIN && val <= INT32_MAX) {
emit_mov_r64_imm32(&e, rd, static_cast<int32_t>(val));
} else {
emit_mov_r64_imm64(&e, rd, static_cast<uint64_t>(val));
}
dbt_trace_fusion(dbt, pc, "auipc_addi");
pc += 8;
count++;
dbt->insns_fused++;
continue;
}
// Unfused AUIPC.
if (insn.rd) {
int rd = rc_write(&e, &rc, insn.rd);
int64_t val = static_cast<int64_t>(pc) + static_cast<int64_t>(insn.imm);
if (val >= INT32_MIN && val <= INT32_MAX) {
emit_mov_r64_imm32(&e, rd, static_cast<int32_t>(val));
} else {
emit_mov_r64_imm64(&e, rd, static_cast<uint64_t>(val));
}
}
pc += 4;
continue;
}
// -- JAL --
//
case OP_JAL: {
uint64_t target = pc + static_cast<int64_t>(insn.imm);
// Superblock: unconditional backward jump to loop start.
if (self_loop && insn.rd == 0 && target == guest_pc) {
// Flush all dirty registers, then JMP to warm_entry.
rc_flush(&e, &rc); fc_flush(&e, &fc);
uint32_t jmp_patch = emit_jmp_rel32(&e);
emit_patch_rel32(&e, jmp_patch, warm_entry);
goto done;
}
// Superblock: forward unconditional jump — follow inline.
if (self_loop && insn.rd == 0 && insn.imm > 0) {
pc = target;
continue;
}
// Intrinsics are handled at block level (try_emit_intrinsic
// in translate_block). The caller's JAL finds the native
// stub through normal block chaining / cache lookup.
// Superblock native CALL: if this is a function call (JAL ra)
// and the target is already translated, emit a native x86-64
// CALL instead of exiting the block. The callee's translated
// code ends with RET (via emit_exit_indirect), which returns
// here. The entire loop body stays in one x86-64 block.
//
if (insn.rd == 1) {
block_entry_t *be = dbt_cache_lookup(dbt, target);
dbt_trace_translate_pc(dbt, guest_pc,
"inline_call guest_pc=0x%llX target=0x%llX found=%d",
static_cast<unsigned long long>(pc),
static_cast<unsigned long long>(target),
be ? 1 : 0);
if (try_emit_inline_call(&e, &rc, &fc, dbt, target, be, pc + 4,
side_exits, &num_side_exits)) {
pc += 4;
count++;
continue;
}
}
if (insn.rd) {
int rd = rc_write(&e, &rc, insn.rd);
emit_mov_r64_imm32(&e, rd, static_cast<int32_t>(pc + 4));
}
rc_flush(&e, &rc); fc_flush(&e, &fc);
if (insn.rd == 1) {
emit_ras_push(&e, pc + 4);
}
emit_exit_chained(&e, dbt, target);
goto done;
}
// -- JALR --
//
case OP_JALR: {
int rs1 = rc_read(&e, &rc, insn.rs1);
emit_mov_r64(&e, X64_RCX, rs1);
emit_add_r64_imm(&e, X64_RCX, insn.imm);
// Clear bit 0 per spec.
emit_and_r64_imm(&e, X64_RCX, ~1);
emit_store_next_pc(&e, X64_RCX);
if (insn.rd) {
int rd = rc_write(&e, &rc, insn.rd);
emit_mov_r64_imm32(&e, rd, static_cast<int32_t>(pc + 4));
}
rc_flush(&e, &rc); fc_flush(&e, &fc);
// RAS push for indirect calls (JALR rd=ra).
if (insn.rd == 1) {
emit_ras_push(&e, pc + 4);
}
emit_load_next_pc(&e, X64_RCX);
// RAS return prediction is DISABLED for now.
//
// The probe JMPs directly to the predicted block on a hit,
// bypassing emit_exit_indirect's RET. When this block runs
// inside an inline CALL context, the outer caller expects
// the callee to RET through the x86 stack. The probe's JMP
// breaks that contract — it transfers control without RET,
// leaving the inline CALL continuation stranded on the stack.
//
// Disabling the probe did NOT fix the 0x1096C cold exits
// (they have a different cause), but the probe remains
// architecturally unsafe for inline-call contexts. Since
// blob blocks serve both inline and dispatch-loop contexts,
// we cannot conditionally enable the probe per-block.
//
// Future: a context flag (e.g., pushed on the x86 stack by
// inline CALL, checked by the probe) could allow safe
// selective enablement.
// Tag this exit with the guest PC for cold-exit diagnostics.
// mov qword [rbx + last_exit_from_off], imm32(pc)
{
static constexpr int32_t EXIT_FROM_OFF =
static_cast<int32_t>(offsetof(dbt_state_t, last_exit_from));
emit_mov_r64_imm32(&e, X64_RAX, static_cast<int32_t>(pc));
emit_byte(&e, rex(1, reg_hi(X64_RAX), 0, 0));
emit_byte(&e, 0x89);
emit_byte(&e, modrm(0x02, X64_RAX, X64_RBX));
emit_u32(&e, EXIT_FROM_OFF);
// Restore RCX from next_pc (RAX clobbered it).
emit_load_next_pc(&e, X64_RCX);
}
emit_exit_indirect(&e, X64_RCX);
goto done;
}
// -- Branches (with diamond merge for short forward branches) --
//
case OP_BRANCH: {
uint64_t target = pc + static_cast<int64_t>(insn.imm);
// Determine the branch condition code.
uint8_t cc;
switch (insn.funct3) {
case 0: cc = JCC_E; break; // BEQ
case 1: cc = JCC_NE; break; // BNE
case 4: cc = JCC_L; break; // BLT
case 5: cc = JCC_GE; break; // BGE
case 6: cc = JCC_B; break; // BLTU
case 7: cc = JCC_AE; break; // BGEU
default:
rc_flush(&e, &rc); fc_flush(&e, &fc);
emit_exit_chained(&e, dbt, pc);
goto done;
}
// Self-loop: back-edge to block start → internal Jcc.
// The entire loop stays in one contiguous x86-64 block.
// Flush to context before jumping since register mapping
// may have diverged from warm_entry's layout.
//
if (self_loop && target == guest_pc) {
int rs1 = rc_read(&e, &rc, insn.rs1);
int rs2 = rc_read(&e, &rc, insn.rs2);
// Flush before CMP to avoid clobbering flags.
// Use store-to-context for dirty regs, then CMP, then Jcc.
rc_flush(&e, &rc); fc_flush(&e, &fc);
emit_cmp_r64(&e, rs1, rs2);
uint32_t jcc_patch = emit_jcc_rel32(&e, cc);
emit_patch_rel32(&e, jcc_patch, warm_entry);
// Fall-through = loop exit.
emit_exit_chained(&e, dbt, pc + 4);
goto done;
}
// Diamond merge: branch-over-one pattern.
// If the branch skips exactly 1 instruction (+8) and that
// instruction is a simple ALU op, convert to CMOVcc.
//
if (insn.imm == 8 && dbt_guest_range_ok(pc, 8, dbt->memory_size)) {
uint32_t skip_word;
memcpy(&skip_word, dbt->memory + pc + 4, 4);
rv64_insn_t skip;
rv64_decode(skip_word, &skip);
// The skipped instruction runs when branch is NOT taken.
// CMOVcc inverse condition: branch taken → skip (no move).
// We need the INVERSE condition for CMOVcc.
//
uint8_t cmov_cc;
switch (insn.funct3) {
case 0: cmov_cc = CMOV_NE; break; // BEQ skips → exec if NE
case 1: cmov_cc = CMOV_E; break; // BNE skips → exec if E
case 4: cmov_cc = CMOV_GE; break; // BLT skips → exec if GE
case 5: cmov_cc = CMOV_L; break; // BGE skips → exec if L
case 6: cmov_cc = CMOV_AE; break; // BLTU skips → exec if AE
case 7: cmov_cc = CMOV_B; break; // BGEU skips → exec if B
default: goto no_diamond;
}
bool can_predicate = false;
// OP_IMM: ADDI, XORI, ORI, ANDI (not shifts — different emit)
if (skip.opcode == OP_IMM && skip.rd != 0
&& (skip.funct3 == ALU_ADDI || skip.funct3 == ALU_XORI
|| skip.funct3 == ALU_ORI || skip.funct3 == ALU_ANDI)) {
can_predicate = true;
}
// OP_REG: ADD, SUB, AND, OR, XOR (not M-ext, not shifts)
if (skip.opcode == OP_REG && skip.rd != 0
&& skip.funct7 != 0x01
&& (skip.funct3 == ALU_ADD || skip.funct3 == ALU_XOR
|| skip.funct3 == ALU_OR || skip.funct3 == ALU_AND)) {
can_predicate = true;
}
// OP_LUI: load upper immediate
if (skip.opcode == OP_LUI && skip.rd != 0) {
can_predicate = true;
}
if (can_predicate) {
// Compute the ALU result in scratch (RCX) FIRST
// (this may clobber flags).
//
if (skip.opcode == OP_LUI) {
emit_mov_r64_imm32(&e, X64_RCX, skip.imm);
} else if (skip.opcode == OP_IMM) {
int hr_src = rc_read(&e, &rc, skip.rs1);
emit_mov_r64(&e, X64_RCX, hr_src);
switch (skip.funct3) {
case ALU_ADDI: emit_add_r64_imm(&e, X64_RCX, skip.imm); break;
case ALU_XORI: emit_xor_r64_imm(&e, X64_RCX, skip.imm); break;
case ALU_ORI: emit_or_r64_imm(&e, X64_RCX, skip.imm); break;
case ALU_ANDI: emit_and_r64_imm(&e, X64_RCX, skip.imm); break;
}
} else { // OP_REG
int hr_s1 = rc_read(&e, &rc, skip.rs1);
int hr_s2 = rc_read(&e, &rc, skip.rs2);
emit_mov_r64(&e, X64_RCX, hr_s1);
switch (skip.funct3) {
case ALU_ADD:
if (skip.funct7 == 0x20)
emit_sub_r64(&e, X64_RCX, hr_s2);
else
emit_add_r64(&e, X64_RCX, hr_s2);
break;
case ALU_XOR: emit_xor_r64_op(&e, X64_RCX, hr_s2); break;
case ALU_OR: emit_or_r64(&e, X64_RCX, hr_s2); break;
case ALU_AND: emit_and_r64(&e, X64_RCX, hr_s2); break;
}
}
// Ensure rd has its OLD value in a host register.
int hr_rd = rc_read(&e, &rc, skip.rd);
// Now do the branch comparison (after ALU, since ALU
// may clobber flags).
int hr_rs1 = rc_read(&e, &rc, insn.rs1);
int hr_rs2 = rc_read(&e, &rc, insn.rs2);
emit_cmp_r64(&e, hr_rs1, hr_rs2);
// CMOVcc: update rd only when branch NOT taken.
emit_cmovcc(&e, cmov_cc, hr_rd, X64_RCX);
// Mark rd dirty (value may have changed).
int slot = rc_find(&rc, skip.rd);
if (slot >= 0) {
rc.slots[slot].dirty = 1;
rc.slots[slot].last_use = ++rc.clock;
}
pc += 8; // consumed branch + skipped instruction
count++;
dbt->insns_fused++;
continue;
}
}
no_diamond:
// Superblock side exit: if we're inside a self-loop scan and
// this is a forward branch, record the taken path as a cold
// side exit and continue translating the fall-through inline.
//
// Flush FP before the possible leave (#1338). Side exits only
// snapshot integer slots; dirty FP writes on the fall-through
// path must hit ctx before a later taken side exit abandons
// the host FP registers.
//
if (self_loop && insn.imm > 0
&& num_side_exits < MAX_SIDE_EXITS
&& count < MAX_BLOCK_INSNS - 4) {
int rs1 = rc_read(&e, &rc, insn.rs1);
int rs2 = rc_read(&e, &rc, insn.rs2);
fc_flush(&e, &fc);
emit_cmp_r64(&e, rs1, rs2);
uint32_t jcc_patch = emit_jcc_rel32(&e, cc);
side_exits[num_side_exits].jcc_patch = jcc_patch;
side_exits[num_side_exits].target_pc = target;
side_exits[num_side_exits].expected_next_pc = 0;
memcpy(side_exits[num_side_exits].snapshot, rc.slots,
sizeof(rc.slots));
num_side_exits++;
pc += 4;
continue;
}
// Normal branch: terminate block with two exits.
{
int rs1 = rc_read(&e, &rc, insn.rs1);
int rs2 = rc_read(&e, &rc, insn.rs2);
emit_cmp_r64(&e, rs1, rs2);
}
// Emit: jcc taken; [fall-through]; jmp not_taken
uint32_t jcc_patch = emit_jcc_rel32(&e, cc);
// Fall-through: continue to pc+4.
rc_flush(&e, &rc); fc_flush(&e, &fc);
emit_exit_chained(&e, dbt, pc + 4);
// Taken path:
emit_patch_rel32(&e, jcc_patch, emit_pos(&e));
rc_flush(&e, &rc); fc_flush(&e, &fc);
emit_exit_chained(&e, dbt, target);
goto done;
}
// -- Loads --
//
case OP_LOAD: {
int rs1 = rc_read(&e, &rc, insn.rs1);
emit_mov_r64(&e, X64_RCX, rs1);
if (insn.imm) emit_add_r64_imm(&e, X64_RCX, insn.imm);
int rd = insn.rd ? rc_write(&e, &rc, insn.rd) : X64_RAX;
switch (insn.funct3) {
case 0: emit_load_mem8s(&e, rd, X64_RCX, 0); break; // LB
case 1: emit_load_mem16s(&e, rd, X64_RCX, 0); break; // LH
case 2: emit_load_mem32s(&e, rd, X64_RCX, 0); break; // LW
case 3: emit_load_mem64(&e, rd, X64_RCX, 0); break; // LD
case 4: emit_load_mem8u(&e, rd, X64_RCX, 0); break; // LBU
case 5: emit_load_mem16u(&e, rd, X64_RCX, 0); break; // LHU
case 6: emit_load_mem32(&e, rd, X64_RCX, 0); break; // LWU
default: break;
}
pc += 4;
continue;
}
// -- Stores --
//
case OP_STORE: {
int rs1 = rc_read(&e, &rc, insn.rs1);
int rs2 = rc_read(&e, &rc, insn.rs2);
emit_mov_r64(&e, X64_RCX, rs1);
if (insn.imm) emit_add_r64_imm(&e, X64_RCX, insn.imm);
emit_mov_r64(&e, X64_RDX, rs2);
switch (insn.funct3) {
case 0: emit_store_mem8(&e, X64_RCX, X64_RDX, 0); break; // SB
case 1: emit_store_mem16(&e, X64_RCX, X64_RDX, 0); break; // SH
case 2: emit_store_mem32(&e, X64_RCX, X64_RDX, 0); break; // SW
case 3: emit_store_mem64(&e, X64_RCX, X64_RDX, 0); break; // SD
default: break;
}
pc += 4;
continue;
}
// -- OP_IMM (ADDI, SLTI, SLTIU, XORI, ORI, ANDI, SLLI, SRLI, SRAI) --
//
case OP_IMM: {
int rs1 = rc_read(&e, &rc, insn.rs1);
int rd = insn.rd ? rc_write(&e, &rc, insn.rd) : X64_RAX;
switch (insn.funct3) {
case ALU_ADDI:
emit_mov_r64(&e, rd, rs1);
emit_add_r64_imm(&e, rd, insn.imm);
break;
case ALU_SLTI: // SLTI
emit_cmp_r64_imm(&e, rs1, insn.imm);
emit_setcc(&e, SETCC_L, rd);
emit_movzx_r64_r8(&e, rd, rd);
break;
case ALU_SLTIU: // SLTIU
emit_cmp_r64_imm(&e, rs1, insn.imm);
emit_setcc(&e, SETCC_B, rd);
emit_movzx_r64_r8(&e, rd, rd);
break;
case ALU_XORI:
emit_mov_r64(&e, rd, rs1);
emit_xor_r64_imm(&e, rd, insn.imm);
break;
case ALU_ORI:
emit_mov_r64(&e, rd, rs1);
emit_or_r64_imm(&e, rd, insn.imm);
break;
case ALU_ANDI:
emit_mov_r64(&e, rd, rs1);
emit_and_r64_imm(&e, rd, insn.imm);
break;
case ALU_SLLI: { // SLLI (6-bit shift amount for RV64)
uint8_t shamt = static_cast<uint8_t>(insn.imm & 0x3F);
emit_mov_r64(&e, rd, rs1);
emit_shl_r64_imm(&e, rd, shamt);
break;
}
case ALU_SRLI: { // SRLI / SRAI (bit 10 of imm selects)
uint8_t shamt = static_cast<uint8_t>(insn.imm & 0x3F);
emit_mov_r64(&e, rd, rs1);
if (insn.imm & 0x400) // SRAI
emit_sar_r64_imm(&e, rd, shamt);
else
emit_shr_r64_imm(&e, rd, shamt);
break;
}
}
pc += 4;
continue;
}
// -- OP_REG (ADD, SUB, SLL, SLT, SLTU, XOR, SRL, SRA, OR, AND, MUL, etc.) --
//
case OP_REG: {
int rs1 = rc_read(&e, &rc, insn.rs1);
int rs2 = rc_read(&e, &rc, insn.rs2);
int rd = insn.rd ? rc_write(&e, &rc, insn.rd) : X64_RAX;
// Guard against rd==rs2 aliasing: when the destination
// register is the same host register as rs2 (but different
// from rs1), "mov rd, rs1" clobbers rs2 before the
// operation reads it. For commutative ops we can just
// swap; for non-commutative ops we save rs2 to a temp.
bool rd_rs2_alias = (rd == rs2 && rd != rs1);
if (insn.funct7 == 0x01) {
// M extension (multiply/divide).
switch (insn.funct3) {
case 0: // MUL (commutative)
if (rd_rs2_alias) {
emit_imul_r64(&e, rd, rs1);
} else {
emit_mov_r64(&e, rd, rs1);
emit_imul_r64(&e, rd, rs2);
}
break;
// The MULH family multiplies through scratch RCX, captured
// BEFORE rc_load overwrites RAX. rc_read materialises x0
// as RAX-as-zero, so when guest rs2 is x0 the host register
// `rs2` IS RAX: loading rs1 into RAX first destroys the zero
// and the multiply then squares rs1 (#1361). This is the
// same hazard DIV/REM guard against just below -- it simply
// was never carried across to the high multiplies.
case 1: // MULH (signed * signed, high 64)
emit_mov_r64(&e, X64_RCX, rs2);
rc_load(&e, &rc, X64_RAX, insn.rs1);
emit_imul1_r64(&e, X64_RCX);
rc_store(&e, &rc, insn.rd, X64_RDX);
break;
case 2: // MULHSU (signed * unsigned, high 64)
// MULHSU(rs1, rs2) = MULHU(rs1, rs2) - (rs1 < 0 ? rs2 : 0)
//
// The adjustment is taken AFTER the multiply: MUL writes
// only RDX:RAX, so RCX still holds rs2, and RAX is free
// once the low half is dead. Reading rs1's cached
// register there is safe because rc_read marked it
// most-recently-used, so the rc_write for rd could not
// have evicted it. When rs1 is x0 -- the one case where
// its host register would be RAX -- the adjustment is
// zero by definition, so the guard skips it.
emit_mov_r64(&e, X64_RCX, rs2);
rc_load(&e, &rc, X64_RAX, insn.rs1);
emit_mul_r64(&e, X64_RCX); // RDX:RAX = unsigned(rs1) * rs2
if (insn.rs1 != 0) {
emit_mov_r64(&e, X64_RAX, rs1);
emit_sar_r64_imm(&e, X64_RAX, 63); // sign mask (-1 or 0)
emit_and_r64(&e, X64_RAX, X64_RCX); // rs2 if rs1 < 0, else 0
emit_sub_r64(&e, X64_RDX, X64_RAX);
}
rc_store(&e, &rc, insn.rd, X64_RDX);
break;
case 3: // MULHU (unsigned * unsigned, high 64)
emit_mov_r64(&e, X64_RCX, rs2);
rc_load(&e, &rc, X64_RAX, insn.rs1);
emit_mul_r64(&e, X64_RCX);
rc_store(&e, &rc, insn.rd, X64_RDX);
break;
// DIV/DIVU/REM/REMU: a bare idiv/div traps (#DE -> SIGFPE)
// on a zero divisor and on INT64_MIN / -1, but RV64 defines
// both cases (#811) — emit_rv_*64 guard them. Copy the
// divisor to scratch RCX before the dividend load clobbers
// RAX: rs2 IS RAX when guest rs2 is x0.
case 4: // DIV
emit_mov_r64(&e, X64_RCX, rs2);
rc_load(&e, &rc, X64_RAX, insn.rs1);
emit_rv_div64(&e);
rc_store(&e, &rc, insn.rd, X64_RAX);
break;
case 5: // DIVU
emit_mov_r64(&e, X64_RCX, rs2);
rc_load(&e, &rc, X64_RAX, insn.rs1);
emit_rv_divu64(&e);
rc_store(&e, &rc, insn.rd, X64_RAX);
break;
case 6: // REM
emit_mov_r64(&e, X64_RCX, rs2);
rc_load(&e, &rc, X64_RAX, insn.rs1);
emit_rv_rem64(&e);
rc_store(&e, &rc, insn.rd, X64_RDX);
break;
case 7: // REMU
emit_mov_r64(&e, X64_RCX, rs2);
rc_load(&e, &rc, X64_RAX, insn.rs1);
emit_rv_remu64(&e);
rc_store(&e, &rc, insn.rd, X64_RDX);
break;
}
} else {
// Base integer.
switch (insn.funct3) {
case ALU_ADD:
if (insn.funct7 == 0x20) {
// SUB (non-commutative)
if (rd_rs2_alias) {
// Temp must not be RAX: rs1 IS RAX when the
// guest rs1 is x0 (rc_read materializes the
// zero there), so an RAX temp clobbered it and
// neg rd,rd computed rd-rd = 0 (#782).
emit_mov_r64(&e, X64_RDX, rs2);
emit_mov_r64(&e, rd, rs1);
emit_sub_r64(&e, rd, X64_RDX);
} else {
emit_mov_r64(&e, rd, rs1);
emit_sub_r64(&e, rd, rs2);
}
} else {
// ADD (commutative)
if (rd_rs2_alias) {
emit_add_r64(&e, rd, rs1);
} else {
emit_mov_r64(&e, rd, rs1);
emit_add_r64(&e, rd, rs2);
}
}
break;
case ALU_SLL:
emit_mov_r64(&e, X64_RCX, rs2);
emit_mov_r64(&e, rd, rs1);
emit_shl_r64_cl(&e, rd);
break;
case ALU_SLT:
emit_cmp_r64(&e, rs1, rs2);
emit_setcc(&e, SETCC_L, rd);
emit_movzx_r64_r8(&e, rd, rd);
break;
case ALU_SLTU:
emit_cmp_r64(&e, rs1, rs2);
emit_setcc(&e, SETCC_B, rd);
emit_movzx_r64_r8(&e, rd, rd);
break;
case ALU_XOR:
if (rd_rs2_alias) {
emit_xor_r64_op(&e, rd, rs1);
} else {
emit_mov_r64(&e, rd, rs1);
emit_xor_r64_op(&e, rd, rs2);
}
break;
case ALU_SRL:
emit_mov_r64(&e, X64_RCX, rs2);
emit_mov_r64(&e, rd, rs1);
if (insn.funct7 == 0x20)
emit_sar_r64_cl(&e, rd);
else
emit_shr_r64_cl(&e, rd);
break;
case ALU_OR:
if (rd_rs2_alias) {
emit_or_r64(&e, rd, rs1);
} else {
emit_mov_r64(&e, rd, rs1);
emit_or_r64(&e, rd, rs2);
}
break;
case ALU_AND:
if (rd_rs2_alias) {
emit_and_r64(&e, rd, rs1);
} else {
emit_mov_r64(&e, rd, rs1);
emit_and_r64(&e, rd, rs2);
}
break;
}
}
pc += 4;
continue;
}
// -- OP_IMM32 (ADDIW, SLLIW, SRLIW, SRAIW) --
//
case OP_IMM32: {
int rs1 = rc_read(&e, &rc, insn.rs1);
int rd = insn.rd ? rc_write(&e, &rc, insn.rd) : X64_RAX;
switch (insn.funct3) {
case 0: // ADDIW
emit_mov_r64(&e, rd, rs1);
emit_add_r32_imm(&e, rd, insn.imm);
emit_movsxd(&e, rd, rd);
break;
case 1: { // SLLIW
uint8_t shamt = static_cast<uint8_t>(insn.imm & 0x1F);
emit_mov_r64(&e, rd, rs1);
emit_shl_r32_imm(&e, rd, shamt);
emit_movsxd(&e, rd, rd);
break;
}
case 5: { // SRLIW / SRAIW
uint8_t shamt = static_cast<uint8_t>(insn.imm & 0x1F);
emit_mov_r64(&e, rd, rs1);
if (insn.imm & 0x400) // SRAIW
emit_sar_r32_imm(&e, rd, shamt);
else
emit_shr_r32_imm(&e, rd, shamt);
emit_movsxd(&e, rd, rd);
break;
}
}
pc += 4;
continue;
}
// -- OP_REG32 (ADDW, SUBW, SLLW, SRLW, SRAW, MULW, DIVW, etc.) --
//
case OP_REG32: {
int rs1 = rc_read(&e, &rc, insn.rs1);
int rs2 = rc_read(&e, &rc, insn.rs2);
int rd = insn.rd ? rc_write(&e, &rc, insn.rd) : X64_RAX;
bool rd_rs2_alias_w = (rd == rs2 && rd != rs1);
if (insn.funct7 == 0x01) {
// M extension W-suffix.
switch (insn.funct3) {
case 0: // MULW (commutative)
if (rd_rs2_alias_w) {
emit_imul_r32(&e, rd, rs1);
} else {
emit_mov_r64(&e, rd, rs1);
emit_imul_r32(&e, rd, rs2);
}
emit_movsxd(&e, rd, rd);
break;
// DIVW/DIVUW/REMW/REMUW: guarded like DIV/REM above (#811),
// testing the low 32 bits of the divisor. Divisor goes to
// scratch RCX before RAX is clobbered (rs2 IS RAX when
// guest rs2 is x0).
case 4: // DIVW
emit_mov_r64(&e, X64_RCX, rs2);
rc_load(&e, &rc, X64_RAX, insn.rs1);
emit_rv_divw(&e);
rc_store(&e, &rc, insn.rd, X64_RAX);
break;
case 5: // DIVUW
emit_mov_r64(&e, X64_RCX, rs2);
rc_load(&e, &rc, X64_RAX, insn.rs1);
emit_rv_divuw(&e);
rc_store(&e, &rc, insn.rd, X64_RAX);
break;
case 6: // REMW
emit_mov_r64(&e, X64_RCX, rs2);
rc_load(&e, &rc, X64_RAX, insn.rs1);
emit_rv_remw(&e);
rc_store(&e, &rc, insn.rd, X64_RDX);
break;
case 7: // REMUW
emit_mov_r64(&e, X64_RCX, rs2);
rc_load(&e, &rc, X64_RAX, insn.rs1);
emit_rv_remuw(&e);
rc_store(&e, &rc, insn.rd, X64_RDX);
break;
}
} else {
switch (insn.funct3) {
case 0: // ADDW / SUBW
if (insn.funct7 == 0x20) {
// SUBW (non-commutative)
if (rd_rs2_alias_w) {
// Temp must not be RAX: see the SUB case above
// (negw rd,rd computed rd-rd = 0, #782).
emit_mov_r64(&e, X64_RDX, rs2);
emit_mov_r64(&e, rd, rs1);
emit_sub_r32(&e, rd, X64_RDX);
} else {
emit_mov_r64(&e, rd, rs1);
emit_sub_r32(&e, rd, rs2);
}
} else {
// ADDW (commutative)
if (rd_rs2_alias_w) {
emit_add_r32(&e, rd, rs1);
} else {
emit_mov_r64(&e, rd, rs1);
emit_add_r32(&e, rd, rs2);
}
}
emit_movsxd(&e, rd, rd);
break;
case 1: // SLLW
emit_mov_r64(&e, X64_RCX, rs2);
emit_mov_r64(&e, rd, rs1);
emit_shl_r32_cl(&e, rd);
emit_movsxd(&e, rd, rd);
break;
case 5: // SRLW / SRAW
emit_mov_r64(&e, X64_RCX, rs2);
emit_mov_r64(&e, rd, rs1);
if (insn.funct7 == 0x20)
emit_sar_r32_cl(&e, rd);
else
emit_shr_r32_cl(&e, rd);
emit_movsxd(&e, rd, rd);
break;
}
}
pc += 4;
continue;
}
// -- FP Load (FLD) --
//
case OP_FP_LOAD: {
if (insn.funct3 == 3) { // FLD
int rs1 = rc_read(&e, &rc, insn.rs1);
emit_mov_r64(&e, X64_RCX, rs1);
if (insn.imm) emit_add_r64_imm(&e, X64_RCX, insn.imm);
int xd = fc_write(&e, &fc, insn.rd);
emit_load_mem_f64(&e, xd, X64_RCX, 0);
}
pc += 4;
continue;
}
// -- FP Store (FSD) --
//
case OP_FP_STORE: {
if (insn.funct3 == 3) { // FSD
int rs1 = rc_read(&e, &rc, insn.rs1);
emit_mov_r64(&e, X64_RCX, rs1);
if (insn.imm) emit_add_r64_imm(&e, X64_RCX, insn.imm);
int xs2 = fc_read(&e, &fc, insn.rs2);
emit_store_mem_f64(&e, X64_RCX, xs2, 0);
}
pc += 4;
continue;
}
// -- FP Arithmetic (OP_FP) --
//
case OP_FP: {
uint8_t funct5 = insn.funct7 >> 2;
uint8_t fmt = insn.funct7 & 3;
if (fmt != FP_FMT_D) goto fallback_interp;
switch (funct5) {
case FP_FADD: {
int xs1 = fc_read(&e, &fc, insn.rs1);
int xs2 = fc_read(&e, &fc, insn.rs2);
int xd = fc_write(&e, &fc, insn.rd);
emit_movsd_xmm(&e, XMM0, xs1);
emit_addsd(&e, XMM0, xs2);
emit_canon_nan_d(&e, XMM0);
emit_movsd_xmm(&e, xd, XMM0);
break;
}
case FP_FSUB: {
int xs1 = fc_read(&e, &fc, insn.rs1);
int xs2 = fc_read(&e, &fc, insn.rs2);
int xd = fc_write(&e, &fc, insn.rd);
emit_movsd_xmm(&e, XMM0, xs1);
emit_subsd(&e, XMM0, xs2);
emit_canon_nan_d(&e, XMM0);
emit_movsd_xmm(&e, xd, XMM0);
break;
}
case FP_FMUL: {
int xs1 = fc_read(&e, &fc, insn.rs1);
int xs2 = fc_read(&e, &fc, insn.rs2);
int xd = fc_write(&e, &fc, insn.rd);
emit_movsd_xmm(&e, XMM0, xs1);
emit_mulsd(&e, XMM0, xs2);
emit_canon_nan_d(&e, XMM0);
emit_movsd_xmm(&e, xd, XMM0);
break;
}
case FP_FDIV: {
int xs1 = fc_read(&e, &fc, insn.rs1);
int xs2 = fc_read(&e, &fc, insn.rs2);
int xd = fc_write(&e, &fc, insn.rd);
emit_movsd_xmm(&e, XMM0, xs1);
emit_divsd(&e, XMM0, xs2);
emit_canon_nan_d(&e, XMM0);
emit_movsd_xmm(&e, xd, XMM0);
break;
}
case FP_FSQRT: {
int xs1 = fc_read(&e, &fc, insn.rs1);
int xd = fc_write(&e, &fc, insn.rd);
emit_movsd_xmm(&e, XMM0, xs1);
emit_sqrtsd(&e, XMM0, XMM0);
emit_canon_nan_d(&e, XMM0);
emit_movsd_xmm(&e, xd, XMM0);
break;
}
case FP_FMINMAX: { // FMIN.D / FMAX.D
int xs1 = fc_read(&e, &fc, insn.rs1);
int xs2 = fc_read(&e, &fc, insn.rs2);
int xd = fc_write(&e, &fc, insn.rd);
emit_fminmax_d(&e, xd, xs1, xs2, insn.funct3 != 0);
break;
}
case FP_FCMP: { // FEQ.D / FLT.D / FLE.D
int xs1 = fc_read(&e, &fc, insn.rs1);
int xs2 = fc_read(&e, &fc, insn.rs2);
int rd = insn.rd ? rc_write(&e, &rc, insn.rd) : X64_RAX;
emit_fcmp_d(&e, rd, xs1, xs2, insn.funct3);
break;
}
case FP_FSGNJ: { // FSGNJ.D / FSGNJN.D / FSGNJX.D
if (insn.rs1 == insn.rs2) {
// Common idioms: fmv.d (0), fneg.d (1), fabs.d (2)
int xs1 = fc_read(&e, &fc, insn.rs1);
emit_movsd_xmm(&e, XMM0, xs1);
if (insn.funct3 == 1) {
// FSGNJN (fneg): xor with sign bit
emit_mov_r64_imm64(&e, X64_RAX, 0x8000000000000000ULL);
emit_movq_xmm_r64(&e, XMM1, X64_RAX);
emit_byte(&e, 0x66); emit_byte(&e, 0x0F);
emit_byte(&e, 0x57); // xorpd
emit_byte(&e, modrm(0x03, XMM0, XMM1));
} else if (insn.funct3 == 2) {
// FSGNJX with rs1==rs2: sign XOR sign = 0, so fabs
emit_mov_r64_imm64(&e, X64_RAX, 0x7FFFFFFFFFFFFFFFULL);
emit_movq_xmm_r64(&e, XMM1, X64_RAX);
emit_byte(&e, 0x66); emit_byte(&e, 0x0F);
emit_byte(&e, 0x54); // andpd
emit_byte(&e, modrm(0x03, XMM0, XMM1));
}
// funct3==0: just copy (XMM0 already loaded)
{ int xd = fc_write(&e, &fc, insn.rd);
emit_movsd_xmm(&e, xd, XMM0); }
} else {
// General case: extract sign from rs2, magnitude from rs1
// Use integer registers for bit manipulation — flush to
// XMM0/XMM1 scratch from cache.
int xs1 = fc_read(&e, &fc, insn.rs1);
int xs2 = fc_read(&e, &fc, insn.rs2);
emit_movsd_xmm(&e, XMM0, xs1);
emit_movsd_xmm(&e, XMM1, xs2);
// Use integer registers for bit manipulation.
// XMM0=rs1, XMM1=rs2 already loaded from cache.
emit_movq_r64_xmm(&e, X64_RAX, XMM0);
emit_movq_r64_xmm(&e, X64_RCX, XMM1);
// RAX = rs1 bits, RCX = rs2 bits
emit_mov_r64_imm64(&e, X64_RDX, 0x7FFFFFFFFFFFFFFFULL);
emit_and_r64(&e, X64_RAX, X64_RDX); // clear rs1 sign
if (insn.funct3 == 0) {
// FSGNJ: use rs2 sign
emit_mov_r64_imm64(&e, X64_RDX, 0x8000000000000000ULL);
emit_and_r64(&e, X64_RCX, X64_RDX);
emit_or_r64(&e, X64_RAX, X64_RCX);
} else if (insn.funct3 == 1) {
// FSGNJN: use negated rs2 sign
emit_mov_r64_imm64(&e, X64_RDX, 0x8000000000000000ULL);
emit_and_r64(&e, X64_RCX, X64_RDX);
emit_xor_r64_op(&e, X64_RCX, X64_RDX);
emit_or_r64(&e, X64_RAX, X64_RCX);
} else {
// FSGNJX: XOR signs
emit_mov_r64_imm64(&e, X64_RDX, 0x8000000000000000ULL);
emit_and_r64(&e, X64_RCX, X64_RDX);
emit_or_r64(&e, X64_RAX, X64_RCX);
// FSGNJX keeps rs1 magnitude, XORs signs.
// Re-read from cache (XMM0/XMM1 may be clobbered).
emit_movsd_xmm(&e, XMM0, xs1);
emit_movq_r64_xmm(&e, X64_RAX, XMM0);
emit_movsd_xmm(&e, XMM0, xs2);
emit_movq_r64_xmm(&e, X64_RCX, XMM0);
emit_xor_r64_op(&e, X64_RAX, X64_RCX);
emit_and_r64(&e, X64_RAX, X64_RDX); // isolate XOR'd sign
// Get rs1 magnitude
emit_movsd_xmm(&e, XMM0, xs1);
emit_movq_r64_xmm(&e, X64_RCX, XMM0);
emit_mov_r64_imm64(&e, X64_RDX, 0x7FFFFFFFFFFFFFFFULL);
emit_and_r64(&e, X64_RCX, X64_RDX);
emit_or_r64(&e, X64_RAX, X64_RCX);
}
{ int xd = fc_write(&e, &fc, insn.rd);
emit_movq_xmm_r64(&e, xd, X64_RAX); }
}
break;
}
case FP_FCVTW: { // FCVT.W.D / FCVT.WU.D / FCVT.L.D / FCVT.LU.D
// Honour insn.funct3 (rm). A bare CVTTSD2SI is always RTZ,
// so RNE — the default, and what the assembler emits when
// no mode is written — was wrong on every fractional input
// (#1320). SSE has directed ROUNDSD for four of the five
// modes; RMM and dynamic frm=fcsr are handled inside the
// shared helper. Saturation/NaN remain #1329.
int xs1 = fc_read(&e, &fc, insn.rs1);
int rd = insn.rd ? rc_write(&e, &rc, insn.rd) : X64_RAX;
// Rounding mode (#1320) plus RISC-V saturation/NaN
// semantics (#1329): emit_fcvt_float_to_int_d rounds per
// rm into XMM0 and then saturates the rounded value.
emit_fcvt_float_to_int_d(&e, rd, xs1, insn.rs2, insn.funct3);
break;
}
case FP_FCVTDW: { // FCVT.D.W / FCVT.D.WU / FCVT.D.L / FCVT.D.LU
int rs1 = rc_read(&e, &rc, insn.rs1);
int xd = fc_write(&e, &fc, insn.rd);
emit_cvtsi2sd_r64(&e, xd, rs1);
break;
}
case FP_FCLASS: { // FMV.X.D (funct3=0) / FCLASS.D (funct3=1)
if (insn.funct3 == 0) {
// FMV.X.D: move FP bits to integer
int xs1 = fc_read(&e, &fc, insn.rs1);
int rd = insn.rd ? rc_write(&e, &rc, insn.rd) : X64_RAX;
emit_movq_r64_xmm(&e, rd, xs1);
} else {
// FCLASS.D: complex classification — fall back
goto fallback_interp;
}
break;
}
case FP_FMVDX: { // FMV.D.X (funct3=0)
int rs1 = rc_read(&e, &rc, insn.rs1);
int xd = fc_write(&e, &fc, insn.rd);
emit_movq_xmm_r64(&e, xd, rs1);
break;
}
default:
goto fallback_interp;
}
pc += 4;
continue;
}
// -- FMA (FMADD, FMSUB, FNMSUB, FNMADD) --
//
case OP_FMADD: case OP_FMSUB: case OP_FNMSUB: case OP_FNMADD: {
uint8_t fmt = insn.funct7 & 3;
if (fmt != FP_FMT_D) goto fallback_interp;
int xs1 = fc_read(&e, &fc, insn.rs1);
int xs2 = fc_read(&e, &fc, insn.rs2);
emit_movsd_xmm(&e, XMM0, xs1);
emit_mulsd(&e, XMM0, xs2);
int xs3 = fc_read(&e, &fc, insn.rs3);
emit_movsd_xmm(&e, XMM1, xs3);
switch (insn.opcode) {
case OP_FMADD: emit_addsd(&e, XMM0, XMM1); break;
case OP_FMSUB: emit_subsd(&e, XMM0, XMM1); break;
case OP_FNMSUB: {
// -(rs1*rs2) + rs3 = rs3 - (rs1*rs2)
emit_subsd(&e, XMM1, XMM0);
emit_canon_nan_d(&e, XMM1);
int xd = fc_write(&e, &fc, insn.rd);
emit_movsd_xmm(&e, xd, XMM1);
pc += 4;
continue;
}
case OP_FNMADD:
// -(rs1*rs2) - rs3
emit_addsd(&e, XMM0, XMM1);
// Negate: xor with sign bit
emit_mov_r64_imm64(&e, X64_RAX, 0x8000000000000000ULL);
emit_movq_xmm_r64(&e, XMM1, X64_RAX);
// xorpd xmm0, xmm1 (66 0F 57)
emit_byte(&e, 0x66);
emit_byte(&e, 0x0F);
emit_byte(&e, 0x57);
emit_byte(&e, modrm(0x03, XMM0, XMM1));
break;
}
emit_canon_nan_d(&e, XMM0);
{ int xd = fc_write(&e, &fc, insn.rd);
emit_movsd_xmm(&e, xd, XMM0); }
pc += 4;
continue;
}
// -- SYSTEM: ECALL / EBREAK / CSR (#1333) --
//
case OP_SYSTEM: {
rc_flush(&e, &rc); fc_flush(&e, &fc);
// ECALL/EBREAK require funct3 == 0. Without that guard, CSR
// addresses 0x000/0x001 are misdecoded as ECALL/EBREAK.
//
if (insn.funct3 == 0) {
if (insn.imm == 0) {
emit_exit_with_pc(&e, pc | 1);
goto done;
}
if (insn.imm == 1) {
emit_exit_with_pc(&e, pc | 2);
goto done;
}
goto fallback_interp;
}
// CSRRW/CSRRS/CSRRC (1-3) and CSRRWI/CSRRSI/CSRRCI (5-7).
//
if ( (insn.funct3 >= 1 && insn.funct3 <= 3)
|| (insn.funct3 >= 5 && insn.funct3 <= 7)) {
const uint32_t csr_addr = static_cast<uint32_t>(insn.imm) & 0xFFFu;
if (csr_addr != 0x001 && csr_addr != 0x002 && csr_addr != 0x003) {
goto fallback_interp;
}
// Call dbt_csr_apply(ctx, csr, funct3, src, rd). SysV:
// rdi/rsi/rdx/rcx/r8. Stack must be 16-byte aligned.
//
emit_stub_prologue(&e);
emit_mov_r64(&e, X64_RDI, X64_RBX);
emit_mov_r64_imm32(&e, X64_RSI, static_cast<int32_t>(csr_addr));
emit_mov_r64_imm32(&e, X64_RDX, insn.funct3);
if (insn.funct3 <= 3) {
if (insn.rs1) {
emit_load_guest(&e, X64_RCX, insn.rs1);
} else {
emit_xor_r64(&e, X64_RCX, X64_RCX);
}
} else {
emit_mov_r64_imm32(&e, X64_RCX, insn.rs1); // zimm
}
emit_mov_r64_imm32(&e, X64_R8, insn.rd);
emit_call_host(&e, reinterpret_cast<void *>(dbt_csr_apply));
emit_stub_epilogue(&e);
rc_invalidate_reload(&e, &rc);
fc_invalidate(&fc);
pc += 4;
continue;
}
goto fallback_interp;
}
default:
fallback_interp:
// Unhandled instruction: refuse to translate this block (#1323).
// The old path advanced to pc+4 without executing the insn
// (rd left stale). dbt_run never single-steps the interpreter,
// so "fallback" was a silent skip. Returning nullptr lets the
// caller decline compiled execution instead of running wrong code.
// XLATE_REFUSE so dbt_run does not reclaim as if the buffer were
// full (#1331).
//
dbt_rollback_patches(dbt, patches_before);
return dbt_xlate_refuse(dbt);
}
}
// Block size limit reached.
rc_flush(&e, &rc);
fc_flush(&e, &fc);
emit_exit_chained(&e, dbt, pc);
done:
// Emit cold stubs for superblock side exits.
// Each stub: restore dirty registers from snapshot at branch point,
// then chained exit to the taken-path target.
//
for (int i = 0; i < num_side_exits; i++) {
emit_patch_rel32(&e, side_exits[i].jcc_patch, emit_pos(&e));
if (side_exits[i].target_pc == 0) {
// Cold exit from inline CALL: callee returned with
// unexpected next_pc. Store diagnostics, then RET
// to the dispatch loop.
//
static constexpr int32_t CE_COUNT_OFF =
static_cast<int32_t>(offsetof(dbt_state_t, cold_exit_count));
static constexpr int32_t CE_ACTUAL_OFF =
static_cast<int32_t>(offsetof(dbt_state_t, cold_exit_actual));
static constexpr int32_t CE_EXPECTED_OFF =
static_cast<int32_t>(offsetof(dbt_state_t, cold_exit_expected));
// Store actual next_pc: mov rax, [rbx + next_pc]; mov [rbx + actual], rax
emit_load_next_pc(&e, X64_RAX);
emit_byte(&e, rex(1, reg_hi(X64_RAX), 0, 0));
emit_byte(&e, 0x89);
emit_byte(&e, modrm(0x02, X64_RAX, X64_RBX));
emit_u32(&e, CE_ACTUAL_OFF);
// Store expected next_pc for this inline CALL cold exit.
emit_mov_r64_imm64(&e, X64_RDX, side_exits[i].expected_next_pc);
emit_byte(&e, rex(1, reg_hi(X64_RDX), 0, 0));
emit_byte(&e, 0x89);
emit_byte(&e, modrm(0x02, X64_RDX, X64_RBX));
emit_u32(&e, CE_EXPECTED_OFF);
// inc qword [rbx + CE_COUNT_OFF]
emit_byte(&e, rex(1, 0, 0, 0));
emit_byte(&e, 0xFF);
emit_byte(&e, modrm(0x02, 0, X64_RBX)); // inc [rbx + disp32]
emit_u32(&e, CE_COUNT_OFF);
emit_ret(&e);
} else {
for (int j = 0; j < RC_NUM_SLOTS; j++) {
if (side_exits[i].snapshot[j].guest_reg >= 0
&& side_exits[i].snapshot[j].dirty) {
emit_store_guest(&e, side_exits[i].snapshot[j].guest_reg,
rc_host_regs[j]);
}
}
emit_exit_chained(&e, dbt, side_exits[i].target_pc);
}
}
if (e.offset > e.capacity) {
dbt_rollback_patches(dbt, patches_before);
return dbt_xlate_full(dbt);
}
dbt->blocks_translated++;
dbt->insns_translated += count;
if (self_loop) {
dbt->superblock_count++;
dbt->side_exits_total += num_side_exits;
}
// Hex dump of JIT code for traced blocks.
if (dbt_trace_translate_enabled(dbt, guest_pc) && e.offset <= 2048) {
fprintf(stderr, "[dbt-xlate] native_code %p +%u:\n", block_start, e.offset);
for (uint32_t i = 0; i < e.offset; i++) {
if (i % 16 == 0) fprintf(stderr, " %04X: ", i);
fprintf(stderr, "%02X ", block_start[i]);
if (i % 16 == 15 || i == e.offset - 1) fprintf(stderr, "\n");
}
}
dbt_trace_translate_pc(dbt, guest_pc,
"block guest_pc=0x%llX bytes=%u insns=%d self_loop=%d side_exits=%d fused=%llu inline_calls=%llu total_fused=%llu total_inline_calls=%llu",
static_cast<unsigned long long>(guest_pc),
e.offset, count, self_loop ? 1 : 0, num_side_exits,
static_cast<unsigned long long>(dbt->insns_fused - fused_before),
static_cast<unsigned long long>(dbt->inline_calls - inline_calls_before),
static_cast<unsigned long long>(dbt->insns_fused),
static_cast<unsigned long long>(dbt->inline_calls));
dbt->code_used += e.offset;
return block_start;
}
// ---------------------------------------------------------------
// Trampoline — sets up callee-saved registers and calls JIT block
// ---------------------------------------------------------------
void dbt_backend_emit_trampoline(dbt_state_t *dbt) {
emit_t e;
e.buf = dbt->code_buf;
e.offset = 0;
e.capacity = 512;
// Save callee-saved registers.
emit_push(&e, X64_RBX);
emit_push(&e, X64_R12);
emit_push(&e, X64_R13);
emit_push(&e, X64_R14);
emit_push(&e, X64_R15);
// RBX = ctx (RDI), R12 = memory (RSI), R13 = cache (RCX)
// mov rbx, rdi
emit_byte(&e, rex(1, 0, 0, 0));
emit_byte(&e, 0x89);
emit_byte(&e, modrm(0x03, X64_RDI, X64_RBX));
// mov r12, rsi
emit_byte(&e, rex(1, 0, 0, 1));
emit_byte(&e, 0x89);
emit_byte(&e, modrm(0x03, X64_RSI, reg_lo(X64_R12)));
// mov r13, rcx
emit_byte(&e, rex(1, 0, 0, 1));
emit_byte(&e, 0x89);
emit_byte(&e, modrm(0x03, X64_RCX, reg_lo(X64_R13)));
// Pre-load pinned guest registers from ctx.
// a0 (x10) → RSI, a1 (x11) → RDI, a2 (x12) → R8, a3 (x13) → R9
//
for (int i = 0; i < RC_NUM_PINNED; i++) {
emit_load_guest(&e, rc_host_regs[i], rc_pinned_guest[i]);
}
// call rdx (block code)
emit_byte(&e, 0xFF);
emit_byte(&e, modrm(0x03, 2, X64_RDX));
// Post-store pinned guest registers back to ctx.
// Ensures ctx is up-to-date when control returns to C++.
//
for (int i = 0; i < RC_NUM_PINNED; i++) {
emit_store_guest(&e, rc_pinned_guest[i], rc_host_regs[i]);
}
// Restore callee-saved.
emit_pop(&e, X64_R15);
emit_pop(&e, X64_R14);
emit_pop(&e, X64_R13);
emit_pop(&e, X64_R12);
emit_pop(&e, X64_RBX);
emit_ret(&e);
dbt->code_used = e.offset;
}