x64Analyzer: add movss/movups/movaps support and an instruction class field

The crash handler's instruction analyzer only understood mov/movzx/movsx,
so a fault on an FP or SIMD load/store (used for lwc1/swc1 and lv.q/sv.q
in the x86 JIT) couldn't be classified. Add decoding for movss (scalar,
distinguished from movups by the mandatory 0xF3 prefix), movups, and
movaps, and add an InstructionClass field (GPR/FP/FP_SIMD) so callers
know how to interpret the decoded register operand. Covered by new
unit tests in TestX64Emitter.cpp that emit each instruction and check
the analyzer's output against it.
This commit is contained in:
Henrik Rydgård committed 2026-08-11 22:36:47 +02:00
1 parent 6a873df4ea
commit 2c315be708
3 files changed
+110 -2

No files matched your search

+27 -1
View File
@@ -33,6 +33,7 @@ bool X86AnalyzeMOV(const unsigned char *codePtr, LSInstructionInfo &info)
info.signExtend = false;
info.hasImmediate = false;
info.isMemoryWrite = false;
info.instructionClass = InstructionClass::GPR;
int addressSize = 8;
u8 modRMbyte = 0;
@@ -40,6 +41,7 @@ bool X86AnalyzeMOV(const unsigned char *codePtr, LSInstructionInfo &info)
bool hasModRM = false;
bool hasSIBbyte = false;
bool hasDisplacement = false;
bool hasF3Prefix = false;
int displacementSize = 0;
@@ -47,12 +49,18 @@ bool X86AnalyzeMOV(const unsigned char *codePtr, LSInstructionInfo &info)
{
info.operandSize = 2;
codePtr++;
}
}
else if (*codePtr == 0x67)
{
addressSize = 4;
codePtr++;
}
else if (*codePtr == 0xF3)
{
// Mandatory prefix, distinguishes MOVSS (scalar) from MOVUPS (full xmm) on the same opcode.
hasF3Prefix = true;
codePtr++;
}
//Check for REX prefix
if ((*codePtr & 0xF0) == 0x40)
@@ -219,6 +227,24 @@ bool X86AnalyzeMOV(const unsigned char *codePtr, LSInstructionInfo &info)
info.signExtend = true;
info.operandSize = 2;
break;
case MOVUPS_MOVSS_FROM_RM: //movups/movss xmm, xmm/m (load)
info.instructionClass = hasF3Prefix ? InstructionClass::FP : InstructionClass::FP_SIMD;
info.operandSize = hasF3Prefix ? 4 : 16;
break;
case MOVUPS_MOVSS_TO_RM: //movups/movss xmm/m, xmm (store)
info.instructionClass = hasF3Prefix ? InstructionClass::FP : InstructionClass::FP_SIMD;
info.operandSize = hasF3Prefix ? 4 : 16;
info.isMemoryWrite = true;
break;
case MOVAPS_FROM_RM: //movaps xmm, xmm/m (load)
info.instructionClass = InstructionClass::FP_SIMD;
info.operandSize = 16;
break;
case MOVAPS_TO_RM: //movaps xmm/m, xmm (store)
info.instructionClass = InstructionClass::FP_SIMD;
info.operandSize = 16;
info.isMemoryWrite = true;
break;
default:
return false;
}
+15 -1
View File
@@ -19,9 +19,17 @@
#include "Common/CommonTypes.h"
// What kind of register regOperandReg (and otherReg, if used as a source/dest rather than
// just an address component) refers to, and thus how the instruction should be interpreted.
enum class InstructionClass {
GPR, // General-purpose register (mov, movzx, movsx, ...)
FP, // Scalar floating point (movss, movsd, ...)
FP_SIMD, // Full vector register (movups, movaps, movdqa, ...)
};
struct LSInstructionInfo
{
int operandSize; //8, 16, 32, 64
int operandSize; //1, 2, 4, 8 (in bytes, despite the field name suggesting bits)
int instructionSize;
int regOperandReg;
int otherReg;
@@ -32,6 +40,7 @@ struct LSInstructionInfo
bool isMemoryWrite;
u64 immediate;
s32 displacement;
InstructionClass instructionClass;
};
struct ModRM
@@ -54,6 +63,11 @@ enum {
MOVE_16_32BIT = 0xC7, //move 16 or 32-bit immediate
MOVE_REG_TO_MEM = 0x89, //move reg to memory
MOVE_MEM_TO_REG = 0x8B, //move memory to reg
// These two opcodes are shared between MOVUPS (no mandatory prefix) and MOVSS (mandatory 0xF3 prefix).
MOVUPS_MOVSS_FROM_RM = 0x10, //movups/movss xmm, xmm/m
MOVUPS_MOVSS_TO_RM = 0x11, //movups/movss xmm/m, xmm
MOVAPS_FROM_RM = 0x28, //movaps xmm, xmm/m
MOVAPS_TO_RM = 0x29, //movaps xmm/m, xmm
};
enum AccessType {
+68
View File
@@ -3,6 +3,7 @@
#if PPSSPP_ARCH(AMD64) || PPSSPP_ARCH(X86)
#include "Common/CPUDetect.h"
#include "Common/x64Analyzer.h"
#include "Common/x64Emitter.h"
#include "Core/MIPS/x86/RegCacheFPU.h"
#include "Core/MIPS/x86/Jit.h"
@@ -21,6 +22,22 @@ static bool CheckLast(const Gen::XEmitter &emit, const char *comp) {
return true;
}
// Emits nothing itself - runs the x64 crash-handler instruction analyzer (Common/x64Analyzer.cpp)
// on the instruction most recently emitted (from prevStart to the emitter's current position),
// and checks that it decoded it the way we expect.
static bool CheckAnalyze(const Gen::XEmitter &emit, bool expectWrite, InstructionClass expectClass, int expectOperandSize, bool expectZeroExtend = false, bool expectSignExtend = false) {
LSInstructionInfo info{};
bool success = X86AnalyzeMOV(prevStart, info);
EXPECT_TRUE(success);
EXPECT_EQ_INT(info.instructionSize, (int)(emit.GetCodePointer() - prevStart));
EXPECT_EQ_INT(info.isMemoryWrite, expectWrite);
EXPECT_EQ_INT((int)info.instructionClass, (int)expectClass);
EXPECT_EQ_INT(info.operandSize, expectOperandSize);
EXPECT_EQ_INT(info.zeroExtend, expectZeroExtend);
EXPECT_EQ_INT(info.signExtend, expectSignExtend);
return true;
}
static void PrintLast(const Gen::XEmitter &emit) {
for (const u8 *p = prevStart; p < emit.GetCodePointer(); p++) {
printf("%02x ", *p);
@@ -47,6 +64,57 @@ bool TestX64Emitter() {
cpu_info.bAVX = prevAVX;
// Exercise Common/x64Analyzer.cpp (used by the JIT crash handler to figure out what a faulting
// load/store instruction was doing) against instructions written by the emitter, for the most
// common memory access instructions.
prevStart = emitter.GetCodePointer();
emitter.MOV(32, R(EAX), MDisp(RCX, 4));
RET(CheckAnalyze(emitter, false, InstructionClass::GPR, 4));
prevStart = emitter.GetCodePointer();
emitter.MOV(32, MDisp(RCX, 4), R(EAX));
RET(CheckAnalyze(emitter, true, InstructionClass::GPR, 4));
prevStart = emitter.GetCodePointer();
emitter.MOVZX(32, 8, EAX, MDisp(RCX, 4));
RET(CheckAnalyze(emitter, false, InstructionClass::GPR, 1, true, false));
prevStart = emitter.GetCodePointer();
emitter.MOVZX(32, 16, EAX, MDisp(RCX, 4));
RET(CheckAnalyze(emitter, false, InstructionClass::GPR, 2, true, false));
prevStart = emitter.GetCodePointer();
emitter.MOVSX(32, 8, EAX, MDisp(RCX, 4));
RET(CheckAnalyze(emitter, false, InstructionClass::GPR, 1, false, true));
prevStart = emitter.GetCodePointer();
emitter.MOVSX(32, 16, EAX, MDisp(RCX, 4));
RET(CheckAnalyze(emitter, false, InstructionClass::GPR, 2, false, true));
prevStart = emitter.GetCodePointer();
emitter.MOVSS(XMM0, MDisp(RCX, 4));
RET(CheckAnalyze(emitter, false, InstructionClass::FP, 4));
prevStart = emitter.GetCodePointer();
emitter.MOVSS(MDisp(RCX, 4), XMM0);
RET(CheckAnalyze(emitter, true, InstructionClass::FP, 4));
prevStart = emitter.GetCodePointer();
emitter.MOVUPS(XMM0, MDisp(RCX, 4));
RET(CheckAnalyze(emitter, false, InstructionClass::FP_SIMD, 16));
prevStart = emitter.GetCodePointer();
emitter.MOVUPS(MDisp(RCX, 4), XMM0);
RET(CheckAnalyze(emitter, true, InstructionClass::FP_SIMD, 16));
prevStart = emitter.GetCodePointer();
emitter.MOVAPS(XMM0, MDisp(RCX, 4));
RET(CheckAnalyze(emitter, false, InstructionClass::FP_SIMD, 16));
prevStart = emitter.GetCodePointer();
emitter.MOVAPS(MDisp(RCX, 4), XMM0);
RET(CheckAnalyze(emitter, true, InstructionClass::FP_SIMD, 16));
// Just for checking.
PrintLast(emitter);
return true;