From 74ad18e721068dd76f3e1e4ea2cb54a96935288b Mon Sep 17 00:00:00 2001 From: Fabian Date: Thu, 5 Oct 2023 13:53:42 -0500 Subject: [PATCH] nasmtests: more tests, new large arithmetic test generation, compile faster --- Makefile | 4 +- tests/nasm/Makefile | 28 --- tests/nasm/create_tests.js | 408 +++++++++++++++++++++++++++++++++---- tests/nasm/footer.inc | 1 + tests/nasm/header.inc | 7 +- tests/nasm/run.js | 30 ++- 6 files changed, 390 insertions(+), 88 deletions(-) delete mode 100644 tests/nasm/Makefile diff --git a/Makefile b/Makefile index 7dfee28479..f0aef1384f 100644 --- a/Makefile +++ b/Makefile @@ -262,12 +262,12 @@ tests-release: build/libv86.js build/v86.wasm build/integration-test-fs/fs.json TEST_RELEASE_BUILD=1 ./tests/full/run.js nasmtests: all-debug - $(MAKE) -C $(NASM_TEST_DIR) all + $(NASM_TEST_DIR)/create_tests.js $(NASM_TEST_DIR)/gen_fixtures.js $(NASM_TEST_DIR)/run.js nasmtests-force-jit: all-debug - $(MAKE) -C $(NASM_TEST_DIR) all + $(NASM_TEST_DIR)/create_tests.js $(NASM_TEST_DIR)/gen_fixtures.js $(NASM_TEST_DIR)/run.js --force-jit diff --git a/tests/nasm/Makefile b/tests/nasm/Makefile deleted file mode 100644 index ab274a2784..0000000000 --- a/tests/nasm/Makefile +++ /dev/null @@ -1,28 +0,0 @@ -source_files := $(wildcard build/*.asm) -source_files += $(addprefix build/,$(wildcard *.asm)) - -obj_files := $(patsubst %.asm,%.o,$(source_files)) -v86_executables := $(patsubst %.asm,%.img,$(source_files)) - -inc_files := $(addprefix build/,$(wildcard *.inc)) - -all: $(source_files) $(obj_files) $(inc_files) $(v86_executables) -.PHONY: all - -build/%.o: build/%.asm $(inc_files) - nasm -w+error -felf32 -o $@ $< - -# used both as a multiboot image for v86 and as a regular elf executable for gdb -build/%.img: build/%.o - ld -g $< -m elf_i386 --section-start=.bss=0x100000 --section-start=.text=0x80000 --section-start=.multiboot=0x20000 -o $@ - -build/%.asm: %.asm - mkdir -p build; cp $< $@ - -build/%.inc: %.inc - mkdir -p build; cp $< $@ - -.PHONY: clean -clean: - rm -f *.o *.bin *.img *.fixture gen_*.asm # old location - rm -f build/*.o build/*.bin build/*.img build/*.fixture build/*.asm diff --git a/tests/nasm/create_tests.js b/tests/nasm/create_tests.js index 079b9604e3..194ed1b12b 100755 --- a/tests/nasm/create_tests.js +++ b/tests/nasm/create_tests.js @@ -2,24 +2,42 @@ "use strict"; // number of tests per instruction -const NO_TESTS = 1; +const NUMBER_TESTS = 5; +// arithmetic tests +const NUMBER_ARITH_TESTS = 100; + +const MAX_PARALLEL_PROCS = +process.env.MAX_PARALLEL_PROCS || 32; const FLAGS_IGNORE = 0xFFFF3200; +const CF = 1 << 0; +const PF = 1 << 2; +const AF = 1 << 4; +const ZF = 1 << 6; +const SF = 1 << 7; +const OF = 1 << 11; + +const BUILD_DIR = __dirname + "/build/"; +const LOG_VERBOSE = false; const assert = require("assert").strict; const fs = require("fs"); +const fse = require("fs/promises"); +const path = require("path"); const encodings = require("../../gen/x86_table.js"); +const util = require("util"); +const execFile = util.promisify(require("child_process").execFile); const Rand = require("./rand.js"); -generate_tests(); +const header = fs.readFileSync(path.join(__dirname, "header.inc")); +const footer = fs.readFileSync(path.join(__dirname, "footer.inc")); -function generate_tests() -{ - const build_folder = __dirname + "/build/"; +main(); +async function main() +{ try { - fs.mkdirSync(build_folder); + fs.mkdirSync(BUILD_DIR); } catch(e) { @@ -29,6 +47,96 @@ function generate_tests() } } + const tests = create_tests().reverse(); + + const workers = []; + for(let i = 0; i < MAX_PARALLEL_PROCS; i++) + { + workers.push(worker(make_test, tests)); + } + + await Promise.all(workers); +} + +async function worker(f, work) +{ + while(work.length) + { + await f(work.pop()); + } +} + +async function make_test(test) +{ + LOG_VERBOSE && console.log("Start", test.name || test.file); + let asm_file; + let img_file; + let tmp_file; + + assert((test.asm && test.name) || test.file); + if(test.asm) + { + asm_file = BUILD_DIR + test.name + ".asm"; + img_file = BUILD_DIR + test.name + ".img"; + tmp_file = "/tmp/" + test.name + ".o"; + + let old_code = undefined; + + try + { + old_code = await fse.readFile(asm_file, { encoding: "ascii" }); + } + catch(e) + { + } + + if(old_code === test.asm) + { + LOG_VERBOSE && console.log("Skip", test.name || test.file); + return; + } + + await fse.writeFile(asm_file, test.asm); + } + else + { + asm_file = path.join(__dirname, test.file); + img_file = BUILD_DIR + test.file.replace(/\.asm$/, ".img"); + tmp_file = "/tmp/" + test.file + ".o"; + + try + { + if((await fse.stat(asm_file)).mtime < (await fse.stat(img_file)).mtime) + { + return; + } + } + catch(e) + { + if(e.code !== "ENOENT") throw e; + } + } + + const options = { + cwd: __dirname, + }; + + LOG_VERBOSE && console.log("nasm", ["-w+error", "-felf32", "-o", tmp_file, asm_file].join(" ")); + await execFile("nasm", ["-w+error", "-felf32", "-o", tmp_file, asm_file], options); + LOG_VERBOSE && console.log("ld", ["-g", tmp_file, "-m", "elf_i386", "--section-start=.bss=0x100000", "--section-start=.text=0x80000", "--section-start=.multiboot=0x20000", "-o", img_file].join(" ")); + await execFile("ld", ["-g", tmp_file, "-m", "elf_i386", "--section-start=.bss=0x100000", "--section-start=.text=0x80000", "--section-start=.multiboot=0x20000", "-o", img_file], options); + await fse.unlink(tmp_file); + + console.log(test.name || test.file); +} + +function create_tests() +{ + const tests = []; + + const asm_files = fs.readdirSync(__dirname).filter(f => f.endsWith(".asm")); + tests.push.apply(tests, asm_files.map(file => ({ file }))); + for(const op of encodings) { const configurations = [ @@ -42,7 +150,7 @@ function generate_tests() for(const config of configurations) { - for(let nth_test = 0; nth_test < NO_TESTS; nth_test++) + for(let nth_test = 0; nth_test < NUMBER_TESTS; nth_test++) { if(nth_test > 0 && op.opcode === 0x8D) { @@ -50,32 +158,25 @@ function generate_tests() continue; } - for(const code of create_nasm(op, config, nth_test)) + for(const asm of create_instruction_test(op, config, nth_test)) { - const filename = "gen_" + format_opcode(op.opcode) + "_" + (op.fixed_g || 0) + "_" + i + ".asm"; - const dirname = build_folder + filename; - - let old_code = undefined; - - try - { - old_code = fs.readFileSync(dirname, { encoding: "ascii" }); - } - catch(e) - { - } - - if(old_code !== code) - { - console.log("Creating %s", filename); - fs.writeFileSync(dirname, code); - } + tests.push({ + name: "gen_" + format_opcode(op.opcode) + "_" + (op.fixed_g || 0) + "_" + i, + asm, + }); i++; } } } } + + for(let i = 0; i < NUMBER_ARITH_TESTS; i++) + { + tests.push(create_arith_test(i)); + } + + return tests; } function format_opcode(n) @@ -152,8 +253,25 @@ function create_nasm_modrm_combinations_32() return result; } +function rand_reg_but_not_esp(rng) +{ + let r = rng.int32() & 7; + return r === 4 ? rand_reg_but_not_esp(rng) : r; +} + +function interesting_immediate(rng) +{ + if(rng.int32() & 1) + { + return rng.int32(); + } + else + { + return rng.int32() << (rng.int32() & 31) >> (rng.int32() & 31); + } +} -function create_nasm(op, config, nth_test) +function create_instruction_test(op, config, nth_test) { if(op.prefix || op.skip) { @@ -184,7 +302,7 @@ function create_nasm(op, config, nth_test) } } - const rng = new Rand(op.opcode + nth_test * 0x10000); + const rng = new Rand(1283698341 ^ op.opcode + nth_test * 0x10000); const size = (op.os || op.opcode % 2 === 1) ? config.size : 8; const is_modrm = op.e || op.fixed_g !== undefined; @@ -254,11 +372,11 @@ function create_nasm(op, config, nth_test) codes.push("push dword " + (rng.int32() & ~(1 << 8 | 1 << 9))); codes.push("popf"); - if(true) + if(rng.int32() & 1) { // generate random flags using arithmetic instruction // not well-distributed, but can trigger bugs in lazy flag calculation - if(true) + if(rng.int32() & 1) { // rarely sets zero flag, other flags mostly well-distributed codes.push("add al, ah"); @@ -322,7 +440,7 @@ function create_nasm(op, config, nth_test) if(is_modrm) { - let g = 7; // edi / di / bh + let g = rand_reg_but_not_esp(rng); if(op.fixed_g !== undefined) { @@ -339,9 +457,11 @@ function create_nasm(op, config, nth_test) } else { - const es = op.is_fpu ? [0, 1, 2, 3, 4, 5, 6, 7] : [ - 2 // edx - ]; + const es = + test_extra ? [0, 1, 2, g] : + op.is_fpu ? [0, 1, 2, 3, 4, 5, 6, 7] : [ + rand_reg_but_not_esp(rng) + ]; const modrm_bytes = es.map(e => "db " + (0xC0 | g << 3 | e)); codes.push(modrm_bytes); } @@ -360,11 +480,11 @@ function create_nasm(op, config, nth_test) if([0x0FA4, 0x0FAC].includes(op.opcode)) { // shld/shrd: immediates larger than opsize are undefined behaviour - codes.push("db 0fh"); + codes.push("db " + (rng.int32() & (size === 16 ? 15 : 31))); } else { - codes.push("db 12h"); + codes.push("db " + (rng.int32() & 0xFF)); } } else @@ -381,12 +501,12 @@ function create_nasm(op, config, nth_test) if(op.imm1632 && size === 16 || op.imm16) { - codes.push("dw 34cdh"); + codes.push("dw " + (rng.int32() & 0xFFFF)); } else { assert(op.imm1632 && size === 32 || op.imm32); - codes.push("dd 1234abcdh"); + codes.push("dd " + rng.int32()); } } } @@ -397,7 +517,8 @@ function create_nasm(op, config, nth_test) codes.push( "pushf", "and dword [esp], ~" + (op.mask_flags | FLAGS_IGNORE), - "popf" + "popf", + "mov dword [esp-4], 0", ); } @@ -417,15 +538,202 @@ function create_nasm(op, config, nth_test) } return all_combinations(codes).map(c => { - return ( - "global _start\n" + - '%include "header.inc"\n\n' + - c.join("\n") + "\n" + - '%include "footer.inc"\n' - ); + return header + c.join("\n") + "\n" + footer; }); } +function create_arith_test(i) +{ + const rng = new Rand(916237867 ^ i); + + const registers_by_size = { + 8: ["al", "ah", "cl", "ch", "dl", "dh", "bl", "bh"], + 16: ["ax", "cx", "dx", "bx", "sp", "bp", "si", "di"], + 32: ["eax", "ecx", "edx", "ebx", "esp", "ebp", "esi", "edi"], + }; + const mask_by_size = { + 8: 0xFF, + 16: 0xFFFF, + 32: -1, + }; + const word_by_size = { + 8: "byte", + 16: "word", + 32: "dword", + }; + const two_operand_instructions = ["add", "sub", "adc", "sbb", "and", "or", "xor", "cmp", "test"]; + const one_operand_instructions = [ + "inc", "dec", "neg", + "mul", //"idiv", "div", // technically also eax:edx, but are implied by assembler + "imul", // handled specifically below to also generate 2-/3-operand form + ]; + const shift_instructions = ["shl", "shr", "sar", "rol", "ror", "rcl", "rcr"]; + // TODO: cmpxchg, xadd, bsf, bsr, shrd/shld, popcnt, bt* + const instructions = [two_operand_instructions, one_operand_instructions, shift_instructions].flat(); + const conditions = [ + // suffix flag + ["o", OF], + ["c", CF], + ["z", ZF], + ["p", PF], + ["s", SF], + ["be", CF | ZF], + ["l", SF | OF], + ["le", SF | OF | ZF], + ]; + + let c = []; + let address = 0x100000; + + for(let reg of registers_by_size[32]) + { + if(reg !== "esp") + { + c.push(`mov ${reg}, ${interesting_immediate(rng)}`); + } + } + + let undefined_flags = 0; + + for(let i = 0; i < 2000; i++) + { + const ins = instructions[rng.uint32() % instructions.length]; + const size = [8, 16, 32][rng.uint32() % 3]; + const size_word = word_by_size[size]; + const dst_is_mem = rng.int32() & 1; + const dst = dst_is_mem ? + `${size_word} [${nasm_hex(address)}]` : + registers_by_size[size][rand_reg_but_not_esp(rng)]; + let src_is_mem = false; + if(ins === "imul" && (rng.int32() & 1)) // other encodings handled in one_operand_instructions + { + // dst must be reg, no 8-bit + const size_imul = [16, 32][rng.int32() & 1]; + const dst_imul = registers_by_size[size_imul][rand_reg_but_not_esp(rng)]; + const src1 = dst_is_mem ? + `${word_by_size[size_imul]} [${nasm_hex(address)}]` : + registers_by_size[size_imul][rand_reg_but_not_esp(rng)]; + if(rng.int32() & 1) + { + c.push(`${ins} ${dst_imul}, ${src1}`); + } + else + { + const src2 = nasm_hex(interesting_immediate(rng) & mask_by_size[size_imul]); + c.push(`${ins} ${dst_imul}, ${src1}, ${src2}`); + } + } + else if(one_operand_instructions.includes(ins)) + { + c.push(`${ins} ${dst}`); + } + else if(two_operand_instructions.includes(ins)) + { + src_is_mem = !dst_is_mem && (rng.int32() & 1); + const src = src_is_mem ? + `${size_word} [${nasm_hex(address)}]` : + (rng.int32() & 1) ? + registers_by_size[size][rand_reg_but_not_esp(rng)] : + nasm_hex(interesting_immediate(rng) & mask_by_size[size]); + c.push(`${ins} ${dst}, ${src}`); + } + else if(shift_instructions.includes(ins)) + { + if(rng.int32() & 1) + { + // unknown CL + undefined_flags |= AF | OF; + c.push(`${ins} ${dst}, cl`); + } + else + { + const shift = interesting_immediate(rng) & 0xFF; + // TODO: shift mod {8,9,16,17,32,33} depending on bitsize/rotate/with-carry, shifts can clear undefined_flags if shift is not zero + undefined_flags |= shift === 1 ? AF : AF | OF; + if(rng.int32() & 1) + { + // known CL + c.push(`mov cl, ${nasm_hex(shift)}`); + c.push(`${ins} ${dst}, cl`); + } + else + { + // immediate + c.push(`${ins} ${dst}, ${nasm_hex(shift)}`); + } + } + } + + if(dst_is_mem || src_is_mem) + { + if(rng.int32() & 1) + { + address += size / 8; + // initialise next word + c.push(`mov dword [${nasm_hex(address)}], ${nasm_hex(interesting_immediate(rng) & 0xFF)}`); + } + } + + if(ins === "imul" || ins === "mul" || ins === "idiv" || ins === "div") + { + undefined_flags = SF | ZF | AF | PF; + } + else if(!shift_instructions.includes(ins)) + { + // adc/sbb/inc/dec read CF, but CF is never undefined + undefined_flags = 0; + } + + if(rng.int32() & 1) + { + // setcc + const cond = random_pick(conditions.filter(([_, flag]) => 0 === (flag & undefined_flags)).map(([suffix]) => suffix), rng); + assert(cond); + const invert = (rng.int32() & 1) ? "n" : ""; + const ins2 = `set${invert}${cond}`; + const dst2 = (rng.int32() & 1) ? `byte [${nasm_hex(address++)}]` : registers_by_size[8][rng.int32() & 7]; + c.push(`${ins2} ${dst2}`); + } + else if(rng.int32() & 1) + { + // cmovcc + const cond = random_pick(conditions.filter(([_, flag]) => 0 === (flag & undefined_flags)).map(([suffix]) => suffix), rng); + assert(cond); + const invert = (rng.int32() & 1) ? "n" : ""; + const ins2 = `cmov${invert}${cond}`; + const size = (rng.int32() & 1) ? 16 : 32; + const src2 = registers_by_size[size][rng.int32() & 7]; + const dst2 = registers_by_size[size][rand_reg_but_not_esp(rng)]; + c.push(`${ins2} ${dst2}, ${src2}`); + } + else if(rng.int32() & 1) + { + c.push("pushf"); + c.push("and dword [esp], ~" + nasm_hex(FLAGS_IGNORE | undefined_flags)); + c.push(`pop ${registers_by_size[32][rand_reg_but_not_esp(rng)]}`); + } + else + { + // intentionally left blank + } + + // TODO: + // cmovcc + // other random instructions (mov, etc.) + } + + c.push("pushf"); + c.push("and dword [esp], ~" + nasm_hex(FLAGS_IGNORE | undefined_flags)); + c.push("popf"); + + assert(address < 0x102000); + + const name = `arith_${i}`; + const asm = header + c.join("\n") + "\n" + footer; + + return { name, asm }; +} + function all_combinations(xs) { let result = [xs]; @@ -454,3 +762,13 @@ function all_combinations(xs) return result; } + +function nasm_hex(x) +{ + return `0${(x >>> 0).toString(16).toUpperCase()}h`; +} + +function random_pick(xs, rng) +{ + return xs[rng.uint32() % xs.length]; +} diff --git a/tests/nasm/footer.inc b/tests/nasm/footer.inc index 39bdb655ca..0eea42d28b 100644 --- a/tests/nasm/footer.inc +++ b/tests/nasm/footer.inc @@ -1,3 +1,4 @@ + loop: hlt jmp loop diff --git a/tests/nasm/header.inc b/tests/nasm/header.inc index 7e57e01bf7..9ae91062df 100644 --- a/tests/nasm/header.inc +++ b/tests/nasm/header.inc @@ -22,16 +22,15 @@ main: xor ecx, ecx xor edx, edx xor ebx, ebx - ; xor esp, esp mov esp, stack_top xor ebp, ebp xor esi, esi xor edi, edi + pxor xmm0, xmm0 + ; make space for memory operations - %rep 8 - push 0 - %endrep + sub esp, 32 push 0 popf diff --git a/tests/nasm/run.js b/tests/nasm/run.js index 980f2cff29..0c799d06d1 100755 --- a/tests/nasm/run.js +++ b/tests/nasm/run.js @@ -22,7 +22,7 @@ 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 TEST_NAME = new RegExp(process.env.TEST_NAME || "", "i"); const SINGLE_TEST_TIMEOUT = 10000; const TEST_RELEASE_BUILD = +process.env.TEST_RELEASE_BUILD; @@ -43,7 +43,7 @@ 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 FPU_STATUS_MASK = 0xFFFF & ~(1 << 9 | 1 << 5 | 1 << 3 | 1 << 1); // bits that are not correctly implemented by v86 const FP_COMPARISON_SIGNIFICANT_DIGITS = 7; try { @@ -170,7 +170,7 @@ if(cluster.isMaster) }).map(name => { return name.slice(0, -4); }).filter(name => { - return !TEST_NAME || name === TEST_NAME; + return TEST_NAME.test(name + ".img"); }); const tests = files.map(name => { @@ -274,17 +274,29 @@ else { if(FORCE_JIT) { + let eip = cpu.instruction_pointer[0]; + cpu.test_hook_did_finalize_wasm = function() { - cpu.test_hook_did_finalize_wasm = null; + eip += 4096; + const last_word = cpu.mem32s[eip - 4 >> 2]; + + if(last_word === 0 || last_word === undefined) + { + cpu.test_hook_did_finalize_wasm = null; - // don't synchronously call into the emulator from this callback - setTimeout(() => { - emulator.run(); - }, 0); + // don't synchronously call into the emulator from this callback + setTimeout(() => { + emulator.run(); + }, 0); + } + else + { + cpu.jit_force_generate(eip); + } }; - cpu.jit_force_generate(cpu.instruction_pointer[0]); + cpu.jit_force_generate(eip); } else {