mirror of
https://github.com/aengelke/rellume
synced 2026-06-21 13:46:57 +00:00
434 lines
14 KiB
C++
434 lines
14 KiB
C++
|
|
#include <rellume/rellume.h>
|
|
|
|
#include <llvm/ExecutionEngine/ExecutionEngine.h>
|
|
#include <llvm/ExecutionEngine/GenericValue.h>
|
|
#include <llvm/IR/LLVMContext.h>
|
|
#include <llvm/IR/Module.h>
|
|
#include <llvm/IR/Verifier.h>
|
|
#include <llvm/Support/TargetSelect.h>
|
|
|
|
#include <cstddef>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <fstream>
|
|
#include <iomanip>
|
|
#include <iostream>
|
|
#include <memory>
|
|
#include <random>
|
|
#include <sstream>
|
|
#include <sys/mman.h>
|
|
#include <unistd.h>
|
|
#include <unordered_map>
|
|
#include <unordered_set>
|
|
#include <vector>
|
|
|
|
|
|
#ifndef MAP_FIXED_NOREPLACE
|
|
#define MAP_FIXED_NOREPLACE 0x100000
|
|
#endif
|
|
|
|
static bool opt_verbose = false;
|
|
static bool opt_jit = false;
|
|
static bool opt_pic = false;
|
|
static bool opt_overflow_intrinsics = false;
|
|
static const char* opt_arch = "x86_64";
|
|
|
|
struct HexBuffer {
|
|
uint8_t* buf;
|
|
size_t size;
|
|
friend std::ostream& operator<<(std::ostream& os, HexBuffer const& self) {
|
|
os << std::hex << std::setfill('0');
|
|
for (size_t i = 0; i != self.size; i++)
|
|
os << std::setw(2) << static_cast<int>(self.buf[i]);
|
|
return os << std::dec;
|
|
}
|
|
};
|
|
|
|
struct CPU {
|
|
uint8_t rip[8];
|
|
uint8_t data[4096-8];
|
|
} __attribute__((aligned(64)));
|
|
|
|
struct RegEntry {
|
|
size_t size;
|
|
off_t offset;
|
|
};
|
|
|
|
class TestCase {
|
|
const std::unordered_map<std::string,RegEntry>* regs;
|
|
std::ostringstream& diagnostic;
|
|
std::vector<std::pair<void*, size_t>> mem_maps;
|
|
|
|
TestCase(std::ostringstream& diagnostic) : diagnostic(diagnostic) {
|
|
static std::unordered_map<std::string,RegEntry> regs_empty = {};
|
|
#ifdef RELLUME_WITH_X86_64
|
|
static std::unordered_map<std::string,RegEntry> regs_x86_64 = {
|
|
#define RELLUME_NAMED_REG(name,nameu,sz,off) {#name, {sz, off}},
|
|
#include <rellume/cpustruct-x86_64-private.inc>
|
|
#undef RELLUME_NAMED_REG
|
|
};
|
|
#endif // RELLUME_WITH_X86_64
|
|
#ifdef RELLUME_WITH_RV64
|
|
static std::unordered_map<std::string,RegEntry> regs_rv64 = {
|
|
#define RELLUME_NAMED_REG(name,nameu,sz,off) {#name, {sz, off}},
|
|
#include <rellume/cpustruct-rv64-private.inc>
|
|
#undef RELLUME_NAMED_REG
|
|
};
|
|
#endif // RELLUME_WITH_RV64
|
|
#ifdef RELLUME_WITH_AARCH64
|
|
static std::unordered_map<std::string,RegEntry> regs_aarch64 = {
|
|
#define RELLUME_NAMED_REG(name,nameu,sz,off) {#name, {sz, off}},
|
|
#include <rellume/cpustruct-aarch64-private.inc>
|
|
#undef RELLUME_NAMED_REG
|
|
};
|
|
#endif // RELLUME_WITH_AARCH64
|
|
|
|
regs = ®s_empty;
|
|
#ifdef RELLUME_WITH_X86_64
|
|
if (!strcmp(opt_arch, "x86_64"))
|
|
regs = ®s_x86_64;
|
|
#endif // RELLUME_WITH_X86_64
|
|
#ifdef RELLUME_WITH_RV64
|
|
if (!strcmp(opt_arch, "rv64"))
|
|
regs = ®s_rv64;
|
|
#endif // RELLUME_WITH_RV64
|
|
#ifdef RELLUME_WITH_AARCH64
|
|
if (!strcmp(opt_arch, "aarch64"))
|
|
regs = ®s_aarch64;
|
|
#endif // RELLUME_WITH_AARCH64
|
|
}
|
|
|
|
~TestCase() {
|
|
for (auto& map : mem_maps) {
|
|
munmap(map.first, map.second);
|
|
}
|
|
}
|
|
|
|
bool SetReg(std::string reg, std::string value_str, CPU* cpu) {
|
|
auto reg_entry = regs->find(reg);
|
|
if (reg_entry == regs->end()) {
|
|
diagnostic << "# invalid register: " << reg << std::endl;
|
|
return true;
|
|
}
|
|
|
|
if (value_str.length() != reg_entry->second.size * 2) {
|
|
diagnostic << "# invalid input length: " << value_str << std::endl;
|
|
return true;
|
|
}
|
|
|
|
uint8_t* cpu_raw = reinterpret_cast<uint8_t*>(cpu);
|
|
uint8_t* buf = cpu_raw + reg_entry->second.offset;
|
|
for (size_t i = 0; i < reg_entry->second.size; i++) {
|
|
char hex_byte[3] = {value_str[i*2],value_str[i*2+1], 0};
|
|
buf[i] = std::strtoul(hex_byte, nullptr, 16);
|
|
}
|
|
|
|
if (!strcmp(opt_arch, "x86_64") && reg == "pf") {
|
|
buf[0] = !buf[0]; // value 1 => actual PF=0; value 0 => PF=1
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool CheckReg(const std::string& reg, const RegEntry& entry,
|
|
uint8_t* expected, uint8_t* state) {
|
|
uint8_t* expected_bytes = expected + entry.offset;
|
|
uint8_t* state_bytes = state + entry.offset;
|
|
|
|
if (!strcmp(opt_arch, "x86_64") && reg == "pf") {
|
|
bool expected_val = (__builtin_popcount(expected_bytes[0]) & 1) == 0;
|
|
bool state_val = (__builtin_popcount(state_bytes[0]) & 1) == 0;
|
|
if (expected_val == state_val)
|
|
return false;
|
|
diagnostic << "# unexpected value for " << reg << std::endl;
|
|
diagnostic << "# expected: " << expected_val << std::endl;
|
|
diagnostic << "# got: " << state_val << std::endl;
|
|
return true;
|
|
}
|
|
|
|
if (memcmp(state_bytes, expected_bytes, entry.size) == 0)
|
|
return false; // everything identical
|
|
|
|
diagnostic << "# unexpected value for " << reg << std::endl;
|
|
diagnostic << "# expected: " << HexBuffer{expected_bytes, entry.size} << std::endl;
|
|
diagnostic << "# got: " << HexBuffer{state_bytes, entry.size} << std::endl;
|
|
return true;
|
|
}
|
|
|
|
|
|
bool AllocMem(std::string key, std::string value_str) {
|
|
uintptr_t addr = std::stoul(key.substr(1), nullptr, 16);
|
|
size_t value_len = value_str.length() / 2;
|
|
|
|
uintptr_t paged_addr = addr & -sysconf(_SC_PAGE_SIZE);
|
|
size_t paged_size = value_len + (addr - paged_addr);
|
|
void* map = mmap(reinterpret_cast<void*>(paged_addr), paged_size,
|
|
PROT_READ|PROT_WRITE,
|
|
MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED_NOREPLACE, -1, 0);
|
|
if (map == MAP_FAILED || reinterpret_cast<uintptr_t>(map) != paged_addr) {
|
|
diagnostic << "# error mapping address " << std::hex << addr << std::endl;
|
|
return true;
|
|
}
|
|
mem_maps.push_back(std::make_pair(map, paged_size));
|
|
|
|
uint8_t* buf = reinterpret_cast<uint8_t*>(addr);
|
|
for (size_t i = 0; i < value_len; i++) {
|
|
char hex_byte[3] = {value_str[i*2],value_str[i*2+1], 0};
|
|
buf[i] = std::strtoul(hex_byte, nullptr, 16);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool CheckMem(std::string key, std::string value_str) {
|
|
uintptr_t addr = std::stoul(key.substr(1), nullptr, 16);
|
|
size_t value_len = value_str.length() / 2;
|
|
|
|
uint8_t* buf = reinterpret_cast<uint8_t*>(addr);
|
|
bool fail = false;
|
|
for (size_t i = 0; i < value_len; i++) {
|
|
char hex_byte[3] = {value_str[i*2],value_str[i*2+1], 0};
|
|
uint8_t val = std::strtoul(hex_byte, nullptr, 16);
|
|
if (buf[i] != val) {
|
|
fail = true;
|
|
diagnostic << "# unexpected value for " << std::hex << addr << std::endl;
|
|
diagnostic << "# expected: " << HexBuffer{&val, 1} << std::endl;
|
|
diagnostic << "# got: " << HexBuffer{&buf[i], 1} << std::endl;
|
|
}
|
|
}
|
|
|
|
return fail;
|
|
}
|
|
|
|
std::pair<std::string, std::string> split_arg(std::string arg) {
|
|
size_t value_off = arg.find('=');
|
|
if (value_off == std::string::npos) {
|
|
std::cerr << "invalid input: " << arg << std::endl;
|
|
std::exit(1);
|
|
}
|
|
|
|
std::string key_str = arg.substr(0, value_off);
|
|
std::string value_str = arg.substr(value_off + 1);
|
|
return std::make_pair(key_str, value_str);
|
|
}
|
|
|
|
template<typename T>
|
|
void Randomize(T& t) {
|
|
using bytes_randomizer = std::independent_bits_engine<std::mt19937, CHAR_BIT, uint8_t>;
|
|
std::mt19937 engine;
|
|
bytes_randomizer rand_bytes(engine);
|
|
|
|
uint8_t* ptr = reinterpret_cast<uint8_t*>(&t);
|
|
for (size_t i = 0; i < sizeof(T); i++)
|
|
ptr[i] = rand_bytes();
|
|
}
|
|
|
|
bool Run(std::string argstring) {
|
|
std::istringstream argstream(argstring);
|
|
std::string arg;
|
|
bool fail = false;
|
|
bool should_pass = true;
|
|
bool use_jit = opt_jit;
|
|
bool use_pic = opt_pic;
|
|
|
|
// 1. Setup initial state
|
|
CPU initial{};
|
|
Randomize(initial);
|
|
|
|
while (argstream >> arg) {
|
|
if (arg == "!") {
|
|
should_pass = false;
|
|
} else if (arg == "+jit") {
|
|
use_jit = true;
|
|
} else if (arg == "-jit") {
|
|
use_jit = false;
|
|
} else if (arg == "+pic") {
|
|
use_pic = true;
|
|
} else if (arg == "-pic") {
|
|
use_pic = false;
|
|
} else if (arg.substr(0, 1) == "~") {
|
|
continue;
|
|
} else if (arg == "=>") {
|
|
goto run_function;
|
|
} else {
|
|
auto kv = split_arg(arg);
|
|
if (kv.first[0] == 'm') {
|
|
if (AllocMem(kv.first, kv.second))
|
|
return true;
|
|
} else {
|
|
if (SetReg(kv.first, kv.second, &initial))
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// We didn't run anything.
|
|
diagnostic << "# error: no emulation command" << std::endl;
|
|
return true;
|
|
|
|
run_function:
|
|
|
|
// 2. Emulate function
|
|
CPU state = initial;
|
|
|
|
llvm::LLVMContext ctx;
|
|
auto mod = std::make_unique<llvm::Module>("rellume_test", ctx);
|
|
|
|
LLConfig* rlcfg = ll_config_new();
|
|
ll_config_enable_verify_ir(rlcfg, true);
|
|
ll_config_set_position_independent_code(rlcfg, use_pic);
|
|
ll_config_enable_overflow_intrinsics(rlcfg, opt_overflow_intrinsics);
|
|
bool success = ll_config_set_architecture(rlcfg, opt_arch);
|
|
if (!success) {
|
|
diagnostic << "# error: unsupported architecture" << std::endl;
|
|
return true;
|
|
}
|
|
|
|
LLFunc* rlfn = ll_func_new(llvm::wrap(mod.get()), rlcfg);
|
|
bool decode_ok = !ll_func_decode_cfg(rlfn, *reinterpret_cast<uint64_t*>(&state.rip), nullptr, nullptr);
|
|
LLVMValueRef fn_wrap = decode_ok ? ll_func_lift(rlfn) : nullptr;
|
|
|
|
ll_func_dispose(rlfn);
|
|
ll_config_free(rlcfg);
|
|
|
|
if (!decode_ok) {
|
|
diagnostic << "# error: could not handle first instruction" << std::endl;
|
|
return should_pass;
|
|
}
|
|
if (!fn_wrap) {
|
|
diagnostic << "# error during lifting" << std::endl;
|
|
return should_pass;
|
|
}
|
|
|
|
llvm::Function* fn = llvm::unwrap<llvm::Function>(fn_wrap);
|
|
fn->setName("test_function");
|
|
if (opt_verbose)
|
|
fn->print(llvm::errs());
|
|
|
|
if (llvm::verifyFunction(*fn, &llvm::errs())) {
|
|
diagnostic << "# error: IR verification failed\n";
|
|
return true;
|
|
}
|
|
|
|
std::string error;
|
|
|
|
llvm::TargetOptions options;
|
|
options.EnableFastISel = true;
|
|
|
|
llvm::EngineBuilder builder(std::move(mod));
|
|
// There are two options: "Interpreter" and "JIT". Because we execute
|
|
// the code once only, the interpreter is usually faster (even compared
|
|
// to the -O0 JIT configuration).
|
|
if (use_jit)
|
|
builder.setEngineKind(llvm::EngineKind::JIT);
|
|
else
|
|
builder.setEngineKind(llvm::EngineKind::Interpreter);
|
|
builder.setErrorStr(&error);
|
|
#if LL_LLVM_MAJOR < 18
|
|
builder.setOptLevel(llvm::CodeGenOpt::None);
|
|
#else
|
|
builder.setOptLevel(llvm::CodeGenOptLevel::None);
|
|
#endif
|
|
builder.setTargetOptions(options);
|
|
|
|
if (llvm::ExecutionEngine* engine = builder.create()) {
|
|
// If we have a JIT compiler, get address of compiled code.
|
|
// Otherwise try to run the function using the interpreter.
|
|
const auto& name = fn->getName();
|
|
if (auto raw_ptr = engine->getFunctionAddress(name.str())) {
|
|
auto fn_ptr = reinterpret_cast<void(*)(CPU*)>(raw_ptr);
|
|
fn_ptr(&state);
|
|
} else {
|
|
engine->runFunction(fn, {llvm::PTOGV(&state)});
|
|
}
|
|
delete engine;
|
|
} else {
|
|
diagnostic << "# error creating engine: " << error << std::endl;
|
|
return true;
|
|
}
|
|
|
|
// 3. Compare with expected values
|
|
// - memory is compared immediately
|
|
// - registers are compared separately to support undefined values
|
|
CPU expected = initial;
|
|
|
|
std::unordered_set<std::string> skip_regs;
|
|
while (argstream >> arg) {
|
|
if (arg.substr(0, 1) == "~")
|
|
continue;
|
|
auto kv = split_arg(arg);
|
|
if (kv.first[0] == 'm') {
|
|
fail |= CheckMem(kv.first, kv.second);
|
|
} else if (kv.second == "undef") {
|
|
skip_regs.insert(kv.first);
|
|
} else {
|
|
SetReg(kv.first, kv.second, &expected);
|
|
}
|
|
}
|
|
|
|
uint8_t* state_raw = reinterpret_cast<uint8_t*>(&state);
|
|
uint8_t* expected_raw = reinterpret_cast<uint8_t*>(&expected);
|
|
for (const auto& reg_entry : *regs) {
|
|
if (skip_regs.count(reg_entry.first) > 0)
|
|
continue;
|
|
fail |= CheckReg(reg_entry.first, reg_entry.second, expected_raw,
|
|
state_raw);
|
|
}
|
|
|
|
return should_pass ? fail : !fail;
|
|
}
|
|
|
|
public:
|
|
static bool Run(unsigned number, std::string caseline,
|
|
std::ostream& output) {
|
|
std::ostringstream diagnostic;
|
|
TestCase test_case(diagnostic);
|
|
bool fail = test_case.Run(caseline);
|
|
if (fail)
|
|
output << "not ";
|
|
output << "ok " << number << " " << caseline << std::endl;
|
|
output << diagnostic.str();
|
|
return fail;
|
|
}
|
|
};
|
|
|
|
int main(int argc, char** argv) {
|
|
int opt;
|
|
while ((opt = getopt(argc, argv, "vjpiA:")) != -1) {
|
|
switch (opt) {
|
|
case 'v': opt_verbose = true; break;
|
|
case 'j': opt_jit = true; break;
|
|
case 'p': opt_pic = true; break;
|
|
case 'i': opt_overflow_intrinsics = true; break;
|
|
case 'A': opt_arch = optarg; break;
|
|
default:
|
|
usage:
|
|
std::cerr << "usage: " << argv[0] << " [-v] [-j] [-p] [-i] [-A arch] casefile" << std::endl;
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
if (optind >= argc)
|
|
goto usage;
|
|
|
|
llvm::InitializeNativeTarget();
|
|
llvm::InitializeNativeTargetAsmPrinter();
|
|
|
|
std::ifstream casefile(argv[optind]);
|
|
if (casefile.fail()) {
|
|
std::cerr << "error opening casefile" << std::endl;
|
|
return 1;
|
|
}
|
|
|
|
std::ostringstream output;
|
|
unsigned count = 0;
|
|
bool fail = false;
|
|
for (std::string caseline; std::getline(casefile, caseline); count++)
|
|
fail |= TestCase::Run(count + 1, caseline, output);
|
|
|
|
std::cout << output.str() << "1.." << count << std::endl;
|
|
|
|
return fail ? 1 : 0;
|
|
}
|