Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
Expand Down
36 changes: 29 additions & 7 deletions id/src/main/java/com/ledmington/cpu/InstructionDecoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
Expand All @@ -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();
}
Expand Down
25 changes: 16 additions & 9 deletions id/src/main/java/com/ledmington/cpu/InstructionEncoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -2297,6 +2300,7 @@ private static byte getImpliedPrefix(final Instruction inst) {
VPCMPEQQ,
VPCMPNEQB,
VPCMPNEQUB,
VPCMPLTB,
VPCMPGTB,
VMOVD,
VPBROADCASTB,
Expand Down Expand Up @@ -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;
};
}
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ public Instruction build() {
}

alreadyBuilt = true;

return new GeneralInstruction(prefix, opcode, destinationMask, destinationMaskZero, op1, op2, op3, op4);
}

Expand Down
68 changes: 68 additions & 0 deletions id/src/main/java/com/ledmington/cpu/x86/Instructions.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* emu - Processor Emulator
* Copyright (C) 2023-2026 Filippo Barbari <filippo.barbari@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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 <i>effectively equal</i>, 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 <code>a</code> 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();
}
}
12 changes: 12 additions & 0 deletions id/src/main/java/com/ledmington/cpu/x86/Opcode.java
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,8 @@ final class TestDecodeIncompleteInstruction extends X64Encodings {

private static Stream<Arguments> incompleteInstructions() {
final Set<List<Byte>> 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 -> {
Expand Down
68 changes: 45 additions & 23 deletions id/src/test/java/com/ledmington/cpu/TestDecoding.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand All @@ -55,40 +58,60 @@ void checkReference(final Instruction inst) {
InstructionEncoder.toIntelSyntax(inst)));
}

private static Stream<Arguments> instAndHex() {
return X64_ENCODINGS.stream().map(x -> Arguments.of(x.instruction(), x.hex()));
private static Stream<Arguments> 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<byte[]> 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<Arguments> 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<Instruction> inst = InstructionDecoder.fromHex(hex, hex.length, true);
assertEquals(
Expand All @@ -105,7 +128,7 @@ void fromHex(final Instruction expected, final byte[] hex) {
}

private static Stream<Arguments> 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
Expand Down Expand Up @@ -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));
}
Expand Down
Loading
Loading