Fix AMD64 lifter bugs found by instruction test suite#124
Conversation
Add per-instruction test files for every implemented x86/AMD64 handler in the translator, organized under lib/translator/x86/tests/. Tests assert correct behavior per the AMD64 Architecture Programmer's Manual — 170 pass and 47 fail, revealing existing lifter bugs including the BTR/BTS handler swap, INC/DEC CF modification, SAHF operand issues, and various arithmetic/SSE flag computation errors. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two bugs in the x86 bit-test instruction family: 1. BTR/BTS dispatch swap (translator.rs): X86_INS_BTR was dispatched to semantics.bts() and vice versa. The semantic functions themselves were correct, just called for the wrong instruction. 2. BTC logic error (semantics.rs): The bit-complement operation shifted the entire base right by offset, XOR'd with 1, then shifted back. This destroys all bits below the target offset position. Fixed to use the same mask-based pattern as BTR/BTS: base ^ (1 << offset). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Bug #1: BT* Family — BTR/BTS Dispatch Swap + BTC Logic ErrorBug 1a: BTR/BTS Dispatch SwapIn // Before (WRONG)
X86_INS_BTR => semantics.bts(...)
X86_INS_BTS => semantics.btr(...)The semantic functions themselves were correct — Bug 1b: BTC Logic Error — Destroys Bits Below Target OffsetIn temp = base >> offset // shifts ENTIRE value right, losing lower bits
CF = truncate(temp, 1)
result = (temp ^ 1) << offset // shift back, but lower bits are now zeroWith
Fixed to use the same mask-based pattern as BTR/BTS: |
Two bugs in SAHF implementation: 1. shr operand order was reversed — shifted the constant by AX instead of AX by the constant 2. Read from AX bits 0/6/7 (AL) instead of AX bits 8/14/15 (AH) SAHF loads SF, ZF, CF from the AH register per the AMD64 manual. AH is bits [15:8] of AX, so CF=bit8, ZF=bit14, SF=bit15. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Bug #2: SAHF — Reading Flags from Wrong Byte of AXWhat we're doingWorking through the AMD64 lifter bugs found by the instruction test suite, fixing them one at a time with detailed explanations. What the bug wasSAHF (Store AH into Flags) loads SF, ZF, and CF from the AH register — bits [15:8] of AX. The implementation had two compounding errors: 1. Reversed // Shifts the constant 6 right by the value of AX (nonsensical)
let zf = Expr::trun(1, Expr::shr(expr_const(6, 16), ax.clone())?)?;2. Wrong bit positions (even if operand order were correct): // Reads bit 0 of AX (AL bit 0) — should be bit 8 (AH bit 0)
let cf = Expr::trun(1, ax.clone())?;
// Would read AX bit 6 (AL bit 6) — should be bit 14 (AH bit 6)
let zf = Expr::trun(1, Expr::shr(ax.clone(), expr_const(6, 16))?)?;The implementation was reading from AL (bits [7:0]) instead of AH (bits [15:8]). How it was fixedShift AX right by the correct bit positions to extract AH's flag bits: let cf = Expr::trun(1, Expr::shr(ax.clone(), expr_const(8, 16))?)?; // AH bit 0 = AX bit 8
let zf = Expr::trun(1, Expr::shr(ax.clone(), expr_const(14, 16))?)?; // AH bit 6 = AX bit 14
let sf = Expr::trun(1, Expr::shr(ax, expr_const(15, 16))?)?; // AH bit 7 = AX bit 15All 4 SAHF tests now pass. |
The shared set_of helper used the subtraction overflow formula (lhs ^ rhs) & (lhs ^ result) for ALL instructions. This is wrong for addition — the correct addition formula is ~(lhs ^ rhs) & (lhs ^ result). Unified into a single set_of with a subtract parameter and updated all 13 call sites. Also removed set_cf from INC/DEC per AMD64 manual. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix: Systemic
|
SBB built an expression tree sub(lhs, add(rhs, zext(CF))) and used it directly for flag computation and operand store. When set_cf overwrote CF (op 4), the subsequent operand_store (op 5) re-evaluated the expression with the new CF value, producing a wrong result. Fix: store in a temp scalar first, matching how SUB already works. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix: SBB Result Corruption from CF ClobberingWhat we were doingFixing SBB (subtract with borrow), which had 3 failing tests where the stored result was wrong. What the bug wasSBB builds an expression tree The expression tree is re-evaluated at Op 5 with the new CF (0) instead of the input CF (1). The borrow is lost. SUB and ADC don't have this bug because they store the result in a temp scalar first. How we fixed itStore the SBB result in a temp scalar before computing flags (matching how SUB already works). The expression is evaluated once with the correct CF input, and all subsequent operations reference the materialized temp value. |
Fix: SBB Result Corruption from CF Clobbering (more detail)What we were doingFixing SBB (Subtract with Borrow). Per the AMD64 manual, SBB computes What the bug wasFalcon's IL works by building expression trees and adding assignment operations to a block. Operations in a block execute sequentially — each one evaluates its expression using the current state of all scalars, then writes the result. The SBB translator built a single expression tree for the result: This expression tree is not evaluated when it's constructed — it's a symbolic tree that references the scalar variables The problem: Op 4 evaluates the expression (which reads the input CF), computes the new carry flag, and writes it back to the CF scalar. Then Op 5 evaluates the same expression tree — but when it reads Concrete trace:
|
CF bug: after shl(lhs, count-1) the last-shifted-out bit sits at the MSB, but trun(1) extracted the LSB. Added shr(cf, bits-1) to bring the MSB down before truncating. OF bug: used cmpeq(cf, MSB(result)) which gives OF=1 when equal. Per AMD64 manual, OF = MSB(result) XOR CF, so OF=1 when they differ. Changed cmpeq to xor. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix: SHL CF and OF Flag ComputationWhat we were doingFixing SHL (shift left), which had 2 failing tests: wrong CF in What the bug wasTwo separate bugs in flag computation: Bug 1 — CF extracts the wrong bit. Per the AMD64 manual, CF receives the last bit shifted out. For The code correctly shifts Bug 2 — OF uses equality instead of XOR. Per the AMD64 manual, for shift count == 1: How we fixed it
|
SHLD: trun(64) extracted the low 64 bits of the 128-bit shifted value, but the answer lives in the high 64 bits. Added shr(shifted, 64) before truncating. Also fixed CF MSB-vs-LSB extraction (same bug as SHL). SHRD: result extraction was correct (low 64 bits), but set_zf/set_sf were called on the 128-bit value. SF checked bit 127 instead of bit 63 of the 64-bit result. Fixed by truncating to 64 bits before computing flags. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix: SHLD/SHRD Result Extraction and Flag ComputationWhat we were doingFixing SHLD (double precision shift left) and SHRD (double precision shift right). Both instructions concatenate dest and src into a 128-bit temporary, shift it, then extract a 64-bit result. SHLD had completely wrong results; SHRD had correct results but wrong SF/ZF flags. What the bug wasSHLD — extracting the wrong half of the 128-bit result. SHLD concatenates SHLD — CF extracts the wrong bit. Same MSB-vs-LSB bug as SHL: after shifting the 128-bit temp left by SHRD — flags computed on 128-bit value instead of 64-bit result. SHRD's result extraction was correct ( How we fixed it
|
The MUL instruction was using cmpeq(upper_half, 0) to set CF/OF, which produces 1 when the upper half IS zero (no overflow) — the opposite of correct. Per AMD64 manual, CF=OF=1 when the upper half of the result is non-zero. Changed to cmpneq for all four widths (8/16/32/64-bit). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix: MUL CF/OF flags invertedWhat we were doing: Testing the MUL (unsigned multiply) instruction with both small results (fitting in lower half) and overflow results (spilling into upper half RDX). What the bug was: The MUL instruction sets CF=OF=1 when the upper half of the result is non-zero (indicating the result doesn't fit in the destination register alone). The implementation used How we fixed it: Changed Tests fixed: |
Fix: MUL CF/OF flags inverted (detailed)What we were doingTesting the
We tested two cases:
What the bug wasThe implementation used Concrete walkthrough for 64-bit case (line 2419 before fix):
The same inverted logic existed in all four operand-width cases (8-bit checks AH, 16-bit checks DX, 32-bit checks EDX, 64-bit checks RDX). How we fixed itChanged
Tests fixed: |
Two bugs: (1) Operands were zero-extended before multiplication instead of sign-extended, producing wrong results for negative inputs. (2) OF/CF checked if upper half != 0, but for signed multiply they should be set when the full result != sign-extension of the lower half. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix: IMUL signed multiply — wrong extension and wrong flag computationWhat we were doingTesting the
We tested three cases:
What the bugs wereBug 1: Zero-extension instead of sign-extension (line ~1718) The code did: Expr::mul(
Expr::zext(bit_width, multiplicand)?,
Expr::zext(bit_width, multiplier)?,
)?IMUL is a signed multiply. Operands must be sign-extended to the double-width before multiplication, not zero-extended. With zero-extension, a negative 64-bit value like -3 ( Concrete example: RAX = -3 (
The test expected RDX=0xFFFFFFFFFFFFFFFF but got RDX=0x3. Bug 2: Wrong OF/CF flag check (line ~1764) The code checked: OF = cmpneq(upper_half, 0)This checks whether the upper half is non-zero. But for signed multiply, CF=OF=1 when the result doesn't fit in the lower half as a signed value. A negative result that fits (like -12) has an all-ones upper half — non-zero, but that's the correct sign-extension, NOT overflow. Per the AMD64 manual, the correct check is whether the full double-width result equals the sign-extension of the lower half: OF = cmpneq(full_result, sext(double_width, lower_half))Concrete example: signed product = -12.
With the old check: upper half = How we fixed it
Tests fixed: |
(1) Divisor zero-extended instead of sign-extended to 128-bit, wrong for negative divisors. (2) Dividend low half read from EAX (32-bit) instead of RAX (64-bit), corrupting the 128-bit dividend. (3) Result truncated to 32 bits instead of 64 when storing quotient/remainder back to RAX/RDX. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix: IDIV — three bugs in 64-bit signed divisionWhat we were doingTesting the
Test case (
What the bugs wereBug 1: Divisor zero-extended instead of sign-extended (line 1578) let divisor = Expr::zext(divisor.bits() * 2, divisor)?; // BUGIDIV performs signed division. The IL's Example: divisor = -7 (
(This bug didn't affect our specific test since the divisor was 7 (positive), but it would produce wrong results for any negative divisor.) Bug 2: Dividend assembled from EAX instead of RAX (line 1604) Expr::zext(128, self.get_register(x86_reg::X86_REG_EAX)?.get()?)? // BUG: EAX is 32-bitFor 64-bit IDIV, the 128-bit dividend is assembled as Concrete trace with RAX=
Should be:
Bug 3: Result truncated to 32 bits instead of 64 (lines 1641-1642) 128 => {
rax.set(block, Expr::trun(32, quotient.into())?)?; // BUG: should be 64
rdx.set(block, Expr::trun(32, remainder.into())?)?; // BUG: should be 64
}Copy-paste from the 64-bit case (which stores to 32-bit EAX/EDX). The 128-bit case stores to 64-bit RAX/RDX, so the quotient and remainder must be truncated to 64 bits. With How we fixed it
Tests fixed: |
Two bugs: (1) src operand received its own original value instead of the original dest value (exchange was not happening). Also required saving original dest to a temp to avoid expression re-evaluation after dest is overwritten. (2) CF used set_cf (subtraction formula) instead of the addition carry formula cmpltu(result, lhs). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix: XADD — src gets wrong value + CF uses subtraction formulaWhat we were doingTesting the
Semantically: Test case (
What the bugs wereBug 1: src operand received the wrong value (line 4131) The code stored self.operand_store(block, &detail.operands[0], result.into())?; // dest = sum ✅
self.operand_store(block, &detail.operands[1], rhs)?; // src = rhs ❌Per the manual, src should receive the original dest value ( Concrete trace with RAX=0x10, RBX=0x20:
The fix saves the original dest to a temp scalar before any stores: let original_dest = self.temp(1, lhs.bits());
block.assign(original_dest.clone(), lhs.clone());
// ... later ...
self.operand_store(block, &detail.operands[1], original_dest.into())?;Now Bug 2: CF used the subtraction formula (line 4127) The code called Example: RAX=0x10, RBX=0x20, sum=0x30.
Changed to inline How we fixed it
Tests fixed: |
Flags were computed from operand[0] - operand[1] instead of accumulator - operand[0] per AMD64 manual. Also saves comparison operands to temps in head block before conditional paths can modify them (expression re-evaluation bug). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix: CMPXCHG — flags computed from wrong operandsWhat we were doingTesting the
The key phrase: flags come from Test case (
What the bugs wereBug 1: Flags computed from the wrong subtraction (line 1393) The tail block computed: let result = Expr::sub(lhs.clone(), rhs.clone())?; // operand[0] - operand[1]Where Concrete walkthrough:
Bug 2: Expression re-evaluation after conditional paths (lines 1390-1396) Even if we fixed the subtraction to use
In either case, by the time the tail block computes the subtraction, the register values have changed. How we fixed itSaved the comparison operands to temp scalars in the head block before branching: // In head block:
let cmp_lhs = self.temp(0, lhs.bits()); // will hold original RAX
let cmp_rhs = self.temp(1, lhs.bits()); // will hold original operand[0]
block.assign(cmp_lhs.clone(), dest.get()?);
block.assign(cmp_rhs.clone(), lhs.clone());
// In tail block:
let result = Expr::sub(cmp_lhs.clone().into(), cmp_rhs.clone().into())?;
self.set_sf(block, result.clone())?;
self.set_of(block, result.clone(), cmp_lhs.clone().into(), cmp_rhs.into(), true)?;
self.set_cf(block, result, cmp_lhs.into())?;Temp scalars are dedicated storage that isn't affected by the register modifications in the conditional paths, so the flags are computed from the correct original values. Tests fixed: |
…alar operand_load creates Scalar::temp(instruction.address, operand.size * 8) for memory operands. For CMPSB, both operands are 8-bit memory loads at the same instruction address, so both calls produce an identical scalar name. The second load overwrites the first, making sub(temp, temp) always 0 and ZF always 1. Fix: save the first operand_load result to a distinct self.temp(0, ...) scalar before calling operand_load for the second operand. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Bug 10: CMPSB — shared temp scalar in operand_loadRoot cause: Fix: Save the first Tests fixed (4): Commit: 2767d1c |
LODSD loads a dword (4 bytes), so RSI should be adjusted by 4 per the AMD64 manual. The code was using expr_const(1, ...) in both the increment and decrement blocks, matching LODSB's byte-sized adjustment instead of LODSD's dword-sized adjustment. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Bug 11: LODSD — RSI adjusted by 1 instead of 4Root cause: Fix: Change Tests fixed (2): Commit: c5890ee |
set_cf computes CF = cmpltu(lhs, result) for subtraction. SCASW computes AX - [RDI], so lhs is AX. The code was passing temp ([RDI] value) instead of ax.get()?, causing CF to be computed from the wrong operand. The sibling scasb function correctly passes al.get()?. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Bug 12: SCASW — set_cf passed RHS instead of LHSRoot cause: Fix: Change Tests fixed (1): Commit: 067be7e |
The CF computation used cmpltu(result, lhs), which detects carry from lhs+rhs but misses carry from the subsequent +CF step. For example, 0 + 0xFFFFFFFFFFFFFFFF + 1 = 0: lhs+rhs=0xFFFFFFFFFFFFFFFF (no carry), then +1 wraps to 0 (carry). The single check sees cmpltu(0, 0) = false. Fix: compute the intermediate sum (lhs+rhs) in a temp, then detect carry from each step independently: carry1 = cmpltu(lhs+rhs, lhs), carry2 = cmpltu(result, lhs+rhs), CF = carry1 | carry2. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Bug 13: ADC — single cmpltu misses carry when both addition steps wrapRoot cause: Fix: Compute the intermediate sum
Tests fixed (1): Commit: b4f2e69 |
The 128-bit codepath for both PADDQ and PSUBQ had three bugs: 1. shr/shl operand order was reversed (e.g. shr(64, lhs) instead of shr(lhs, 64)) 2. Upper qword addition/subtraction used lhs for both operands instead of lhs and rhs 3. PSUBQ used sub(mask, ...) instead of and(mask, ...) to isolate the lower qword Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The loop computing per-byte subtractions used self.temp(0, 8) for every iteration, so each assignment overwrote the previous. The reconstruction loop then read the same scalar 16 times, producing a result where every byte equaled the last byte's subtraction. Fix: use self.temp(i + 2, 8) to give each byte lane its own temp scalar. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Each block.assign overwrote the temp scalar with just the shifted dword, discarding previously placed dwords. Only the last dword (result3 << 96) survived. Fix: OR each shifted dword with the accumulated temp value. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PSLLDQ and PSRLDQ take a byte count as the immediate operand, but the code passed this directly to shl/shr which operate on bits. This caused e.g. "shift left by 4 bytes" to shift by only 4 bits instead of 32 bits. Fix: multiply the byte count by 8 (shl by 3) before passing to the bit-level shift operation. Also removed a duplicate zext check in PSLLDQ. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Bugs 14-17: SSE instruction fixesBug 14: PADDQ/PSUBQ — swapped operands and wrong register in 128-bit pathRoot cause: The 128-bit codepath for both PADDQ and PSUBQ had three bugs:
Tests fixed (4): Commit: 3cc1cb1 Bug 15: PSUBB — all byte lanes share the same temp scalarRoot cause: The loop computing per-byte subtractions used Fix: Use Tests fixed (1): Commit: bbfa704 Bug 16: PSHUFD — dword results overwritten instead of ORed togetherRoot cause: Each Fix: OR each shifted dword with the accumulated temp value. Tests fixed (2): Commit: 4440c8f Bug 17: PSLLDQ/PSRLDQ — shift amount is bytes, not bitsRoot cause: PSLLDQ and PSRLDQ take a byte count as the immediate, but the code passed it directly to Fix: Multiply the byte count by 8 ( Tests fixed (4): Commit: 9651b09 |
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
This PR adds a comprehensive AMD64 instruction test suite (217 tests across 90 files) and begins fixing the bugs it found. The test suite asserts correct behavior per the AMD64 manual, not Falcon's current implementation — so some tests are expected to fail until the corresponding bugs are fixed.
Bug fixes included so far:
BT family (BTR/BTS dispatch swap + BTC logic error)*
translator.rs:105-106):X86_INS_BTRwas dispatched tosemantics.bts()andX86_INS_BTStosemantics.btr(). The semantic functions were correct, just called for the wrong instruction.semantics.rs:833-840): The bit-complement shifted the entire base right by offset, XOR'd with 1, then shifted back — destroying all bits below the target position. Fixed to use mask-based pattern:base ^ (1 << offset).Test plan
🤖 Generated with Claude Code