diff --git a/id/src/main/java/com/ledmington/cpu/InstructionChecker.java b/id/src/main/java/com/ledmington/cpu/InstructionChecker.java index 7708e623..1f1aef88 100644 --- a/id/src/main/java/com/ledmington/cpu/InstructionChecker.java +++ b/id/src/main/java/com/ledmington/cpu/InstructionChecker.java @@ -294,6 +294,7 @@ public final class InstructionChecker { Map.entry(Opcode.VPMINUD, List.of(RY_RY_RY, RY_RY_M256)), Map.entry(Opcode.VPMOVMSKB, List.of(R32_RX, R32_RY)), Map.entry(Opcode.VPCMPEQB, List.of(RK_RX_RX, RK_RY_RY, RY_RY_M256, RK_RX_M128, RK_RY_M256)), + Map.entry(Opcode.VPCMPLTB, List.of(RK_RY_RY)), Map.entry(Opcode.VPCMPEQD, List.of(RK_RY_RY, RY_RY_M256, RK_RY_M256)), Map.entry(Opcode.VPCMPEQQ, List.of(RX_RX_M128)), Map.entry(Opcode.VPCMPNEQB, List.of(RK_RY_RY, RK_RY_M256)), diff --git a/id/src/main/java/com/ledmington/cpu/InstructionDecoder.java b/id/src/main/java/com/ledmington/cpu/InstructionDecoder.java index b3c4a874..e6206057 100644 --- a/id/src/main/java/com/ledmington/cpu/InstructionDecoder.java +++ b/id/src/main/java/com/ledmington/cpu/InstructionDecoder.java @@ -4111,9 +4111,10 @@ private static Instruction parseEvexOpcodes( final byte VMOVAPS_OPCODE = (byte) 0x29; final byte VPMINUD_OPCODE = (byte) 0x3b; final byte VPCMPNEQUB_OPCODE = (byte) 0x3e; - final byte VPCMPEQB_OPCODE = (byte) 0x3f; + final byte VPCMPxxB_OPCODE = (byte) 0x3f; final byte VMOVQ_RX_M128_OPCODE = (byte) 0x6e; final byte VMOVDQU64_RZMM_M512_OPCODE = (byte) 0x6f; + final byte VPCMPEQB_OPCODE = (byte) 0x74; final byte VPBROADCASTB_OPCODE = (byte) 0x7a; final byte VPBROADCASTD_OPCODE = (byte) 0x7c; final byte VMOVQ_R64_RX_OPCODE = (byte) 0x7e; @@ -4251,7 +4252,7 @@ yield new GeneralInstruction( b.read1(); yield tmp; } - case VPCMPEQB_OPCODE -> { + case VPCMPxxB_OPCODE -> { final ModRM modrm = modrm(b); final byte r1 = or(evex.v1() ? 0 : (byte) 0b00010000, getByteFromV(evex)); final byte r2 = or(evex.x() ? 0 : (byte) 0b00010000, getByteFromRM(evex, modrm)); @@ -4267,13 +4268,34 @@ yield new GeneralInstruction( if (evex.a() != (byte) 0) { ib.mask(MaskRegister.fromByte(evex.a())); } - final byte nextByte = b.read1(); - switch (nextByte) { + final byte opcodeByte = b.read1(); + switch (opcodeByte) { case (byte) 0x00 -> ib.opcode(Opcode.VPCMPEQB); + case (byte) 0x01 -> ib.opcode(Opcode.VPCMPLTB); + case (byte) 0x02 -> ib.opcode(Opcode.VPCMPLEB); case (byte) 0x04 -> ib.opcode(Opcode.VPCMPNEQB); - default -> - throw new IllegalArgumentException( - String.format("Unknown opcode corresponding to byte 0x%02x.", nextByte)); + case (byte) 0x05 -> ib.opcode(Opcode.VPCMPNLTB); + case (byte) 0x06 -> ib.opcode(Opcode.VPCMPNLEB); + default -> throw new UnknownOpcode(opcodeByte); + } + yield ib.build(); + } + case VPCMPEQB_OPCODE -> { + final ModRM modrm = modrm(b); + final byte r1 = or(evex.v1() ? 0 : (byte) 0b00010000, getByteFromV(evex)); + final byte r2 = or(evex.x() ? 0 : (byte) 0b00010000, getByteFromRM(evex, modrm)); + final InstructionBuilder ib = Instruction.builder() + .opcode(Opcode.VPCMPEQB) + .op(MaskRegister.fromByte(getByteFromReg(evex, modrm))) + .op(evex.l() ? RegisterYMM.fromByte(r1) : RegisterXMM.fromByte(r1)) + .op( + isIndirectOperandNeeded(modrm) + ? parseIndirectOperand(b, pref, modrm) + .pointer(evex.l() ? PointerSize.YMMWORD_PTR : PointerSize.XMMWORD_PTR) + .build() + : (evex.l() ? RegisterYMM.fromByte(r2) : RegisterXMM.fromByte(r2))); + if (evex.a() != (byte) 0) { + ib.mask(MaskRegister.fromByte(evex.a())); } yield ib.build(); } diff --git a/id/src/main/java/com/ledmington/cpu/InstructionEncoder.java b/id/src/main/java/com/ledmington/cpu/InstructionEncoder.java index 290c1caf..1a74efd8 100644 --- a/id/src/main/java/com/ledmington/cpu/InstructionEncoder.java +++ b/id/src/main/java/com/ledmington/cpu/InstructionEncoder.java @@ -237,7 +237,7 @@ public static String toIntelSyntax( if (inst.hasFirstOperand()) { if (opcode.length() < opcodePad) { - sb.append(" ".repeat(opcodePad - opcode.length())); + sb.repeat(" ", opcodePad - opcode.length()); } sb.append(' ').append(operandString(inst, inst.firstOperand(), shortHex)); if (inst.hasDestinationMask()) { @@ -382,7 +382,7 @@ private static Prefix checkRequiredPrefix(final Instruction inst) { ? Prefix.EVEX : ((isFirstMS(inst) || isSecondM(inst)) ? Prefix.VEX2 : Prefix.VEX3); case VPMINUB -> countEvexExtensions(inst) > 0 ? Prefix.EVEX : Prefix.VEX2; - case VPCMPEQB -> + case VPCMPEQB, VPCMPEQD, VPCMPNEQB -> (isFirstMask(inst) && isSecondEER(inst) && isThirdM(inst)) ? Prefix.EVEX : (isThirdMS(inst) ? Prefix.VEX2 : Prefix.VEX3); @@ -2016,22 +2016,25 @@ private static void encodeThreeOperandsInstruction(final WriteOnlyByteBuffer wb, } case VPCMPEQD -> { if (isFirstMask(inst) && isSecondR(inst) && isThirdM(inst)) { - encodeEvexPrefix(wb, inst); wb.write((byte) 0x1f); lastByte = (byte) 0x00; } else { - encodeVex2Prefix(wb, inst); wb.write((byte) 0x76); } } case VPCMPEQQ -> wb.write((byte) 0x29); case VPCMPNEQB -> { if (isFirstMask(inst) && isSecondR(inst) && inst.thirdOperand() instanceof IndirectOperand) { - encodeEvexPrefix(wb, inst); wb.write((byte) 0x3f); lastByte = (byte) 0x04; } } + case VPCMPLTB -> { + if (isFirstMask(inst) && isSecondR(inst) && (isThirdM(inst) || isThirdR(inst))) { + wb.write((byte) 0x3f); + lastByte = (byte) 0x01; + } + } case PEXTRW -> wb.write(DOUBLE_BYTE_OPCODE_PREFIX, (byte) 0xc5); case SARX -> wb.write((byte) 0xf7); case BZHI -> wb.write((byte) 0xf5); @@ -2297,6 +2300,7 @@ private static byte getImpliedPrefix(final Instruction inst) { VPCMPEQQ, VPCMPNEQB, VPCMPNEQUB, + VPCMPLTB, VPCMPGTB, VMOVD, VPBROADCASTB, @@ -2422,11 +2426,14 @@ private static boolean is64Bits(final Instruction inst) { }; } - private static byte getEvexOpcodeMap(final Opcode opcode) { - return switch (opcode) { + private static byte getEvexOpcodeMap(final Instruction inst) { + return switch (inst.opcode()) { + // 0F map (mm = 01) case VMOVUPS, VMOVAPS, VMOVDQU8, VMOVDQU64, VMOVNTDQ, VMOVQ, VPXORQ, VPORQ, VPMINUB -> (byte) 0b001; + // 0F 38 map (mm = 10) case VBROADCASTSS, VPBROADCASTB, VPBROADCASTD, VPTESTMB, VPMINUD -> (byte) 0b010; - case VPCMPNEQUB, VPCMPEQB, VPCMPEQD, VPCMPNEQB, VPTERNLOGD -> (byte) 0b011; + // 0F 3A map (mm = 11) + case VPCMPNEQUB, VPCMPEQD, VPCMPNEQB, VPTERNLOGD, VPCMPEQB, VPCMPLTB -> (byte) 0b011; default -> (byte) 0b000; }; } @@ -2450,7 +2457,7 @@ && isFirstEER(inst) && isSecondR(inst) && (isThirdR(inst) || isThirdM(inst)) && inst.fourthOperand() instanceof Immediate), - getEvexOpcodeMap(inst.opcode())); + getEvexOpcodeMap(inst)); // TODO: refactor this chain of if-elses Register rvvvv = null; diff --git a/id/src/main/java/com/ledmington/cpu/x86/InstructionBuilder.java b/id/src/main/java/com/ledmington/cpu/x86/InstructionBuilder.java index 8730da72..9100871d 100644 --- a/id/src/main/java/com/ledmington/cpu/x86/InstructionBuilder.java +++ b/id/src/main/java/com/ledmington/cpu/x86/InstructionBuilder.java @@ -135,6 +135,7 @@ public Instruction build() { } alreadyBuilt = true; + return new GeneralInstruction(prefix, opcode, destinationMask, destinationMaskZero, op1, op2, op3, op4); } diff --git a/id/src/main/java/com/ledmington/cpu/x86/Instructions.java b/id/src/main/java/com/ledmington/cpu/x86/Instructions.java new file mode 100644 index 00000000..57b59693 --- /dev/null +++ b/id/src/main/java/com/ledmington/cpu/x86/Instructions.java @@ -0,0 +1,68 @@ +/* + * emu - Processor Emulator + * Copyright (C) 2023-2026 Filippo Barbari + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.ledmington.cpu.x86; + +import java.util.Objects; + +/** A collection of common utilities for Instructions. */ +public final class Instructions { + + private Instructions() {} + + /** + * Returns true if the given instructions are effectively equal, meaning that the type of the class + * implementing {@link Instruction} may not be the same. This method is, therefore, slightly different from + * {@link Object#equals(Object)}. + * + * @param a An Instruction. + * @param b An Instruction to be compared with a for equality. + * @return True if the arguments are equal to each other, false otherwise + */ + public static boolean equals(final Instruction a, final Instruction b) { + Objects.requireNonNull(a); + Objects.requireNonNull(b); + + if (a.hasPrefix() != b.hasPrefix()) { + return false; + } + if (a.hasPrefix() && a.getPrefix() != b.getPrefix()) { + return false; + } + + if (a.hasZeroDestinationMask() != b.hasZeroDestinationMask()) { + return false; + } + if (a.hasDestinationMask() != b.hasDestinationMask()) { + return false; + } + if (a.hasDestinationMask() && a.getDestinationMask() != b.getDestinationMask()) { + return false; + } + + for (int i = 0; i < 4; i++) { + if (a.hasOperand(i) != b.hasOperand(i)) { + return false; + } + if (a.hasOperand(i) && !a.operand(i).equals(b.operand(i))) { + return false; + } + } + + return a.opcode() == b.opcode(); + } +} diff --git a/id/src/main/java/com/ledmington/cpu/x86/Opcode.java b/id/src/main/java/com/ledmington/cpu/x86/Opcode.java index 527a67b9..ce690d8a 100644 --- a/id/src/main/java/com/ledmington/cpu/x86/Opcode.java +++ b/id/src/main/java/com/ledmington/cpu/x86/Opcode.java @@ -746,12 +746,24 @@ public enum Opcode { /** Packed compare implicit length strings, return index. */ VPCMPISTRI, + /** Compare packed data for less than or equal. */ + VPCMPLEB, + + /** Compare packed data for less than. */ + VPCMPLTB, + /** Compare packed data for not equal. */ VPCMPNEQB, /** Compare packed byte values into mask. */ VPCMPNEQUB, + /** Compare packed data for not less than or equal. */ + VPCMPNLEB, + + /** Compare packed data for not less than. */ + VPCMPNLTB, + /** Minimum of packed unsigned byte integers. */ VPMINUB, diff --git a/id/src/test/java/com/ledmington/cpu/TestDecodeIncompleteInstruction.java b/id/src/test/java/com/ledmington/cpu/TestDecodeIncompleteInstruction.java index 828ad313..94e9a675 100644 --- a/id/src/test/java/com/ledmington/cpu/TestDecodeIncompleteInstruction.java +++ b/id/src/test/java/com/ledmington/cpu/TestDecodeIncompleteInstruction.java @@ -35,9 +35,8 @@ final class TestDecodeIncompleteInstruction extends X64Encodings { private static Stream incompleteInstructions() { final Set> validInstructions = X64_ENCODINGS.stream() - .map(x -> IntStream.range(0, x.hex().length) - .mapToObj(i -> x.hex()[i]) - .toList()) + .flatMap(x -> x.allowedEncodings().stream()) + .map(hex -> IntStream.range(0, hex.length).mapToObj(i -> hex[i]).toList()) .collect(Collectors.toSet()); return validInstructions.stream() .flatMap(splitted -> { diff --git a/id/src/test/java/com/ledmington/cpu/TestDecoding.java b/id/src/test/java/com/ledmington/cpu/TestDecoding.java index e34e0e9d..befe9fc9 100644 --- a/id/src/test/java/com/ledmington/cpu/TestDecoding.java +++ b/id/src/test/java/com/ledmington/cpu/TestDecoding.java @@ -17,11 +17,13 @@ */ package com.ledmington.cpu; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.Arrays; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; @@ -31,6 +33,7 @@ import org.junit.jupiter.params.provider.MethodSource; import com.ledmington.cpu.x86.Instruction; +import com.ledmington.cpu.x86.Instructions; import com.ledmington.utils.BitUtils; final class TestDecoding extends X64Encodings { @@ -55,40 +58,60 @@ void checkReference(final Instruction inst) { InstructionEncoder.toIntelSyntax(inst))); } - private static Stream instAndHex() { - return X64_ENCODINGS.stream().map(x -> Arguments.of(x.instruction(), x.hex())); + private static Stream instructionAndAllowedEncodings() { + return X64_ENCODINGS.stream().map(x -> Arguments.of(x.instruction(), x.allowedEncodings())); } + /** + * This test checks that the given instruction maps to any of the given encodings. This is less strict than decoding + * from binary ({@link TestDecoding#fromHex(Instruction, byte[])}) since we do not care about exact binary encoding. + * + * @param inst The instruction to encode + * @param allowedEncodings The allowed encodings for the given instruction. + */ @ParameterizedTest - @MethodSource("instAndHex") - void toHex(final Instruction inst, final byte[] expected) { + @MethodSource("instructionAndAllowedEncodings") + void toHex(final Instruction inst, final Set allowedEncodings) { final byte[] actual = InstructionEncoder.toHex(inst, true); - assertArrayEquals(expected, actual, () -> { + final boolean matchFound = allowedEncodings.stream().anyMatch(allowed -> Arrays.equals(allowed, actual)); + assertTrue(matchFound, () -> { + final String allowedStr = + allowedEncodings.stream().map(TestDecoding::asString).collect(Collectors.joining(", ")); String s = String.format( - "Expected '%s' to be encoded as '%s' but was '%s'.", - inst.toString(), asString(expected), asString(actual)); - if (expected.length == actual.length) { + "Expected '%s' to be encoded as one of [%s] but was '%s'.", + inst.toString(), allowedStr, asString(actual)); + final byte[] closestMatch = allowedEncodings.stream() + .filter(allowed -> allowed.length == actual.length) + .findFirst() + .orElse(null); + if (closestMatch != null) { int i = 0; - for (; i < expected.length; i++) { - if (expected[i] != actual[i]) { + for (; i < closestMatch.length; i++) { + if (closestMatch[i] != actual[i]) { break; } } - s = s - + String.format( - " First different byte is at index %,d: expected 0b%s but was 0b%s.", - i, BitUtils.toBinaryString(expected[i]), BitUtils.toBinaryString(actual[i])); + s += String.format( + " First different byte (vs. same-length encoding) is at index %,d: expected 0b%s but was 0b%s.", + i, BitUtils.toBinaryString(closestMatch[i]), BitUtils.toBinaryString(actual[i])); } else { - s = s - + String.format( - " Wrong lengths: expected %,d bytes but were %,d.", expected.length, actual.length); + final String allowedLengths = allowedEncodings.stream() + .map(allowed -> String.valueOf(allowed.length)) + .collect(Collectors.joining(", ")); + s += String.format( + " Wrong length: allowed lengths are [%s] bytes but were %,d.", allowedLengths, actual.length); } return s; }); } + private static Stream instructionAndHex() { + return X64_ENCODINGS.stream() + .flatMap(x -> x.allowedEncodings().stream().map(hex -> Arguments.of(x.instruction(), hex))); + } + @ParameterizedTest - @MethodSource("instAndHex") + @MethodSource("instructionAndHex") void fromHex(final Instruction expected, final byte[] hex) { final List inst = InstructionDecoder.fromHex(hex, hex.length, true); assertEquals( @@ -105,7 +128,7 @@ void fromHex(final Instruction expected, final byte[] hex) { } private static Stream onlyHex() { - return X64_ENCODINGS.stream().map(x -> Arguments.of((Object) x.hex())); + return X64_ENCODINGS.stream().flatMap(x -> x.allowedEncodings().stream().map(Arguments::of)); } @ParameterizedTest @@ -143,9 +166,8 @@ void toIntelSyntax(final Instruction inst, final String expected) { @MethodSource("instAndIntelSyntax") void fromIntelSyntax(final Instruction expected, final String intelSyntax) { final Instruction actual = InstructionDecoder.fromIntelSyntax(intelSyntax); - assertEquals( - expected, - actual, + assertTrue( + Instructions.equals(expected, actual), () -> String.format( "Expected '%s' to be decoded into '%s' but was '%s'.", intelSyntax, expected, actual)); } diff --git a/id/src/test/java/com/ledmington/cpu/X64Encodings.java b/id/src/test/java/com/ledmington/cpu/X64Encodings.java index 46fcc5a2..c805c162 100644 --- a/id/src/test/java/com/ledmington/cpu/X64Encodings.java +++ b/id/src/test/java/com/ledmington/cpu/X64Encodings.java @@ -189,11 +189,11 @@ public sealed class X64Encodings permits TestDecoding, TestDecodeIncompleteInstr .toList(); // FIXME: this is ugly - protected record X64EncodingTestCase(Instruction instruction, String intelSyntax, byte[] hex) { - @SuppressWarnings("PMD.MethodReturnsInternalArray") + // TODO: do the allowed encodings need to be a set (instead of a list)? + protected record X64EncodingTestCase(Instruction instruction, String intelSyntax, Set allowedEncodings) { @SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "This object as is it is for now.") - public byte[] hex() { - return hex; + public Set allowedEncodings() { + return allowedEncodings; } } @@ -203,7 +203,21 @@ private static X64EncodingTestCase test(final Instruction instruction, final Str for (int i = 0; i < splitted.length; i++) { code[i] = BitUtils.asByte(Integer.parseInt(splitted[i], 16)); } - return new X64EncodingTestCase(instruction, intelSyntax, code); + return new X64EncodingTestCase(instruction, intelSyntax, Set.of(code)); + } + + private static X64EncodingTestCase test( + final Instruction instruction, final String intelSyntax, final Set allowedEncodings) { + final Set encodings = new HashSet<>(); + for (final String hex : allowedEncodings) { + final String[] splitted = hex.strip().split(" "); + final byte[] code = new byte[splitted.length]; + for (int i = 0; i < splitted.length; i++) { + code[i] = BitUtils.asByte(Integer.parseInt(splitted[i], 16)); + } + encodings.add(code); + } + return new X64EncodingTestCase(instruction, intelSyntax, encodings); } private static List nop() { @@ -10214,10 +10228,10 @@ Opcode.MOVABS, new SegmentedAddress(DS, new Immediate(0x12345678deadbeefL)), EAX .opcode(Opcode.VPCMPEQB) .op(K0) .op(YMM16) - .op(YMM18) + .op(YMM19) .build(), - "vpcmpeqb k0,ymm16,ymm18", - "62 b3 7d 20 3f c2 00"), + "vpcmpeqb k0,ymm16,ymm19", + Set.of("62 b3 7d 20 3f c3 00", "62 b1 7d 20 74 c3")), test( Instruction.builder() .opcode(Opcode.VPCMPEQB) @@ -10243,6 +10257,16 @@ Opcode.MOVABS, new SegmentedAddress(DS, new Immediate(0x12345678deadbeefL)), EAX .build(), "vpcmpeqb k1{k2},xmm17,xmm18", "62 b3 75 02 3f ca 00"), + // Vpcmpltb + test( + Instruction.builder() + .opcode(Opcode.VPCMPLTB) + .op(K0) + .op(YMM16) + .op(YMM18) + .build(), + "vpcmpltb k0,ymm16,ymm18", + "62 b3 7d 20 3f c2 01"), // Vpcmpeqd test( new GeneralInstruction( @@ -11088,29 +11112,31 @@ private static String asString(final byte[] v) { static { // Check that there are no duplicates - final Set inst = new HashSet<>(); - final Set is = new HashSet<>(); - final Set> hex = new HashSet<>(); + final Set allInstructions = new HashSet<>(); + final Set allInstructionStrings = new HashSet<>(); + final Set> allInstructionBytes = new HashSet<>(); for (final X64EncodingTestCase t : X64_ENCODINGS) { - if (inst.contains(t.instruction())) { + if (allInstructions.contains(t.instruction())) { throw new IllegalArgumentException( String.format("Duplicate instruction in test cases: '%s'.", t.instruction())); } - inst.add(t.instruction()); + allInstructions.add(t.instruction()); - if (is.contains(t.intelSyntax())) { + if (allInstructionStrings.contains(t.intelSyntax())) { throw new IllegalArgumentException( String.format("Duplicate intel syntax in test cases: '%s'.", t.intelSyntax())); } - is.add(t.intelSyntax()); + allInstructionStrings.add(t.intelSyntax()); - final int n = t.hex().length; - final List b = IntStream.range(0, n).mapToObj(i -> t.hex()[i]).toList(); - if (hex.contains(b)) { - throw new IllegalArgumentException( - String.format("Duplicate hex representation in test cases: '%s'.", asString(t.hex()))); + for (final byte[] hex : t.allowedEncodings()) { + final int n = hex.length; + final List b = IntStream.range(0, n).mapToObj(i -> hex[i]).toList(); + if (allInstructionBytes.contains(b)) { + throw new IllegalArgumentException( + String.format("Duplicate hex representation in test cases: '%s'.", asString(hex))); + } + allInstructionBytes.add(b); } - hex.add(b); } } }