commit d26f11aec4616ca8b0d7ded509dc923eb4a8ac26 Author: veclav talica Date: Mon Sep 18 23:31:59 2023 +0500 start of x86-64 implementation; stack pushing, sinking, add with overflow, procedure return ops diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..70605a4 --- /dev/null +++ b/build.zig @@ -0,0 +1,70 @@ +const std = @import("std"); + +// Although this function looks imperative, note that its job is to +// declaratively construct a build graph that will be executed by an external +// runner. +pub fn build(b: *std.Build) void { + // Standard target options allows the person running `zig build` to choose + // what target to build for. Here we do not override the defaults, which + // means any target is allowed, and the default is native. Other options + // for restricting supported target set are available. + const target = b.standardTargetOptions(.{}); + + // Standard optimization options allow the person running `zig build` to select + // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not + // set a preferred release mode, allowing the user to decide how to optimize. + const optimize = b.standardOptimizeOption(.{}); + + const exe = b.addExecutable(.{ + .name = "nmvm", + // In this case the main source file is merely a path, however, in more + // complicated build scripts, this could be a generated file. + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + + // This declares intent for the executable to be installed into the + // standard location when the user invokes the "install" step (the default + // step when running `zig build`). + b.installArtifact(exe); + + // This *creates* a Run step in the build graph, to be executed when another + // step is evaluated that depends on it. The next line below will establish + // such a dependency. + const run_cmd = b.addRunArtifact(exe); + + // By making the run step depend on the install step, it will be run from the + // installation directory rather than directly from within the cache directory. + // This is not necessary, however, if the application depends on other installed + // files, this ensures they will be present and in the expected location. + run_cmd.step.dependOn(b.getInstallStep()); + + // This allows the user to pass arguments to the application in the build + // command itself, like this: `zig build run -- arg1 arg2 etc` + if (b.args) |args| { + run_cmd.addArgs(args); + } + + // This creates a build step. It will be visible in the `zig build --help` menu, + // and can be selected like this: `zig build run` + // This will evaluate the `run` step rather than the default, which is "install". + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); + + // Creates a step for unit testing. This only builds the test executable + // but does not run it. + const unit_tests = b.addTest(.{ + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + + const run_unit_tests = b.addRunArtifact(unit_tests); + + // Similar to creating the run step earlier, this exposes a `test` step to + // the `zig build --help` menu, providing a way for the user to request + // running the unit tests. + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_unit_tests.step); +} diff --git a/idea.md b/idea.md new file mode 100644 index 0000000..2d7192f --- /dev/null +++ b/idea.md @@ -0,0 +1,2 @@ +# .nmvm Near Metal Virtual Machine +Exercise in building low overhead VM via architecture specific means. diff --git a/src/arch/x86-64.zig b/src/arch/x86-64.zig new file mode 100644 index 0000000..62b821a --- /dev/null +++ b/src/arch/x86-64.zig @@ -0,0 +1,91 @@ +// Execution thread convention: +// rdi <- binary thread + +// Resources used: +// https://mort.coffee/home/fast-interpreters/ +// https://blog.reverberate.org/2021/04/21/musttail-efficient-interpreters.html +// https://en.wikibooks.org/wiki/X86_Assembly/GNU_assembly_syntax +// https://www.cs.princeton.edu/courses/archive/spr18/cos217/lectures/15_AssemblyFunctions.pdf +// https://ziglang.org/documentation/master/#toc-Assembly +// https://csiflabs.cs.ucdavis.edu/~ssdavis/50/att-syntax.htm + +pub const Word = u64; + +// todo: Variant that pushes array of words. +/// (iw | -- iw) +pub fn opPushWord() callconv(.Naked) noreturn { + asm volatile ( + \\ add $0x10, %%rdi + \\ pushq -8(%%rdi) + \\ jmpq *(%%rdi) + ); +} + +// todo: Variant that discards array of words. +/// (w --) +pub fn opSinkWord() callconv(.Naked) noreturn { + asm volatile ( + \\ add $0x08, %%rdi + \\ addq $0x08, %%rsp + \\ jmpq *(%%rdi) + ); +} + +/// (iw | -- (iw'nth word from stack) ) +// fn opTakeWord(binary: [*]const Word, cond: bool) noreturn { +// @setRuntimeSafety(false); +// takeWord(binary[1].word); +// @call(.always_tail, binary[2].function, .{ &binary[2], cond }); +// } + +/// (iw | w) +// fn opSetWord(binary: [*]const Word, cond: bool) noreturn { +// @setRuntimeSafety(false); +// setWord(binary[1].word, popWord()); +// @call(.always_tail, binary[2].function, .{ &binary[2], cond }); +// } + +// todo: Generate operation permutations procedurally. +// todo: Jump on overflow instead of cond setting? +/// (w1 w2 -- sum overflow) +pub fn opSumWordsWithOverflow() callconv(.Naked) noreturn { + // https://www.felixcloutier.com/x86/adc + // https://www.felixcloutier.com/x86/setcc + // idea: Could https://www.felixcloutier.com/x86/cmovcc be better for overflow push? + asm volatile ( + \\ movq (%%rsp), %%rax + \\ adcq 8(%%rsp), %%rax + \\ movq %%rax, 8(%%rsp) + \\ setc %%al + \\ movb %%al, 7(%%rsp) + \\ addq $0x08, %%rdi + \\ jmpq *(%%rdi) + ); +} + +// todo: Generate operation permutations procedurally. +// todo: We might not need cond register if conditions and jumps are combined? +/// (w1 w2) +// fn opRelativeJumpIfGreaterThan(binary: [*]const Word, cond: bool) noreturn { +// @setRuntimeSafety(false); +// const offset = if (popWord() > popWord()) binary[1].word else 2; +// @call(.always_tail, binary[offset].function, .{ &binary[offset], cond }); +// } + +/// (addr) +pub fn opReturn() callconv(.Naked) noreturn { + // https://www.felixcloutier.com/x86/ret + asm volatile ("ret"); +} + +pub fn execute(binary: []const Word, entry_addr: usize) void { + // todo: Ensure correctness. + // https://wiki.osdev.org/System_V_ABI + // https://www.felixcloutier.com/x86/call + asm volatile ( + \\ call *(%%rdi) + : + : [thread] "rdi" (&binary[entry_addr]), + : "rflags", "rax", "rbx", "rsp", "rbp", "r12", "r13", "r14", "r15", "rsi", "rdx", "rcx", "r8", "r9", "r10", "r11", "memory" + ); +} diff --git a/src/interpreter.zig b/src/interpreter.zig new file mode 100644 index 0000000..e303f30 --- /dev/null +++ b/src/interpreter.zig @@ -0,0 +1,17 @@ +// todo: Interpreter context as binary local variable. +// It would hold memory mappings, as well as error stack. +// todo: Define procedure call for user code. +// todo: Instruction set extensions, such as memory management schemes, non-exhaustive logging, +// exception mechanism, coroutines via yield/resume and etc. +// todo: Threading scheme. +// todo: Extension for native floating point stack ops. +// todo: Try using small code model with nopie/nopic binary. + +// idea: Specialized opcodes that have side effects on read and write, such as +// zero-check on push/pop, or jump if condition bit met. This would create a lot +// of permutations tho, we might try to discover which code devices are most used. + +// idea: 'JIT' could be done by simple op* compiled binary copying up until `jmpq *(%%rdi)`, +// with immediate operand prelude modified, which could be done procedurally. + +usingnamespace @import("arch/x86-64.zig"); diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..873ffa9 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,16 @@ +const int = @import("interpreter.zig"); + +pub fn main() !void { + const binary = [_]int.Word{ + @as(int.Word, @intFromPtr(&int.opPushWord)), + ~@as(int.Word, 1), + @as(int.Word, @intFromPtr(&int.opPushWord)), + ~@as(int.Word, 1), + @as(int.Word, @intFromPtr(&int.opSumWordsWithOverflow)), + @as(int.Word, @intFromPtr(&int.opSinkWord)), + @as(int.Word, @intFromPtr(&int.opSinkWord)), + @as(int.Word, @intFromPtr(&int.opReturn)), + }; + + int.execute(&binary, 0); +}