tests: [WIP] add unit tests for some of the instructions

Signed-off-by: Amneesh Singh <natto@weirdnatto.in>
This commit is contained in:
2023-09-15 14:07:23 +05:30
parent aa96237c37
commit be7deb349a
11 changed files with 445 additions and 173 deletions

View File

@@ -6,6 +6,7 @@
#include <fstream>
#include <iostream>
#include <memory>
#include <ostream>
#include <unistd.h>
#include <vector>
@@ -80,6 +81,9 @@ main(int argc, const char* argv[]) {
return 1;
}
std::flush(std::cout);
std::flush(std::cerr);
{
Memory memory(std::move(bios), std::move(rom));
Bus bus(memory);

View File

@@ -8,17 +8,23 @@
systems = [
"x86_64-linux"
"aarch64-linux"
# "i686-linux"
];
eachSystem = with nixpkgs.lib; f: foldAttrs mergeAttrs { }
(map (s: mapAttrs (_: v: { ${s} = v; }) (f s)) systems);
in
eachSystem (system:
let
pkgs = import nixpkgs { inherit system; };
# aliases
llvm = pkgs.llvmPackages_16;
stdenv = llvm.libcxxStdenv;
# packages
catch2_v3 = pkgs.callPackage ./nix/catch2.nix { inherit stdenv; };
#dependencies
nativeBuildInputs = with pkgs; [
meson
ninja
@@ -26,6 +32,7 @@
# libraries
pkg-config
fmt.dev
catch2_v3.dev
];
in
rec {
@@ -51,6 +58,8 @@
matar = pkgs.mkShell.override { inherit stdenv; } {
name = "matar";
packages = nativeBuildInputs ++ (with pkgs; [
llvm.libcxx
# dev tools
clang-tools_16
]);

View File

@@ -35,7 +35,7 @@ class Cpu {
bool is_flushed;
void chg_mode(const Mode to);
void exec_arm(const ArmInstruction instruction);
void exec_arm(const arm::ArmInstruction instruction);
struct {
std::array<uint32_t, GPR_COUNT - GPR_FIQ_FIRST - 1> fiq;

View File

@@ -9,16 +9,7 @@ struct overloaded : Ts... {
template<class... Ts>
overloaded(Ts...) -> overloaded<Ts...>;
class ArmInstruction {
public:
ArmInstruction() = delete;
ArmInstruction(uint32_t insn);
auto get_condition() const { return condition; }
auto get_data() const { return data; }
std::string disassemble();
namespace arm {
struct BranchAndExchange {
uint8_t rn;
};
@@ -161,7 +152,11 @@ class ArmInstruction {
Undefined,
SoftwareInterrupt>;
private:
struct ArmInstruction {
Condition condition;
InstructionData data;
ArmInstruction(uint32_t insn);
std::string disassemble();
};
}

View File

@@ -3,10 +3,32 @@ project('matar', 'cpp',
license : 'GPLv3',
default_options : ['warning_level=3',
'werror=true',
'optimization=3',
'cpp_std=c++20'])
'optimization=3'])
compiler = meson.get_compiler('cpp')
if compiler.has_argument('-std=c++23')
add_global_arguments('-std=c++23', language: 'cpp')
elif compiler.has_argument('-std=c++2b')
add_global_arguments('-std=c++2b', language: 'cpp')
else
error(compiler.get_id() + ' ' + compiler.version() + 'does not meet the compiler requirements')
endif
'''
TODO: use <print> and <format> instead of libfmt once LLVM 17 is out
if compiler.has_argument('-fexperimental-library')
add_global_arguments('-fexperimental-library', language: 'cpp')
else
error(compiler.get_id() + ' ' + compiler.version() + 'does not support -fexperimental-library')
endif
'''
inc = include_directories('include')
subdir('include')
subdir('src')
subdir('apps')
subdir('tests')

17
nix/catch2.nix Normal file
View File

@@ -0,0 +1,17 @@
{ stdenv, fetchFromGitHub, meson, ninja }:
stdenv.mkDerivation rec {
name = "catch2";
version = "3.4.0";
src = fetchFromGitHub {
owner = "catchorg";
repo = "Catch2";
rev = "v${version}";
sha256 = "sha256-DqGGfNjKPW9HFJrX9arFHyNYjB61uoL6NabZatTWrr0=";
};
nativeBuildInputs = [ meson ninja ];
outputs = [ "out" "dev" ];
}

View File

@@ -116,27 +116,29 @@ Cpu::chg_mode(const Mode to) {
}
void
Cpu::exec_arm(const ArmInstruction instruction) {
auto cond = instruction.get_condition();
auto data = instruction.get_data();
Cpu::exec_arm(const arm::ArmInstruction instruction) {
auto cond = instruction.condition;
auto data = instruction.data;
if (!cpsr.condition(cond)) {
return;
}
auto pc_error = [](uint8_t r) {
if (r == 15)
if (r == PC_INDEX)
log_error("Using PC (R15) as operand register");
};
auto pc_warn = [](uint8_t r) {
if (r == 15)
if (r == PC_INDEX)
log_warn("Using PC (R15) as operand register");
};
using namespace arm;
std::visit(
overloaded{
[this, pc_warn](ArmInstruction::BranchAndExchange& data) {
[this, pc_warn](BranchAndExchange& data) {
State state = static_cast<State>(data.rn & 1);
pc_warn(data.rn);
@@ -156,7 +158,7 @@ Cpu::exec_arm(const ArmInstruction instruction) {
// pc is affected so flush the pipeline
is_flushed = true;
},
[this](ArmInstruction::Branch& data) {
[this](Branch& data) {
if (data.link)
gpr[14] = pc - ARM_INSTRUCTION_SIZE;
@@ -168,7 +170,7 @@ Cpu::exec_arm(const ArmInstruction instruction) {
// pc is affected so flush the pipeline
is_flushed = true;
},
[this, pc_error](ArmInstruction::Multiply& data) {
[this, pc_error](Multiply& data) {
if (data.rd == data.rm)
log_error("rd and rm are not distinct in {}",
typeid(data).name());
@@ -186,7 +188,7 @@ Cpu::exec_arm(const ArmInstruction instruction) {
cpsr.set_c(0);
}
},
[this, pc_error](ArmInstruction::MultiplyLong& data) {
[this, pc_error](MultiplyLong& data) {
if (data.rdhi == data.rdlo || data.rdhi == data.rm ||
data.rdlo == data.rm)
log_error("rdhi, rdlo and rm are not distinct in {}",
@@ -226,8 +228,8 @@ Cpu::exec_arm(const ArmInstruction instruction) {
cpsr.set_v(0);
}
},
[](ArmInstruction::Undefined) { log_warn("Undefined instruction"); },
[this, pc_error](ArmInstruction::SingleDataSwap& data) {
[](Undefined) { log_warn("Undefined instruction"); },
[this, pc_error](SingleDataSwap& data) {
pc_error(data.rm);
pc_error(data.rn);
pc_error(data.rd);
@@ -240,7 +242,7 @@ Cpu::exec_arm(const ArmInstruction instruction) {
bus->write_word(gpr[data.rn], gpr[data.rm]);
}
},
[this, pc_warn, pc_error](ArmInstruction::SingleDataTransfer& data) {
[this, pc_warn, pc_error](SingleDataTransfer& data) {
uint32_t offset = 0;
uint32_t address = gpr[data.rn];
@@ -316,7 +318,7 @@ Cpu::exec_arm(const ArmInstruction instruction) {
if (data.rd == PC_INDEX && data.load)
is_flushed = true;
},
[this, pc_warn, pc_error](ArmInstruction::HalfwordTransfer& data) {
[this, pc_warn, pc_error](HalfwordTransfer& data) {
uint32_t address = gpr[data.rn];
if (!data.pre && data.write)
@@ -345,15 +347,16 @@ Cpu::exec_arm(const ArmInstruction instruction) {
gpr[data.rd] = bus->read_halfword(address);
// sign extend the halfword
if (get_bit(gpr[data.rd], PC_INDEX))
gpr[data.rd] |= 0xFFFF0000;
gpr[data.rd] =
(static_cast<int32_t>(gpr[data.rd]) << 16) >> 16;
// byte
} else {
gpr[data.rd] = bus->read_byte(address);
// sign extend the byte
if (get_bit(gpr[data.rd], 7))
gpr[data.rd] |= 0xFFFFFF00;
gpr[data.rd] =
(static_cast<int32_t>(gpr[data.rd]) << 24) >> 24;
}
// unsigned halfword
} else if (data.half) {
@@ -379,7 +382,7 @@ Cpu::exec_arm(const ArmInstruction instruction) {
if (data.rd == PC_INDEX && data.load)
is_flushed = true;
},
[this, pc_error](ArmInstruction::BlockDataTransfer& data) {
[this, pc_error](BlockDataTransfer& data) {
uint32_t address = gpr[data.rn];
Mode mode = cpsr.mode();
uint8_t alignment = 4; // word
@@ -449,7 +452,7 @@ Cpu::exec_arm(const ArmInstruction instruction) {
// load back the original mode registers
chg_mode(mode);
},
[this, pc_error](ArmInstruction::PsrTransfer& data) {
[this, pc_error](PsrTransfer& data) {
if (data.spsr && cpsr.mode() == Mode::User) {
log_error("Accessing SPSR in User mode in {}",
typeid(data).name());
@@ -458,18 +461,18 @@ Cpu::exec_arm(const ArmInstruction instruction) {
Psr& psr = data.spsr ? spsr : cpsr;
switch (data.type) {
case ArmInstruction::PsrTransfer::Type::Mrs:
case PsrTransfer::Type::Mrs:
pc_error(data.operand);
gpr[data.operand] = psr.raw();
break;
case ArmInstruction::PsrTransfer::Type::Msr:
case PsrTransfer::Type::Msr:
pc_error(data.operand);
if (cpsr.mode() != Mode::User) {
psr.set_all(gpr[data.operand]);
break;
}
case ArmInstruction::PsrTransfer::Type::Msr_flg:
case PsrTransfer::Type::Msr_flg:
psr.set_n(get_bit(data.operand, 31));
psr.set_z(get_bit(data.operand, 30));
psr.set_c(get_bit(data.operand, 29));
@@ -477,7 +480,7 @@ Cpu::exec_arm(const ArmInstruction instruction) {
break;
}
},
[this, pc_error](ArmInstruction::DataProcessing& data) {
[this, pc_error](DataProcessing& data) {
uint32_t op_1 = gpr[data.rn];
uint32_t op_2 = 0;
@@ -671,7 +674,7 @@ Cpu::exec_arm(const ArmInstruction instruction) {
is_flushed = true;
}
},
[this](ArmInstruction::SoftwareInterrupt) {
[this](SoftwareInterrupt) {
chg_mode(Mode::Supervisor);
pc = 0x08;
spsr = cpsr;
@@ -690,7 +693,7 @@ Cpu::step() {
if (cpsr.state() == State::Arm) {
debug(cur_pc);
uint32_t x = bus->read_word(cur_pc);
ArmInstruction instruction(x);
arm::ArmInstruction instruction(x);
log_info("{:#034b}", x);
exec_arm(instruction);

View File

@@ -336,7 +336,8 @@ ArmInstruction::disassemble() {
if (*offset == 0) {
expression = "";
} else {
expression = fmt::format(",#{:d}", *offset);
expression =
fmt::format(",{}#{:d}", (data.up ? '+' : '-'), *offset);
}
} else if (const Shift* shift = std::get_if<Shift>(&data.offset)) {
// Shifts are always immediate in single data transfer
@@ -365,7 +366,8 @@ ArmInstruction::disassemble() {
if (data.offset == 0) {
expression = "";
} else {
expression = fmt::format(",#{:d}", data.offset);
expression = fmt::format(
",{}#{:d}", (data.up ? '+' : '-'), data.offset);
}
} else {
expression =

198
tests/cpu/instruction.cc Normal file
View File

@@ -0,0 +1,198 @@
#include "cpu/instruction.hh"
#include "cpu/utility.hh"
#include <catch2/catch_test_macros.hpp>
#include <iostream>
static constexpr auto TAG = "disassembler";
using namespace arm;
TEST_CASE("Branch and Exchange", TAG) {
uint32_t raw = 0b11000001001011111111111100011010;
ArmInstruction instruction(raw);
BranchAndExchange* bx = nullptr;
REQUIRE((bx = std::get_if<BranchAndExchange>(&instruction.data)));
REQUIRE(instruction.condition == Condition::GT);
REQUIRE(bx->rn == 10);
REQUIRE(instruction.disassemble() == "BXGT R10");
}
TEST_CASE("Branch", TAG) {
uint32_t raw = 0b11101011100001010111111111000011;
ArmInstruction instruction(raw);
Branch* b = nullptr;
REQUIRE((b = std::get_if<Branch>(&instruction.data)));
REQUIRE(instruction.condition == Condition::AL);
// last 24 bits = 8748995
// (8748995 << 8) >> 6 sign extended = 0xFE15FF0C
// Also +8 since PC is two instructions ahead
REQUIRE(b->offset == 0xFE15FF14);
REQUIRE(b->link == true);
REQUIRE(instruction.disassemble() == "BL 0xFE15FF14");
b->link = false;
REQUIRE(instruction.disassemble() == "B 0xFE15FF14");
}
TEST_CASE("Multiply", TAG) {
uint32_t raw = 0b00000000001110101110111110010000;
ArmInstruction instruction(raw);
Multiply* mul = nullptr;
REQUIRE((mul = std::get_if<Multiply>(&instruction.data)));
REQUIRE(instruction.condition == Condition::EQ);
REQUIRE(mul->rm == 0);
REQUIRE(mul->rs == 15);
REQUIRE(mul->rn == 14);
REQUIRE(mul->rd == 10);
REQUIRE(mul->acc == true);
REQUIRE(mul->set == true);
REQUIRE(instruction.disassemble() == "MLAEQS R10,R0,R15,R14");
mul->acc = false;
mul->set = false;
REQUIRE(instruction.disassemble() == "MULEQ R10,R0,R15");
}
TEST_CASE("Multiply Long", TAG) {
uint32_t raw = 0b00010000100111100111011010010010;
ArmInstruction instruction(raw);
MultiplyLong* mull = nullptr;
REQUIRE((mull = std::get_if<MultiplyLong>(&instruction.data)));
REQUIRE(instruction.condition == Condition::NE);
REQUIRE(mull->rm == 2);
REQUIRE(mull->rs == 6);
REQUIRE(mull->rdlo == 7);
REQUIRE(mull->rdhi == 14);
REQUIRE(mull->acc == false);
REQUIRE(mull->set == true);
REQUIRE(mull->uns == false);
REQUIRE(instruction.disassemble() == "SMULLNES R7,R14,R2,R6");
mull->acc = true;
REQUIRE(instruction.disassemble() == "SMLALNES R7,R14,R2,R6");
mull->uns = true;
mull->set = false;
REQUIRE(instruction.disassemble() == "UMLALNE R7,R14,R2,R6");
}
TEST_CASE("Single Data Swap", TAG) {
uint32_t raw = 0b10100001000010010101000010010110;
ArmInstruction instruction(raw);
SingleDataSwap* swp = nullptr;
REQUIRE((swp = std::get_if<SingleDataSwap>(&instruction.data)));
REQUIRE(instruction.condition == Condition::GE);
REQUIRE(swp->rm == 6);
REQUIRE(swp->rd == 5);
REQUIRE(swp->rn == 9);
REQUIRE(swp->byte == false);
REQUIRE(instruction.disassemble() == "SWPGE R5,R6,[R9]");
swp->byte = true;
REQUIRE(instruction.disassemble() == "SWPGEB R5,R6,[R9]");
}
TEST_CASE("Single Data Transfer", TAG) {
uint32_t raw = 0b11100111101000101010111100000110;
ArmInstruction instruction(raw);
SingleDataTransfer* ldr = nullptr;
Shift* shift = nullptr;
REQUIRE((ldr = std::get_if<SingleDataTransfer>(&instruction.data)));
REQUIRE(instruction.condition == Condition::AL);
REQUIRE((shift = std::get_if<Shift>(&ldr->offset)));
REQUIRE(shift->rm == 6);
REQUIRE(shift->data.immediate == true);
REQUIRE(shift->data.type == ShiftType::LSL);
REQUIRE(shift->data.operand == 30);
REQUIRE(ldr->rd == 10);
REQUIRE(ldr->rn == 2);
REQUIRE(ldr->load == false);
REQUIRE(ldr->write == true);
REQUIRE(ldr->byte == false);
REQUIRE(ldr->up == true);
REQUIRE(ldr->pre == true);
ldr->load = true;
ldr->byte = true;
ldr->write = false;
shift->data.type = ShiftType::ROR;
REQUIRE(instruction.disassemble() == "LDRB R10,[R2,+R6,ROR #30]");
ldr->up = false;
ldr->pre = false;
REQUIRE(instruction.disassemble() == "LDRB R10,[R2],-R6,ROR #30");
ldr->offset = static_cast<uint16_t>(9023);
REQUIRE(instruction.disassemble() == "LDRB R10,[R2],-#9023");
ldr->pre = true;
REQUIRE(instruction.disassemble() == "LDRB R10,[R2,-#9023]");
}
TEST_CASE("Halfword Transfer", TAG) {
uint32_t raw = 0b00110001101011110010000010110110;
ArmInstruction instruction(raw);
HalfwordTransfer* ldr = nullptr;
REQUIRE((ldr = std::get_if<HalfwordTransfer>(&instruction.data)));
REQUIRE(instruction.condition == Condition::CC);
// offset is not immediate
REQUIRE(ldr->imm == 0);
// hence this offset is a register number (rm)
REQUIRE(ldr->offset == 6);
REQUIRE(ldr->half == true);
REQUIRE(ldr->sign == false);
REQUIRE(ldr->rd == 2);
REQUIRE(ldr->rn == 15);
REQUIRE(ldr->load == false);
REQUIRE(ldr->write == true);
REQUIRE(ldr->up == true);
REQUIRE(ldr->pre == true);
REQUIRE(instruction.disassemble() == "STRCCH R2,[R15,+R6]!");
ldr->pre = false;
ldr->load = true;
ldr->sign = true;
ldr->up = false;
REQUIRE(instruction.disassemble() == "LDRCCSH R2,[R15],-R6");
ldr->half = false;
REQUIRE(instruction.disassemble() == "LDRCCSB R2,[R15],-R6");
// not a register anymore
ldr->load = false;
ldr->imm = 1;
ldr->offset = 90;
REQUIRE(instruction.disassemble() == "STRCCSB R2,[R15],-#90");
}
TEST_CASE("Undefined", TAG) {
// notice how this is the same as single data transfer except the shift is
// now a register based shift
uint32_t raw = 0b11100111101000101010111100010110;
REQUIRE(ArmInstruction(raw).disassemble() == "UNDEFINED");
raw = 0b11100110000000000000000000010000;
REQUIRE(ArmInstruction(raw).disassemble() == "UNDEFINED");
}

3
tests/cpu/meson.build Normal file
View File

@@ -0,0 +1,3 @@
tests_sources += files(
'instruction.cc'
)

19
tests/meson.build Normal file
View File

@@ -0,0 +1,19 @@
tests_deps = [
lib
]
tests_sources = files()
subdir('cpu')
catch2 = dependency('catch2-with-main', version: '>=3.4.0', static: true)
catch2_tests = executable(
meson.project_name() + '_tests',
tests_sources,
dependencies: catch2,
link_with: tests_deps,
include_directories: inc,
build_by_default: false
)
test('catch2 tests', catch2_tests)