535 lines
16 KiB
JavaScript
Executable file
535 lines
16 KiB
JavaScript
Executable file
#!/usr/bin/env node
|
|
"use strict";
|
|
|
|
process.on("unhandledRejection", exn => { throw exn; });
|
|
|
|
// Mapping between signals and x86 exceptions:
|
|
// "Program received signal SIGILL, Illegal instruction." -> #UD (6)
|
|
// "Program received signal SIGFPE, Arithmetic exception." -> #DE (0)
|
|
// to be determined -> #GP
|
|
// to be determined -> #NM
|
|
// to be determined -> #TS
|
|
// to be determined -> #NP
|
|
// to be determined -> #SS
|
|
// to be determined -> #PF
|
|
|
|
// A #UD might indicate a bug in the test generation
|
|
|
|
const assert = require("assert").strict;
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const os = require("os");
|
|
const cluster = require("cluster");
|
|
|
|
const MAX_PARALLEL_TESTS = +process.env.MAX_PARALLEL_TESTS || 99;
|
|
const TEST_NAME = process.env.TEST_NAME;
|
|
const SINGLE_TEST_TIMEOUT = 10000;
|
|
const TEST_RELEASE_BUILD = +process.env.TEST_RELEASE_BUILD;
|
|
|
|
const TEST_DIR = __dirname + "/build/";
|
|
const DONE_MSG = "DONE";
|
|
const TERMINATE_MSG = "DONE";
|
|
|
|
const FORCE_JIT = process.argv.includes("--force-jit");
|
|
|
|
// see --section-start= in makefile
|
|
const V86_TEXT_OFFSET = 0x8000;
|
|
const NASM_TEXT_OFFSET = 0x800000;
|
|
|
|
// alternative representation for infinity for json
|
|
const JSON_POS_INFINITY = "+INFINITY";
|
|
const JSON_NEG_INFINITY = "-INFINITY";
|
|
const JSON_POS_NAN = "+NAN";
|
|
const JSON_NEG_NAN = "-NAN";
|
|
|
|
const MASK_ARITH = 1 | 1 << 2 | 1 << 4 | 1 << 6 | 1 << 7 | 1 << 11;
|
|
const FPU_TAG_ALL_INVALID = 0xAAAA;
|
|
const FPU_STATUS_MASK = 0xFFFF & ~(1 << 9 | 1 << 5 | 1 << 3); // bits that are not correctly implemented by v86
|
|
const FP_COMPARISON_SIGNIFICANT_DIGITS = 7;
|
|
|
|
try {
|
|
var V86 = require(`../../build/${TEST_RELEASE_BUILD ? "libv86" : "libv86-debug"}.js`).V86;
|
|
}
|
|
catch(e) {
|
|
console.error(e);
|
|
console.error("Failed to import build/libv86-debug.js. Run " +
|
|
"`make build/libv86-debug.js` first.");
|
|
process.exit(1);
|
|
}
|
|
|
|
function float_equal(x, y)
|
|
{
|
|
assert(typeof x === "number");
|
|
assert(typeof y === "number");
|
|
|
|
if(x === Infinity && y === Infinity || x === -Infinity && y === -Infinity || isNaN(x) && isNaN(y))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
const epsilon = Math.pow(10, -FP_COMPARISON_SIGNIFICANT_DIGITS);
|
|
return Math.abs(x - y) < epsilon;
|
|
}
|
|
|
|
function format_value(v)
|
|
{
|
|
if(typeof v === "number")
|
|
{
|
|
if((v >>> 0) !== v && (v | 0) !== v)
|
|
{
|
|
return String(v);
|
|
}
|
|
else
|
|
{
|
|
return "0x" + (v >>> 0).toString(16);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return String(v);
|
|
}
|
|
}
|
|
|
|
if(cluster.isMaster)
|
|
{
|
|
function extract_json(name, fixture_text)
|
|
{
|
|
let exception;
|
|
|
|
if(fixture_text.includes("(signal SIGFPE)"))
|
|
{
|
|
exception = "DE";
|
|
}
|
|
|
|
if(fixture_text.includes("(signal SIGILL)"))
|
|
{
|
|
exception = "UD";
|
|
}
|
|
|
|
if(fixture_text.includes("(signal SIGSEGV)"))
|
|
{
|
|
exception = "GP";
|
|
}
|
|
|
|
if(fixture_text.includes("(signal SIGBUS)"))
|
|
{
|
|
exception = "PF";
|
|
}
|
|
|
|
if(!exception && fixture_text.includes("Program received signal"))
|
|
{
|
|
throw new Error("Test was killed during execution by gdb: " + name + "\n" + fixture_text);
|
|
}
|
|
|
|
fixture_text = fixture_text.toString()
|
|
.replace(/-inf\b/g, JSON.stringify(JSON_NEG_INFINITY))
|
|
.replace(/\binf\b/g, JSON.stringify(JSON_POS_INFINITY))
|
|
.replace(/-nan\b/g, JSON.stringify(JSON_NEG_NAN))
|
|
.replace(/\bnan\b/g, JSON.stringify(JSON_POS_NAN));
|
|
|
|
const json_regex = /---BEGIN JSON---([\s\[\]\.\+\w":\-,]*)---END JSON---/;
|
|
const regex_match = json_regex.exec(fixture_text);
|
|
if (!regex_match || regex_match.length < 2) {
|
|
throw new Error("Could not find JSON in fixture text: " + fixture_text + "\nTest: " + name);
|
|
}
|
|
|
|
let array = JSON.parse(regex_match[1]);
|
|
return {
|
|
array: array,
|
|
exception,
|
|
};
|
|
}
|
|
|
|
|
|
function send_work_to_worker(worker, message) {
|
|
if(current_test < tests.length) {
|
|
const test = tests[current_test];
|
|
worker.send(test);
|
|
current_test++;
|
|
}
|
|
else {
|
|
worker.send(TERMINATE_MSG);
|
|
worker.disconnect();
|
|
|
|
setTimeout(() => {
|
|
// The emulator currently doesn't cleanly exit, so this is necessary
|
|
console.log("Worker killed");
|
|
worker.kill();
|
|
}, 100);
|
|
|
|
finished_workers++;
|
|
if(finished_workers === nr_of_cpus)
|
|
{
|
|
test_finished();
|
|
}
|
|
}
|
|
}
|
|
|
|
const dir_files = fs.readdirSync(TEST_DIR);
|
|
const files = dir_files.filter((name) => {
|
|
return name.endsWith(".asm");
|
|
}).map(name => {
|
|
return name.slice(0, -4);
|
|
}).filter(name => {
|
|
return !TEST_NAME || name === TEST_NAME;
|
|
});
|
|
|
|
const tests = files.map(name => {
|
|
let fixture_name = name + ".fixture";
|
|
let img_name = name + ".img";
|
|
let fixture_text = fs.readFileSync(TEST_DIR + fixture_name);
|
|
let fixture = extract_json(name, fixture_text);
|
|
|
|
return {
|
|
img_name: img_name,
|
|
fixture: fixture,
|
|
};
|
|
});
|
|
|
|
const nr_of_cpus = Math.min(
|
|
os.cpus().length || 1,
|
|
tests.length,
|
|
MAX_PARALLEL_TESTS
|
|
);
|
|
console.log("Using %d cpus", nr_of_cpus);
|
|
|
|
let current_test = 0;
|
|
|
|
let failed_tests = [];
|
|
let finished_workers = 0;
|
|
|
|
for(let i = 0; i < nr_of_cpus; i++)
|
|
{
|
|
let worker = cluster.fork();
|
|
|
|
worker.on("message", function(message) {
|
|
if (message !== DONE_MSG) {
|
|
failed_tests.push(message);
|
|
}
|
|
send_work_to_worker(this);
|
|
});
|
|
|
|
worker.on("online", send_work_to_worker.bind(null, worker));
|
|
|
|
worker.on("exit", function(code, signal) {
|
|
if(code !== 0 && code !== null) {
|
|
console.log("Worker error code:", code);
|
|
process.exit(code);
|
|
}
|
|
});
|
|
|
|
worker.on("error", function(error) {
|
|
console.error("Worker error: ", error.toString(), error);
|
|
process.exit(1);
|
|
});
|
|
}
|
|
|
|
function test_finished()
|
|
{
|
|
console.log(
|
|
"\n[+] Passed %d/%d tests.",
|
|
tests.length - failed_tests.length,
|
|
tests.length
|
|
);
|
|
if (failed_tests.length > 0) {
|
|
console.log("[-] Failed %d test(s).", failed_tests.length);
|
|
failed_tests.forEach(function(test_failure) {
|
|
|
|
console.error("\n[-] %s:", test_failure.img_name);
|
|
|
|
test_failure.failures.forEach(function(failure) {
|
|
console.error("\n\t" + failure.name);
|
|
console.error("\tActual: " + failure.actual);
|
|
console.error("\tExpected: " + failure.expected);
|
|
});
|
|
});
|
|
process.exit(1);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
function run_test(test)
|
|
{
|
|
if(!loaded)
|
|
{
|
|
first_test = test;
|
|
return;
|
|
}
|
|
|
|
waiting_to_receive_next_test = false;
|
|
current_test = test;
|
|
console.info("Testing", test.img_name);
|
|
|
|
var cpu = emulator.v86.cpu;
|
|
|
|
assert(!emulator.running);
|
|
|
|
cpu.reboot_internal();
|
|
cpu.reset_memory();
|
|
cpu.load_multiboot(fs.readFileSync(TEST_DIR + current_test.img_name).buffer);
|
|
|
|
test_timeout = setTimeout(() => {
|
|
console.error("Test " + test.img_name + " timed out after " + (SINGLE_TEST_TIMEOUT / 1000) + " seconds.");
|
|
process.exit(2);
|
|
}, SINGLE_TEST_TIMEOUT);
|
|
|
|
if(FORCE_JIT)
|
|
{
|
|
cpu.test_hook_did_finalize_wasm = function()
|
|
{
|
|
cpu.test_hook_did_finalize_wasm = null;
|
|
|
|
// don't synchronously call into the emulator from this callback
|
|
setTimeout(() => {
|
|
emulator.run();
|
|
}, 0);
|
|
};
|
|
|
|
cpu.jit_force_generate(cpu.instruction_pointer[0]);
|
|
}
|
|
else
|
|
{
|
|
emulator.run();
|
|
}
|
|
}
|
|
|
|
let loaded = false;
|
|
let current_test = undefined;
|
|
let first_test = undefined;
|
|
let waiting_to_receive_next_test = false;
|
|
let recorded_exceptions = [];
|
|
let test_timeout;
|
|
|
|
let emulator = new V86({
|
|
autostart: false,
|
|
memory_size: 2 * 1024 * 1024,
|
|
log_level: 0,
|
|
});
|
|
|
|
emulator.add_listener("emulator-loaded", function()
|
|
{
|
|
loaded = true;
|
|
|
|
if(first_test)
|
|
{
|
|
run_test(first_test);
|
|
}
|
|
});
|
|
|
|
emulator.cpu_exception_hook = function(n)
|
|
{
|
|
emulator.v86.cpu.instruction_counter[0] += 100000; // always make progress
|
|
|
|
if(waiting_to_receive_next_test)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
const exceptions = {
|
|
0: "DE",
|
|
6: "UD",
|
|
13: "GP",
|
|
};
|
|
|
|
const exception = exceptions[n];
|
|
|
|
if(exception === undefined)
|
|
{
|
|
console.error("Unexpected CPU exception: " + n);
|
|
process.exit(1);
|
|
}
|
|
|
|
const eip = emulator.v86.cpu.instruction_pointer[0];
|
|
emulator.v86.cpu.write32(emulator.v86.cpu.translate_address_system_read(eip), 0xF4F4F4F4); // hlt
|
|
|
|
// XXX: On gdb execution is stopped at this point. On v86 we
|
|
// currently don't have this ability, so we record the exception
|
|
// and continue execution
|
|
recorded_exceptions.push({ exception, eip });
|
|
finish_test();
|
|
return true;
|
|
};
|
|
|
|
emulator.bus.register("cpu-event-halt", function() {
|
|
finish_test();
|
|
});
|
|
|
|
function finish_test()
|
|
{
|
|
if(waiting_to_receive_next_test)
|
|
{
|
|
return;
|
|
}
|
|
|
|
waiting_to_receive_next_test = true;
|
|
clearTimeout(test_timeout);
|
|
|
|
emulator.stop();
|
|
var cpu = emulator.v86.cpu;
|
|
|
|
const evaluated_fpu_regs = new Float64Array(8).map((_, i) => cpu.fpu_get_sti_f64(i));
|
|
const evaluated_mmxs = new Int32Array(16).map((_, i) => cpu.fpu_st[(i & ~1) << 1 | (i & 1)]);
|
|
const evaluated_xmms = cpu.reg_xmm32s;
|
|
const evaluated_memory = new Int32Array(cpu.mem8.slice(0x120000 - 16 * 4, 0x120000).buffer);
|
|
const evaluated_fpu_tag = cpu.fpu_load_tag_word();
|
|
const evaluated_fpu_status = cpu.fpu_load_status_word() & FPU_STATUS_MASK;
|
|
|
|
let individual_failures = [];
|
|
|
|
assert(current_test.fixture.array);
|
|
|
|
const FLOAT_TRANSLATION = {
|
|
[JSON_POS_INFINITY]: Infinity,
|
|
[JSON_NEG_INFINITY]: -Infinity,
|
|
[JSON_POS_NAN]: NaN,
|
|
[JSON_NEG_NAN]: NaN, // XXX: Ignore sign of NaN
|
|
};
|
|
|
|
let offset = 0;
|
|
const expected_reg32 = current_test.fixture.array.slice(offset, offset += 8);
|
|
const expected_eip = current_test.fixture.array[offset++];
|
|
const expected_fpu_regs =
|
|
current_test.fixture.array.slice(offset, offset += 8) .map(x => x in FLOAT_TRANSLATION ? FLOAT_TRANSLATION[x] : x);
|
|
const expected_mmx_registers = current_test.fixture.array.slice(offset, offset += 16);
|
|
const expected_xmm_registers = current_test.fixture.array.slice(offset, offset += 32);
|
|
const expected_memory = current_test.fixture.array.slice(offset, offset += 16);
|
|
const expected_eflags = current_test.fixture.array[offset++] & MASK_ARITH;
|
|
const fpu_tag = current_test.fixture.array[offset++];
|
|
const fpu_status = current_test.fixture.array[offset++] & FPU_STATUS_MASK;
|
|
|
|
if(!current_test.fixture.exception)
|
|
{
|
|
for (let i = 0; i < cpu.reg32.length; i++) {
|
|
let reg = cpu.reg32[i];
|
|
if (reg !== expected_reg32[i]) {
|
|
individual_failures.push({
|
|
name: "cpu.reg32[" + i + "]",
|
|
expected: expected_reg32[i],
|
|
actual: reg,
|
|
});
|
|
}
|
|
}
|
|
|
|
if(fpu_tag !== FPU_TAG_ALL_INVALID)
|
|
{
|
|
for (let i = 0; i < evaluated_fpu_regs.length; i++) {
|
|
if (expected_fpu_regs[i] !== "invalid" &&
|
|
!float_equal(evaluated_fpu_regs[i], expected_fpu_regs[i])) {
|
|
individual_failures.push({
|
|
name: "st" + i,
|
|
expected: expected_fpu_regs[i],
|
|
actual: evaluated_fpu_regs[i],
|
|
});
|
|
}
|
|
}
|
|
|
|
if(fpu_status !== evaluated_fpu_status)
|
|
{
|
|
individual_failures.push({
|
|
name: "fpu status word",
|
|
expected: fpu_status,
|
|
actual: evaluated_fpu_status,
|
|
});
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (let i = 0; i < evaluated_mmxs.length; i++) {
|
|
if (evaluated_mmxs[i] !== expected_mmx_registers[i]) {
|
|
individual_failures.push({
|
|
name: "mm" + (i >> 1) + ".int32[" + (i & 1) + "]",
|
|
expected: expected_mmx_registers[i],
|
|
actual: evaluated_mmxs[i],
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
for (let i = 0; i < evaluated_xmms.length; i++) {
|
|
if (evaluated_xmms[i] !== expected_xmm_registers[i]) {
|
|
individual_failures.push({
|
|
name: "xmm" + (i >> 2) + ".int32[" + (i & 3) + "] (cpu.reg_xmm[" + i + "])",
|
|
expected: expected_xmm_registers[i],
|
|
actual: evaluated_xmms[i],
|
|
});
|
|
}
|
|
}
|
|
|
|
for (let i = 0; i < evaluated_memory.length; i++) {
|
|
if (evaluated_memory[i] !== expected_memory[i]) {
|
|
individual_failures.push({
|
|
name: "mem[" + i + "]",
|
|
expected: expected_memory[i],
|
|
actual: evaluated_memory[i],
|
|
});
|
|
}
|
|
}
|
|
|
|
const seen_eflags = cpu.get_eflags() & MASK_ARITH;
|
|
if(seen_eflags !== expected_eflags)
|
|
{
|
|
individual_failures.push({
|
|
name: "eflags",
|
|
expected: expected_eflags,
|
|
actual: seen_eflags,
|
|
});
|
|
}
|
|
}
|
|
|
|
if(current_test.fixture.exception)
|
|
{
|
|
const seen_eip = (recorded_exceptions[0] || {}).eip;
|
|
if(seen_eip - V86_TEXT_OFFSET !== expected_eip - NASM_TEXT_OFFSET)
|
|
{
|
|
individual_failures.push({
|
|
name: "exception eip",
|
|
expected: expected_eip - NASM_TEXT_OFFSET,
|
|
actual: seen_eip === undefined ? "(none)" : seen_eip - V86_TEXT_OFFSET,
|
|
});
|
|
}
|
|
}
|
|
|
|
const seen_exception = (recorded_exceptions[0] || {}).exception;
|
|
if(current_test.fixture.exception !== seen_exception)
|
|
{
|
|
individual_failures.push({
|
|
name: "Exception",
|
|
actual: seen_exception || "(none)",
|
|
expected: current_test.fixture.exception,
|
|
});
|
|
}
|
|
|
|
individual_failures = individual_failures.map(({ name, actual, expected }) => {
|
|
return {
|
|
name,
|
|
actual: format_value(actual),
|
|
expected: format_value(expected),
|
|
};
|
|
});
|
|
|
|
recorded_exceptions = [];
|
|
|
|
if (individual_failures.length > 0) {
|
|
process.send({
|
|
failures: individual_failures,
|
|
img_name: current_test.img_name
|
|
});
|
|
}
|
|
else {
|
|
process.send(DONE_MSG);
|
|
}
|
|
}
|
|
|
|
cluster.worker.on("message", function(message) {
|
|
if(message === TERMINATE_MSG)
|
|
{
|
|
emulator.stop();
|
|
emulator = null;
|
|
}
|
|
else
|
|
{
|
|
run_test(message);
|
|
}
|
|
});
|
|
}
|