From 00a3adf8904277255e1bb8f53bd405b7c2145246 Mon Sep 17 00:00:00 2001 From: zer0condition Date: Wed, 19 Aug 2026 01:23:15 +0530 Subject: [PATCH] initial commit --- .github/workflows/build.yml | 48 + .gitignore | 48 + Goodmans.sln | 42 + LICENSE | 21 + README.md | 107 + build.cmd | 78 + cli/cli.vcxproj | 48 + cli/main.c | 303 +++ deploy/Goodmans.inf | 48 + deploy/README.txt | 60 + deploy/gen_cert.cmd | 44 + deploy/install.cmd | 57 + deploy/uninstall.cmd | 23 + driver/Goodmans.inf | 48 + driver/Goodmans.vcxproj | 124 + driver/driver.c | 121 + driver/host_imports.c | 1275 ++++++++++ driver/ih.c | 94 + driver/inc/gvm.h | 103 + driver/ioctl_handler.c | 457 ++++ driver/kshim/corecrt.h | 2 + driver/kshim/inttypes.h | 28 + driver/kshim/kshim.c | 248 ++ driver/kshim/kshim.h | 24 + driver/kshim/malloc.h | 2 + driver/kshim/stdio.h | 29 + driver/kshim/stdlib.h | 25 + driver/log_ring.c | 95 + driver/module_table.c | 150 ++ driver/trace_ring.c | 137 ++ driver/wasm3/m3_bind.c | 175 ++ driver/wasm3/m3_bind.h | 20 + driver/wasm3/m3_code.c | 246 ++ driver/wasm3/m3_code.h | 80 + driver/wasm3/m3_compile.c | 3139 +++++++++++++++++++++++++ driver/wasm3/m3_compile.h | 208 ++ driver/wasm3/m3_config.h | 168 ++ driver/wasm3/m3_config_platforms.h | 216 ++ driver/wasm3/m3_core.c | 717 ++++++ driver/wasm3/m3_core.h | 311 +++ driver/wasm3/m3_env.c | 1239 ++++++++++ driver/wasm3/m3_env.h | 221 ++ driver/wasm3/m3_exception.h | 33 + driver/wasm3/m3_exec.c | 8 + driver/wasm3/m3_exec.h | 1527 ++++++++++++ driver/wasm3/m3_exec_defs.h | 76 + driver/wasm3/m3_function.c | 233 ++ driver/wasm3/m3_function.h | 103 + driver/wasm3/m3_info.c | 564 +++++ driver/wasm3/m3_info.h | 38 + driver/wasm3/m3_math_utils.h | 316 +++ driver/wasm3/m3_module.c | 175 ++ driver/wasm3/m3_parse.c | 846 +++++++ driver/wasm3/m3_validate.c | 881 +++++++ driver/wasm3/m3_validate.h | 25 + driver/wasm3/wasm3.h | 391 +++ driver/wasm3/wasm3_defs.h | 293 +++ driver/wasm_call.c | 103 + driver/watchdog.c | 103 + feature_guests/build_all.cmd | 22 + feature_guests/feature_guests.vcxproj | 56 + feature_guests/process_tracer.c | 119 + feature_guests/toolkit.c | 138 ++ guest_sdk/gvm.h | 249 ++ gui-qt/CMakeLists.txt | 47 + gui-qt/command_palette.cpp | 144 ++ gui-qt/command_palette.h | 31 + gui-qt/driver.cpp | 295 +++ gui-qt/driver.h | 72 + gui-qt/main.cpp | 25 + gui-qt/mainwindow.cpp | 522 ++++ gui-qt/mainwindow.h | 76 + gui-qt/pages.cpp | 1746 ++++++++++++++ gui-qt/pages.h | 208 ++ gui-qt/resources.qrc | 5 + gui-qt/style.qss | 355 +++ sample_guest/build_all.cmd | 25 + sample_guest/ffi_demo.c | 87 + sample_guest/handle_stripper.c | 82 + sample_guest/infinity_hook.c | 168 ++ sample_guest/pslist_dumper.c | 111 + sample_guest/sample_guest.c | 66 + sample_guest/sample_guest.vcxproj | 57 + shared/goodmans_ioctl.h | 195 ++ tests/fuzz/build.cmd | 29 + tests/fuzz/fuzz_parse.c | 46 + windbg/gvm_ext.cpp | 233 ++ windbg/gvm_ext.def | 8 + windbg/gvm_ext.vcxproj | 47 + 89 files changed, 21608 insertions(+) create mode 100644 .github/workflows/build.yml create mode 100644 .gitignore create mode 100644 Goodmans.sln create mode 100644 LICENSE create mode 100644 README.md create mode 100644 build.cmd create mode 100644 cli/cli.vcxproj create mode 100644 cli/main.c create mode 100644 deploy/Goodmans.inf create mode 100644 deploy/README.txt create mode 100644 deploy/gen_cert.cmd create mode 100644 deploy/install.cmd create mode 100644 deploy/uninstall.cmd create mode 100644 driver/Goodmans.inf create mode 100644 driver/Goodmans.vcxproj create mode 100644 driver/driver.c create mode 100644 driver/host_imports.c create mode 100644 driver/ih.c create mode 100644 driver/inc/gvm.h create mode 100644 driver/ioctl_handler.c create mode 100644 driver/kshim/corecrt.h create mode 100644 driver/kshim/inttypes.h create mode 100644 driver/kshim/kshim.c create mode 100644 driver/kshim/kshim.h create mode 100644 driver/kshim/malloc.h create mode 100644 driver/kshim/stdio.h create mode 100644 driver/kshim/stdlib.h create mode 100644 driver/log_ring.c create mode 100644 driver/module_table.c create mode 100644 driver/trace_ring.c create mode 100644 driver/wasm3/m3_bind.c create mode 100644 driver/wasm3/m3_bind.h create mode 100644 driver/wasm3/m3_code.c create mode 100644 driver/wasm3/m3_code.h create mode 100644 driver/wasm3/m3_compile.c create mode 100644 driver/wasm3/m3_compile.h create mode 100644 driver/wasm3/m3_config.h create mode 100644 driver/wasm3/m3_config_platforms.h create mode 100644 driver/wasm3/m3_core.c create mode 100644 driver/wasm3/m3_core.h create mode 100644 driver/wasm3/m3_env.c create mode 100644 driver/wasm3/m3_env.h create mode 100644 driver/wasm3/m3_exception.h create mode 100644 driver/wasm3/m3_exec.c create mode 100644 driver/wasm3/m3_exec.h create mode 100644 driver/wasm3/m3_exec_defs.h create mode 100644 driver/wasm3/m3_function.c create mode 100644 driver/wasm3/m3_function.h create mode 100644 driver/wasm3/m3_info.c create mode 100644 driver/wasm3/m3_info.h create mode 100644 driver/wasm3/m3_math_utils.h create mode 100644 driver/wasm3/m3_module.c create mode 100644 driver/wasm3/m3_parse.c create mode 100644 driver/wasm3/m3_validate.c create mode 100644 driver/wasm3/m3_validate.h create mode 100644 driver/wasm3/wasm3.h create mode 100644 driver/wasm3/wasm3_defs.h create mode 100644 driver/wasm_call.c create mode 100644 driver/watchdog.c create mode 100644 feature_guests/build_all.cmd create mode 100644 feature_guests/feature_guests.vcxproj create mode 100644 feature_guests/process_tracer.c create mode 100644 feature_guests/toolkit.c create mode 100644 guest_sdk/gvm.h create mode 100644 gui-qt/CMakeLists.txt create mode 100644 gui-qt/command_palette.cpp create mode 100644 gui-qt/command_palette.h create mode 100644 gui-qt/driver.cpp create mode 100644 gui-qt/driver.h create mode 100644 gui-qt/main.cpp create mode 100644 gui-qt/mainwindow.cpp create mode 100644 gui-qt/mainwindow.h create mode 100644 gui-qt/pages.cpp create mode 100644 gui-qt/pages.h create mode 100644 gui-qt/resources.qrc create mode 100644 gui-qt/style.qss create mode 100644 sample_guest/build_all.cmd create mode 100644 sample_guest/ffi_demo.c create mode 100644 sample_guest/handle_stripper.c create mode 100644 sample_guest/infinity_hook.c create mode 100644 sample_guest/pslist_dumper.c create mode 100644 sample_guest/sample_guest.c create mode 100644 sample_guest/sample_guest.vcxproj create mode 100644 shared/goodmans_ioctl.h create mode 100644 tests/fuzz/build.cmd create mode 100644 tests/fuzz/fuzz_parse.c create mode 100644 windbg/gvm_ext.cpp create mode 100644 windbg/gvm_ext.def create mode 100644 windbg/gvm_ext.vcxproj diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..7c0aa2f --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,48 @@ +name: build + +on: + push: + branches: [ main, master ] + pull_request: + workflow_dispatch: + +jobs: + driver: + runs-on: windows-2022 + steps: + - uses: actions/checkout@v4 + + - name: install WDK + run: | + choco install --no-progress windowsdriverkit10 + shell: pwsh + + - name: install LLVM (for wasm32) + run: | + choco install --no-progress llvm --version=18.1.8 + shell: pwsh + + - name: locate MSBuild + uses: microsoft/setup-msbuild@v2 + + - name: build driver + cli + guest (Debug) + run: | + $env:LLVM_HOME = "C:\Program Files\LLVM" + msbuild Goodmans.sln /p:Configuration=Debug /p:Platform=x64 /m /nologo + shell: pwsh + + - name: build driver + cli + guest (Release) + run: | + $env:LLVM_HOME = "C:\Program Files\LLVM" + msbuild Goodmans.sln /p:Configuration=Release /p:Platform=x64 /m /nologo + shell: pwsh + + - name: upload artifacts + uses: actions/upload-artifact@v4 + with: + name: goodmans-x64-release + path: | + driver/x64/Release/Goodmans.sys + cli/x64/Release/goodmans.exe + sample_guest/sample_guest.wasm + if-no-files-found: warn diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..244a85d --- /dev/null +++ b/.gitignore @@ -0,0 +1,48 @@ +# build outputs +x64/ +Debug/ +Release/ +*.obj +*.o +*.pdb +*.ipdb +*.iobj +*.ilk +*.exp +*.lib +*.tlog +*.log +*.recipe +*.suo +*.user +.vs/ + +# driver artifacts +*.sys +*.inf.bak +*.cat + +# built wasm +sample_guest/*.wasm +feature_guests/*.wasm + +# CLI +cli/*.exe +!goodmans.exe.manifest + +# generated certs (users make their own via deploy/gen_cert.cmd) +*.cer +*.pfx + +# staged deploy binaries (users copy after build) +deploy/*.sys +deploy/*.exe +deploy/*.wasm + +# VS +*.sdf +*.opendb +*.VC.db + +# misc +*.zip diff --git a/Goodmans.sln b/Goodmans.sln new file mode 100644 index 0000000..5be7826 --- /dev/null +++ b/Goodmans.sln @@ -0,0 +1,42 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.0.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Goodmans", "driver\Goodmans.vcxproj", "{6D9E2A50-1F1A-4B4E-8D9F-1B0C0D0A0001}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "goodmans-cli", "cli\cli.vcxproj", "{6D9E2A50-1F1A-4B4E-8D9F-1B0C0D0A0002}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "sample-guest", "sample_guest\sample_guest.vcxproj", "{6D9E2A50-1F1A-4B4E-8D9F-1B0C0D0A0003}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "feature-guests", "feature_guests\feature_guests.vcxproj", "{6D9E2A50-1F1A-4B4E-8D9F-1B0C0D0A0007}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "gvm-windbg", "windbg\gvm_ext.vcxproj", "{6D9E2A50-1F1A-4B4E-8D9F-1B0C0D0A0004}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6D9E2A50-1F1A-4B4E-8D9F-1B0C0D0A0001}.Debug|x64.ActiveCfg = Debug|x64 + {6D9E2A50-1F1A-4B4E-8D9F-1B0C0D0A0001}.Debug|x64.Build.0 = Debug|x64 + {6D9E2A50-1F1A-4B4E-8D9F-1B0C0D0A0001}.Release|x64.ActiveCfg = Release|x64 + {6D9E2A50-1F1A-4B4E-8D9F-1B0C0D0A0001}.Release|x64.Build.0 = Release|x64 + {6D9E2A50-1F1A-4B4E-8D9F-1B0C0D0A0002}.Debug|x64.ActiveCfg = Debug|x64 + {6D9E2A50-1F1A-4B4E-8D9F-1B0C0D0A0002}.Debug|x64.Build.0 = Debug|x64 + {6D9E2A50-1F1A-4B4E-8D9F-1B0C0D0A0002}.Release|x64.ActiveCfg = Release|x64 + {6D9E2A50-1F1A-4B4E-8D9F-1B0C0D0A0002}.Release|x64.Build.0 = Release|x64 + {6D9E2A50-1F1A-4B4E-8D9F-1B0C0D0A0003}.Debug|x64.ActiveCfg = Debug|x64 + {6D9E2A50-1F1A-4B4E-8D9F-1B0C0D0A0003}.Debug|x64.Build.0 = Debug|x64 + {6D9E2A50-1F1A-4B4E-8D9F-1B0C0D0A0003}.Release|x64.ActiveCfg = Release|x64 + {6D9E2A50-1F1A-4B4E-8D9F-1B0C0D0A0003}.Release|x64.Build.0 = Release|x64 + {6D9E2A50-1F1A-4B4E-8D9F-1B0C0D0A0004}.Debug|x64.ActiveCfg = Debug|x64 + {6D9E2A50-1F1A-4B4E-8D9F-1B0C0D0A0004}.Debug|x64.Build.0 = Debug|x64 + {6D9E2A50-1F1A-4B4E-8D9F-1B0C0D0A0004}.Release|x64.ActiveCfg = Release|x64 + {6D9E2A50-1F1A-4B4E-8D9F-1B0C0D0A0004}.Release|x64.Build.0 = Release|x64 + {6D9E2A50-1F1A-4B4E-8D9F-1B0C0D0A0007}.Debug|x64.ActiveCfg = Debug|x64 + {6D9E2A50-1F1A-4B4E-8D9F-1B0C0D0A0007}.Debug|x64.Build.0 = Debug|x64 + {6D9E2A50-1F1A-4B4E-8D9F-1B0C0D0A0007}.Release|x64.ActiveCfg = Release|x64 + {6D9E2A50-1F1A-4B4E-8D9F-1B0C0D0A0007}.Release|x64.Build.0 = Release|x64 + EndGlobalSection +EndGlobal diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..aa3f5a1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 zer0condition + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..cd6beed --- /dev/null +++ b/README.md @@ -0,0 +1,107 @@ +# Goodmans + +Signed WDM driver embedding wasm3. Loads unsigned `.wasm` modules, gives +them nt/hal FFI. Write kernel logic in C/Rust, compile to `wasm32`, load, +iterate. HVCI-compliant. + +## Features + +- wasm3 in the kernel, 64KB expanded stack, no JIT +- Direct FFI to any nt/hal export by name +- Per-module capability manifest +- Per-slot KMUTEX, per-slot pool budget +- SEH-guarded kernel VA read/write +- InfinityHook trampoline +- Process/image notify callbacks +- Watchdog + trace/log rings +- Qt6 GUI + +## Build + +Needs VS 2022 + WDK 10.0.26100.0, Qt 6.9, clang wasm32. + +``` +build.cmd Release +``` + +## Install + +``` +cd deploy +gen_cert.cmd +install.cmd +``` + +Needs `bcdedit /set testsigning on` if not signed. + + +## IOCTLs + +| Code | Purpose | +|------|---------| +| `LOAD_MODULE` | load .wasm, link imports | +| `CALL_EXPORT` | invoke export, up to 8 args | +| `UNLOAD_MODULE` | tear down runtime | +| `LIST_MODULES` / `MODULE_INFO` | enumerate | +| `UNLOAD_ALL` / `FORCE_UNLOAD` | bulk teardown | +| `TAIL_LOG` / `TAIL_TRACE` | poll rings | +| `TRACE_CTL` | toggle trace | +| `READ_GUEST` | read guest linear memory | +| `NOTIFY_STOP` | deregister callbacks | + +## Host Imports + +| Import | Sig | +|--------|-----| +| `host_dbg_print` | `v(ii)` | +| `host_alloc` / `host_free` | `I(i)` / `v(I)` | +| `host_read_u8/32/64` | `i/I(I)` | +| `host_write_u64` | `v(II)` | +| `host_read_bytes` / `host_write_bytes` | `i(Iii)` | +| `host_current_irql` | `i()` | +| `host_process_id` / `host_thread_id` | `i()` | +| `host_current_process` | `I()` | +| `host_cpuid` | `v(iii)` | +| `host_rdtsc` | `I()` | +| `host_readmsr` / `host_writemsr` | `I(i)` / `i(iI)` | +| `host_phys_read` / `host_phys_write` | `i(Iii)` | +| `host_call` | `I(iiiIIIIIIII)` | +| `host_ih_trampoline` / `host_ih_configure` / `host_ih_quiesce` | | +| `host_notify_enable` / `host_notify_poll` / `host_dispatch_start` / `host_dispatch_stop` | | +| `host_mem_base` / `host_mem_size` | `I()` / `i()` | +| `host_make_unistr` / `host_free_unistr` | | + +Guest bindings in `guest_sdk/gvm.h`. + +## Capabilities + +| Bit | Name | Grants | +|-----|------|--------| +| 0 | `ALLOC` | host_alloc, host_free | +| 1 | `READ_KMEM` | host_read_* | +| 2 | `WRITE_KMEM` | host_write_* | +| 3 | `MSR_READ` | host_readmsr | +| 4 | `MSR_WRITE` | host_writemsr | +| 5 | `PHYSMEM` | host_phys_* | +| 6 | `CPUID_TSC` | host_cpuid, host_rdtsc | +| 7 | `CALLBACKS` | process/image notify | +| 8 | `HOSTCALL` | host_call | +| 9 | `INTROSPECT` | pid/tid/irql/current_process | + +## Limits + +- no f32/f64 in guests +- wasm memory bounds checks compiled out +- one caller per module +- 16MB ioctl payload +- 32 module slots + +## Credits + +- wasm3 by Volodymyr Shymanskyy and Steven Massey (MIT). https://github.com/wasm3/wasm3 +- InfinityHook by everdox (MIT). https://github.com/everdox/InfinityHook +- Qt 6 (LGPL v3) + +## License + +MIT. diff --git a/build.cmd b/build.cmd new file mode 100644 index 0000000..b4595a7 --- /dev/null +++ b/build.cmd @@ -0,0 +1,78 @@ +@echo off +setlocal EnableDelayedExpansion +rem goodmans - one-shot build: driver + samples + gui, all staged into deploy\ +rem usage: build.cmd [Debug|Release] + +set "CFG=%~1" +if "%CFG%"=="" set "CFG=Debug" + +set "ROOT=%~dp0" +if "%ROOT:~-1%"=="\" set "ROOT=%ROOT:~0,-1%" + +set "MSBUILD=C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe" +if not exist "%MSBUILD%" ( + for /f "usebackq tokens=*" %%i in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -latest -requires Microsoft.Component.MSBuild -find "MSBuild\**\Bin\MSBuild.exe"`) do set "MSBUILD=%%i" +) +if not exist "%MSBUILD%" ( + echo [X] MSBuild not found. install Visual Studio 2022. + exit /b 1 +) + +set "QT_DIR=C:\Qt\6.9.3\msvc2022_64" +if not exist "%QT_DIR%\bin\Qt6Core.dll" ( + echo [!] Qt not at %QT_DIR% - gui step will be skipped + set "SKIP_GUI=1" +) + +rem stop driver silently so deploy\Goodmans.sys isn't locked during copy +sc stop Goodmans >nul 2>&1 + +echo. +echo [1/4] driver + samples + cli + windbg ext +"%MSBUILD%" "%ROOT%\Goodmans.sln" /p:Configuration=%CFG% /p:Platform=x64 /m /nologo /v:m +if errorlevel 1 ( + echo [X] sln build failed + exit /b 1 +) + +if defined SKIP_GUI goto :after_gui + +echo. +echo [2/4] gui-qt configure +if not exist "%ROOT%\gui-qt\build\CMakeCache.txt" ( + cmake -S "%ROOT%\gui-qt" -B "%ROOT%\gui-qt\build" -G "Visual Studio 17 2022" -A x64 -DCMAKE_PREFIX_PATH="%QT_DIR%" >nul + if errorlevel 1 ( + echo [X] cmake configure failed + exit /b 1 + ) +) + +echo. +echo [3/4] gui-qt build +cmake --build "%ROOT%\gui-qt\build" --config Release --parallel +if errorlevel 1 ( + echo [X] gui-qt build failed + exit /b 1 +) + +:after_gui + +echo. +echo [4/4] summary +echo. +echo deploy tree: +echo %ROOT%\deploy\Goodmans.sys +echo %ROOT%\deploy\Goodmans.inf +echo %ROOT%\deploy\GoodmansTest.cer / .pfx +echo %ROOT%\deploy\install.cmd / uninstall.cmd / gen_cert.cmd +echo %ROOT%\deploy\gui\goodmans-gui.exe + Qt DLLs + toolkit.wasm +echo %ROOT%\deploy\features\toolkit.wasm + process_tracer.wasm +echo %ROOT%\deploy\samples\*.wasm (demo guests) +echo. +echo to install: +echo 1. cd deploy +echo 2. gen_cert.cmd (first time only, then run as Admin) +echo 3. install.cmd (as Admin) +echo 4. gui\goodmans-gui.exe + +endlocal diff --git a/cli/cli.vcxproj b/cli/cli.vcxproj new file mode 100644 index 0000000..7e3a974 --- /dev/null +++ b/cli/cli.vcxproj @@ -0,0 +1,48 @@ + + + + + Debug + x64 + + + Release + x64 + + + + {6D9E2A50-1F1A-4B4E-8D9F-1B0C0D0A0002} + goodmans + 10.0 + Win32Proj + + + + Application + v143 + MultiByte + true + false + goodmans + + + + + Level3 + _CRT_SECURE_NO_WARNINGS;WIN32;%(PreprocessorDefinitions) + false + true + + + Console + true + + + + + + + + + + diff --git a/cli/main.c b/cli/main.c new file mode 100644 index 0000000..6709ffd --- /dev/null +++ b/cli/main.c @@ -0,0 +1,303 @@ +/* main.c - user-mode client */ +#include +#include +#include +#include + +#include "../shared/goodmans_ioctl.h" + +static HANDLE open_dev(void) +{ + HANDLE h = CreateFileA(GVM_USER_PATH, GENERIC_READ | GENERIC_WRITE, 0, NULL, + OPEN_EXISTING, 0, NULL); + if (h == INVALID_HANDLE_VALUE) + fprintf(stderr, "CreateFile(%s) failed: %lu\n", GVM_USER_PATH, GetLastError()); + return h; +} + +static int cmd_load(int argc, char** argv) +{ + if (argc < 3) { fprintf(stderr, "load \n"); return 1; } + const char* path = argv[2]; + + HANDLE f = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, NULL, + OPEN_EXISTING, 0, NULL); + if (f == INVALID_HANDLE_VALUE) { + fprintf(stderr, "open %s: %lu\n", path, GetLastError()); + return 1; + } + LARGE_INTEGER sz; + GetFileSizeEx(f, &sz); + if (sz.QuadPart <= 0 || sz.QuadPart > (16LL << 20)) { + fprintf(stderr, "bad size %lld\n", sz.QuadPart); + CloseHandle(f); + return 1; + } + + size_t buf_sz = sizeof(gvm_load_in) + (size_t)sz.QuadPart; + unsigned char* buf = (unsigned char*)malloc(buf_sz); + if (!buf) { CloseHandle(f); return 1; } + + gvm_load_in* in = (gvm_load_in*)buf; + memset(in, 0, sizeof(*in)); + in->wasm_size = (unsigned int)sz.QuadPart; + in->stack_bytes = 0; + strncpy_s(in->name, GVM_MAX_MODULE_NAME, path, _TRUNCATE); + + DWORD rd = 0; + ReadFile(f, buf + sizeof(gvm_load_in), (DWORD)sz.QuadPart, &rd, NULL); + CloseHandle(f); + + HANDLE dev = open_dev(); + if (dev == INVALID_HANDLE_VALUE) { free(buf); return 1; } + + gvm_load_out out = { 0 }; + DWORD ret = 0; + BOOL ok = DeviceIoControl(dev, IOCTL_GVM_LOAD_MODULE, + buf, (DWORD)buf_sz, + &out, sizeof(out), &ret, NULL); + CloseHandle(dev); + free(buf); + + if (!ok) { fprintf(stderr, "ioctl failed: %lu\n", GetLastError()); return 1; } + if (out.status != 0) { fprintf(stderr, "load status=%d: %s\n", out.status, out.err_msg); return 1; } + printf("module_id=%u (%s)\n", out.module_id, out.err_msg); + return 0; +} + +static int cmd_call(int argc, char** argv) +{ + if (argc < 3) { + fprintf(stderr, "call [--timeout=ms] [arg1..arg8]\n"); + return 1; + } + + unsigned int timeout_ms = 0; + int base = 2; + if (argc > base && strncmp(argv[base], "--timeout=", 10) == 0) { + timeout_ms = (unsigned int)strtoul(argv[base] + 10, NULL, 0); + base++; + } + if (argc <= base) { fprintf(stderr, "missing module id\n"); return 1; } + + gvm_call_in in = { 0 }; + in.module_id = (unsigned int)strtoul(argv[base], NULL, 0); + in.timeout_ms = timeout_ms; + if (argc > base + 1) strncpy_s(in.export_name, GVM_MAX_EXPORT_NAME, argv[base + 1], _TRUNCATE); + in.argc = 0; + for (int i = base + 2; i < argc && in.argc < GVM_MAX_ARGS; i++) + in.argv[in.argc++] = _strtoui64(argv[i], NULL, 0); + + HANDLE dev = open_dev(); + if (dev == INVALID_HANDLE_VALUE) return 1; + + gvm_call_out out = { 0 }; + DWORD ret = 0; + BOOL ok = DeviceIoControl(dev, IOCTL_GVM_CALL_EXPORT, + &in, sizeof(in), + &out, sizeof(out), &ret, NULL); + CloseHandle(dev); + + if (!ok) { fprintf(stderr, "ioctl failed: %lu\n", GetLastError()); return 1; } + if (out.status != 0) { fprintf(stderr, "call status=%d: %s\n", out.status, out.err_msg); return 1; } + printf("rv=0x%llx (%llu)\n", out.rv, out.rv); + return 0; +} + +static int cmd_unload(int argc, char** argv) +{ + if (argc < 3) { fprintf(stderr, "unload \n"); return 1; } + gvm_unload_in in = { 0 }; + in.module_id = (unsigned int)strtoul(argv[2], NULL, 0); + + HANDLE dev = open_dev(); + if (dev == INVALID_HANDLE_VALUE) return 1; + + DWORD ret = 0; + BOOL ok = DeviceIoControl(dev, IOCTL_GVM_UNLOAD_MODULE, + &in, sizeof(in), NULL, 0, &ret, NULL); + CloseHandle(dev); + + if (!ok) { fprintf(stderr, "ioctl failed: %lu\n", GetLastError()); return 1; } + printf("unloaded\n"); + return 0; +} + +static int cmd_modules(void) +{ + HANDLE dev = open_dev(); + if (dev == INVALID_HANDLE_VALUE) return 1; + + gvm_list_out* out = (gvm_list_out*)malloc(sizeof(gvm_list_out)); + if (!out) { CloseHandle(dev); return 1; } + memset(out, 0, sizeof(*out)); + + DWORD ret = 0; + BOOL ok = DeviceIoControl(dev, IOCTL_GVM_LIST_MODULES, + NULL, 0, out, sizeof(*out), &ret, NULL); + CloseHandle(dev); + + if (!ok) { fprintf(stderr, "ioctl failed: %lu\n", GetLastError()); free(out); return 1; } + + printf("%-4s %-8s %-8s %-8s %-16s %s\n", + "id", "wasm", "mem", "pool", "hash", "name"); + for (unsigned int i = 0; i < out->count; i++) { + gvm_module_entry* e = &out->entries[i]; + printf("%-4u %-6uB %-4uKB %-6lluB %016llx %s\n", + e->id, e->wasm_size, e->mem_pages * 64, + e->pool_bytes, e->hash, e->name); + } + printf("(%u module%s)\n", out->count, out->count == 1 ? "" : "s"); + free(out); + return 0; +} + +static int cmd_info(int argc, char** argv) +{ + if (argc < 3) { fprintf(stderr, "info \n"); return 1; } + + gvm_info_in in = { 0 }; + in.module_id = (unsigned int)strtoul(argv[2], NULL, 0); + + HANDLE dev = open_dev(); + if (dev == INVALID_HANDLE_VALUE) return 1; + + gvm_info_out* out = (gvm_info_out*)malloc(sizeof(gvm_info_out)); + if (!out) { CloseHandle(dev); return 1; } + memset(out, 0, sizeof(*out)); + + DWORD ret = 0; + BOOL ok = DeviceIoControl(dev, IOCTL_GVM_MODULE_INFO, + &in, sizeof(in), + out, sizeof(*out), &ret, NULL); + CloseHandle(dev); + + if (!ok) { fprintf(stderr, "ioctl failed: %lu\n", GetLastError()); free(out); return 1; } + if (out->status != 0) { fprintf(stderr, "info status=%d: %s\n", out->status, out->err_msg); free(out); return 1; } + + printf("id: %u\n", out->base.id); + printf("name: %s\n", out->base.name); + printf("wasm size: %u bytes\n", out->base.wasm_size); + printf("mem pages: %u (%u KB)\n", out->base.mem_pages, out->base.mem_pages * 64); + printf("hash: %016llx\n", out->base.hash); + free(out); + return 0; +} + +static int cmd_unload_all(void) +{ + HANDLE dev = open_dev(); + if (dev == INVALID_HANDLE_VALUE) return 1; + + DWORD ret = 0; + BOOL ok = DeviceIoControl(dev, IOCTL_GVM_UNLOAD_ALL, + NULL, 0, NULL, 0, &ret, NULL); + CloseHandle(dev); + + if (!ok) { fprintf(stderr, "ioctl failed: %lu\n", GetLastError()); return 1; } + printf("all modules unloaded\n"); + return 0; +} + +static int write_file(const char* path, const char* contents) +{ + HANDLE h = CreateFileA(path, GENERIC_WRITE, 0, NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL); + if (h == INVALID_HANDLE_VALUE) { fprintf(stderr, "cannot create %s: %lu\n", path, GetLastError()); return 1; } + DWORD w = 0; WriteFile(h, contents, (DWORD)strlen(contents), &w, NULL); + CloseHandle(h); + return 0; +} + +static int cmd_new(int argc, char** argv) +{ + if (argc < 3) { fprintf(stderr, "new \n"); return 1; } + const char* name = argv[2]; + if (!CreateDirectoryA(name, NULL) && GetLastError() != ERROR_ALREADY_EXISTS) { + fprintf(stderr, "cannot create dir %s: %lu\n", name, GetLastError()); + return 1; + } + char cpath[MAX_PATH], bpath[MAX_PATH]; + _snprintf_s(cpath, sizeof(cpath), _TRUNCATE, "%s\\%s.c", name, name); + _snprintf_s(bpath, sizeof(bpath), _TRUNCATE, "%s\\build.cmd", name); + + char src[2048]; + _snprintf_s(src, sizeof(src), _TRUNCATE, + "/* %s.c - goodmans wasm guest generated by `goodmans new`\n" + " * declare capabilities in GVM_MANIFEST, use gvm_* helpers from the SDK.\n" + " */\n" + "\n" + "#include \"gvm.h\"\n" + "\n" + "GVM_MANIFEST(GVM_CAP_ALLOC | GVM_CAP_READ_KMEM | GVM_CAP_INTROSPECT);\n" + "\n" + "GVM_EXPORT(hello)\n" + "u32 hello(u32 x)\n" + "{\n" + " // write to the driver debug ring - shows up in the GUI Log tab\n" + " static const char msg[] = \"hello from guest\";\n" + " gvm_dbg_print_raw(msg, sizeof(msg) - 1);\n" + " return x * 2 + 1;\n" + "}\n" + "\n" + "GVM_EXPORT(current_pid)\n" + "u32 current_pid(void) { return gvm_process_id(); }\n" + "\n" + "GVM_EXPORT(run_all)\n" + "u32 run_all(void)\n" + "{\n" + " (void)hello(21);\n" + " return current_pid();\n" + "}\n", + name); + + char build[1024]; + _snprintf_s(build, sizeof(build), _TRUNCATE, + "@echo off\r\n" + "setlocal\r\n" + "cd /d \"%%~dp0\"\r\n" + "set CLANG=%%LLVM_HOME%%\\bin\\clang.exe\r\n" + "if not exist \"%%CLANG%%\" set CLANG=C:\\Program Files\\LLVM\\bin\\clang.exe\r\n" + "if not exist \"%%CLANG%%\" ( echo [X] clang not found ^& exit /b 1 )\r\n" + "set SDK=..\\guest_sdk\r\n" + "\"%%CLANG%%\" -O2 --target=wasm32 -nostdlib -fno-builtin -I \"%%SDK%%\" ^\r\n" + " -Wl,--no-entry -Wl,--export-dynamic -Wl,--allow-undefined -Wl,--strip-all ^\r\n" + " -o %s.wasm %s.c\r\n" + "if errorlevel 1 exit /b 1\r\n" + "echo built %s.wasm\r\n", + name, name, name); + + // put a copy of gvm.h next to the source for offline builds; if user has + // the repo checked out, they can point SDK= to guest_sdk instead. + if (write_file(cpath, src)) return 1; + if (write_file(bpath, build)) return 1; + printf("created %s\\%s.c\n", name, name); + printf("created %s\\build.cmd\n", name); + printf("\nnext:\n cd %s\n build.cmd\n goodmans load %s.wasm\n", name, name); + return 0; +} + +int main(int argc, char** argv) +{ + if (argc < 2) { + fprintf(stderr, + "goodmans - Goodmans kernel VM CLI\n\n" + " goodmans load \n" + " goodmans call [--timeout=ms] [arg1..arg8]\n" + " goodmans unload \n" + " goodmans modules\n" + " goodmans info \n" + " goodmans unload-all\n" + " goodmans new scaffold a new guest\n"); + return 1; + } + if (!strcmp(argv[1], "load")) return cmd_load(argc, argv); + if (!strcmp(argv[1], "call")) return cmd_call(argc, argv); + if (!strcmp(argv[1], "unload")) return cmd_unload(argc, argv); + if (!strcmp(argv[1], "modules")) return cmd_modules(); + if (!strcmp(argv[1], "info")) return cmd_info(argc, argv); + if (!strcmp(argv[1], "unload-all")) return cmd_unload_all(); + if (!strcmp(argv[1], "new")) return cmd_new(argc, argv); + + fprintf(stderr, "unknown cmd: %s\n", argv[1]); + return 1; +} diff --git a/deploy/Goodmans.inf b/deploy/Goodmans.inf new file mode 100644 index 0000000..05df078 --- /dev/null +++ b/deploy/Goodmans.inf @@ -0,0 +1,48 @@ +; Goodmans.inf - minimal WDM install + +[Version] +Signature = "$WINDOWS NT$" +Class = System +ClassGuid = {4d36e97d-e325-11ce-bfc1-08002be10318} +Provider = %Provider% +DriverVer = +CatalogFile = Goodmans.cat +PnpLockdown = 1 + +[DestinationDirs] +DefaultDestDir = 12 + +[SourceDisksNames] +1 = %DiskName% + +[SourceDisksFiles] +Goodmans.sys = 1 + +[Manufacturer] +%Provider% = Standard,NT$ARCH$ + +[Standard.NT$ARCH$] +%DeviceDesc% = Goodmans_Install,Root\Goodmans + +[Goodmans_Install.NT] +CopyFiles = Goodmans.CopyFiles + +[Goodmans_Install.NT.Services] +AddService = Goodmans,%SPSVCINST_ASSOCSERVICE%,Goodmans_Service + +[Goodmans.CopyFiles] +Goodmans.sys + +[Goodmans_Service] +DisplayName = %ServiceDesc% +ServiceType = 1 +StartType = 3 +ErrorControl = 1 +ServiceBinary = %12%\Goodmans.sys + +[Strings] +SPSVCINST_ASSOCSERVICE = 0x00000002 +Provider = "Goodmans" +DeviceDesc = "Goodmans WASM Kernel VM" +ServiceDesc = "Goodmans" +DiskName = "Goodmans Install Disk" diff --git a/deploy/README.txt b/deploy/README.txt new file mode 100644 index 0000000..e196ab3 --- /dev/null +++ b/deploy/README.txt @@ -0,0 +1,60 @@ +Goodmans deploy folder + +After running build.cmd from the repo root, this directory contains +everything needed to install and run Goodmans on a test VM. + +layout after build: + deploy\ + Goodmans.sys signed driver (test-cert) + Goodmans.inf driver inf + GoodmansTest.cer test code-signing cert + GoodmansTest.pfx pfx used for signing (password: goodmans) + gen_cert.cmd creates cer+pfx (run once, elevated) + install.cmd imports cert + sc create + sc start + uninstall.cmd sc stop + sc delete + README.txt this file + gui\ + goodmans-gui.exe debug GUI (Qt6) + Qt6*.dll + platform + tls + imageformats plugins + toolkit.wasm toolkit guest for the Explorer tab + samples\ + sample_guest.wasm demo host-import roundtrips + ffi_demo.wasm direct nt/hal export calls via host_call + pslist_dumper.wasm walks PsLoadedModuleList + handle_stripper.wasm ObDereferenceObject demo + infinity_hook.wasm full IH port in wasm + features\ + toolkit.wasm + process_tracer.wasm + +workflow on the target VM + +1. enable test signing (once, then reboot): + bcdedit /set testsigning on + +2. copy the whole `deploy\` folder to the VM. from an elevated cmd here: + gen_cert.cmd (first-time setup, creates the cer+pfx) + install.cmd (imports cert + sc create/start Goodmans) + +3. run the GUI: + gui\goodmans-gui.exe + the toolkit.wasm loads automatically; the Explorer tab wakes up. + load any sample from Workbench, Browse, samples\*.wasm + +4. teardown: + uninstall.cmd + +common errors + +sc start returns 577 driver isnt signed with a trusted cert. + re-run install.cmd. verify cert landed in + Cert:\LocalMachine\Root and TrustedPublisher. + +sc start returns 1275 testsigning off, or HVCI blocking. verify: + bcdedit /enum {current} + testsigning should say Yes. + +CreateFile fails 2 driver started but DriverEntry bailed. open the + GUI Log tab or DbgView filtered on [goodmans]. + +bugcheck on start paste the bugcheck code + parameters into an issue. diff --git a/deploy/gen_cert.cmd b/deploy/gen_cert.cmd new file mode 100644 index 0000000..110df07 --- /dev/null +++ b/deploy/gen_cert.cmd @@ -0,0 +1,44 @@ +@echo off +setlocal + +net session >nul 2>&1 +if errorlevel 1 ( + echo [X] must be run as Administrator. + pause + exit /b 1 +) + +set "HERE=%~dp0" +if "%HERE:~-1%"=="\" set "HERE=%HERE:~0,-1%" + +set "CER=%HERE%\GoodmansTest.cer" +set "PFX=%HERE%\GoodmansTest.pfx" + +if exist "%CER%" if exist "%PFX%" ( + echo [*] cert already exists: %CER% + echo delete both files first if you want to regenerate. + exit /b 0 +) + +echo [*] generating self-signed code-signing cert +powershell -NoProfile -ExecutionPolicy Bypass -Command ^ + "$c = New-SelfSignedCertificate -Type CodeSigningCert -Subject 'CN=GoodmansTest' -CertStoreLocation Cert:\CurrentUser\My -KeyUsage DigitalSignature -KeyAlgorithm RSA -KeyLength 2048 -NotAfter (Get-Date).AddYears(5);" ^ + "Export-Certificate -Cert $c -FilePath '%CER%' | Out-Null;" ^ + "$pw = ConvertTo-SecureString -String 'goodmans' -Force -AsPlainText;" ^ + "Export-PfxCertificate -Cert $c -FilePath '%PFX%' -Password $pw | Out-Null;" ^ + "Write-Host ('thumbprint: ' + $c.Thumbprint)" + +if errorlevel 1 ( + echo [X] cert generation failed + exit /b 1 +) + +echo. +echo [+] generated: +echo %CER% +echo %PFX% (password: goodmans) +echo. +echo [*] to sign a driver: +echo signtool sign /fd sha256 /f "%PFX%" /p goodmans Goodmans.sys + +endlocal diff --git a/deploy/install.cmd b/deploy/install.cmd new file mode 100644 index 0000000..ffb60f5 --- /dev/null +++ b/deploy/install.cmd @@ -0,0 +1,57 @@ +@echo off +setlocal + +net session >nul 2>&1 +if errorlevel 1 ( + echo [X] must be run as Administrator. + pause + exit /b 1 +) + +set "HERE=%~dp0" +if "%HERE:~-1%"=="\" set "HERE=%HERE:~0,-1%" + +set "SYS=%HERE%\Goodmans.sys" +set "CER=%HERE%\GoodmansTest.cer" + +if not exist "%SYS%" ( + echo [X] missing: %SYS% + echo build the driver and copy Goodmans.sys into this folder. + exit /b 1 +) +if not exist "%CER%" ( + echo [X] missing: %CER% + echo run gen_cert.cmd first to create the test cert. + exit /b 1 +) + + +echo [*] Debug Print Filter mask +reg add "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Debug Print Filter" /v DEFAULT /t REG_DWORD /d 0xFFFFFFFF /f >nul + +echo [*] importing cert to Root + TrustedPublisher +certutil -addstore Root "%CER%" >nul 2>&1 +certutil -addstore TrustedPublisher "%CER%" >nul 2>&1 + +echo [*] removing any prior Goodmans service +sc query Goodmans >nul 2>&1 +if not errorlevel 1 ( + sc stop Goodmans >nul 2>&1 + sc delete Goodmans >nul 2>&1 + timeout /t 1 /nobreak >nul +) + +echo [*] sc create + start +sc create Goodmans type= kernel binPath= "%SYS%" +if errorlevel 1 ( echo [X] sc create failed & exit /b 1 ) +sc start Goodmans +if errorlevel 1 ( echo [X] sc start failed. check testsigning + cert import. & exit /b 1 ) + +echo. +echo [+] driver running. open DbgView as admin, filter on [goodmans] +echo then: +echo goodmans.exe load sample_guest.wasm +echo goodmans.exe call 1 run_all +echo goodmans.exe unload 1 + +endlocal diff --git a/deploy/uninstall.cmd b/deploy/uninstall.cmd new file mode 100644 index 0000000..3373072 --- /dev/null +++ b/deploy/uninstall.cmd @@ -0,0 +1,23 @@ +@echo off +setlocal + +net session >nul 2>&1 +if errorlevel 1 ( + echo [X] must be run as Administrator. + pause + exit /b 1 +) + +sc query Goodmans >nul 2>&1 +if errorlevel 1 ( + echo [*] service not present + exit /b 0 +) + +echo [*] sc stop Goodmans +sc stop Goodmans >nul 2>&1 + +echo [*] sc delete Goodmans +sc delete Goodmans + +endlocal diff --git a/driver/Goodmans.inf b/driver/Goodmans.inf new file mode 100644 index 0000000..05df078 --- /dev/null +++ b/driver/Goodmans.inf @@ -0,0 +1,48 @@ +; Goodmans.inf - minimal WDM install + +[Version] +Signature = "$WINDOWS NT$" +Class = System +ClassGuid = {4d36e97d-e325-11ce-bfc1-08002be10318} +Provider = %Provider% +DriverVer = +CatalogFile = Goodmans.cat +PnpLockdown = 1 + +[DestinationDirs] +DefaultDestDir = 12 + +[SourceDisksNames] +1 = %DiskName% + +[SourceDisksFiles] +Goodmans.sys = 1 + +[Manufacturer] +%Provider% = Standard,NT$ARCH$ + +[Standard.NT$ARCH$] +%DeviceDesc% = Goodmans_Install,Root\Goodmans + +[Goodmans_Install.NT] +CopyFiles = Goodmans.CopyFiles + +[Goodmans_Install.NT.Services] +AddService = Goodmans,%SPSVCINST_ASSOCSERVICE%,Goodmans_Service + +[Goodmans.CopyFiles] +Goodmans.sys + +[Goodmans_Service] +DisplayName = %ServiceDesc% +ServiceType = 1 +StartType = 3 +ErrorControl = 1 +ServiceBinary = %12%\Goodmans.sys + +[Strings] +SPSVCINST_ASSOCSERVICE = 0x00000002 +Provider = "Goodmans" +DeviceDesc = "Goodmans WASM Kernel VM" +ServiceDesc = "Goodmans" +DiskName = "Goodmans Install Disk" diff --git a/driver/Goodmans.vcxproj b/driver/Goodmans.vcxproj new file mode 100644 index 0000000..7e2e31f --- /dev/null +++ b/driver/Goodmans.vcxproj @@ -0,0 +1,124 @@ + + + + + Debug + x64 + + + Release + x64 + + + + {6D9E2A50-1F1A-4B4E-8D9F-1B0C0D0A0001} + {497e31cb-056b-4f31-abb8-447fd55ee5a5} + 10.0.26100.0 + 17.0 + Debug + x64 + v4.5 + Goodmans + 10.0.26100.0 + + + + Windows10 + true + false + WindowsKernelModeDriver10.0 + Driver + WDM + Universal + false + + + + + + + + + + false + Level3 + $(ProjectDir);$(ProjectDir)kshim;$(ProjectDir)wasm3;$(ProjectDir)inc;$(ProjectDir)..\shared;%(AdditionalIncludeDirectories) + _KERNEL_MODE;d_m3HasFloat=0;d_m3RecordBacktraces=0;d_m3LogTimestamps=0;d_m3VerboseErrorMessages=0;d_m3EnableValidation=0;d_m3SkipStackCheck=1;d_m3SkipMemoryBoundsCheck=1;d_m3CascadedOpcodes=1;%(PreprocessorDefinitions) + false + false + 4100;4127;4152;4189;4201;4204;4214;4245;4267;4310;4459;4505;4706;4996;4055;4131;4132;4244;4324;4456;4457;4090 + + + + + Native + + /INTEGRITYCHECK %(AdditionalOptions) + + + * + + + sha256 + + + + false + Off + + + false + Off + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $(SolutionDir)deploy + $(OutDir)Goodmans.sys + + + + + + + diff --git a/driver/driver.c b/driver/driver.c new file mode 100644 index 0000000..db447ab --- /dev/null +++ b/driver/driver.c @@ -0,0 +1,121 @@ +/* driver.c - entry, device create, dispatch */ +#include "inc/gvm.h" +#include "../shared/goodmans_ioctl.h" + +DRIVER_INITIALIZE DriverEntry; +static DRIVER_UNLOAD gvm_unload; +static DRIVER_DISPATCH gvm_create_close; +static DRIVER_DISPATCH gvm_device_control; + +PDEVICE_OBJECT g_device; +static UNICODE_STRING g_symlink; + +NTSTATUS +DriverEntry(_In_ PDRIVER_OBJECT drv, _In_ PUNICODE_STRING regpath) +{ + UNREFERENCED_PARAMETER(regpath); + + // filter mask so DbgView captures our lines without Verbose + DbgSetDebugFilterState(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, TRUE); + DbgSetDebugFilterState(DPFLTR_IHVDRIVER_ID, DPFLTR_WARNING_LEVEL, TRUE); + DbgSetDebugFilterState(DPFLTR_IHVDRIVER_ID, DPFLTR_TRACE_LEVEL, TRUE); + DbgSetDebugFilterState(DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, TRUE); + + gvm_log_init(); + gvm_trace_init(); + gvm_watchdog_start(); + gvm_log("driver load"); + + UNICODE_STRING dev_name; + RtlInitUnicodeString(&dev_name, GVM_DEVICE_NAME_U); + + NTSTATUS s = IoCreateDevice(drv, 0, &dev_name, GVM_DEVICE_TYPE, + FILE_DEVICE_SECURE_OPEN, FALSE, &g_device); + if (!NT_SUCCESS(s)) { + DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "[goodmans] IoCreateDevice=%08x\n", s); + return s; + } + + RtlInitUnicodeString(&g_symlink, GVM_SYMLINK_NAME_U); + s = IoCreateSymbolicLink(&g_symlink, &dev_name); + if (!NT_SUCCESS(s)) { + IoDeleteDevice(g_device); + g_device = NULL; + return s; + } + + s = gvm_modtab_init(); + if (!NT_SUCCESS(s)) { + IoDeleteSymbolicLink(&g_symlink); + IoDeleteDevice(g_device); + g_device = NULL; + return s; + } + + drv->MajorFunction[IRP_MJ_CREATE] = gvm_create_close; + drv->MajorFunction[IRP_MJ_CLOSE] = gvm_create_close; + drv->MajorFunction[IRP_MJ_DEVICE_CONTROL] = gvm_device_control; + drv->DriverUnload = gvm_unload; + + g_device->Flags |= DO_BUFFERED_IO; + g_device->Flags &= ~DO_DEVICE_INITIALIZING; + + gvm_log("driver ready, device \\\\.\\Goodmans"); + return STATUS_SUCCESS; +} + +static VOID +gvm_unload(_In_ PDRIVER_OBJECT drv) +{ + UNREFERENCED_PARAMETER(drv); + DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "[goodmans] unload\n"); + + gvm_ih_teardown(); + gvm_watchdog_stop(); + gvm_notify_teardown(); + gvm_modtab_teardown(); + + if (g_symlink.Buffer) + IoDeleteSymbolicLink(&g_symlink); + if (g_device) + IoDeleteDevice(g_device); +} + +static NTSTATUS +gvm_create_close(_In_ PDEVICE_OBJECT dev, _Inout_ PIRP irp) +{ + UNREFERENCED_PARAMETER(dev); + irp->IoStatus.Status = STATUS_SUCCESS; + irp->IoStatus.Information = 0; + IoCompleteRequest(irp, IO_NO_INCREMENT); + return STATUS_SUCCESS; +} + +static NTSTATUS +gvm_device_control(_In_ PDEVICE_OBJECT dev, _Inout_ PIRP irp) +{ + UNREFERENCED_PARAMETER(dev); + + PIO_STACK_LOCATION sp = IoGetCurrentIrpStackLocation(irp); + NTSTATUS s = STATUS_INVALID_DEVICE_REQUEST; + + switch (sp->Parameters.DeviceIoControl.IoControlCode) { + case IOCTL_GVM_LOAD_MODULE: s = gvm_ioctl_load(irp, sp); break; + case IOCTL_GVM_CALL_EXPORT: s = gvm_ioctl_call(irp, sp); break; + case IOCTL_GVM_UNLOAD_MODULE: s = gvm_ioctl_unload(irp, sp); break; + case IOCTL_GVM_LIST_MODULES: s = gvm_ioctl_list(irp, sp); break; + case IOCTL_GVM_MODULE_INFO: s = gvm_ioctl_info(irp, sp); break; + case IOCTL_GVM_UNLOAD_ALL: s = gvm_ioctl_unload_all(irp, sp); break; + case IOCTL_GVM_TAIL_LOG: s = gvm_ioctl_tail_log(irp, sp); break; + case IOCTL_GVM_READ_GUEST: s = gvm_ioctl_read_guest(irp, sp); break; + case IOCTL_GVM_TAIL_TRACE: s = gvm_ioctl_tail_trace(irp, sp); break; + case IOCTL_GVM_TRACE_CTL: s = gvm_ioctl_trace_ctl(irp, sp); break; + case IOCTL_GVM_FORCE_UNLOAD: s = gvm_ioctl_force_unload(irp, sp); break; + case IOCTL_GVM_NOTIFY_STOP: s = gvm_ioctl_notify_stop(irp, sp); break; + default: break; + } + + irp->IoStatus.Status = s; + IoCompleteRequest(irp, IO_NO_INCREMENT); + return s; +} diff --git a/driver/host_imports.c b/driver/host_imports.c new file mode 100644 index 0000000..a250dd6 --- /dev/null +++ b/driver/host_imports.c @@ -0,0 +1,1275 @@ +/* host_imports.c - kernel APIs exposed to wasm guests */ +#include "inc/gvm.h" +#include "../shared/goodmans_ioctl.h" +#include "wasm3/wasm3.h" +#include "wasm3/m3_env.h" +#include +#include + +// checks the caller module's capability bitmask against a required cap. +// returns TRUE if allowed OR if we can't identify the caller (fail-open only +// happens when we can't find the module for the runtime, which shouldn't +// occur in normal use). +static __forceinline BOOLEAN gvm_cap_ok(IM3Runtime rt, unsigned int need) +{ + gvm_module* owner = gvm_modtab_owner_of_runtime(rt); + if (!owner) return TRUE; + return (owner->caps & need) == need; +} + +// non-canonical / unmapped kernel VA short-circuits so no #GP escapes SEH +static __forceinline BOOLEAN gvm_addr_ok(uint64_t addr, size_t len) +{ + if (addr == 0) return FALSE; + uint64_t hi = addr >> 48; + if (hi != 0x0000 && hi != 0xFFFF) return FALSE; + if ((addr + len) < addr) return FALSE; + if (!MmIsAddressValid((void*)(uintptr_t)addr)) return FALSE; + if (len > 1 && !MmIsAddressValid((void*)(uintptr_t)(addr + len - 1))) return FALSE; + return TRUE; +} + +#define GVM_EVT_RING 512 + +typedef struct { + uint32_t kind; // 0 = process create, 1 = process exit, 2 = image load + uint32_t pid; + uint32_t ppid; + uint32_t flags; + uint64_t eprocess; + uint64_t image_base; // image_load only + uint32_t image_size; // image_load only + uint32_t _pad; + char name[80]; // image name (basename only) +} gvm_event; + +static gvm_event g_evt_ring[GVM_EVT_RING]; +static ULONG g_evt_head = 0; +static ULONG g_evt_tail = 0; +static KSPIN_LOCK g_evt_lock; +static BOOLEAN g_evt_lock_init = FALSE; +static BOOLEAN g_process_notify_on = FALSE; +static BOOLEAN g_image_notify_on = FALSE; + +static KEVENT g_evt_wake; +static KEVENT g_evt_shutdown; +static PETHREAD g_dispatch_thread = NULL; +static BOOLEAN g_dispatch_on = FALSE; + +static void gvm_push_event(const gvm_event* e) +{ + KIRQL irql; + if (!g_evt_lock_init) return; + KeAcquireSpinLock(&g_evt_lock, &irql); + ULONG next = (g_evt_head + 1) % GVM_EVT_RING; + if (next != g_evt_tail) { + g_evt_ring[g_evt_head] = *e; + g_evt_head = next; + if (g_dispatch_on) + KeSetEvent(&g_evt_wake, IO_NO_INCREMENT, FALSE); + } + KeReleaseSpinLock(&g_evt_lock, irql); +} + +// used by ih.c to push a SYSCALL event (kind=3) from the trampoline. +// re-uses the existing event ring so we get async dispatch to guest exports +// through the same worker that handles process/image events. +void gvm_push_event_generic(unsigned int kind, uint32_t pid, + uint64_t a, uint64_t b, uint32_t c) +{ + gvm_event e = { 0 }; + e.kind = kind; + e.pid = pid; + e.image_base = a; + e.eprocess = b; + e.image_size = c; + gvm_push_event(&e); +} + +static BOOLEAN gvm_pop_event(gvm_event* out) +{ + KIRQL irql; + BOOLEAN got = FALSE; + if (!g_evt_lock_init) return FALSE; + KeAcquireSpinLock(&g_evt_lock, &irql); + if (g_evt_head != g_evt_tail) { + *out = g_evt_ring[g_evt_tail]; + g_evt_tail = (g_evt_tail + 1) % GVM_EVT_RING; + got = TRUE; + } + KeReleaseSpinLock(&g_evt_lock, irql); + return got; +} + +static void copy_wide_to_basename_ascii(char* dst, size_t dst_sz, PCUNICODE_STRING us) +{ + if (!us || !us->Buffer || dst_sz == 0) { if (dst_sz) dst[0] = 0; return; } + USHORT len = us->Length / sizeof(WCHAR); + // find last backslash + USHORT start = 0; + for (USHORT i = 0; i < len; i++) + if (us->Buffer[i] == L'\\') start = i + 1; + USHORT n = len - start; + if (n > dst_sz - 1) n = (USHORT)(dst_sz - 1); + for (USHORT i = 0; i < n; i++) dst[i] = (char)us->Buffer[start + i]; + dst[n] = 0; +} + +static VOID gvm_process_notify_ex( + _Inout_ PEPROCESS Process, + _In_ HANDLE ProcessId, + _Inout_opt_ PPS_CREATE_NOTIFY_INFO Info) +{ + gvm_event e = { 0 }; + e.pid = (uint32_t)(uintptr_t)ProcessId; + e.eprocess = (uint64_t)(uintptr_t)Process; + + if (Info) { + e.kind = 0; + e.ppid = (uint32_t)(uintptr_t)Info->ParentProcessId; + copy_wide_to_basename_ascii(e.name, sizeof(e.name), Info->ImageFileName); + gvm_log("PROC_CREATE pid=%u ppid=%u %s", e.pid, e.ppid, e.name); + } else { + e.kind = 1; + gvm_log("PROC_EXIT pid=%u", e.pid); + } + gvm_push_event(&e); +} + +static VOID gvm_image_notify( + _In_opt_ PUNICODE_STRING FullImageName, + _In_ HANDLE ProcessId, + _In_ PIMAGE_INFO ImageInfo) +{ + gvm_event e = { 0 }; + e.kind = 2; + e.pid = (uint32_t)(uintptr_t)ProcessId; + e.image_base = (uint64_t)(uintptr_t)ImageInfo->ImageBase; + e.image_size = (uint32_t)ImageInfo->ImageSize; + // IMAGE_INFO's first ULONG is a packed bitfield: SystemModeImage, ImageSignatureLevel, etc + e.flags = *(ULONG*)ImageInfo; + copy_wide_to_basename_ascii(e.name, sizeof(e.name), FullImageName); + gvm_log("IMAGE_LOAD pid=%u base=%p size=%u %s", + e.pid, (PVOID)(uintptr_t)e.image_base, e.image_size, e.name); + gvm_push_event(&e); +} + +void gvm_notify_init(void) +{ + if (!g_evt_lock_init) { + KeInitializeSpinLock(&g_evt_lock); + KeInitializeEvent(&g_evt_wake, SynchronizationEvent, FALSE); + KeInitializeEvent(&g_evt_shutdown, NotificationEvent, FALSE); + g_evt_lock_init = TRUE; + } +} + +// dispatch worker: on each wake, drain the ring and for every event look up +// on_process_create / on_process_exit / on_image_load in every loaded module. +// holds the module's call_mutex across FindFunction+Call so a concurrent +// unload can't free the runtime out from under us. +static const char* g_export_names[4] = { + "on_process_create", + "on_process_exit", + "on_image_load", + "on_syscall", +}; + +typedef struct { + IM3Function fn; + unsigned int argc; + const void** argp; + M3Result r; +} gvm_disp_ctx; + +static VOID gvm_disp_callout(_In_ PVOID p) +{ + gvm_disp_ctx* c = (gvm_disp_ctx*)p; + c->r = m3_Call(c->fn, c->argc, c->argp); +} + +static void gvm_dispatch_worker(_In_ PVOID ctx) +{ + UNREFERENCED_PARAMETER(ctx); + PVOID waits[2] = { &g_evt_wake, &g_evt_shutdown }; + + for (;;) { + NTSTATUS s = KeWaitForMultipleObjects(2, waits, WaitAny, + Executive, KernelMode, FALSE, NULL, NULL); + if (s == STATUS_WAIT_1) break; + + gvm_event e; + while (gvm_pop_event(&e)) { + if (e.kind >= 4) continue; + const char* export_name = g_export_names[e.kind]; + + for (unsigned int i = 0; i < GVM_MAX_MODULES; i++) { + gvm_module* m = gvm_modtab_iter(i); + if (!m) continue; + + KeWaitForSingleObject(&m->call_mutex, Executive, KernelMode, FALSE, NULL); + + if (m->used && m->runtime) { + IM3Function fn = NULL; + M3Result fr = m3_FindFunction(&fn, m->runtime, export_name); + if (!fr && fn) { + uint64_t a0 = e.pid; + uint64_t a1 = (e.kind == 2 || e.kind == 3) ? e.image_base : e.ppid; + uint64_t a2 = (e.kind == 2 || e.kind == 3) ? (uint64_t)e.image_size : 0; + const void* argp[3] = { &a0, &a1, &a2 }; + gvm_disp_ctx dc = { fn, 3, argp, m3Err_none }; + KeExpandKernelStackAndCalloutEx(gvm_disp_callout, &dc, + 64 * 1024, FALSE, NULL); + } + } + + KeReleaseMutex(&m->call_mutex, FALSE); + } + } + } + + PsTerminateSystemThread(STATUS_SUCCESS); +} + +extern PDEVICE_OBJECT g_device; +static volatile LONG g_dispatch_pending = 0; + +static void gvm_spawn_worker_now(void) +{ + if (g_dispatch_on) return; + HANDLE h = NULL; + OBJECT_ATTRIBUTES oa; + InitializeObjectAttributes(&oa, NULL, OBJ_KERNEL_HANDLE, NULL, NULL); + NTSTATUS s = PsCreateSystemThread(&h, THREAD_ALL_ACCESS, &oa, NULL, NULL, + gvm_dispatch_worker, NULL); + if (!NT_SUCCESS(s)) { gvm_log("dispatch: PsCreateSystemThread=0x%x", s); return; } + + ObReferenceObjectByHandle(h, THREAD_ALL_ACCESS, *PsThreadType, KernelMode, + (PVOID*)&g_dispatch_thread, NULL); + ZwClose(h); + g_dispatch_on = TRUE; + gvm_log("dispatch: worker started"); +} + +static IO_WORKITEM_ROUTINE_EX gvm_dispatch_spawner; +static VOID gvm_dispatch_spawner(_In_ PVOID io_object, + _In_opt_ PVOID context, + _In_ PIO_WORKITEM wi) +{ + UNREFERENCED_PARAMETER(io_object); + UNREFERENCED_PARAMETER(context); + gvm_spawn_worker_now(); + IoFreeWorkItem(wi); + InterlockedExchange(&g_dispatch_pending, 0); +} + +// safe from any PASSIVE_LEVEL context, including from inside a wasm expand- +// stack callout. queues thread creation onto a system worker thread rather +// than spawning it inline (spawning inside the callout was causing IRQL +// bugchecks in the newly-created thread's early stack setup). +void gvm_dispatch_start(void) +{ + if (g_dispatch_on) return; + if (InterlockedCompareExchange(&g_dispatch_pending, 1, 0) != 0) return; + gvm_notify_init(); + if (!g_device) { InterlockedExchange(&g_dispatch_pending, 0); return; } + + PIO_WORKITEM wi = IoAllocateWorkItem(g_device); + if (!wi) { InterlockedExchange(&g_dispatch_pending, 0); return; } + IoQueueWorkItemEx(wi, gvm_dispatch_spawner, DelayedWorkQueue, NULL); +} + +// signal-only: safe to call from any thread that may hold a module mutex, +// because we don't wait for the worker to complete its current callback. +void gvm_dispatch_stop_signal(void) +{ + if (g_dispatch_on) KeSetEvent(&g_evt_shutdown, IO_NO_INCREMENT, FALSE); +} + +// signal + block until worker exits. only safe when caller does NOT hold any +// module mutex (typical case: driver unload). +void gvm_dispatch_stop_wait(void) +{ + if (!g_dispatch_on) return; + KeSetEvent(&g_evt_shutdown, IO_NO_INCREMENT, FALSE); + if (g_dispatch_thread) { + KeWaitForSingleObject(g_dispatch_thread, Executive, KernelMode, FALSE, NULL); + ObDereferenceObject(g_dispatch_thread); + g_dispatch_thread = NULL; + } + g_dispatch_on = FALSE; + KeClearEvent(&g_evt_shutdown); +} + +void gvm_notify_teardown(void) +{ + gvm_dispatch_stop_wait(); + + if (g_process_notify_on) { + PsSetCreateProcessNotifyRoutineEx(gvm_process_notify_ex, TRUE); + g_process_notify_on = FALSE; + } + if (g_image_notify_on) { + PsRemoveLoadImageNotifyRoutine(gvm_image_notify); + g_image_notify_on = FALSE; + } +} + +NTSTATUS +gvm_ioctl_notify_stop(PIRP irp, PIO_STACK_LOCATION sp) +{ + UNREFERENCED_PARAMETER(sp); + gvm_notify_teardown(); + gvm_log("notify: all callbacks removed, dispatch worker stopped"); + irp->IoStatus.Information = 0; + return STATUS_SUCCESS; +} + +// host_mem_base() -> i64 (kernel VA of the guest's wasm linear memory base) +// wasm3 memory is one contiguous non-paged buffer, so base + wasm_off gives +// the real kernel VA of any byte in the guest's linear memory. exposed so +// guests can construct pointer args for kernel APIs called via generic FFI. +static const void* +host_mem_base(IM3Runtime rt, IM3ImportContext ctx, uint64_t* sp, void* mem) +{ + UNREFERENCED_PARAMETER(rt); UNREFERENCED_PARAMETER(ctx); + *sp = (uint64_t)(uintptr_t)mem; + return m3Err_none; +} + +// host_mem_size() -> i32 (linear memory byte count, tracks memory.grow) +static const void* +host_mem_size(IM3Runtime rt, IM3ImportContext ctx, uint64_t* sp, void* mem) +{ + UNREFERENCED_PARAMETER(ctx); UNREFERENCED_PARAMETER(mem); + uint32_t sz = 0; + m3_GetMemory(rt, &sz, 0); + *(uint32_t*)sp = sz; + return m3Err_none; +} + +// host_make_unistr(wasm_str_off, byte_len) -> i64 +// builds a UNICODE_STRING in non-paged pool from a UTF-16LE string in guest +// memory. returns kernel VA usable directly as PUNICODE_STRING for kernel +// APIs. caller must free with host_free_unistr when done. +static const void* +host_make_unistr(IM3Runtime rt, IM3ImportContext ctx, uint64_t* sp, void* mem) +{ + UNREFERENCED_PARAMETER(ctx); + uint32_t off = (uint32_t)*(sp + 1); + uint32_t blen = (uint32_t)*(sp + 2); + *sp = 0; + + if (!gvm_cap_ok(rt, GVM_CAP_ALLOC)) return m3Err_none; + uint32_t mem_sz = 0; m3_GetMemory(rt, &mem_sz, 0); + if (blen == 0 || blen > 0x1000 || (uint64_t)off + blen > (uint64_t)mem_sz) + return m3Err_none; + + UNICODE_STRING* us = (UNICODE_STRING*)ExAllocatePoolWithTag( + NonPagedPoolNx, sizeof(UNICODE_STRING) + blen, GVM_TAG_WBUF); + if (!us) return m3Err_none; + WCHAR* buf = (WCHAR*)((unsigned char*)us + sizeof(UNICODE_STRING)); + RtlCopyMemory(buf, (unsigned char*)mem + off, blen); + us->Length = (USHORT)blen; + us->MaximumLength = (USHORT)blen; + us->Buffer = buf; + *sp = (uint64_t)(uintptr_t)us; + return m3Err_none; +} + +// host_free_unistr(kva) -> void +static const void* +host_free_unistr(IM3Runtime rt, IM3ImportContext ctx, uint64_t* sp, void* mem) +{ + UNREFERENCED_PARAMETER(rt); UNREFERENCED_PARAMETER(ctx); UNREFERENCED_PARAMETER(mem); + uint64_t va = *(sp + 0); + if (va) ExFreePoolWithTag((PVOID)(uintptr_t)va, GVM_TAG_WBUF); + return m3Err_none; +} + +static const void* +host_dbg_print(IM3Runtime rt, IM3ImportContext ctx, uint64_t* sp, void* mem) +{ + UNREFERENCED_PARAMETER(ctx); + + uint32_t off = (uint32_t)*(sp + 0); + uint32_t len = (uint32_t)*(sp + 1); + + uint32_t mem_sz = 0; + m3_GetMemory(rt, &mem_sz, 0); + if ((uint64_t)off + len > (uint64_t)mem_sz || len == 0) + return m3Err_none; + + const char* s = (const char*)mem + off; + char tmp[512]; + ULONG n = (len < sizeof(tmp) - 1) ? (ULONG)len : (ULONG)(sizeof(tmp) - 1); + RtlCopyMemory(tmp, s, n); + tmp[n] = 0; + DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "[goodmans/guest] %s\n", tmp); + return m3Err_none; +} + +// pool budget accounting: prepend an 8-byte size prefix so host_free can +// refund the charge. guest sees the pointer AFTER the prefix. +typedef struct { + uint64_t size; + unsigned char data[1]; +} gvm_alloc_hdr; + +#define GVM_HDR_OFF ((size_t)((gvm_alloc_hdr*)0)->data) + +static const void* +host_alloc(IM3Runtime rt, IM3ImportContext ctx, uint64_t* sp, void* mem) +{ + UNREFERENCED_PARAMETER(ctx); UNREFERENCED_PARAMETER(mem); + + uint64_t* raw_return = sp++; + uint32_t size = (uint32_t)*sp; + if (size == 0) size = 1; + + if (!gvm_cap_ok(rt, GVM_CAP_ALLOC)) { *raw_return = 0; return m3Err_none; } + + gvm_module* owner = gvm_modtab_owner_of_runtime(rt); + if (owner && owner->pool_budget > 0) { + LONG64 want = (LONG64)size + (LONG64)GVM_HDR_OFF; + LONG64 after = InterlockedAdd64(&owner->pool_used, want); + if (after > owner->pool_budget) { + InterlockedAdd64(&owner->pool_used, -want); + *raw_return = 0; + return m3Err_none; + } + } + + gvm_alloc_hdr* h = (gvm_alloc_hdr*)ExAllocatePoolWithTag( + NonPagedPoolNx, (SIZE_T)size + GVM_HDR_OFF, GVM_TAG_MOD); + if (!h) { + if (owner && owner->pool_budget > 0) + InterlockedAdd64(&owner->pool_used, -((LONG64)size + (LONG64)GVM_HDR_OFF)); + *raw_return = 0; + return m3Err_none; + } + h->size = size; + *raw_return = (uint64_t)(uintptr_t)h->data; + return m3Err_none; +} + +static const void* +host_free(IM3Runtime rt, IM3ImportContext ctx, uint64_t* sp, void* mem) +{ + UNREFERENCED_PARAMETER(ctx); UNREFERENCED_PARAMETER(mem); + if (!gvm_cap_ok(rt, GVM_CAP_ALLOC)) return m3Err_none; + uint64_t p = *(sp + 0); + if (!p) return m3Err_none; + + gvm_alloc_hdr* h = (gvm_alloc_hdr*)((unsigned char*)(uintptr_t)p - GVM_HDR_OFF); + uint64_t sz = h->size; + ExFreePoolWithTag(h, GVM_TAG_MOD); + + gvm_module* owner = gvm_modtab_owner_of_runtime(rt); + if (owner && owner->pool_budget > 0) + InterlockedAdd64(&owner->pool_used, -((LONG64)sz + (LONG64)GVM_HDR_OFF)); + return m3Err_none; +} + +static const void* +host_read_u8(IM3Runtime rt, IM3ImportContext ctx, uint64_t* sp, void* mem) +{ + UNREFERENCED_PARAMETER(ctx); UNREFERENCED_PARAMETER(mem); + uint32_t* raw_return = (uint32_t*)sp; sp++; + uint64_t addr = *sp; + uint32_t v = 0xFFFFFFFF; + if (gvm_cap_ok(rt, GVM_CAP_READ_KMEM) && gvm_addr_ok(addr, 1)) { + __try { v = *(volatile unsigned char*)(uintptr_t)addr; } + __except (EXCEPTION_EXECUTE_HANDLER) { v = 0xFFFFFFFF; } + } + *raw_return = v; + return m3Err_none; +} + +static const void* +host_read_u32(IM3Runtime rt, IM3ImportContext ctx, uint64_t* sp, void* mem) +{ + UNREFERENCED_PARAMETER(ctx); UNREFERENCED_PARAMETER(mem); + uint32_t* raw_return = (uint32_t*)sp; sp++; + uint64_t addr = *sp; + uint32_t v = 0xFFFFFFFF; + if (gvm_cap_ok(rt, GVM_CAP_READ_KMEM) && gvm_addr_ok(addr, 4)) { + __try { v = *(volatile uint32_t*)(uintptr_t)addr; } + __except (EXCEPTION_EXECUTE_HANDLER) { v = 0xFFFFFFFF; } + } + *raw_return = v; + return m3Err_none; +} + +static const void* +host_read_u64(IM3Runtime rt, IM3ImportContext ctx, uint64_t* sp, void* mem) +{ + UNREFERENCED_PARAMETER(ctx); UNREFERENCED_PARAMETER(mem); + uint64_t* raw_return = sp++; + uint64_t addr = *sp; + uint64_t v = 0; + if (gvm_cap_ok(rt, GVM_CAP_READ_KMEM) && gvm_addr_ok(addr, 8)) { + __try { v = *(volatile uint64_t*)(uintptr_t)addr; } + __except (EXCEPTION_EXECUTE_HANDLER) { v = 0; } + } + *raw_return = v; + return m3Err_none; +} + +static const void* +host_write_u64(IM3Runtime rt, IM3ImportContext ctx, uint64_t* sp, void* mem) +{ + UNREFERENCED_PARAMETER(ctx); UNREFERENCED_PARAMETER(mem); + uint64_t addr = *(sp + 0); + uint64_t val = *(sp + 1); + if (gvm_cap_ok(rt, GVM_CAP_WRITE_KMEM) && gvm_addr_ok(addr, 8)) { + __try { *(volatile uint64_t*)(uintptr_t)addr = val; } + __except (EXCEPTION_EXECUTE_HANDLER) { } + } + return m3Err_none; +} + +static const void* +host_read_bytes(IM3Runtime rt, IM3ImportContext ctx, uint64_t* sp, void* mem) +{ + UNREFERENCED_PARAMETER(ctx); + uint32_t* raw_return = (uint32_t*)sp; sp++; + uint64_t kaddr = *(sp + 0); + uint32_t guest_off = (uint32_t)*(sp + 1); + uint32_t len = (uint32_t)*(sp + 2); + + uint32_t mem_sz = 0; + m3_GetMemory(rt, &mem_sz, 0); + if ((uint64_t)guest_off + len > (uint64_t)mem_sz || len == 0) { + *raw_return = 0; + return m3Err_none; + } + + unsigned char* dst = (unsigned char*)mem + guest_off; + uint32_t copied = 0; + if (gvm_cap_ok(rt, GVM_CAP_READ_KMEM) && gvm_addr_ok(kaddr, len)) { + __try { + RtlCopyMemory(dst, (const void*)(uintptr_t)kaddr, len); + copied = len; + } __except (EXCEPTION_EXECUTE_HANDLER) { + copied = 0; + } + } + *raw_return = copied; + return m3Err_none; +} + +static const void* +host_write_bytes(IM3Runtime rt, IM3ImportContext ctx, uint64_t* sp, void* mem) +{ + UNREFERENCED_PARAMETER(ctx); + uint32_t* raw_return = (uint32_t*)sp; sp++; + uint64_t kaddr = *(sp + 0); + uint32_t guest_off = (uint32_t)*(sp + 1); + uint32_t len = (uint32_t)*(sp + 2); + + uint32_t mem_sz = 0; + m3_GetMemory(rt, &mem_sz, 0); + if ((uint64_t)guest_off + len > (uint64_t)mem_sz || len == 0) { + *raw_return = 0; + return m3Err_none; + } + + unsigned char* src = (unsigned char*)mem + guest_off; + uint32_t copied = 0; + if (gvm_cap_ok(rt, GVM_CAP_WRITE_KMEM) && gvm_addr_ok(kaddr, len)) { + __try { + RtlCopyMemory((void*)(uintptr_t)kaddr, src, len); + copied = len; + } __except (EXCEPTION_EXECUTE_HANDLER) { + copied = 0; + } + } + *raw_return = copied; + return m3Err_none; +} + +static const void* +host_current_irql(IM3Runtime rt, IM3ImportContext ctx, uint64_t* sp, void* mem) +{ + UNREFERENCED_PARAMETER(ctx); UNREFERENCED_PARAMETER(mem); + uint32_t* raw_return = (uint32_t*)sp; + *raw_return = gvm_cap_ok(rt, GVM_CAP_INTROSPECT) ? (uint32_t)KeGetCurrentIrql() : 0; + return m3Err_none; +} + +static const void* +host_process_id(IM3Runtime rt, IM3ImportContext ctx, uint64_t* sp, void* mem) +{ + UNREFERENCED_PARAMETER(ctx); UNREFERENCED_PARAMETER(mem); + uint32_t* raw_return = (uint32_t*)sp; + *raw_return = gvm_cap_ok(rt, GVM_CAP_INTROSPECT) ? (uint32_t)(uintptr_t)PsGetCurrentProcessId() : 0; + return m3Err_none; +} + +static const void* +host_thread_id(IM3Runtime rt, IM3ImportContext ctx, uint64_t* sp, void* mem) +{ + UNREFERENCED_PARAMETER(ctx); UNREFERENCED_PARAMETER(mem); + uint32_t* raw_return = (uint32_t*)sp; + *raw_return = gvm_cap_ok(rt, GVM_CAP_INTROSPECT) ? (uint32_t)(uintptr_t)PsGetCurrentThreadId() : 0; + return m3Err_none; +} + +// host_current_process() -> i64 (PEPROCESS) +static const void* +host_current_process(IM3Runtime rt, IM3ImportContext ctx, uint64_t* sp, void* mem) +{ + UNREFERENCED_PARAMETER(ctx); UNREFERENCED_PARAMETER(mem); + uint64_t* raw_return = sp; + *raw_return = gvm_cap_ok(rt, GVM_CAP_INTROSPECT) ? (uint64_t)(uintptr_t)PsGetCurrentProcess() : 0; + return m3Err_none; +} + +// host_cpuid(leaf, subleaf, guest_out_off) -> void +// writes eax/ebx/ecx/edx as 4 consecutive u32s at guest_out_off +static const void* +host_cpuid(IM3Runtime rt, IM3ImportContext ctx, uint64_t* sp, void* mem) +{ + UNREFERENCED_PARAMETER(ctx); + uint32_t leaf = (uint32_t)*(sp + 0); + uint32_t subleaf = (uint32_t)*(sp + 1); + uint32_t out_off = (uint32_t)*(sp + 2); + + if (!gvm_cap_ok(rt, GVM_CAP_CPUID_TSC)) return m3Err_none; + + uint32_t mem_sz = 0; + m3_GetMemory(rt, &mem_sz, 0); + if ((uint64_t)out_off + 16 > (uint64_t)mem_sz) + return m3Err_none; + + int regs[4] = { 0 }; + __cpuidex(regs, (int)leaf, (int)subleaf); + uint32_t* dst = (uint32_t*)((unsigned char*)mem + out_off); + dst[0] = (uint32_t)regs[0]; + dst[1] = (uint32_t)regs[1]; + dst[2] = (uint32_t)regs[2]; + dst[3] = (uint32_t)regs[3]; + return m3Err_none; +} + +// host_writemsr(idx, val) -> i32 (0 = ok, -1 = #GP) +static const void* +host_writemsr(IM3Runtime rt, IM3ImportContext ctx, uint64_t* sp, void* mem) +{ + UNREFERENCED_PARAMETER(ctx); UNREFERENCED_PARAMETER(mem); + uint32_t* raw_return = (uint32_t*)sp; sp++; + uint32_t idx = (uint32_t)*(sp + 0); + uint64_t val = *(sp + 1); + uint32_t rv = 0; + if (!gvm_cap_ok(rt, GVM_CAP_MSR_WRITE)) { *raw_return = (uint32_t)-1; return m3Err_none; } + __try { __writemsr(idx, val); } + __except (EXCEPTION_EXECUTE_HANDLER) { rv = (uint32_t)-1; } + *raw_return = rv; + return m3Err_none; +} + +// host_phys_read(pa, guest_off, len) -> i32 (bytes copied, 0 on fail) +static const void* +host_phys_read(IM3Runtime rt, IM3ImportContext ctx, uint64_t* sp, void* mem) +{ + UNREFERENCED_PARAMETER(ctx); + uint32_t* raw_return = (uint32_t*)sp; sp++; + uint64_t pa = *(sp + 0); + uint32_t guest_off = (uint32_t)*(sp + 1); + uint32_t len = (uint32_t)*(sp + 2); + + if (!gvm_cap_ok(rt, GVM_CAP_PHYSMEM)) { *raw_return = 0; return m3Err_none; } + + uint32_t mem_sz = 0; + m3_GetMemory(rt, &mem_sz, 0); + if ((uint64_t)guest_off + len > (uint64_t)mem_sz || len == 0 || len > 4096) { + *raw_return = 0; return m3Err_none; + } + + PHYSICAL_ADDRESS pha; pha.QuadPart = (LONGLONG)pa; + PVOID kva = MmMapIoSpaceEx(pha, len, PAGE_READONLY); + if (!kva) { *raw_return = 0; return m3Err_none; } + + uint32_t copied = 0; + __try { + RtlCopyMemory((unsigned char*)mem + guest_off, kva, len); + copied = len; + } __except (EXCEPTION_EXECUTE_HANDLER) { copied = 0; } + + MmUnmapIoSpace(kva, len); + *raw_return = copied; + return m3Err_none; +} + +// host_phys_write(pa, guest_off, len) -> i32 (bytes written, 0 on fail) +static const void* +host_phys_write(IM3Runtime rt, IM3ImportContext ctx, uint64_t* sp, void* mem) +{ + UNREFERENCED_PARAMETER(ctx); + uint32_t* raw_return = (uint32_t*)sp; sp++; + uint64_t pa = *(sp + 0); + uint32_t guest_off = (uint32_t)*(sp + 1); + uint32_t len = (uint32_t)*(sp + 2); + + if (!gvm_cap_ok(rt, GVM_CAP_PHYSMEM)) { *raw_return = 0; return m3Err_none; } + + uint32_t mem_sz = 0; + m3_GetMemory(rt, &mem_sz, 0); + if ((uint64_t)guest_off + len > (uint64_t)mem_sz || len == 0 || len > 4096) { + *raw_return = 0; return m3Err_none; + } + + PHYSICAL_ADDRESS pha; pha.QuadPart = (LONGLONG)pa; + PVOID kva = MmMapIoSpaceEx(pha, len, PAGE_READWRITE); + if (!kva) { *raw_return = 0; return m3Err_none; } + + uint32_t written = 0; + __try { + RtlCopyMemory(kva, (unsigned char*)mem + guest_off, len); + written = len; + } __except (EXCEPTION_EXECUTE_HANDLER) { written = 0; } + + MmUnmapIoSpace(kva, len); + *raw_return = written; + return m3Err_none; +} + +// host_readmsr(idx) -> i64 (0 on #GP) +static const void* +host_readmsr(IM3Runtime rt, IM3ImportContext ctx, uint64_t* sp, void* mem) +{ + UNREFERENCED_PARAMETER(ctx); UNREFERENCED_PARAMETER(mem); + uint64_t* raw_return = sp++; + uint32_t idx = (uint32_t)*sp; + uint64_t v = 0; + if (!gvm_cap_ok(rt, GVM_CAP_MSR_READ)) { *raw_return = 0; return m3Err_none; } + __try { v = __readmsr(idx); } + __except (EXCEPTION_EXECUTE_HANDLER) { v = 0; } + *raw_return = v; + return m3Err_none; +} + +// host_rdtsc() -> i64 +static const void* +host_rdtsc(IM3Runtime rt, IM3ImportContext ctx, uint64_t* sp, void* mem) +{ + UNREFERENCED_PARAMETER(ctx); UNREFERENCED_PARAMETER(mem); + uint64_t* raw_return = sp; + *raw_return = gvm_cap_ok(rt, GVM_CAP_CPUID_TSC) ? __rdtsc() : 0; + return m3Err_none; +} + +// host_notify_enable(kind) -> i32 +// kind: 0 = process (create+exit), 1 = image load +// returns 0 on success, negative on error +static const void* +host_notify_enable(IM3Runtime rt, IM3ImportContext ctx, uint64_t* sp, void* mem) +{ + UNREFERENCED_PARAMETER(ctx); UNREFERENCED_PARAMETER(mem); + uint32_t* raw_return = (uint32_t*)sp; sp++; + uint32_t kind = (uint32_t)*sp; + + if (!gvm_cap_ok(rt, GVM_CAP_CALLBACKS)) { *raw_return = (uint32_t)-1; return m3Err_none; } + gvm_notify_init(); + + NTSTATUS s = STATUS_INVALID_PARAMETER; + switch (kind) { + case 0: + if (!g_process_notify_on) { + s = PsSetCreateProcessNotifyRoutineEx(gvm_process_notify_ex, FALSE); + if (NT_SUCCESS(s)) g_process_notify_on = TRUE; + } else { + s = STATUS_SUCCESS; + } + break; + case 1: + if (!g_image_notify_on) { + s = PsSetLoadImageNotifyRoutine(gvm_image_notify); + if (NT_SUCCESS(s)) g_image_notify_on = TRUE; + } else { + s = STATUS_SUCCESS; + } + break; + } + *raw_return = NT_SUCCESS(s) ? 0 : (uint32_t)-1; + return m3Err_none; +} + +// host_dispatch_start() -> i32 +// starts the driver-side worker that invokes on_process_create/exit/on_image_load +// exports of every loaded module as events fire. call once, no unregistering needed. +static const void* +host_dispatch_start(IM3Runtime rt, IM3ImportContext ctx, uint64_t* sp, void* mem) +{ + UNREFERENCED_PARAMETER(ctx); UNREFERENCED_PARAMETER(mem); + uint32_t* raw_return = (uint32_t*)sp; + if (!gvm_cap_ok(rt, GVM_CAP_CALLBACKS)) { *raw_return = (uint32_t)-1; return m3Err_none; } + gvm_dispatch_start(); + *raw_return = 0; + return m3Err_none; +} + +// host_dispatch_stop() -> i32 +// host_ih_trampoline() -> i64 +// returns the kernel VA of the driver's native ETW-hook trampoline. guest +// writes this into WMI_LOGGER_CONTEXT.GetCpuClock via gvm_write_u64 after +// resolving the slot itself. +static const void* +host_ih_trampoline(IM3Runtime rt, IM3ImportContext ctx, uint64_t* sp, void* mem) +{ + UNREFERENCED_PARAMETER(ctx); UNREFERENCED_PARAMETER(mem); + if (!gvm_cap_ok(rt, GVM_CAP_WRITE_KMEM | GVM_CAP_CALLBACKS)) { *sp = 0; return m3Err_none; } + gvm_dispatch_start(); + *sp = gvm_ih_trampoline_addr(); + return m3Err_none; +} + +// host_ih_configure(rate, nt_base, nt_size) -> void +// tells the trampoline: sample 1-in-rate calls, only push events when the +// return address falls inside [nt_base, nt_base+nt_size). +static const void* +host_ih_configure(IM3Runtime rt, IM3ImportContext ctx, uint64_t* sp, void* mem) +{ + UNREFERENCED_PARAMETER(rt); UNREFERENCED_PARAMETER(ctx); UNREFERENCED_PARAMETER(mem); + uint32_t rate = (uint32_t)*(sp + 0); + uint64_t nt_base = *(sp + 1); + uint32_t nt_size = (uint32_t)*(sp + 2); + gvm_ih_configure(rate, nt_base, nt_size); + return m3Err_none; +} + +// host_ih_quiesce() -> i64 (returns total trampoline hit count) +// spins until in-flight trampoline calls drain. call after guest restored +// the original GetCpuClock pointer, before it unloads. +static const void* +host_ih_quiesce(IM3Runtime rt, IM3ImportContext ctx, uint64_t* sp, void* mem) +{ + UNREFERENCED_PARAMETER(rt); UNREFERENCED_PARAMETER(ctx); UNREFERENCED_PARAMETER(mem); + gvm_ih_wait_quiescent(); + *sp = gvm_ih_hit_count(); + return m3Err_none; +} + +static const void* +host_dispatch_stop(IM3Runtime rt, IM3ImportContext ctx, uint64_t* sp, void* mem) +{ + UNREFERENCED_PARAMETER(rt); UNREFERENCED_PARAMETER(ctx); UNREFERENCED_PARAMETER(mem); + uint32_t* raw_return = (uint32_t*)sp; + gvm_dispatch_stop_signal(); + *raw_return = 0; + return m3Err_none; +} + +// host_notify_poll(out_off, out_len) -> i32 +// copies one gvm_event into guest memory. returns bytes written or 0 if empty. +static const void* +host_notify_poll(IM3Runtime rt, IM3ImportContext ctx, uint64_t* sp, void* mem) +{ + UNREFERENCED_PARAMETER(ctx); + uint32_t* raw_return = (uint32_t*)sp; sp++; + uint32_t out_off = (uint32_t)*(sp + 0); + uint32_t out_len = (uint32_t)*(sp + 1); + + uint32_t mem_sz = 0; + m3_GetMemory(rt, &mem_sz, 0); + if ((uint64_t)out_off + sizeof(gvm_event) > (uint64_t)mem_sz || out_len < sizeof(gvm_event)) { + *raw_return = 0; + return m3Err_none; + } + + gvm_event e; + if (!gvm_pop_event(&e)) { + *raw_return = 0; + return m3Err_none; + } + + RtlCopyMemory((unsigned char*)mem + out_off, &e, sizeof(e)); + *raw_return = (uint32_t)sizeof(gvm_event); + return m3Err_none; +} + +// generic dispatcher: resolve `name` via MmGetSystemRoutineAddress, call via win64 ABI +static const void* +host_call(IM3Runtime rt, IM3ImportContext ctx, uint64_t* sp, void* mem) +{ + UNREFERENCED_PARAMETER(ctx); + uint64_t* raw_return = sp++; + + if (!gvm_cap_ok(rt, GVM_CAP_HOSTCALL)) { *raw_return = 0; return m3Err_none; } + if (gvm_deadline_exceeded(gvm_modtab_owner_of_runtime(rt))) { *raw_return = 0; return m3Err_none; } + + uint32_t name_off = (uint32_t)*(sp + 0); + uint32_t name_len = (uint32_t)*(sp + 1); + uint32_t argc = (uint32_t)*(sp + 2); + uint64_t a0 = *(sp + 3), a1 = *(sp + 4), a2 = *(sp + 5), a3 = *(sp + 6); + uint64_t a4 = *(sp + 7), a5 = *(sp + 8), a6 = *(sp + 9), a7 = *(sp + 10); + + *raw_return = 0; + + uint32_t mem_sz = 0; + m3_GetMemory(rt, &mem_sz, 0); + if ((uint64_t)name_off + name_len > (uint64_t)mem_sz || name_len == 0 || name_len > 128) + return m3Err_none; + + WCHAR wname[130]; + unsigned char* src = (unsigned char*)mem + name_off; + for (uint32_t i = 0; i < name_len; i++) wname[i] = (WCHAR)src[i]; + wname[name_len] = 0; + + UNICODE_STRING us; + us.Buffer = wname; + us.Length = (USHORT)(name_len * sizeof(WCHAR)); + us.MaximumLength = (USHORT)((name_len + 1) * sizeof(WCHAR)); + + PVOID target = MmGetSystemRoutineAddress(&us); + if (!target) + return m3Err_none; + + typedef uint64_t (*fn0)(void); + typedef uint64_t (*fn1)(uint64_t); + typedef uint64_t (*fn2)(uint64_t, uint64_t); + typedef uint64_t (*fn3)(uint64_t, uint64_t, uint64_t); + typedef uint64_t (*fn4)(uint64_t, uint64_t, uint64_t, uint64_t); + typedef uint64_t (*fn5)(uint64_t, uint64_t, uint64_t, uint64_t, uint64_t); + typedef uint64_t (*fn6)(uint64_t, uint64_t, uint64_t, uint64_t, uint64_t, uint64_t); + typedef uint64_t (*fn7)(uint64_t, uint64_t, uint64_t, uint64_t, uint64_t, uint64_t, uint64_t); + typedef uint64_t (*fn8)(uint64_t, uint64_t, uint64_t, uint64_t, uint64_t, uint64_t, uint64_t, uint64_t); + + uint64_t rv = 0; + __try { + switch (argc) { + case 0: rv = ((fn0)target)(); break; + case 1: rv = ((fn1)target)(a0); break; + case 2: rv = ((fn2)target)(a0, a1); break; + case 3: rv = ((fn3)target)(a0, a1, a2); break; + case 4: rv = ((fn4)target)(a0, a1, a2, a3); break; + case 5: rv = ((fn5)target)(a0, a1, a2, a3, a4); break; + case 6: rv = ((fn6)target)(a0, a1, a2, a3, a4, a5); break; + case 7: rv = ((fn7)target)(a0, a1, a2, a3, a4, a5, a6); break; + case 8: rv = ((fn8)target)(a0, a1, a2, a3, a4, a5, a6, a7); break; + default: rv = 0; break; + } + } __except (EXCEPTION_EXECUTE_HANDLER) { + rv = 0; + } + *raw_return = rv; + return m3Err_none; +} + +// per-import trace wrapper. ret_slot = 1 for functions that return a value +// (sp[0] is the return slot, args start at sp[1]), ret_slot = 0 for void +// functions (args start at sp[0]). +#define WRAP_TR(name, ret_slot, argc_hint) \ + static const void* name##_tr(IM3Runtime rt, IM3ImportContext ctx, uint64_t* sp, void* mem) { \ + gvm_module* mo = gvm_modtab_owner_of_runtime(rt); \ + unsigned int mid = mo ? mo->id : 0; \ + if (!gvm_trace_enabled_for(mid)) return name(rt, ctx, sp, mem); \ + uint64_t saved[4] = {0,0,0,0}; \ + int cap = (argc_hint) < 4 ? (argc_hint) : 4; \ + for (int i = 0; i < cap; i++) saved[i] = sp[(ret_slot) + i]; \ + const void* r = name(rt, ctx, sp, mem); \ + unsigned long long targv[4] = {saved[0], saved[1], saved[2], saved[3]}; \ + unsigned long long rv = (ret_slot) ? sp[0] : 0; \ + gvm_trace_push(mid, GVM_TRK_IMPORT, #name, cap, targv, rv); \ + return r; \ + } + +WRAP_TR(host_dbg_print, 0, 2) +WRAP_TR(host_alloc, 1, 1) +WRAP_TR(host_free, 0, 1) +WRAP_TR(host_read_u8, 1, 1) +WRAP_TR(host_read_u32, 1, 1) +WRAP_TR(host_read_u64, 1, 1) +WRAP_TR(host_write_u64, 0, 2) +WRAP_TR(host_read_bytes, 1, 3) +WRAP_TR(host_write_bytes, 1, 3) +WRAP_TR(host_current_irql, 1, 0) +WRAP_TR(host_process_id, 1, 0) +WRAP_TR(host_thread_id, 1, 0) +WRAP_TR(host_current_process, 1, 0) +WRAP_TR(host_cpuid, 0, 3) +WRAP_TR(host_readmsr, 1, 1) +WRAP_TR(host_writemsr, 1, 2) +WRAP_TR(host_rdtsc, 1, 0) +WRAP_TR(host_phys_read, 1, 3) +WRAP_TR(host_phys_write, 1, 3) +WRAP_TR(host_notify_enable, 1, 1) +WRAP_TR(host_notify_poll, 1, 2) +WRAP_TR(host_dispatch_start, 1, 0) +WRAP_TR(host_dispatch_stop, 1, 0) +WRAP_TR(host_ih_trampoline, 1, 0) +WRAP_TR(host_ih_configure, 0, 3) +WRAP_TR(host_ih_quiesce, 1, 0) +WRAP_TR(host_call, 1, 4) +WRAP_TR(host_mem_base, 1, 0) +WRAP_TR(host_mem_size, 1, 0) +WRAP_TR(host_make_unistr, 1, 2) +WRAP_TR(host_free_unistr, 0, 1) + +M3Result +gvm_link_host_imports(IM3Module module) +{ + M3Result r; + + #define LINK(name, sig, fn) do { \ + r = m3_LinkRawFunction(module, "env", (name), (sig), (fn)); \ + if (r && r != m3Err_functionLookupFailed) { \ + DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, \ + "[goodmans] link err: env.%s (%s) -> %s\n", (name), (sig), r); \ + return r; \ + } \ + } while (0) + + LINK("host_dbg_print", "v(ii)", &host_dbg_print_tr); + LINK("host_alloc", "I(i)", &host_alloc_tr); + LINK("host_free", "v(I)", &host_free_tr); + LINK("host_read_u8", "i(I)", &host_read_u8_tr); + LINK("host_read_u32", "i(I)", &host_read_u32_tr); + LINK("host_read_u64", "I(I)", &host_read_u64_tr); + LINK("host_write_u64", "v(II)", &host_write_u64_tr); + LINK("host_read_bytes", "i(Iii)", &host_read_bytes_tr); + LINK("host_write_bytes", "i(Iii)", &host_write_bytes_tr); + LINK("host_current_irql", "i()", &host_current_irql_tr); + LINK("host_process_id", "i()", &host_process_id_tr); + LINK("host_thread_id", "i()", &host_thread_id_tr); + LINK("host_current_process", "I()", &host_current_process_tr); + LINK("host_cpuid", "v(iii)", &host_cpuid_tr); + LINK("host_readmsr", "I(i)", &host_readmsr_tr); + LINK("host_writemsr", "i(iI)", &host_writemsr_tr); + LINK("host_rdtsc", "I()", &host_rdtsc_tr); + LINK("host_phys_read", "i(Iii)", &host_phys_read_tr); + LINK("host_phys_write", "i(Iii)", &host_phys_write_tr); + LINK("host_notify_enable", "i(i)", &host_notify_enable_tr); + LINK("host_notify_poll", "i(ii)", &host_notify_poll_tr); + LINK("host_dispatch_start", "i()", &host_dispatch_start_tr); + LINK("host_dispatch_stop", "i()", &host_dispatch_stop_tr); + LINK("host_ih_trampoline", "I()", &host_ih_trampoline_tr); + LINK("host_ih_configure", "v(iIi)", &host_ih_configure_tr); + LINK("host_ih_quiesce", "I()", &host_ih_quiesce_tr); + LINK("host_call", "I(iiiIIIIIIII)", &host_call_tr); + LINK("host_mem_base", "I()", &host_mem_base_tr); + LINK("host_mem_size", "i()", &host_mem_size_tr); + LINK("host_make_unistr", "I(ii)", &host_make_unistr_tr); + LINK("host_free_unistr", "v(I)", &host_free_unistr_tr); + + #undef LINK + return m3Err_none; +} + +// generic FFI trampoline. dispatches to the kernel export stashed in +// ctx->userdata under the Windows x64 int-arg calling convention. up to 8 +// integer/pointer args. floats are not supported in kernel mode without +// KeSaveFloatingPointState fencing, and no kernel export we care about +// takes floats anyway. return value writes back to sp[0]. +typedef uint64_t (*gvm_fn_i8)(uint64_t, uint64_t, uint64_t, uint64_t, + uint64_t, uint64_t, uint64_t, uint64_t); + +static const void* +gvm_ffi_kernel(IM3Runtime rt, IM3ImportContext ctx, uint64_t* sp, void* mem) +{ + UNREFERENCED_PARAMETER(mem); + gvm_fn_i8 fn = (gvm_fn_i8)ctx->userdata; + IM3Function f = ctx->function; + uint16_t numArgs = f->funcType ? f->funcType->numArgs : 0; + uint16_t numRets = f->funcType ? f->funcType->numRets : 0; + uint16_t sp_off = numRets ? 1 : 0; + if (numArgs > 8) numArgs = 8; + + uint64_t a[8] = {0}; + for (uint16_t i = 0; i < numArgs; i++) a[i] = sp[sp_off + i]; + + const char* fname = f->import.fieldUtf8 ? f->import.fieldUtf8 : "?"; + DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, + "[goodmans] ffi-> %s fn=%p nargs=%u a0=%llx a1=%llx a2=%llx a3=%llx a4=%llx\n", + fname, fn, numArgs, a[0], a[1], a[2], a[3], a[4]); + + uint64_t rv = 0; + __try { + rv = fn(a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7]); + } __except (EXCEPTION_EXECUTE_HANDLER) { + DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, + "[goodmans] ffi-EXC %s code=%08x\n", fname, GetExceptionCode()); + rv = 0; + } + + DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, + "[goodmans] ffi<- %s rv=%llx\n", fname, rv); + + if (numRets) sp[0] = rv; + + gvm_module* mo = gvm_modtab_owner_of_runtime(rt); + if (mo && gvm_trace_enabled_for(mo->id)) { + const char* name = (f->import.fieldUtf8) ? f->import.fieldUtf8 : ""; + gvm_trace_push(mo->id, GVM_TRK_IMPORT, name, numArgs, a, numRets ? rv : 0); + } + return m3Err_none; +} + +// build a wasm3 raw-link signature string from the function type, +// e.g. numArgs=2 (i64, i32) numRets=1 (i32) -> "i(Ii)" +static void +gvm_wasm_sig(char* out, size_t out_sz, IM3FuncType t) +{ + if (out_sz < 4) { if (out_sz) out[0] = 0; return; } + size_t p = 0; + char c = 'v'; + if (t && t->numRets > 0) { + switch (t->types[0]) { + case c_m3Type_i32: c = 'i'; break; + case c_m3Type_i64: c = 'I'; break; + case c_m3Type_f32: c = 'f'; break; + case c_m3Type_f64: c = 'F'; break; + default: c = 'i'; break; + } + } + out[p++] = c; + out[p++] = '('; + uint16_t nargs = t ? t->numArgs : 0; + // FuncType.types layout is [rets...][args...] per m3_function.h + for (uint16_t i = 0; i < nargs && p + 2 < out_sz; i++) { + u8 at = t->types[t->numRets + i]; + char ac; + switch (at) { + case c_m3Type_i32: ac = 'i'; break; + case c_m3Type_i64: ac = 'I'; break; + case c_m3Type_f32: ac = 'f'; break; + case c_m3Type_f64: ac = 'F'; break; + default: ac = 'i'; break; + } + out[p++] = ac; + } + if (p + 1 < out_sz) out[p++] = ')'; + out[p] = 0; +} + +// LDR_DATA_TABLE_ENTRY-ish subset. only fields we need. must not be paged. +typedef struct _GVM_KLDR_ENTRY { + LIST_ENTRY InLoadOrderLinks; + PVOID Rsv1[3]; + PVOID DllBase; + PVOID EntryPoint; + ULONG SizeOfImage; + UNICODE_STRING FullDllName; + UNICODE_STRING BaseDllName; +} GVM_KLDR_ENTRY; + +extern LIST_ENTRY PsLoadedModuleList; + +// walk PsLoadedModuleList, find a loaded driver by base name (case-insensitive, +// with or without .sys), return its ImageBase. NULL if not present. +static PVOID +gvm_find_module_base(const char* mod_name) +{ + if (!mod_name || !mod_name[0]) return NULL; + + ANSI_STRING as; UNICODE_STRING want; NTSTATUS st; + RtlInitAnsiString(&as, mod_name); + if (!NT_SUCCESS(RtlAnsiStringToUnicodeString(&want, &as, TRUE))) return NULL; + + PVOID found = NULL; + for (PLIST_ENTRY e = PsLoadedModuleList.Flink; + e && e != &PsLoadedModuleList; + e = e->Flink) + { + GVM_KLDR_ENTRY* le = CONTAINING_RECORD(e, GVM_KLDR_ENTRY, InLoadOrderLinks); + if (!le->BaseDllName.Buffer) continue; + // try full match then match-without-.sys + if (RtlEqualUnicodeString(&le->BaseDllName, &want, TRUE)) { + found = le->DllBase; break; + } + // strip .sys off le->BaseDllName if present + UNICODE_STRING trimmed = le->BaseDllName; + if (trimmed.Length >= 8) { + WCHAR* end = (WCHAR*)((unsigned char*)trimmed.Buffer + trimmed.Length - 8); + if (_wcsnicmp(end, L".sys", 4) == 0) trimmed.Length -= 8; + } + if (RtlEqualUnicodeString(&trimmed, &want, TRUE)) { + found = le->DllBase; break; + } + } + RtlFreeUnicodeString(&want); + return found; +} + +// resolve a named export from a loaded module by walking its PE export +// directory. avoids needing the driver's PDB. +static PVOID +gvm_find_export(PVOID module_base, const char* export_name) +{ + if (!module_base || !export_name) return NULL; + unsigned char* base = (unsigned char*)module_base; + + IMAGE_DOS_HEADER* dos = (IMAGE_DOS_HEADER*)base; + if (dos->e_magic != IMAGE_DOS_SIGNATURE) return NULL; + IMAGE_NT_HEADERS64* nt = (IMAGE_NT_HEADERS64*)(base + dos->e_lfanew); + if (nt->Signature != IMAGE_NT_SIGNATURE) return NULL; + + IMAGE_DATA_DIRECTORY* dd = + &nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]; + if (dd->Size == 0 || dd->VirtualAddress == 0) return NULL; + + IMAGE_EXPORT_DIRECTORY* ex = + (IMAGE_EXPORT_DIRECTORY*)(base + dd->VirtualAddress); + ULONG* names = (ULONG*)(base + ex->AddressOfNames); + USHORT* ords = (USHORT*)(base + ex->AddressOfNameOrdinals); + ULONG* funcs = (ULONG*)(base + ex->AddressOfFunctions); + + for (ULONG i = 0; i < ex->NumberOfNames; i++) { + const char* n = (const char*)(base + names[i]); + if (strcmp(n, export_name) == 0) { + ULONG rva = funcs[ords[i]]; + // forwarder if RVA points inside the export dir + if (rva >= dd->VirtualAddress && rva < dd->VirtualAddress + dd->Size) + return NULL; // forwarder resolution skipped + return base + rva; + } + } + return NULL; +} + +// walk the module's import table and auto-link anything that resolves via +// MmGetSystemRoutineAddress (nt/hal) OR by walking PsLoadedModuleList for +// exports of any other loaded driver. import naming: +// env.SymbolName -> nt/hal +// drv$modulename.SymbolName -> that specific loaded driver +M3Result +gvm_link_kernel_fallback(IM3Module module) +{ + if (!module) return m3Err_none; + for (u32 i = 0; i < module->numFuncImports; i++) { + IM3Function f = &module->functions[i]; + if (f->compiled) continue; + if (!f->import.moduleUtf8 || !f->import.fieldUtf8) continue; + + const char* mod = f->import.moduleUtf8; + const char* fname = f->import.fieldUtf8; + PVOID kfn = NULL; + + if (strcmp(mod, "env") == 0) { + UNICODE_STRING us; ANSI_STRING as; + RtlInitAnsiString(&as, fname); + if (NT_SUCCESS(RtlAnsiStringToUnicodeString(&us, &as, TRUE))) { + kfn = MmGetSystemRoutineAddress(&us); + RtlFreeUnicodeString(&us); + } + } else if (strncmp(mod, "drv$", 4) == 0) { + PVOID base = gvm_find_module_base(mod + 4); + if (base) kfn = gvm_find_export(base, fname); + } + + if (!kfn) continue; + + char sig[40]; + gvm_wasm_sig(sig, sizeof(sig), f->funcType); + M3Result r = m3_LinkRawFunctionEx(module, mod, fname, sig, + &gvm_ffi_kernel, kfn); + if (r && r != m3Err_functionLookupFailed) { + DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, + "[goodmans] ffi link err: %s.%s (%s) -> %s\n", mod, fname, sig, r); + } else if (!r) { + gvm_log("ffi resolved %s.%s (%s) -> %p", mod, fname, sig, kfn); + } + } + return m3Err_none; +} diff --git a/driver/ih.c b/driver/ih.c new file mode 100644 index 0000000..87d250c --- /dev/null +++ b/driver/ih.c @@ -0,0 +1,94 @@ +/* ih.c - the ONE piece of InfinityHook that cannot live in wasm. + * + * this file exposes a native trampoline that guests install into + * WMI_LOGGER_CONTEXT.GetCpuClock. everything else (nt base discovery, + * EtwpDebuggerData pattern scan, offset resolution, atomic pointer swap) + * lives in sample_guest/infinity_hook.c as a portable wasm demo. + * + * the trampoline runs at ETW callback IRQL (up to DISPATCH_LEVEL), samples + * calls, pushes a SYSCALL event onto the shared dispatch ring, and returns + * the real QPC. the ring is drained by the same dispatch worker that + * handles process/image notify events. + */ + +#include "inc/gvm.h" +#include "../shared/goodmans_ioctl.h" + +extern void gvm_push_event_generic(unsigned int kind, uint32_t pid, + uint64_t a, uint64_t b, uint32_t c); + +static volatile LONG64 g_ih_hits = 0; +static volatile LONG g_ih_inflight = 0; +static volatile LONG g_ih_rate = 1000; + +// caller (guest) sets this before installing the hook so the trampoline +// can reject callers outside nt's image range (avoids false positives from +// unrelated ETW paths). +static volatile UINT64 g_ih_nt_lo = 0; +static volatile UINT64 g_ih_nt_hi = 0; + +// signature matches WMI_LOGGER_CONTEXT.GetCpuClock: takes no args, returns +// a QPC value. must be __stdcall/default x64 ABI (matches). +static UINT64 gvm_ih_trampoline(void) +{ + InterlockedIncrement(&g_ih_inflight); + LONG64 hits = InterlockedIncrement64(&g_ih_hits); + LONG rate = g_ih_rate; + if (rate <= 0) rate = 1; + + if ((hits % rate) == 0) { + PVOID retaddr = _ReturnAddress(); + UINT64 lo = g_ih_nt_lo, hi = g_ih_nt_hi; + BOOLEAN in_nt = (lo && (UINT64)(uintptr_t)retaddr >= lo && + (UINT64)(uintptr_t)retaddr < hi); + if (in_nt || !lo) { + HANDLE tid = PsGetCurrentThreadId(); + gvm_push_event_generic(3, // SYSCALL kind + (uint32_t)(uintptr_t)tid, + (UINT64)(uintptr_t)retaddr, // a1 in on_syscall + 0, + (UINT32)hits); // a2 in on_syscall + } + } + + LARGE_INTEGER qpc = KeQueryPerformanceCounter(NULL); + UINT64 rv = (UINT64)qpc.QuadPart; + InterlockedDecrement(&g_ih_inflight); + return rv; +} + +// exposed as host imports: guests get the trampoline VA to plug into the +// GetCpuClock slot they located, and configure sampling / nt-range so the +// trampoline can skip non-syscall callers. + +UINT64 gvm_ih_trampoline_addr(void) +{ + return (UINT64)(uintptr_t)&gvm_ih_trampoline; +} + +void gvm_ih_configure(UINT32 rate, UINT64 nt_base, UINT32 nt_size) +{ + if (rate > 0) InterlockedExchange(&g_ih_rate, (LONG)rate); + g_ih_nt_lo = nt_base; + g_ih_nt_hi = nt_base + nt_size; +} + +// spin until any in-flight trampoline calls drain. call after guest writes +// the original pointer back to the GetCpuClock slot but before it unloads. +void gvm_ih_wait_quiescent(void) +{ + for (int i = 0; i < 1000 && g_ih_inflight > 0; i++) { + LARGE_INTEGER li; li.QuadPart = -10000; // 1ms + KeDelayExecutionThread(KernelMode, FALSE, &li); + } +} + +// stats (guest asks for hit count so it can report accurately) +UINT64 gvm_ih_hit_count(void) { return (UINT64)g_ih_hits; } + +// driver unload safety: nothing to tear down since we don't own the slot, +// but we can spin briefly in case guests left the hook installed. +void gvm_ih_teardown(void) +{ + gvm_ih_wait_quiescent(); +} diff --git a/driver/inc/gvm.h b/driver/inc/gvm.h new file mode 100644 index 0000000..1d36b92 --- /dev/null +++ b/driver/inc/gvm.h @@ -0,0 +1,103 @@ +/* gvm.h - internal driver-wide declarations */ +#pragma once + +#include +#include "kshim/kshim.h" +#include "wasm3/wasm3.h" + +#define GVM_TAG_MOD 'doMG' +#define GVM_TAG_WBUF 'BWMG' + +#define GVM_STACK_DEFAULT (64u * 1024u) + +typedef struct _gvm_module { + unsigned int id; + BOOLEAN used; + IM3Environment env; + IM3Runtime runtime; + IM3Module module; + unsigned char* wasm_bytes; + unsigned int wasm_size; + unsigned long long hash; + volatile LONG refcount; // touch only with Interlocked* + KMUTEX call_mutex; // held around all m3_* activity on this module + // pool budget for host_alloc/host_free + volatile LONG64 pool_used; + LONG64 pool_budget; + // capability bitmask parsed from the guest's __gvm_caps export. + // absent export defaults to GVM_CAP_ALL for backward compat. + unsigned int caps; + // cooperative abort deadline in QPC ticks. 0 = no limit + volatile ULONG64 exec_deadline_qpc; + // QPC tick at which call_mutex was acquired. watchdog polls this to + // find guests stuck past GVM_WATCHDOG_MAX_MS. 0 = not held + volatile ULONG64 mutex_hold_qpc; + // watchdog sets this to reject further calls into a wedged guest. + // subsequent force-unload skips waiting on call_mutex + volatile LONG poisoned; + char name[64]; +} gvm_module; + +// ioctl_handler.c +unsigned int gvm_read_module_caps(gvm_module* mod); +BOOLEAN gvm_deadline_exceeded(gvm_module* mod); + +// wasm_call.c +M3Result gvm_call_locked(gvm_module* mod, IM3Function fn, unsigned int argc, const void** argp); +void gvm_set_deadline_ms(gvm_module* mod, unsigned int timeout_ms); + +NTSTATUS gvm_modtab_init(void); +void gvm_modtab_teardown(void); +gvm_module* gvm_modtab_alloc(void); +gvm_module* gvm_modtab_get(unsigned int id); +gvm_module* gvm_modtab_find_by_hash(unsigned long long hash); +gvm_module* gvm_modtab_find_by_hash_incref(unsigned long long hash, unsigned int expected_size); +gvm_module* gvm_modtab_iter(unsigned int idx); // returns null past end +gvm_module* gvm_modtab_owner_of_runtime(IM3Runtime rt); +void gvm_modtab_free(gvm_module* m); + +M3Result gvm_link_host_imports(IM3Module module); +M3Result gvm_link_kernel_fallback(IM3Module module); +void gvm_notify_init(void); +void gvm_notify_teardown(void); + +// callback dispatch (host_imports.c) +void gvm_dispatch_start(void); +void gvm_dispatch_stop_signal(void); // just sets shutdown, returns +void gvm_dispatch_stop_wait(void); // signal + wait for worker exit (unload only) + +NTSTATUS gvm_ioctl_load(PIRP irp, PIO_STACK_LOCATION sp); +NTSTATUS gvm_ioctl_call(PIRP irp, PIO_STACK_LOCATION sp); +NTSTATUS gvm_ioctl_unload(PIRP irp, PIO_STACK_LOCATION sp); +NTSTATUS gvm_ioctl_list(PIRP irp, PIO_STACK_LOCATION sp); +NTSTATUS gvm_ioctl_info(PIRP irp, PIO_STACK_LOCATION sp); +NTSTATUS gvm_ioctl_unload_all(PIRP irp, PIO_STACK_LOCATION sp); +NTSTATUS gvm_ioctl_tail_log(PIRP irp, PIO_STACK_LOCATION sp); +NTSTATUS gvm_ioctl_read_guest(PIRP irp, PIO_STACK_LOCATION sp); +NTSTATUS gvm_ioctl_tail_trace(PIRP irp, PIO_STACK_LOCATION sp); +NTSTATUS gvm_ioctl_trace_ctl(PIRP irp, PIO_STACK_LOCATION sp); +NTSTATUS gvm_ioctl_force_unload(PIRP irp, PIO_STACK_LOCATION sp); +NTSTATUS gvm_ioctl_notify_stop(PIRP irp, PIO_STACK_LOCATION sp); + +// watchdog thread (watchdog.c) +void gvm_watchdog_start(void); +void gvm_watchdog_stop(void); + +// infinity-hook (ih.c) - only the trampoline. install/uninstall happens in +// wasm via kernel FFI + gvm_write_u64. +unsigned long long gvm_ih_trampoline_addr(void); +void gvm_ih_configure(unsigned int rate, unsigned long long nt_base, unsigned int nt_size); +void gvm_ih_wait_quiescent(void); +unsigned long long gvm_ih_hit_count(void); +void gvm_ih_teardown(void); + +// trace ring (trace_ring.c) +void gvm_trace_init(void); +void gvm_trace_push(unsigned int module_id, unsigned int kind, const char* name, + unsigned int argc, const unsigned long long* argv, unsigned long long rv); +BOOLEAN gvm_trace_enabled_for(unsigned int module_id); + +// log ring (log_ring.c) +void gvm_log_init(void); +void gvm_log_push(const char* fmt, ...); +#define gvm_log(fmt, ...) gvm_log_push(fmt, ##__VA_ARGS__) diff --git a/driver/ioctl_handler.c b/driver/ioctl_handler.c new file mode 100644 index 0000000..8431aca --- /dev/null +++ b/driver/ioctl_handler.c @@ -0,0 +1,457 @@ +/* ioctl_handler.c - LOAD / CALL / UNLOAD / LIST / INFO / UNLOAD_ALL */ +#include "inc/gvm.h" +#include "../shared/goodmans_ioctl.h" +#include "wasm3/m3_env.h" +#include "wasm3/m3_function.h" + +static void +copy_errmsg(char* dst, size_t dst_sz, const char* src) +{ + if (!dst || dst_sz == 0) return; + dst[0] = 0; + if (src) RtlStringCbCopyA(dst, dst_sz, src); +} + +// FNV-1a 64-bit hash for wasm-blob dedup +static unsigned long long fnv1a64(const unsigned char* p, size_t n) +{ + unsigned long long h = 0xcbf29ce484222325ULL; + for (size_t i = 0; i < n; i++) { + h ^= p[i]; + h *= 0x100000001b3ULL; + } + return h; +} + +// reads the module's capability bitmask by invoking its exported __gvm_caps +// function. must run AFTER link so the guest can use hosts inside its manifest +// function if it needs to (typical case: returns a constant). if the export +// is absent, returns GVM_CAP_ALL (open policy, backward compat). +unsigned int gvm_read_module_caps(gvm_module* mod) +{ + if (!mod || !mod->runtime) return GVM_CAP_ALL; + + IM3Function fn = NULL; + M3Result r = m3_FindFunction(&fn, mod->runtime, "__gvm_caps"); + if (r || !fn) return GVM_CAP_ALL; + + // must be called under the mutex + big stack, but at this point no one + // else has a handle on this module yet. still, use the standard path. + // start with wide-open caps so the manifest call itself isn't gated. + mod->caps = GVM_CAP_ALL; + r = gvm_call_locked(mod, fn, 0, NULL); + if (r) return GVM_CAP_ALL; + + unsigned int caps = 0; + void* rvp = ∩︀ + m3_GetResults(fn, 1, (const void**)&rvp); + return caps ? caps : GVM_CAP_ALL; +} + +BOOLEAN gvm_deadline_exceeded(gvm_module* mod) +{ + if (!mod || mod->exec_deadline_qpc == 0) return FALSE; + LARGE_INTEGER now = KeQueryPerformanceCounter(NULL); + return (ULONG64)now.QuadPart >= mod->exec_deadline_qpc; +} + +NTSTATUS +gvm_ioctl_load(PIRP irp, PIO_STACK_LOCATION sp) +{ + ULONG in_len = sp->Parameters.DeviceIoControl.InputBufferLength; + ULONG out_len = sp->Parameters.DeviceIoControl.OutputBufferLength; + void* buf = irp->AssociatedIrp.SystemBuffer; + + if (in_len < sizeof(gvm_load_in) || out_len < sizeof(gvm_load_out) || !buf) { + irp->IoStatus.Information = 0; + return STATUS_INVALID_PARAMETER; + } + + gvm_load_in in; + RtlCopyMemory(&in, buf, sizeof(in)); + + if (in.wasm_size == 0 || in.wasm_size > (16u * 1024u * 1024u)) { + irp->IoStatus.Information = 0; + return STATUS_INVALID_PARAMETER; + } + if (in_len < sizeof(gvm_load_in) + in.wasm_size) { + irp->IoStatus.Information = 0; + return STATUS_INVALID_PARAMETER; + } + + gvm_load_out out = { 0 }; + unsigned char* wasm_start = (unsigned char*)buf + sizeof(gvm_load_in); + + // dedup: hash first, return existing module id if match. atomic incref + // via find_by_hash_incref so a concurrent unload can't win the race. + unsigned long long h = fnv1a64(wasm_start, in.wasm_size); + gvm_module* existing = gvm_modtab_find_by_hash_incref(h, in.wasm_size); + if (existing) { + out.module_id = existing->id; + out.status = 0; + copy_errmsg(out.err_msg, sizeof(out.err_msg), "reused"); + goto done; + } + + gvm_module* mod = gvm_modtab_alloc(); + if (!mod) { + out.status = -1; + copy_errmsg(out.err_msg, sizeof(out.err_msg), "module table full"); + goto done; + } + + mod->wasm_bytes = (unsigned char*)ExAllocatePoolWithTag(NonPagedPoolNx, in.wasm_size, GVM_TAG_WBUF); + if (!mod->wasm_bytes) { + gvm_modtab_free(mod); + out.status = -2; + copy_errmsg(out.err_msg, sizeof(out.err_msg), "wasm alloc failed"); + goto done; + } + RtlCopyMemory(mod->wasm_bytes, wasm_start, in.wasm_size); + mod->wasm_size = in.wasm_size; + mod->hash = h; + mod->pool_budget = in.pool_budget ? (LONG64)in.pool_budget : (4LL * 1024 * 1024); + mod->pool_used = 0; + mod->caps = GVM_CAP_ALL; // set to real value after link+manifest call + mod->exec_deadline_qpc = 0; + RtlStringCbCopyA(mod->name, sizeof(mod->name), in.name[0] ? in.name : "guest"); + + mod->env = m3_NewEnvironment(); + if (!mod->env) { + gvm_modtab_free(mod); + out.status = -3; + copy_errmsg(out.err_msg, sizeof(out.err_msg), "NewEnvironment failed"); + goto done; + } + + unsigned int stack = in.stack_bytes ? in.stack_bytes : GVM_STACK_DEFAULT; + mod->runtime = m3_NewRuntime(mod->env, stack, NULL); + if (!mod->runtime) { + gvm_modtab_free(mod); + out.status = -4; + copy_errmsg(out.err_msg, sizeof(out.err_msg), "NewRuntime failed"); + goto done; + } + + M3Result r = m3_ParseModule(mod->env, &mod->module, mod->wasm_bytes, mod->wasm_size); + if (r) { + gvm_modtab_free(mod); + out.status = -5; + copy_errmsg(out.err_msg, sizeof(out.err_msg), r); + goto done; + } + + r = m3_LoadModule(mod->runtime, mod->module); + if (r) { + gvm_modtab_free(mod); + out.status = -6; + copy_errmsg(out.err_msg, sizeof(out.err_msg), r); + goto done; + } + + r = gvm_link_host_imports(mod->module); + if (r && r != m3Err_functionLookupFailed) { + gvm_modtab_free(mod); + out.status = -7; + copy_errmsg(out.err_msg, sizeof(out.err_msg), r); + goto done; + } + + // resolve any leftover env.* imports against ntoskrnl/hal export table + gvm_link_kernel_fallback(mod->module); + + // read the guest's declared capability manifest (if any) and lock down. + mod->caps = gvm_read_module_caps(mod); + gvm_log("load module_id=%u name=%s wasm=%u bytes hash=%016llx caps=%08x", + mod->id, mod->name, mod->wasm_size, mod->hash, mod->caps); + + out.module_id = mod->id; + out.status = 0; + copy_errmsg(out.err_msg, sizeof(out.err_msg), "ok"); + +done: + RtlCopyMemory(buf, &out, sizeof(out)); + irp->IoStatus.Information = sizeof(out); + return STATUS_SUCCESS; +} + +NTSTATUS +gvm_ioctl_call(PIRP irp, PIO_STACK_LOCATION sp) +{ + ULONG in_len = sp->Parameters.DeviceIoControl.InputBufferLength; + ULONG out_len = sp->Parameters.DeviceIoControl.OutputBufferLength; + void* buf = irp->AssociatedIrp.SystemBuffer; + + if (in_len < sizeof(gvm_call_in) || out_len < sizeof(gvm_call_out) || !buf) { + irp->IoStatus.Information = 0; + return STATUS_INVALID_PARAMETER; + } + + gvm_call_in in; + RtlCopyMemory(&in, buf, sizeof(in)); + in.export_name[sizeof(in.export_name) - 1] = 0; + + DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, + "[goodmans] IOCTL_CALL mod=%u export=%s argc=%u\n", + in.module_id, in.export_name, in.argc); + + gvm_call_out out = { 0 }; + + gvm_module* mod = gvm_modtab_get(in.module_id); + if (!mod) { + out.status = -1; + copy_errmsg(out.err_msg, sizeof(out.err_msg), "no such module"); + goto done; + } + + IM3Function fn = NULL; + M3Result r = m3Err_none; + __try { + r = m3_FindFunction(&fn, mod->runtime, in.export_name); + } __except (EXCEPTION_EXECUTE_HANDLER) { + r = "m3_FindFunction raised kernel exception"; + } + DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, + "[goodmans] m3_FindFunction export=%s fn=%p r=%s\n", + in.export_name, fn, r ? r : "ok"); + if (r || !fn) { + out.status = -2; + copy_errmsg(out.err_msg, sizeof(out.err_msg), r ? r : "export not found"); + goto done; + } + + if (in.argc > GVM_MAX_ARGS) in.argc = GVM_MAX_ARGS; + + const void* argp[GVM_MAX_ARGS]; + for (unsigned int i = 0; i < in.argc; i++) + argp[i] = &in.argv[i]; + + gvm_set_deadline_ms(mod, in.timeout_ms); + + DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, + "[goodmans] pre-gvm_call_locked mod=%u fn=%p export=%s\n", + in.module_id, fn, in.export_name); + + r = gvm_call_locked(mod, fn, in.argc, argp); + + DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, + "[goodmans] post-gvm_call_locked r=%s\n", r ? r : "ok"); + + if (!r && gvm_deadline_exceeded(mod)) r = "execution deadline exceeded"; + if (r) { + out.status = -3; + copy_errmsg(out.err_msg, sizeof(out.err_msg), r); + M3ErrorInfo einfo = { 0 }; + m3_GetErrorInfo(mod->runtime, &einfo); + DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, + "[goodmans] m3_Call fail: r=%s message=%s file=%s line=%u\n", + r, einfo.message ? einfo.message : "(none)", + einfo.file ? einfo.file : "(none)", einfo.line); + goto done; + } + + unsigned long long rv = 0; + void* rvp = &rv; + r = m3_GetResults(fn, 1, (const void**)&rvp); + // r may be non-null when the export returns void + out.rv = rv; + out.status = 0; + copy_errmsg(out.err_msg, sizeof(out.err_msg), "ok"); + gvm_log("call mod=%u %s(argc=%u) rv=0x%llx", in.module_id, in.export_name, in.argc, rv); + +done: + RtlCopyMemory(buf, &out, sizeof(out)); + irp->IoStatus.Information = sizeof(out); + return STATUS_SUCCESS; +} + +NTSTATUS +gvm_ioctl_unload(PIRP irp, PIO_STACK_LOCATION sp) +{ + ULONG in_len = sp->Parameters.DeviceIoControl.InputBufferLength; + void* buf = irp->AssociatedIrp.SystemBuffer; + + if (in_len < sizeof(gvm_unload_in) || !buf) { + irp->IoStatus.Information = 0; + return STATUS_INVALID_PARAMETER; + } + + gvm_unload_in in; + RtlCopyMemory(&in, buf, sizeof(in)); + + gvm_module* mod = gvm_modtab_get(in.module_id); + if (!mod) { + irp->IoStatus.Information = 0; + return STATUS_NOT_FOUND; + } + gvm_modtab_free(mod); + irp->IoStatus.Information = 0; + return STATUS_SUCCESS; +} + +static void fill_entry(gvm_module_entry* e, const gvm_module* m) +{ + e->id = m->id; + e->wasm_size = m->wasm_size; + e->hash = m->hash; + e->exports = 0; + e->mem_pages = 0; + e->pool_bytes = (unsigned long long)m->pool_used; + if (m->runtime) { + uint32_t mem_sz = 0; + m3_GetMemory(m->runtime, &mem_sz, 0); + e->mem_pages = mem_sz / 65536; + } + RtlCopyMemory(e->name, m->name, sizeof(e->name)); +} + +NTSTATUS +gvm_ioctl_list(PIRP irp, PIO_STACK_LOCATION sp) +{ + ULONG out_len = sp->Parameters.DeviceIoControl.OutputBufferLength; + void* buf = irp->AssociatedIrp.SystemBuffer; + + if (out_len < sizeof(gvm_list_out) || !buf) { + irp->IoStatus.Information = 0; + return STATUS_INVALID_PARAMETER; + } + + gvm_list_out out = { 0 }; + for (unsigned int i = 0; i < GVM_MAX_MODULES; i++) { + gvm_module* m = gvm_modtab_iter(i); + if (!m || !m->used) continue; + if (out.count >= GVM_MAX_MODULES) break; + fill_entry(&out.entries[out.count], m); + out.count++; + } + + RtlCopyMemory(buf, &out, sizeof(out)); + irp->IoStatus.Information = sizeof(out); + return STATUS_SUCCESS; +} + +NTSTATUS +gvm_ioctl_info(PIRP irp, PIO_STACK_LOCATION sp) +{ + ULONG in_len = sp->Parameters.DeviceIoControl.InputBufferLength; + ULONG out_len = sp->Parameters.DeviceIoControl.OutputBufferLength; + void* buf = irp->AssociatedIrp.SystemBuffer; + + if (in_len < sizeof(gvm_info_in) || out_len < sizeof(gvm_info_out) || !buf) { + irp->IoStatus.Information = 0; + return STATUS_INVALID_PARAMETER; + } + + gvm_info_in in; + RtlCopyMemory(&in, buf, sizeof(in)); + + gvm_info_out out = { 0 }; + + gvm_module* mod = gvm_modtab_get(in.module_id); + if (!mod) { + out.status = -1; + copy_errmsg(out.err_msg, sizeof(out.err_msg), "no such module"); + goto done; + } + + fill_entry(&out.base, mod); + + // enumerate exports + imports from the wasm3 module chain + if (mod->runtime) { + IM3Module m = mod->runtime->modules; + while (m) { + for (u32 i = 0; i < m->numFunctions; i++) { + IM3Function fn = &m->functions[i]; + if (i < m->numFuncImports && fn->import.fieldUtf8 && out.import_count < GVM_MAX_INFO_IMPORTS) { + RtlStringCbCopyA(out.imports[out.import_count], GVM_INFO_NAME_LEN, fn->import.fieldUtf8); + out.import_count++; + } + if (fn->export_name && out.export_count < GVM_MAX_INFO_EXPORTS) { + RtlStringCbCopyA(out.exports[out.export_count], GVM_INFO_NAME_LEN, fn->export_name); + out.export_count++; + } + } + m = m->next; + } + } + + out.status = 0; + copy_errmsg(out.err_msg, sizeof(out.err_msg), "ok"); + +done: + RtlCopyMemory(buf, &out, sizeof(out)); + irp->IoStatus.Information = sizeof(out); + return STATUS_SUCCESS; +} + +NTSTATUS +gvm_ioctl_unload_all(PIRP irp, PIO_STACK_LOCATION sp) +{ + UNREFERENCED_PARAMETER(sp); + for (unsigned int i = 0; i < GVM_MAX_MODULES; i++) { + gvm_module* m = gvm_modtab_iter(i); + if (!m || !m->used) continue; + // drain refcount atomically to 1, then free (which decrements to 0) + while (InterlockedCompareExchange(&m->refcount, 1, m->refcount) != 1) { + if (!m->used) break; + } + gvm_modtab_free(m); + } + irp->IoStatus.Information = 0; + return STATUS_SUCCESS; +} + +NTSTATUS +gvm_ioctl_read_guest(PIRP irp, PIO_STACK_LOCATION sp) +{ + ULONG in_len = sp->Parameters.DeviceIoControl.InputBufferLength; + ULONG out_len = sp->Parameters.DeviceIoControl.OutputBufferLength; + void* buf = irp->AssociatedIrp.SystemBuffer; + + if (in_len < sizeof(gvm_read_guest_in) || out_len < sizeof(gvm_read_guest_out) || !buf) { + irp->IoStatus.Information = 0; + return STATUS_INVALID_PARAMETER; + } + + gvm_read_guest_in in; + RtlCopyMemory(&in, buf, sizeof(in)); + + gvm_read_guest_out out; + RtlZeroMemory(&out, sizeof(out)); + + if (in.length == 0 || in.length > GVM_MAX_READ_GUEST_BYTES) { + out.status = -1; + copy_errmsg(out.err_msg, sizeof(out.err_msg), "bad length"); + goto done; + } + + gvm_module* mod = gvm_modtab_get(in.module_id); + if (!mod || !mod->runtime) { + out.status = -2; + copy_errmsg(out.err_msg, sizeof(out.err_msg), "no such module"); + goto done; + } + + uint32_t mem_sz = 0; + uint8_t* mem = m3_GetMemory(mod->runtime, &mem_sz, 0); + if (!mem) { + out.status = -3; + copy_errmsg(out.err_msg, sizeof(out.err_msg), "no memory"); + goto done; + } + if ((uint64_t)in.offset + in.length > mem_sz) { + out.status = -4; + copy_errmsg(out.err_msg, sizeof(out.err_msg), "out of range"); + goto done; + } + + RtlCopyMemory(out.data, mem + in.offset, in.length); + out.length = in.length; + out.status = 0; + copy_errmsg(out.err_msg, sizeof(out.err_msg), "ok"); + +done: + RtlCopyMemory(buf, &out, sizeof(out)); + irp->IoStatus.Information = sizeof(out); + return STATUS_SUCCESS; +} diff --git a/driver/kshim/corecrt.h b/driver/kshim/corecrt.h new file mode 100644 index 0000000..f90290a --- /dev/null +++ b/driver/kshim/corecrt.h @@ -0,0 +1,2 @@ +/* corecrt.h - blocks ucrt/corecrt.h chain */ +#pragma once diff --git a/driver/kshim/inttypes.h b/driver/kshim/inttypes.h new file mode 100644 index 0000000..a246f35 --- /dev/null +++ b/driver/kshim/inttypes.h @@ -0,0 +1,28 @@ +/* inttypes.h - printf macros wasm3 uses */ +#pragma once + +#include + +#define PRId8 "d" +#define PRIi8 "i" +#define PRIu8 "u" +#define PRIx8 "x" +#define PRIX8 "X" + +#define PRId16 "d" +#define PRIi16 "i" +#define PRIu16 "u" +#define PRIx16 "x" +#define PRIX16 "X" + +#define PRId32 "d" +#define PRIi32 "i" +#define PRIu32 "u" +#define PRIx32 "x" +#define PRIX32 "X" + +#define PRId64 "lld" +#define PRIi64 "lli" +#define PRIu64 "llu" +#define PRIx64 "llx" +#define PRIX64 "llX" diff --git a/driver/kshim/kshim.c b/driver/kshim/kshim.c new file mode 100644 index 0000000..1273e6f --- /dev/null +++ b/driver/kshim/kshim.c @@ -0,0 +1,248 @@ +/* kshim.c - stdlib/stdio bodies wasm3 links against */ +#include "kshim.h" +#include "stdio.h" +#include "stdlib.h" + +FILE* const stdout = (FILE*)(void*)1; +FILE* const stderr = (FILE*)(void*)2; +FILE* const stdin = (FILE*)(void*)3; + +typedef struct { + size_t size; + unsigned char data[1]; +} kshim_hdr; + +#define HDR_OFF FIELD_OFFSET(kshim_hdr, data) + +void* __cdecl malloc(size_t n) +{ + if (n == 0) + return NULL; + + kshim_hdr* h = (kshim_hdr*)ExAllocatePoolWithTag(NonPagedPoolNx, n + HDR_OFF, GVM_POOL_TAG); + if (!h) + return NULL; + + h->size = n; + return h->data; +} + +void* __cdecl calloc(size_t n, size_t sz) +{ + size_t total = n * sz; + if (sz != 0 && total / sz != n) + return NULL; + + void* p = malloc(total); + if (p) + RtlZeroMemory(p, total); + return p; +} + +void __cdecl free(void* p) +{ + if (!p) + return; + kshim_hdr* h = (kshim_hdr*)((unsigned char*)p - HDR_OFF); + ExFreePoolWithTag(h, GVM_POOL_TAG); +} + +void* __cdecl realloc(void* p, size_t new_sz) +{ + if (!p) + return malloc(new_sz); + + if (new_sz == 0) { + free(p); + return NULL; + } + + kshim_hdr* h = (kshim_hdr*)((unsigned char*)p - HDR_OFF); + size_t old_sz = h->size; + if (old_sz >= new_sz) + return p; + + void* np = malloc(new_sz); + if (!np) + return NULL; + + RtlCopyMemory(np, p, old_sz); + free(p); + return np; +} + +void __cdecl abort(void) +{ + DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "[goodmans] abort\n"); + __fastfail(0); +} + +void __cdecl exit(int status) +{ + UNREFERENCED_PARAMETER(status); + abort(); +} + +int __cdecl printf(const char* fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + ULONG r = vDbgPrintExWithPrefix("[goodmans] ", DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, fmt, ap); + va_end(ap); + return (int)r; +} + +int __cdecl fprintf(FILE* stream, const char* fmt, ...) +{ + UNREFERENCED_PARAMETER(stream); + va_list ap; + va_start(ap, fmt); + ULONG r = vDbgPrintExWithPrefix("[goodmans] ", DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, fmt, ap); + va_end(ap); + return (int)r; +} + +int __cdecl snprintf(char* buf, size_t sz, const char* fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + NTSTATUS s = RtlStringCbVPrintfA(buf, sz, fmt, ap); + va_end(ap); + if (!NT_SUCCESS(s)) + return -1; + size_t len = 0; + while (buf[len]) len++; + return (int)len; +} + +int __cdecl vsnprintf(char* buf, size_t sz, const char* fmt, va_list ap) +{ + NTSTATUS s = RtlStringCbVPrintfA(buf, sz, fmt, ap); + if (!NT_SUCCESS(s)) + return -1; + size_t len = 0; + while (buf[len]) len++; + return (int)len; +} + +int __cdecl puts(const char* s) +{ + DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "[goodmans] %s\n", s ? s : ""); + return 0; +} + +int __cdecl fputs(const char* s, FILE* stream) +{ + UNREFERENCED_PARAMETER(stream); + DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "[goodmans] %s", s ? s : ""); + return 0; +} + +int __cdecl fputc(int c, FILE* stream) +{ + UNREFERENCED_PARAMETER(stream); + DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "%c", c); + return c; +} + +int __cdecl putchar(int c) +{ + return fputc(c, NULL); +} + +int __cdecl fflush(FILE* stream) +{ + UNREFERENCED_PARAMETER(stream); + return 0; +} + +// m3_CallArgv wants strtoul/strtoull, base-10 or 0x hex +static int _kshim_digit(int c, int base) +{ + int v = -1; + if (c >= '0' && c <= '9') v = c - '0'; + else if (c >= 'a' && c <= 'z') v = c - 'a' + 10; + else if (c >= 'A' && c <= 'Z') v = c - 'A' + 10; + if (v < 0 || v >= base) return -1; + return v; +} + +unsigned long __cdecl strtoul(const char* nptr, char** endptr, int base) +{ + const char* p = nptr; + while (*p == ' ' || *p == '\t') p++; + if (*p == '+') p++; + + if (base == 0) { + if (p[0] == '0' && (p[1] == 'x' || p[1] == 'X')) { base = 16; p += 2; } + else if (p[0] == '0') { base = 8; p++; } + else base = 10; + } else if (base == 16 && p[0] == '0' && (p[1] == 'x' || p[1] == 'X')) { + p += 2; + } + + unsigned long acc = 0; + int d; + while ((d = _kshim_digit((unsigned char)*p, base)) >= 0) { + acc = acc * (unsigned long)base + (unsigned long)d; + p++; + } + if (endptr) *endptr = (char*)p; + return acc; +} + +unsigned long long __cdecl strtoull(const char* nptr, char** endptr, int base) +{ + const char* p = nptr; + while (*p == ' ' || *p == '\t') p++; + if (*p == '+') p++; + + if (base == 0) { + if (p[0] == '0' && (p[1] == 'x' || p[1] == 'X')) { base = 16; p += 2; } + else if (p[0] == '0') { base = 8; p++; } + else base = 10; + } else if (base == 16 && p[0] == '0' && (p[1] == 'x' || p[1] == 'X')) { + p += 2; + } + + unsigned long long acc = 0; + int d; + while ((d = _kshim_digit((unsigned char)*p, base)) >= 0) { + acc = acc * (unsigned long long)base + (unsigned long long)d; + p++; + } + if (endptr) *endptr = (char*)p; + return acc; +} + +// kshim_* aliases for direct kshim.h consumers +void* kshim_alloc(size_t n) { return malloc(n); } +void* kshim_calloc(size_t n, size_t sz) { return calloc(n, sz); } +void* kshim_realloc(void* p, size_t new_sz){ return realloc(p, new_sz); } +void kshim_free(void* p) { free(p); } +void kshim_abort(const char* msg) { UNREFERENCED_PARAMETER(msg); abort(); } +int kshim_printf(const char* fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + ULONG r = vDbgPrintExWithPrefix("[goodmans] ", DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, fmt, ap); + va_end(ap); + return (int)r; +} +int kshim_snprintf(char* buf, size_t sz, const char* fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + NTSTATUS s = RtlStringCbVPrintfA(buf, sz, fmt, ap); + va_end(ap); + if (!NT_SUCCESS(s)) return -1; + size_t len = 0; while (buf[len]) len++; + return (int)len; +} +int kshim_vsnprintf(char* buf, size_t sz, const char* fmt, va_list ap) +{ + NTSTATUS s = RtlStringCbVPrintfA(buf, sz, fmt, ap); + if (!NT_SUCCESS(s)) return -1; + size_t len = 0; while (buf[len]) len++; + return (int)len; +} diff --git a/driver/kshim/kshim.h b/driver/kshim/kshim.h new file mode 100644 index 0000000..fc4837f --- /dev/null +++ b/driver/kshim/kshim.h @@ -0,0 +1,24 @@ +/* kshim.h - interface for kshim.c and driver TUs */ +#pragma once + +#include +#include + +#define GVM_POOL_TAG 'MVoG' + +#ifdef __cplusplus +extern "C" { +#endif + +void* kshim_alloc(size_t n); +void* kshim_calloc(size_t n, size_t sz); +void* kshim_realloc(void* p, size_t new_sz); +void kshim_free(void* p); +void kshim_abort(const char* msg); +int kshim_printf(const char* fmt, ...); +int kshim_snprintf(char* buf, size_t sz, const char* fmt, ...); +int kshim_vsnprintf(char* buf, size_t sz, const char* fmt, va_list ap); + +#ifdef __cplusplus +} +#endif diff --git a/driver/kshim/malloc.h b/driver/kshim/malloc.h new file mode 100644 index 0000000..ec343d7 --- /dev/null +++ b/driver/kshim/malloc.h @@ -0,0 +1,2 @@ +/* malloc.h - blocks km/crt/malloc.h, decls in stdlib.h */ +#pragma once diff --git a/driver/kshim/stdio.h b/driver/kshim/stdio.h new file mode 100644 index 0000000..296ced8 --- /dev/null +++ b/driver/kshim/stdio.h @@ -0,0 +1,29 @@ +/* stdio.h - kshim decls, bodies in kshim.c */ +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef void FILE; + +extern FILE* const stdout; +extern FILE* const stderr; +extern FILE* const stdin; + +int __cdecl printf(const char* fmt, ...); +int __cdecl fprintf(FILE* stream, const char* fmt, ...); +int __cdecl snprintf(char* buf, size_t sz, const char* fmt, ...); +int __cdecl vsnprintf(char* buf, size_t sz, const char* fmt, va_list ap); +int __cdecl puts(const char* s); +int __cdecl fputs(const char* s, FILE* stream); +int __cdecl fputc(int c, FILE* stream); +int __cdecl putchar(int c); +int __cdecl fflush(FILE* stream); + +#ifdef __cplusplus +} +#endif diff --git a/driver/kshim/stdlib.h b/driver/kshim/stdlib.h new file mode 100644 index 0000000..c02e4fe --- /dev/null +++ b/driver/kshim/stdlib.h @@ -0,0 +1,25 @@ +/* stdlib.h - kshim decls */ +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef NULL +#define NULL ((void*)0) +#endif + +void* __cdecl malloc(size_t n); +void* __cdecl calloc(size_t n, size_t sz); +void* __cdecl realloc(void* p, size_t new_sz); +void __cdecl free(void* p); +void __cdecl abort(void); +void __cdecl exit(int status); + +#define _abs64(x) ((x) < 0 ? -(x) : (x)) + +#ifdef __cplusplus +} +#endif diff --git a/driver/log_ring.c b/driver/log_ring.c new file mode 100644 index 0000000..070fc5a --- /dev/null +++ b/driver/log_ring.c @@ -0,0 +1,95 @@ +/* log_ring.c - kernel-side ring of recent debug lines. GUI polls via ioctl. */ +#include "inc/gvm.h" +#include "../shared/goodmans_ioctl.h" + +static gvm_log_entry g_ring[GVM_LOG_ENTRIES]; +static ULONG64 g_seq = 0; // strictly increasing, monotonic +static ULONG g_head = 0; // next write slot +static KSPIN_LOCK g_lock; +static BOOLEAN g_init = FALSE; + +void +gvm_log_init(void) +{ + if (g_init) return; + RtlZeroMemory(g_ring, sizeof(g_ring)); + KeInitializeSpinLock(&g_lock); + g_init = TRUE; +} + +void +gvm_log_push(const char* fmt, ...) +{ + if (!g_init) return; + char tmp[GVM_LOG_ENTRY_LEN]; + va_list ap; va_start(ap, fmt); + RtlStringCbVPrintfA(tmp, sizeof(tmp), fmt, ap); + va_end(ap); + + // also mirror to DbgPrint so DebugView keeps working + DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "[goodmans] %s\n", tmp); + + LARGE_INTEGER now; + KeQuerySystemTimePrecise(&now); + + KIRQL irql; + KeAcquireSpinLock(&g_lock, &irql); + gvm_log_entry* e = &g_ring[g_head]; + e->timestamp_100ns = (ULONG64)now.QuadPart; + RtlStringCbCopyA(e->line, sizeof(e->line), tmp); + g_seq++; + g_head = (g_head + 1) % GVM_LOG_ENTRIES; + KeReleaseSpinLock(&g_lock, irql); +} + +NTSTATUS +gvm_ioctl_tail_log(PIRP irp, PIO_STACK_LOCATION sp) +{ + ULONG in_len = sp->Parameters.DeviceIoControl.InputBufferLength; + ULONG out_len = sp->Parameters.DeviceIoControl.OutputBufferLength; + void* buf = irp->AssociatedIrp.SystemBuffer; + + if (in_len < sizeof(gvm_tail_in) || out_len < sizeof(gvm_tail_out) || !buf) { + irp->IoStatus.Information = 0; + return STATUS_INVALID_PARAMETER; + } + + gvm_tail_in in; + RtlCopyMemory(&in, buf, sizeof(in)); + + gvm_tail_out* out = (gvm_tail_out*)ExAllocatePoolWithTag( + NonPagedPoolNx, sizeof(gvm_tail_out), 'gLog'); + if (!out) return STATUS_INSUFFICIENT_RESOURCES; + RtlZeroMemory(out, sizeof(*out)); + + KIRQL irql; + KeAcquireSpinLock(&g_lock, &irql); + + ULONG64 seq_here = g_seq; + ULONG64 want_from = in.last_seq; + ULONG64 available = 0; + + if (seq_here > want_from) { + available = seq_here - want_from; + if (available > GVM_LOG_ENTRIES) { + out->dropped = (unsigned int)(available - GVM_LOG_ENTRIES); + available = GVM_LOG_ENTRIES; + } + } + + // walk backwards from head, taking `available` newest entries in order + ULONG head = g_head; + for (ULONG64 i = 0; i < available; i++) { + ULONG idx = (head + GVM_LOG_ENTRIES - (ULONG)available + (ULONG)i) % GVM_LOG_ENTRIES; + out->entries[i] = g_ring[idx]; + } + out->count = (unsigned int)available; + out->next_seq = seq_here; + + KeReleaseSpinLock(&g_lock, irql); + + RtlCopyMemory(buf, out, sizeof(*out)); + ExFreePoolWithTag(out, 'gLog'); + irp->IoStatus.Information = sizeof(gvm_tail_out); + return STATUS_SUCCESS; +} diff --git a/driver/module_table.c b/driver/module_table.c new file mode 100644 index 0000000..03c0b6d --- /dev/null +++ b/driver/module_table.c @@ -0,0 +1,150 @@ +/* module_table.c - fixed-size slot allocator for loaded modules. + * refcount is atomic (LONG). teardown holds the module's call_mutex so + * any in-flight m3_Call has to finish before the runtime is freed. */ +#include "inc/gvm.h" +#include "../shared/goodmans_ioctl.h" + +static gvm_module g_modules[GVM_MAX_MODULES]; +static KSPIN_LOCK g_lock; + +NTSTATUS +gvm_modtab_init(void) +{ + RtlZeroMemory(g_modules, sizeof(g_modules)); + KeInitializeSpinLock(&g_lock); + // init every mutex up front so iterators that wait on all slots (e.g. the + // callback dispatch worker) don't touch an uninitialized DISPATCHER_HEADER + for (unsigned int i = 0; i < GVM_MAX_MODULES; i++) + KeInitializeMutex(&g_modules[i].call_mutex, 0); + return STATUS_SUCCESS; +} + +void +gvm_modtab_teardown(void) +{ + for (unsigned int i = 0; i < GVM_MAX_MODULES; i++) { + if (g_modules[i].used) { + g_modules[i].refcount = 1; // force teardown + gvm_modtab_free(&g_modules[i]); + } + } +} + +gvm_module* +gvm_modtab_alloc(void) +{ + KIRQL irql; + gvm_module* out = NULL; + + KeAcquireSpinLock(&g_lock, &irql); + for (unsigned int i = 0; i < GVM_MAX_MODULES; i++) { + if (!g_modules[i].used) { + g_modules[i].used = TRUE; + g_modules[i].id = i + 1; + g_modules[i].refcount = 1; + KeInitializeMutex(&g_modules[i].call_mutex, 0); + out = &g_modules[i]; + break; + } + } + KeReleaseSpinLock(&g_lock, irql); + return out; +} + +gvm_module* +gvm_modtab_get(unsigned int id) +{ + if (id == 0 || id > GVM_MAX_MODULES) return NULL; + gvm_module* m = &g_modules[id - 1]; + return m->used ? m : NULL; +} + +gvm_module* +gvm_modtab_find_by_hash(unsigned long long hash) +{ + KIRQL irql; + gvm_module* out = NULL; + + KeAcquireSpinLock(&g_lock, &irql); + for (unsigned int i = 0; i < GVM_MAX_MODULES; i++) { + if (g_modules[i].used && g_modules[i].hash == hash) { + out = &g_modules[i]; + break; + } + } + KeReleaseSpinLock(&g_lock, irql); + return out; +} + +// atomic find + increment under the table lock. safer than +// find_by_hash + separate refcount++ which races with concurrent unload. +gvm_module* +gvm_modtab_find_by_hash_incref(unsigned long long hash, unsigned int expected_size) +{ + KIRQL irql; + gvm_module* out = NULL; + + KeAcquireSpinLock(&g_lock, &irql); + for (unsigned int i = 0; i < GVM_MAX_MODULES; i++) { + if (g_modules[i].used + && g_modules[i].hash == hash + && g_modules[i].wasm_size == expected_size) { + InterlockedIncrement(&g_modules[i].refcount); + out = &g_modules[i]; + break; + } + } + KeReleaseSpinLock(&g_lock, irql); + return out; +} + +gvm_module* +gvm_modtab_iter(unsigned int idx) +{ + if (idx >= GVM_MAX_MODULES) return NULL; + return &g_modules[idx]; +} + +gvm_module* +gvm_modtab_owner_of_runtime(IM3Runtime rt) +{ + if (!rt) return NULL; + for (unsigned int i = 0; i < GVM_MAX_MODULES; i++) { + if (g_modules[i].used && g_modules[i].runtime == rt) + return &g_modules[i]; + } + return NULL; +} + +void +gvm_modtab_free(gvm_module* m) +{ + if (!m || !m->used) return; + + // only the last reference proceeds to teardown + if (InterlockedDecrement(&m->refcount) > 0) return; + + // block until any in-flight m3_Call finishes so the runtime is quiescent + KeWaitForSingleObject(&m->call_mutex, Executive, KernelMode, FALSE, NULL); + + if (m->runtime) { + m3_FreeRuntime(m->runtime); + m->runtime = NULL; + } + if (m->env) { + m3_FreeEnvironment(m->env); + m->env = NULL; + } + if (m->wasm_bytes) { + ExFreePoolWithTag(m->wasm_bytes, GVM_TAG_WBUF); + m->wasm_bytes = NULL; + } + m->module = NULL; + m->wasm_size = 0; + m->hash = 0; + + // publish used=FALSE before releasing so waiters see it on retry + m->used = FALSE; + + KeReleaseMutex(&m->call_mutex, FALSE); +} diff --git a/driver/trace_ring.c b/driver/trace_ring.c new file mode 100644 index 0000000..743de82 --- /dev/null +++ b/driver/trace_ring.c @@ -0,0 +1,137 @@ +/* trace_ring.c - per-import call trace for wasm guest debugging. + * ring buffer holds recent host-import invocations, export calls, and traps. + * GUI polls via ioctl for live view. + */ +#include "inc/gvm.h" +#include "../shared/goodmans_ioctl.h" + +static gvm_trace_entry g_ring[GVM_TRACE_ENTRIES]; +static ULONG64 g_seq = 0; +static ULONG g_head = 0; +static KSPIN_LOCK g_lock; +static BOOLEAN g_init = FALSE; +static volatile LONG g_mode = GVM_TRACE_OFF; +static volatile LONG g_only = 0; // module_id filter when mode==ON_MODULE + +void +gvm_trace_init(void) +{ + if (g_init) return; + RtlZeroMemory(g_ring, sizeof(g_ring)); + KeInitializeSpinLock(&g_lock); + g_init = TRUE; +} + +BOOLEAN +gvm_trace_enabled_for(unsigned int module_id) +{ + LONG m = g_mode; + if (m == GVM_TRACE_OFF) return FALSE; + if (m == GVM_TRACE_ON_ALL) return TRUE; + return (LONG)module_id == g_only; +} + +void +gvm_trace_push(unsigned int module_id, unsigned int kind, const char* name, + unsigned int argc, const ULONG64* argv, ULONG64 rv) +{ + if (!g_init) return; + if (!gvm_trace_enabled_for(module_id)) return; + + LARGE_INTEGER now; + KeQuerySystemTimePrecise(&now); + + // capture pre-lock so lock IRQL doesn't confuse the value + unsigned int cap_irql = (unsigned int)KeGetCurrentIrql(); + unsigned int cap_tid = (unsigned int)(ULONG_PTR)PsGetCurrentThreadId(); + + KIRQL irql; + KeAcquireSpinLock(&g_lock, &irql); + gvm_trace_entry* e = &g_ring[g_head]; + RtlZeroMemory(e, sizeof(*e)); + e->timestamp_100ns = (ULONG64)now.QuadPart; + e->module_id = module_id; + e->kind = kind; + e->thread_id = cap_tid; + e->irql = cap_irql; + e->argc = argc > 4 ? 4 : argc; + for (unsigned int i = 0; i < e->argc; i++) e->argv[i] = argv[i]; + e->rv = rv; + if (name) RtlStringCbCopyA(e->name, sizeof(e->name), name); + g_seq++; + g_head = (g_head + 1) % GVM_TRACE_ENTRIES; + KeReleaseSpinLock(&g_lock, irql); +} + +NTSTATUS +gvm_ioctl_tail_trace(PIRP irp, PIO_STACK_LOCATION sp) +{ + ULONG in_len = sp->Parameters.DeviceIoControl.InputBufferLength; + ULONG out_len = sp->Parameters.DeviceIoControl.OutputBufferLength; + void* buf = irp->AssociatedIrp.SystemBuffer; + + if (in_len < sizeof(gvm_trace_in) || out_len < sizeof(gvm_trace_out) || !buf) { + irp->IoStatus.Information = 0; + return STATUS_INVALID_PARAMETER; + } + + gvm_trace_in in; + RtlCopyMemory(&in, buf, sizeof(in)); + + gvm_trace_out* out = (gvm_trace_out*)ExAllocatePoolWithTag( + NonPagedPoolNx, sizeof(gvm_trace_out), 'gTrc'); + if (!out) return STATUS_INSUFFICIENT_RESOURCES; + RtlZeroMemory(out, sizeof(*out)); + + KIRQL irql; + KeAcquireSpinLock(&g_lock, &irql); + + ULONG64 seq_here = g_seq; + ULONG64 want_from = in.last_seq; + ULONG64 available = 0; + ULONG64 dropped = 0; + + if (seq_here > want_from) { + available = seq_here - want_from; + if (available > GVM_TRACE_ENTRIES) { + dropped = available - GVM_TRACE_ENTRIES; + available = GVM_TRACE_ENTRIES; + } + ULONG start = (g_head + GVM_TRACE_ENTRIES - (ULONG)available) % GVM_TRACE_ENTRIES; + for (ULONG64 i = 0; i < available; i++) { + ULONG src = (start + (ULONG)i) % GVM_TRACE_ENTRIES; + out->entries[i] = g_ring[src]; + } + } + out->next_seq = seq_here; + out->count = (unsigned int)available; + out->dropped = (unsigned int)dropped; + + KeReleaseSpinLock(&g_lock, irql); + + RtlCopyMemory(buf, out, sizeof(*out)); + ExFreePoolWithTag(out, 'gTrc'); + irp->IoStatus.Information = sizeof(gvm_trace_out); + return STATUS_SUCCESS; +} + +NTSTATUS +gvm_ioctl_trace_ctl(PIRP irp, PIO_STACK_LOCATION sp) +{ + ULONG in_len = sp->Parameters.DeviceIoControl.InputBufferLength; + void* buf = irp->AssociatedIrp.SystemBuffer; + + if (in_len < sizeof(gvm_trace_ctl_in) || !buf) { + irp->IoStatus.Information = 0; + return STATUS_INVALID_PARAMETER; + } + gvm_trace_ctl_in in; + RtlCopyMemory(&in, buf, sizeof(in)); + + InterlockedExchange(&g_mode, (LONG)in.mode); + InterlockedExchange(&g_only, (LONG)in.module_id); + + gvm_log_push("trace mode=%u module=%u", in.mode, in.module_id); + irp->IoStatus.Information = 0; + return STATUS_SUCCESS; +} diff --git a/driver/wasm3/m3_bind.c b/driver/wasm3/m3_bind.c new file mode 100644 index 0000000..f6dc88b --- /dev/null +++ b/driver/wasm3/m3_bind.c @@ -0,0 +1,175 @@ +// +// m3_bind.c +// +// Created by Steven Massey on 4/29/19. +// Copyright © 2019 Steven Massey. All rights reserved. +// + +#include "m3_env.h" +#include "m3_exception.h" +#include "m3_info.h" + + +u8 ConvertTypeCharToTypeId (char i_code) +{ + switch (i_code) { + case 'v': return c_m3Type_none; + case 'i': return c_m3Type_i32; + case 'I': return c_m3Type_i64; + case 'f': return c_m3Type_f32; + case 'F': return c_m3Type_f64; + case '*': return c_m3Type_i32; + } + return c_m3Type_unknown; +} + + +M3Result SignatureToFuncType (IM3FuncType * o_functionType, ccstr_t i_signature) +{ + IM3FuncType funcType = NULL; + +_try { + if (not o_functionType) + _throw ("null function type"); + + if (not i_signature) + _throw ("null function signature"); + + cstr_t sig = i_signature; + + size_t maxNumTypes = strlen (i_signature); + + // assume min signature is "()" + _throwif (m3Err_malformedFunctionSignature, maxNumTypes < 2); + maxNumTypes -= 2; + + _throwif (m3Err_tooManyArgsRets, maxNumTypes > d_m3MaxSaneFunctionArgRetCount); + +_ (AllocFuncType (& funcType, (u32) maxNumTypes)); + + u8 * typelist = funcType->types; + + bool parsingRets = true; + while (* sig) + { + char typeChar = * sig++; + + if (typeChar == '(') + { + parsingRets = false; + continue; + } + else if ( typeChar == ' ') + continue; + else if (typeChar == ')') + break; + + u8 type = ConvertTypeCharToTypeId (typeChar); + + _throwif ("unknown argument type char", c_m3Type_unknown == type); + + if (type == c_m3Type_none) + continue; + + if (parsingRets) + { + _throwif ("malformed signature; return count overflow", funcType->numRets >= maxNumTypes); + funcType->numRets++; + *typelist++ = type; + } + else + { + _throwif ("malformed signature; arg count overflow", (u32)(funcType->numRets) + funcType->numArgs >= maxNumTypes); + funcType->numArgs++; + *typelist++ = type; + } + } + +} _catch: + + if (result) + m3_Free (funcType); + + * o_functionType = funcType; + + return result; +} + + +static +M3Result ValidateSignature (IM3Function i_function, ccstr_t i_linkingSignature) +{ + M3Result result = m3Err_none; + + IM3FuncType ftype = NULL; +_ (SignatureToFuncType (& ftype, i_linkingSignature)); + + if (not AreFuncTypesEqual (ftype, i_function->funcType)) + { + m3log (module, "expected: %s", SPrintFuncTypeSignature (ftype)); + m3log (module, " found: %s", SPrintFuncTypeSignature (i_function->funcType)); + + _throw ("function signature mismatch"); + } + + _catch: + + m3_Free (ftype); + + return result; +} + + +M3Result FindAndLinkFunction (IM3Module io_module, + ccstr_t i_moduleName, + ccstr_t i_functionName, + ccstr_t i_signature, + voidptr_t i_function, + voidptr_t i_userdata) +{ +_try { + _throwif(m3Err_moduleNotLinked, !io_module->runtime); + + const bool wildcardModule = (strcmp (i_moduleName, "*") == 0); + + result = m3Err_functionLookupFailed; + + for (u32 i = 0; i < io_module->numFunctions; ++i) + { + const IM3Function f = & io_module->functions [i]; + + if (f->import.moduleUtf8 and f->import.fieldUtf8) + { + if (strcmp (f->import.fieldUtf8, i_functionName) == 0 and + (wildcardModule or strcmp (f->import.moduleUtf8, i_moduleName) == 0)) + { + if (i_signature) { +_ (ValidateSignature (f, i_signature)); + } +_ (CompileRawFunction (io_module, f, i_function, i_userdata)); + } + } + } +} _catch: + return result; +} + +M3Result m3_LinkRawFunctionEx (IM3Module io_module, + const char * const i_moduleName, + const char * const i_functionName, + const char * const i_signature, + M3RawCall i_function, + const void * i_userdata) +{ + return FindAndLinkFunction (io_module, i_moduleName, i_functionName, i_signature, (voidptr_t)i_function, i_userdata); +} + +M3Result m3_LinkRawFunction (IM3Module io_module, + const char * const i_moduleName, + const char * const i_functionName, + const char * const i_signature, + M3RawCall i_function) +{ + return FindAndLinkFunction (io_module, i_moduleName, i_functionName, i_signature, (voidptr_t)i_function, NULL); +} + diff --git a/driver/wasm3/m3_bind.h b/driver/wasm3/m3_bind.h new file mode 100644 index 0000000..a80317a --- /dev/null +++ b/driver/wasm3/m3_bind.h @@ -0,0 +1,20 @@ +// +// m3_bind.h +// +// Created by Steven Massey on 2/27/20. +// Copyright © 2020 Steven Massey. All rights reserved. +// + +#ifndef m3_bind_h +#define m3_bind_h + +#include "m3_env.h" + +d_m3BeginExternC + +u8 ConvertTypeCharToTypeId (char i_code); +M3Result SignatureToFuncType (IM3FuncType * o_functionType, ccstr_t i_signature); + +d_m3EndExternC + +#endif /* m3_bind_h */ diff --git a/driver/wasm3/m3_code.c b/driver/wasm3/m3_code.c new file mode 100644 index 0000000..af273eb --- /dev/null +++ b/driver/wasm3/m3_code.c @@ -0,0 +1,246 @@ +// +// m3_code.c +// +// Created by Steven Massey on 4/19/19. +// Copyright © 2019 Steven Massey. All rights reserved. +// + +#include +#include "m3_code.h" +#include "m3_env.h" + +//--------------------------------------------------------------------------------------------------------------------------------- + + +IM3CodePage NewCodePage (IM3Runtime i_runtime, u32 i_minNumLines) +{ + IM3CodePage page; + + // check multiplication overflow + if (i_minNumLines > UINT_MAX / sizeof (code_t)) { + return NULL; + } + u32 pageSize = sizeof (M3CodePageHeader) + sizeof (code_t) * i_minNumLines; + + // check addition overflow + if (pageSize < sizeof (M3CodePageHeader)) { + return NULL; + } + + pageSize = (pageSize + (d_m3CodePageAlignSize-1)) & ~(d_m3CodePageAlignSize-1); // align + // check alignment overflow + if (pageSize == 0) { + return NULL; + } + + page = (IM3CodePage)m3_Malloc ("M3CodePage", pageSize); + + if (page) + { + page->info.sequence = ++i_runtime->newCodePageSequence; + page->info.numLines = (pageSize - sizeof (M3CodePageHeader)) / sizeof (code_t); + +#if d_m3RecordBacktraces + u32 pageSizeBt = sizeof (M3CodeMappingPage) + sizeof (M3CodeMapEntry) * page->info.numLines; + page->info.mapping = (M3CodeMappingPage *)m3_Malloc ("M3CodeMappingPage", pageSizeBt); + + if (page->info.mapping) + { + page->info.mapping->size = 0; + page->info.mapping->capacity = page->info.numLines; + } + else + { + m3_Free (page); + return NULL; + } + page->info.mapping->basePC = GetPageStartPC(page); +#endif // d_m3RecordBacktraces + + m3log (runtime, "new page: %p; seq: %d; bytes: %d; lines: %d", GetPagePC (page), page->info.sequence, pageSize, page->info.numLines); + } + + return page; +} + + +void FreeCodePages (IM3CodePage * io_list) +{ + IM3CodePage page = * io_list; + + while (page) + { + m3log (code, "free page: %d; %p; util: %3.1f%%", page->info.sequence, page, 100. * page->info.lineIndex / page->info.numLines); + + IM3CodePage next = page->info.next; +#if d_m3RecordBacktraces + m3_Free (page->info.mapping); +#endif // d_m3RecordBacktraces + m3_Free (page); + page = next; + } + + * io_list = NULL; +} + + +u32 NumFreeLines (IM3CodePage i_page) +{ + d_m3Assert (i_page->info.lineIndex <= i_page->info.numLines); + + return i_page->info.numLines - i_page->info.lineIndex; +} + + +void EmitWord_impl (IM3CodePage i_page, void * i_word) +{ d_m3Assert (i_page->info.lineIndex+1 <= i_page->info.numLines); + i_page->code [i_page->info.lineIndex++] = i_word; +} + +void EmitWord32 (IM3CodePage i_page, const u32 i_word) +{ d_m3Assert (i_page->info.lineIndex+1 <= i_page->info.numLines); + memcpy (& i_page->code[i_page->info.lineIndex++], & i_word, sizeof(i_word)); +} + +void EmitWord64 (IM3CodePage i_page, const u64 i_word) +{ +#if M3_SIZEOF_PTR == 4 + d_m3Assert (i_page->info.lineIndex+2 <= i_page->info.numLines); + memcpy (& i_page->code[i_page->info.lineIndex], & i_word, sizeof(i_word)); + i_page->info.lineIndex += 2; +#else + d_m3Assert (i_page->info.lineIndex+1 <= i_page->info.numLines); + memcpy (& i_page->code[i_page->info.lineIndex], & i_word, sizeof(i_word)); + i_page->info.lineIndex += 1; +#endif +} + + +#if d_m3RecordBacktraces +void EmitMappingEntry (IM3CodePage i_page, u32 i_moduleOffset) +{ + M3CodeMappingPage * page = i_page->info.mapping; + d_m3Assert (page->size < page->capacity); + + M3CodeMapEntry * entry = & page->entries[page->size++]; + pc_t pc = GetPagePC (i_page); + + entry->pcOffset = pc - page->basePC; + entry->moduleOffset = i_moduleOffset; +} +#endif // d_m3RecordBacktraces + +pc_t GetPageStartPC (IM3CodePage i_page) +{ + return & i_page->code [0]; +} + + +pc_t GetPagePC (IM3CodePage i_page) +{ + if (i_page) + return & i_page->code [i_page->info.lineIndex]; + else + return NULL; +} + + +void PushCodePage (IM3CodePage * i_list, IM3CodePage i_codePage) +{ + IM3CodePage next = * i_list; + i_codePage->info.next = next; + * i_list = i_codePage; +} + + +IM3CodePage PopCodePage (IM3CodePage * i_list) +{ + IM3CodePage page = * i_list; + * i_list = page->info.next; + page->info.next = NULL; + + return page; +} + + + +u32 FindCodePageEnd (IM3CodePage i_list, IM3CodePage * o_end) +{ + u32 numPages = 0; + * o_end = NULL; + + while (i_list) + { + * o_end = i_list; + ++numPages; + i_list = i_list->info.next; + } + + return numPages; +} + + +u32 CountCodePages (IM3CodePage i_list) +{ + IM3CodePage unused; + return FindCodePageEnd (i_list, & unused); +} + + +IM3CodePage GetEndCodePage (IM3CodePage i_list) +{ + IM3CodePage end; + FindCodePageEnd (i_list, & end); + + return end; +} + +#if d_m3RecordBacktraces +bool ContainsPC (IM3CodePage i_page, pc_t i_pc) +{ + return GetPageStartPC (i_page) <= i_pc && i_pc < GetPagePC (i_page); +} + + +bool MapPCToOffset (IM3CodePage i_page, pc_t i_pc, u32 * o_moduleOffset) +{ + M3CodeMappingPage * mapping = i_page->info.mapping; + + u32 pcOffset = i_pc - mapping->basePC; + + u32 left = 0; + u32 right = mapping->size; + + while (left < right) + { + u32 mid = left + (right - left) / 2; + + if (mapping->entries[mid].pcOffset < pcOffset) + { + left = mid + 1; + } + else if (mapping->entries[mid].pcOffset > pcOffset) + { + right = mid; + } + else + { + *o_moduleOffset = mapping->entries[mid].moduleOffset; + return true; + } + } + + // Getting here means left is now one more than the element we want. + if (left > 0) + { + left--; + *o_moduleOffset = mapping->entries[left].moduleOffset; + return true; + } + else return false; +} +#endif // d_m3RecordBacktraces + +//--------------------------------------------------------------------------------------------------------------------------------- + + diff --git a/driver/wasm3/m3_code.h b/driver/wasm3/m3_code.h new file mode 100644 index 0000000..25dcb81 --- /dev/null +++ b/driver/wasm3/m3_code.h @@ -0,0 +1,80 @@ +// +// m3_code.h +// +// Created by Steven Massey on 4/19/19. +// Copyright © 2019 Steven Massey. All rights reserved. +// + +#ifndef m3_code_h +#define m3_code_h + +#include "m3_core.h" + +d_m3BeginExternC + +typedef struct M3CodePage +{ + M3CodePageHeader info; + code_t code [1]; +} +M3CodePage; + +typedef M3CodePage * IM3CodePage; + + +IM3CodePage NewCodePage (IM3Runtime i_runtime, u32 i_minNumLines); + +void FreeCodePages (IM3CodePage * io_list); + +u32 NumFreeLines (IM3CodePage i_page); +pc_t GetPageStartPC (IM3CodePage i_page); +pc_t GetPagePC (IM3CodePage i_page); +void EmitWord_impl (IM3CodePage i_page, void* i_word); +void EmitWord32 (IM3CodePage i_page, u32 i_word); +void EmitWord64 (IM3CodePage i_page, u64 i_word); +# if d_m3RecordBacktraces +void EmitMappingEntry (IM3CodePage i_page, u32 i_moduleOffset); +# endif // d_m3RecordBacktraces + +void PushCodePage (IM3CodePage * io_list, IM3CodePage i_codePage); +IM3CodePage PopCodePage (IM3CodePage * io_list); + +IM3CodePage GetEndCodePage (IM3CodePage i_list); // i_list = NULL is valid +u32 CountCodePages (IM3CodePage i_list); // i_list = NULL is valid + +# if d_m3RecordBacktraces +bool ContainsPC (IM3CodePage i_page, pc_t i_pc); +bool MapPCToOffset (IM3CodePage i_page, pc_t i_pc, u32 * o_moduleOffset); +# endif // d_m3RecordBacktraces + +# ifdef DEBUG +void dump_code_page (IM3CodePage i_codePage, pc_t i_startPC); +# endif + +#define EmitWord(page, val) EmitWord_impl(page, (void*)(val)) + +//--------------------------------------------------------------------------------------------------------------------------------- + +# if d_m3RecordBacktraces + +typedef struct M3CodeMapEntry +{ + u32 pcOffset; + u32 moduleOffset; +} +M3CodeMapEntry; + +typedef struct M3CodeMappingPage +{ + pc_t basePC; + u32 size; + u32 capacity; + M3CodeMapEntry entries []; +} +M3CodeMappingPage; + +# endif // d_m3RecordBacktraces + +d_m3EndExternC + +#endif // m3_code_h diff --git a/driver/wasm3/m3_compile.c b/driver/wasm3/m3_compile.c new file mode 100644 index 0000000..643d8f0 --- /dev/null +++ b/driver/wasm3/m3_compile.c @@ -0,0 +1,3139 @@ +// +// m3_compile.c +// +// Created by Steven Massey on 4/17/19. +// Copyright © 2019 Steven Massey. All rights reserved. +// + +// Allow using opcodes for compilation process +#define M3_COMPILE_OPCODES + +#include "m3_env.h" +#include "m3_compile.h" +#include "m3_exec.h" +#include "m3_exception.h" +#include "m3_info.h" +#include "m3_validate.h" + +//----- EMIT -------------------------------------------------------------------------------------------------------------- + +static inline +pc_t GetPC (IM3Compilation o) +{ + return GetPagePC (o->page); +} + +static M3_NOINLINE +M3Result EnsureCodePageNumLines (IM3Compilation o, u32 i_numLines) +{ + M3Result result = m3Err_none; + + i_numLines += 2; // room for Bridge + + if (NumFreeLines (o->page) < i_numLines) + { + IM3CodePage page = AcquireCodePageWithCapacity (o->runtime, i_numLines); + + if (page) + { + m3log (emit, "bridging new code page from: %d %p (free slots: %d) to: %d", o->page->info.sequence, GetPC (o), NumFreeLines (o->page), page->info.sequence); + d_m3Assert (NumFreeLines (o->page) >= 2); + + EmitWord (o->page, op_Branch); + EmitWord (o->page, GetPagePC (page)); + + ReleaseCodePage (o->runtime, o->page); + + o->page = page; + } + else result = m3Err_mallocFailedCodePage; + } + + return result; +} + +static M3_NOINLINE +M3Result EmitOp (IM3Compilation o, IM3Operation i_operation) +{ + M3Result result = m3Err_none; d_m3Assert (i_operation or IsStackPolymorphic (o)); + + // it's OK for page to be null; when compile-walking the bytecode without emitting + if (o->page) + { +# if d_m3EnableOpTracing + if (i_operation != op_DumpStack) + o->numEmits++; +# endif + + // have execution jump to a new page if slots are critically low + result = EnsureCodePageNumLines (o, d_m3CodePageFreeLinesThreshold); + + if (not result) + { if (d_m3LogEmit) log_emit (o, i_operation); +# if d_m3RecordBacktraces + EmitMappingEntry (o->page, o->lastOpcodeStart - o->module->wasmStart); +# endif // d_m3RecordBacktraces + EmitWord (o->page, i_operation); + } + } + + return result; +} + +// Push an immediate constant into the M3 codestream +static M3_NOINLINE +void EmitConstant32 (IM3Compilation o, const u32 i_immediate) +{ + if (o->page) + EmitWord32 (o->page, i_immediate); +} + +static M3_NOINLINE +void EmitSlotOffset (IM3Compilation o, const i32 i_offset) +{ + if (o->page) + EmitWord32 (o->page, i_offset); +} + +static M3_NOINLINE +pc_t EmitPointer (IM3Compilation o, const void * const i_pointer) +{ + pc_t ptr = GetPagePC (o->page); + + if (o->page) + EmitWord (o->page, i_pointer); + + return ptr; +} + +static M3_NOINLINE +void * ReservePointer (IM3Compilation o) +{ + pc_t ptr = GetPagePC (o->page); + EmitPointer (o, NULL); + return (void *) ptr; +} + + +//------------------------------------------------------------------------------------------------------------------------- + +#define d_indent " | %s" + +// just want less letters and numbers to stare at down the way in the compiler table +#define i_32 c_m3Type_i32 +#define i_64 c_m3Type_i64 +#define f_32 c_m3Type_f32 +#define f_64 c_m3Type_f64 +#define none c_m3Type_none +#define any (u8)-1 + +#if d_m3HasFloat +# define FPOP(x) x +#else +# define FPOP(x) NULL +#endif + +static const IM3Operation c_preserveSetSlot [] = { NULL, op_PreserveSetSlot_i32, op_PreserveSetSlot_i64, + FPOP(op_PreserveSetSlot_f32), FPOP(op_PreserveSetSlot_f64) }; +static const IM3Operation c_setSetOps [] = { NULL, op_SetSlot_i32, op_SetSlot_i64, + FPOP(op_SetSlot_f32), FPOP(op_SetSlot_f64) }; +static const IM3Operation c_setGlobalOps [] = { NULL, op_SetGlobal_i32, op_SetGlobal_i64, + FPOP(op_SetGlobal_f32), FPOP(op_SetGlobal_f64) }; +static const IM3Operation c_setRegisterOps [] = { NULL, op_SetRegister_i32, op_SetRegister_i64, + FPOP(op_SetRegister_f32), FPOP(op_SetRegister_f64) }; + +static const IM3Operation c_intSelectOps [2] [4] = { { op_Select_i32_rss, op_Select_i32_srs, op_Select_i32_ssr, op_Select_i32_sss }, + { op_Select_i64_rss, op_Select_i64_srs, op_Select_i64_ssr, op_Select_i64_sss } }; +#if d_m3HasFloat +static const IM3Operation c_fpSelectOps [2] [2] [3] = { { { op_Select_f32_sss, op_Select_f32_srs, op_Select_f32_ssr }, // selector in slot + { op_Select_f32_rss, op_Select_f32_rrs, op_Select_f32_rsr } }, // selector in reg + { { op_Select_f64_sss, op_Select_f64_srs, op_Select_f64_ssr }, // selector in slot + { op_Select_f64_rss, op_Select_f64_rrs, op_Select_f64_rsr } } }; // selector in reg +#endif + +// all args & returns are 64-bit aligned, so use 2 slots for a d_m3Use32BitSlots=1 build +static const u16 c_ioSlotCount = sizeof (u64) / sizeof (m3slot_t); + +static +M3Result AcquireCompilationCodePage (IM3Compilation o, IM3CodePage * o_codePage) +{ + M3Result result = m3Err_none; + + IM3CodePage page = AcquireCodePage (o->runtime); + + if (page) + { +# if (d_m3EnableCodePageRefCounting) + { + if (o->function) + { + IM3Function func = o->function; + page->info.usageCount++; + + u32 index = func->numCodePageRefs++; +_ (m3ReallocArray (& func->codePageRefs, IM3CodePage, func->numCodePageRefs, index)); + func->codePageRefs [index] = page; + } + } +# endif + } + else _throw (m3Err_mallocFailedCodePage); + + _catch: + + * o_codePage = page; + + return result; +} + +static inline +void ReleaseCompilationCodePage (IM3Compilation o) +{ + ReleaseCodePage (o->runtime, o->page); +} + +static inline +u16 GetTypeNumSlots (u8 i_type) +{ + // v128 is 16 bytes - 4 slots in 32-bit-slot mode, 2 in 64-bit. + // (Slot-allocator only; no v128 ops execute.) + if (i_type == c_m3Type_v128) +# if d_m3Use32BitSlots + return 4; +# else + return 2; +# endif +# if d_m3Use32BitSlots + return Is64BitType (i_type) ? 2 : 1; +# else + return 1; +# endif +} + +static inline +void AlignSlotToType (u16 * io_slot, u8 i_type) +{ + // align 64-bit words to even slots (if d_m3Use32BitSlots) + u16 numSlots = GetTypeNumSlots (i_type); + + u16 mask = numSlots - 1; + * io_slot = (* io_slot + mask) & ~mask; +} + +static inline +i16 GetStackTopIndex (IM3Compilation o) +{ d_m3Assert (o->stackIndex > o->stackFirstDynamicIndex or IsStackPolymorphic (o)); + return o->stackIndex - 1; +} + + +// Items in the static portion of the stack (args/locals) are hidden from GetStackTypeFromTop () +// In other words, only "real" Wasm stack items can be inspected. This is important when +// returning values, etc. and you need an accurate wasm-view of the stack. +static +u8 GetStackTypeFromTop (IM3Compilation o, u16 i_offset) +{ + u8 type = c_m3Type_none; + + ++i_offset; + if (o->stackIndex >= i_offset) + { + u16 index = o->stackIndex - i_offset; + + if (index >= o->stackFirstDynamicIndex) + type = o->typeStack [index]; + } + + return type; +} + +static inline +u8 GetStackTopType (IM3Compilation o) +{ + return GetStackTypeFromTop (o, 0); +} + +static inline +u8 GetStackTypeFromBottom (IM3Compilation o, u16 i_offset) +{ + u8 type = c_m3Type_none; + + if (i_offset < o->stackIndex) + type = o->typeStack [i_offset]; + + return type; +} + + +static inline bool IsConstantSlot (IM3Compilation o, u16 i_slot) { return (i_slot >= o->slotFirstConstIndex and i_slot < o->slotMaxConstIndex); } +static inline bool IsSlotAllocated (IM3Compilation o, u16 i_slot) { return o->m3Slots [i_slot]; } + +static inline +bool IsStackIndexInRegister (IM3Compilation o, i32 i_stackIndex) +{ d_m3Assert (i_stackIndex < o->stackIndex or IsStackPolymorphic (o)); + if (i_stackIndex >= 0 and i_stackIndex < o->stackIndex) + return (o->wasmStack [i_stackIndex] >= d_m3Reg0SlotAlias); + else + return false; +} + +static inline u16 GetNumBlockValuesOnStack (IM3Compilation o) { return o->stackIndex - o->block.blockStackIndex; } + +static inline bool IsStackTopInRegister (IM3Compilation o) { return IsStackIndexInRegister (o, (i32) GetStackTopIndex (o)); } +static inline bool IsStackTopMinus1InRegister (IM3Compilation o) { return IsStackIndexInRegister (o, (i32) GetStackTopIndex (o) - 1); } +static inline bool IsStackTopMinus2InRegister (IM3Compilation o) { return IsStackIndexInRegister (o, (i32) GetStackTopIndex (o) - 2); } + +static inline bool IsStackTopInSlot (IM3Compilation o) { return not IsStackTopInRegister (o); } + +static inline bool IsValidSlot (u16 i_slot) { return (i_slot < d_m3MaxFunctionSlots); } + +static inline +u16 GetStackTopSlotNumber (IM3Compilation o) +{ + i16 i = GetStackTopIndex (o); + + u16 slot = c_slotUnused; + + if (i >= 0) + slot = o->wasmStack [i]; + + return slot; +} + + +// from bottom +static inline +u16 GetSlotForStackIndex (IM3Compilation o, u16 i_stackIndex) +{ d_m3Assert (i_stackIndex < o->stackIndex or IsStackPolymorphic (o)); + u16 slot = c_slotUnused; + + if (i_stackIndex < o->stackIndex) + slot = o->wasmStack [i_stackIndex]; + + return slot; +} + +static inline +u16 GetExtraSlotForStackIndex (IM3Compilation o, u16 i_stackIndex) +{ + u16 baseSlot = GetSlotForStackIndex (o, i_stackIndex); + + if (baseSlot != c_slotUnused) + { + u16 extraSlot = GetTypeNumSlots (GetStackTypeFromBottom (o, i_stackIndex)) - 1; + baseSlot += extraSlot; + } + + return baseSlot; +} + + +static inline +void TouchSlot (IM3Compilation o, u16 i_slot) +{ + // op_Entry uses this value to track and detect stack overflow + o->maxStackSlots = M3_MAX (o->maxStackSlots, i_slot + 1); +} + +static inline +void MarkSlotAllocated (IM3Compilation o, u16 i_slot) +{ d_m3Assert (o->m3Slots [i_slot] == 0); // shouldn't be already allocated + o->m3Slots [i_slot] = 1; + + o->slotMaxAllocatedIndexPlusOne = M3_MAX (o->slotMaxAllocatedIndexPlusOne, i_slot + 1); + + TouchSlot (o, i_slot); +} + +static inline +M3Result MarkSlotsAllocated (IM3Compilation o, u16 i_slot, u16 i_numSlots) +{ + if (i_slot + i_numSlots > d_m3MaxFunctionSlots) + return m3Err_functionStackOverflow; + + while (i_numSlots--) + MarkSlotAllocated (o, i_slot++); + + return m3Err_none; +} + +static inline +M3Result MarkSlotsAllocatedByType (IM3Compilation o, u16 i_slot, u8 i_type) +{ + u16 numSlots = GetTypeNumSlots (i_type); + return MarkSlotsAllocated (o, i_slot, numSlots); +} + + +static +M3Result AllocateSlotsWithinRange (IM3Compilation o, u16 * o_slot, u8 i_type, u16 i_startSlot, u16 i_endSlot) +{ + M3Result result = m3Err_functionStackOverflow; + + u16 numSlots = GetTypeNumSlots (i_type); + u16 searchOffset = numSlots - 1; + + AlignSlotToType (& i_startSlot, i_type); + + // search for 1 or 2 consecutive slots in the execution stack + u16 i = i_startSlot; + while (i + searchOffset < i_endSlot) + { + if (i + searchOffset < d_m3MaxFunctionSlots and o->m3Slots [i] == 0 and o->m3Slots [i + searchOffset] == 0) + { + MarkSlotsAllocated (o, i, numSlots); + + * o_slot = i; + result = m3Err_none; + break; + } + + // keep 2-slot allocations even-aligned + i += numSlots; + } + + return result; +} + +static inline +M3Result AllocateSlots (IM3Compilation o, u16 * o_slot, u8 i_type) +{ + return AllocateSlotsWithinRange (o, o_slot, i_type, o->slotFirstDynamicIndex, d_m3MaxFunctionSlots); +} + +static inline +M3Result AllocateConstantSlots (IM3Compilation o, u16 * o_slot, u8 i_type) +{ + u16 maxTableIndex = o->slotFirstConstIndex + d_m3MaxConstantTableSize; + return AllocateSlotsWithinRange (o, o_slot, i_type, o->slotFirstConstIndex, M3_MIN(o->slotFirstDynamicIndex, maxTableIndex)); +} + + +// TOQUE: this usage count system could be eliminated. real world code doesn't frequently trigger it. just copy to multiple +// unique slots. +static inline +M3Result IncrementSlotUsageCount (IM3Compilation o, u16 i_slot) +{ d_m3Assert (i_slot < d_m3MaxFunctionSlots); + M3Result result = m3Err_none; d_m3Assert (o->m3Slots [i_slot] > 0); + + // OPTZ (memory): 'm3Slots' could still be fused with 'typeStack' if 4 bits were used to indicate: [0,1,2,many]. The many-case + // would scan 'wasmStack' to determine the actual usage count + if (o->m3Slots [i_slot] < 0xFF) + { + o->m3Slots [i_slot]++; + } + else result = "slot usage count overflow"; + + return result; +} + +static inline +void DeallocateSlot (IM3Compilation o, i16 i_slot, u8 i_type) +{ d_m3Assert (i_slot >= o->slotFirstDynamicIndex); + d_m3Assert (i_slot < o->slotMaxAllocatedIndexPlusOne); + for (u16 i = 0; i < GetTypeNumSlots (i_type); ++i, ++i_slot) + { d_m3Assert (o->m3Slots [i_slot]); + -- o->m3Slots [i_slot]; + } +} + + +static inline +bool IsRegisterTypeAllocated (IM3Compilation o, u8 i_type) +{ + return IsRegisterAllocated (o, IsFpType (i_type)); +} + +static inline +void AllocateRegister (IM3Compilation o, u32 i_register, u16 i_stackIndex) +{ d_m3Assert (not IsRegisterAllocated (o, i_register)); + o->regStackIndexPlusOne [i_register] = i_stackIndex + 1; +} + +static inline +void DeallocateRegister (IM3Compilation o, u32 i_register) +{ d_m3Assert (IsRegisterAllocated (o, i_register)); + o->regStackIndexPlusOne [i_register] = c_m3RegisterUnallocated; +} + +static inline +u16 GetRegisterStackIndex (IM3Compilation o, u32 i_register) +{ d_m3Assert (IsRegisterAllocated (o, i_register)); + return o->regStackIndexPlusOne [i_register] - 1; +} + +u16 GetMaxUsedSlotPlusOne (IM3Compilation o) +{ + while (o->slotMaxAllocatedIndexPlusOne > o->slotFirstDynamicIndex) + { + if (IsSlotAllocated (o, o->slotMaxAllocatedIndexPlusOne - 1)) + break; + + o->slotMaxAllocatedIndexPlusOne--; + } + +# ifdef DEBUG + u16 maxSlot = o->slotMaxAllocatedIndexPlusOne; + while (maxSlot < d_m3MaxFunctionSlots) + { + d_m3Assert (o->m3Slots [maxSlot] == 0); + maxSlot++; + } +# endif + + return o->slotMaxAllocatedIndexPlusOne; +} + +static +M3Result PreserveRegisterIfOccupied (IM3Compilation o, u8 i_registerType) +{ + M3Result result = m3Err_none; + + u32 regSelect = IsFpType (i_registerType); + + if (IsRegisterAllocated (o, regSelect)) + { + u16 stackIndex = GetRegisterStackIndex (o, regSelect); + DeallocateRegister (o, regSelect); + + u8 type = GetStackTypeFromBottom (o, stackIndex); + + // and point to a exec slot + u16 slot = c_slotUnused; +_ (AllocateSlots (o, & slot, type)); + o->wasmStack [stackIndex] = slot; + + // Ensure type is within the valid range + if (type < sizeof(c_setSetOps) / sizeof(c_setSetOps[0])) { +_ (EmitOp (o, c_setSetOps [type])); + } else + _throw(m3Err_functionStackOverflow); + + EmitSlotOffset (o, slot); + } + + _catch: return result; +} + + +// all values must be in slots before entering loop, if, and else blocks +// otherwise they'd end up preserve-copied in the block to probably different locations (if/else) +static inline +M3Result PreserveRegisters (IM3Compilation o) +{ + M3Result result; + +_ (PreserveRegisterIfOccupied (o, c_m3Type_f64)); +_ (PreserveRegisterIfOccupied (o, c_m3Type_i64)); + + _catch: return result; +} + +static +M3Result PreserveNonTopRegisters (IM3Compilation o) +{ + M3Result result = m3Err_none; + + i16 stackTop = GetStackTopIndex (o); + + if (stackTop >= 0) + { + if (IsRegisterAllocated (o, 0)) // r0 + { + if (GetRegisterStackIndex (o, 0) != stackTop) +_ (PreserveRegisterIfOccupied (o, c_m3Type_i64)); + } + + if (IsRegisterAllocated (o, 1)) // fp0 + { + if (GetRegisterStackIndex (o, 1) != stackTop) +_ (PreserveRegisterIfOccupied (o, c_m3Type_f64)); + } + } + + _catch: return result; +} + + +//---------------------------------------------------------------------------------------------------------------------- + +static +M3Result Push (IM3Compilation o, u8 i_type, u16 i_slot) +{ + M3Result result = m3Err_none; + +#if !d_m3HasFloat + if (i_type == c_m3Type_f32 || i_type == c_m3Type_f64) { + return m3Err_unknownOpcode; + } +#endif + + u16 stackIndex = o->stackIndex++; // printf ("push: %d\n", (i32) i); + + if (stackIndex < d_m3MaxFunctionStackHeight) + { + o->wasmStack [stackIndex] = i_slot; + o->typeStack [stackIndex] = i_type; + + if (IsRegisterSlotAlias (i_slot)) + { + u32 regSelect = IsFpRegisterSlotAlias (i_slot); + AllocateRegister (o, regSelect, stackIndex); + } + + if (d_m3LogWasmStack) dump_type_stack (o); + } + else result = m3Err_functionStackOverflow; + + return result; +} + +static inline +M3Result PushRegister (IM3Compilation o, u8 i_type) +{ + M3Result result = m3Err_none; d_m3Assert ((u16) d_m3Reg0SlotAlias > (u16) d_m3MaxFunctionSlots); + u16 slot = IsFpType (i_type) ? d_m3Fp0SlotAlias : d_m3Reg0SlotAlias; d_m3Assert (i_type or IsStackPolymorphic (o)); + +_ (Push (o, i_type, slot)); + + _catch: return result; +} + +static +M3Result Pop (IM3Compilation o) +{ + M3Result result = m3Err_none; + + if (o->stackIndex > o->block.blockStackIndex) + { + o->stackIndex--; // printf ("pop: %d\n", (i32) o->stackIndex); + + u16 slot = o->wasmStack [o->stackIndex]; + u8 type = o->typeStack [o->stackIndex]; + + if (IsRegisterSlotAlias (slot)) + { + u32 regSelect = IsFpRegisterSlotAlias (slot); + DeallocateRegister (o, regSelect); + } + else if (slot < 0 || slot >= o->slotMaxAllocatedIndexPlusOne) { + return m3Err_functionStackUnderrun; // Return error for invalid slot indices + } + else if (slot >= o->slotFirstDynamicIndex) + { + DeallocateSlot (o, slot, type); + } + } + else if (not IsStackPolymorphic (o)) + result = m3Err_functionStackUnderrun; + + return result; +} + +static +M3Result PopType (IM3Compilation o, u8 i_type) +{ + M3Result result = m3Err_none; + + u8 topType = GetStackTopType (o); + + if (i_type == topType or o->block.isPolymorphic) + { +_ (Pop (o)); + } + else _throw (m3Err_typeMismatch); + + _catch: + return result; +} + +static +M3Result _PushAllocatedSlotAndEmit (IM3Compilation o, u8 i_type, bool i_doEmit) +{ + M3Result result = m3Err_none; + + u16 slot = c_slotUnused; + +_ (AllocateSlots (o, & slot, i_type)); +_ (Push (o, i_type, slot)); + + if (i_doEmit) + EmitSlotOffset (o, slot); + +// printf ("push: %d\n", (u32) slot); + + _catch: return result; +} + +static inline +M3Result PushAllocatedSlotAndEmit (IM3Compilation o, u8 i_type) +{ + return _PushAllocatedSlotAndEmit (o, i_type, true); +} + +static inline +M3Result PushAllocatedSlot (IM3Compilation o, u8 i_type) +{ + return _PushAllocatedSlotAndEmit (o, i_type, false); +} + +static +M3Result PushConst (IM3Compilation o, u64 i_word, u8 i_type) +{ + M3Result result = m3Err_none; + + // Early-exit if we're not emitting + if (!o->page) return result; + + bool matchFound = false; + bool is64BitType = Is64BitType (i_type); + + u16 numRequiredSlots = GetTypeNumSlots (i_type); + u16 numUsedConstSlots = o->slotMaxConstIndex - o->slotFirstConstIndex; + + // search for duplicate matching constant slot to reuse + if (numRequiredSlots == 2 and numUsedConstSlots >= 2) + { + u16 firstConstSlot = o->slotFirstConstIndex; + AlignSlotToType (& firstConstSlot, c_m3Type_i64); + + for (u16 slot = firstConstSlot; slot < o->slotMaxConstIndex - 1; slot += 2) + { + if (IsSlotAllocated (o, slot) and IsSlotAllocated (o, slot + 1)) + { + u64 constant; + memcpy (&constant, &o->constants [slot - o->slotFirstConstIndex], sizeof(constant)); + + if (constant == i_word) + { + matchFound = true; +_ (Push (o, i_type, slot)); + break; + } + } + } + } + else if (numRequiredSlots == 1) + { + for (u16 i = 0; i < numUsedConstSlots; ++i) + { + u16 slot = o->slotFirstConstIndex + i; + + if (IsSlotAllocated (o, slot)) + { + bool matches; + if (is64BitType) { + u64 constant; + memcpy (&constant, &o->constants [i], sizeof(constant)); + matches = (constant == i_word); + } else { + u32 constant; + memcpy (&constant, &o->constants [i], sizeof(constant)); + matches = (constant == i_word); + } + if (matches) + { + matchFound = true; +_ (Push (o, i_type, slot)); + break; + } + } + } + } + + if (not matchFound) + { + u16 slot = c_slotUnused; + result = AllocateConstantSlots (o, & slot, i_type); + + if (result || slot == c_slotUnused) // no more constant table space; use inline constants + { + result = m3Err_none; + + if (is64BitType) { +_ (EmitOp (o, op_Const64)); + EmitWord64 (o->page, i_word); + } else { +_ (EmitOp (o, op_Const32)); + EmitWord32 (o->page, (u32) i_word); + } + +_ (PushAllocatedSlotAndEmit (o, i_type)); + } + else + { + u16 constTableIndex = slot - o->slotFirstConstIndex; + + d_m3Assert(constTableIndex < d_m3MaxConstantTableSize); + + if (is64BitType) { + memcpy (& o->constants [constTableIndex], &i_word, sizeof(i_word)); + } else { + u32 word32 = i_word; + memcpy (& o->constants [constTableIndex], &word32, sizeof(word32)); + } + +_ (Push (o, i_type, slot)); + + o->slotMaxConstIndex = M3_MAX (slot + numRequiredSlots, o->slotMaxConstIndex); + } + } + + _catch: return result; +} + +static inline +M3Result EmitSlotNumOfStackTopAndPop (IM3Compilation o) +{ + // no emit if value is in register + if (IsStackTopInSlot (o)) + EmitSlotOffset (o, GetStackTopSlotNumber (o)); + + return Pop (o); +} + + +// Or, maybe: EmitTrappingOp +M3Result AddTrapRecord (IM3Compilation o) +{ + M3Result result = m3Err_none; + + if (o->function) + { + } + + return result; +} + +static +M3Result UnwindBlockStack (IM3Compilation o) +{ + M3Result result = m3Err_none; + + u32 popCount = 0; + while (o->stackIndex > o->block.blockStackIndex) + { +_ (Pop (o)); + ++popCount; + } + + if (popCount) + { + m3log (compile, "unwound stack top: %d", popCount); + } + + _catch: return result; +} + +static inline +M3Result SetStackPolymorphic (IM3Compilation o) +{ + o->block.isPolymorphic = true; m3log (compile, "stack set polymorphic"); + return UnwindBlockStack (o); +} + +static +void PatchBranches (IM3Compilation o) +{ + pc_t pc = GetPC (o); + + pc_t patches = o->block.patches; + o->block.patches = NULL; + + while (patches) + { m3log (compile, "patching location: %p to pc: %p", patches, pc); + pc_t next = * (pc_t *) patches; + * (pc_t *) patches = pc; + patches = next; + } +} + +//------------------------------------------------------------------------------------------------------------------------- + +static +M3Result CopyStackIndexToSlot (IM3Compilation o, u16 i_destSlot, u16 i_stackIndex) // NoPushPop +{ + M3Result result = m3Err_none; + + IM3Operation op; + + u8 type = GetStackTypeFromBottom (o, i_stackIndex); + bool inRegister = IsStackIndexInRegister (o, i_stackIndex); + + if (inRegister) + { + op = c_setSetOps [type]; + } + else op = Is64BitType (type) ? op_CopySlot_64 : op_CopySlot_32; + +_ (EmitOp (o, op)); + EmitSlotOffset (o, i_destSlot); + + if (not inRegister) + { + u16 srcSlot = GetSlotForStackIndex (o, i_stackIndex); + EmitSlotOffset (o, srcSlot); + } + + _catch: return result; +} + +static +M3Result CopyStackTopToSlot (IM3Compilation o, u16 i_destSlot) // NoPushPop +{ + M3Result result; + + i16 stackTop = GetStackTopIndex (o); +_ (CopyStackIndexToSlot (o, i_destSlot, (u16) stackTop)); + + _catch: return result; +} + + +// a copy-on-write strategy is used with locals. when a get local occurs, it's not copied anywhere. the stack +// entry just has a index pointer to that local memory slot. +// then, when a previously referenced local is set, the current value needs to be preserved for those references + +// TODO: consider getting rid of these specialized operations: PreserveSetSlot & PreserveCopySlot. +// They likely just take up space (which seems to reduce performance) without improving performance. +static +M3Result PreservedCopyTopSlot (IM3Compilation o, u16 i_destSlot, u16 i_preserveSlot) +{ + M3Result result = m3Err_none; d_m3Assert (i_destSlot != i_preserveSlot); + + IM3Operation op; + + u8 type = GetStackTopType (o); + + if (IsStackTopInRegister (o)) + { + op = c_preserveSetSlot [type]; + } + else op = Is64BitType (type) ? op_PreserveCopySlot_64 : op_PreserveCopySlot_32; + +_ (EmitOp (o, op)); + EmitSlotOffset (o, i_destSlot); + + if (IsStackTopInSlot (o)) + EmitSlotOffset (o, GetStackTopSlotNumber (o)); + + EmitSlotOffset (o, i_preserveSlot); + + _catch: return result; +} + +static +M3Result CopyStackTopToRegister (IM3Compilation o, bool i_updateStack) +{ + M3Result result = m3Err_none; + + if (IsStackTopInSlot (o)) + { + u8 type = GetStackTopType (o); + +_ (PreserveRegisterIfOccupied (o, type)); + + IM3Operation op = c_setRegisterOps [type]; + +_ (EmitOp (o, op)); + EmitSlotOffset (o, GetStackTopSlotNumber (o)); + + if (i_updateStack) + { +_ (PopType (o, type)); +_ (PushRegister (o, type)); + } + } + + _catch: return result; +} + + +// if local is unreferenced, o_preservedSlotNumber will be equal to localIndex on return +static +M3Result FindReferencedLocalWithinCurrentBlock (IM3Compilation o, u16 * o_preservedSlotNumber, u32 i_localSlot) +{ + M3Result result = m3Err_none; + + IM3CompilationScope scope = & o->block; + u16 startIndex = scope->blockStackIndex; + + while (scope->opcode == c_waOp_block) + { + scope = scope->outer; + if (not scope) + break; + + startIndex = scope->blockStackIndex; + } + + * o_preservedSlotNumber = (u16) i_localSlot; + + for (u32 i = startIndex; i < o->stackIndex; ++i) + { + if (o->wasmStack [i] == i_localSlot) + { + if (* o_preservedSlotNumber == i_localSlot) + { + u8 type = GetStackTypeFromBottom (o, i); d_m3Assert (type != c_m3Type_none) + +_ (AllocateSlots (o, o_preservedSlotNumber, type)); + } + else +_ (IncrementSlotUsageCount (o, * o_preservedSlotNumber)); + + o->wasmStack [i] = * o_preservedSlotNumber; + } + } + + _catch: return result; +} + +static +M3Result GetBlockScope (IM3Compilation o, IM3CompilationScope * o_scope, u32 i_depth) +{ + M3Result result = m3Err_none; + + IM3CompilationScope scope = & o->block; + + while (i_depth--) + { + scope = scope->outer; + _throwif ("invalid block depth", not scope); + } + + * o_scope = scope; + + _catch: + return result; +} + +static +M3Result CopyStackSlotsR (IM3Compilation o, u16 i_targetSlotStackIndex, u16 i_stackIndex, u16 i_endStackIndex, u16 i_tempSlot) +{ + M3Result result = m3Err_none; + + if (i_stackIndex < i_endStackIndex) + { + u16 srcSlot = GetSlotForStackIndex (o, i_stackIndex); + + u8 type = GetStackTypeFromBottom (o, i_stackIndex); + u16 numSlots = GetTypeNumSlots (type); + u16 extraSlot = numSlots - 1; + + u16 targetSlot = GetSlotForStackIndex (o, i_targetSlotStackIndex); + + u16 preserveIndex = i_stackIndex; + u16 collisionSlot = srcSlot; + + if (targetSlot != srcSlot) + { + // search for collisions + u16 checkIndex = i_stackIndex + 1; + while (checkIndex < i_endStackIndex) + { + u16 otherSlot1 = GetSlotForStackIndex (o, checkIndex); + u16 otherSlot2 = GetExtraSlotForStackIndex (o, checkIndex); + + if (targetSlot == otherSlot1 or + targetSlot == otherSlot2 or + targetSlot + extraSlot == otherSlot1) + { + _throwif (m3Err_functionStackOverflow, i_tempSlot >= d_m3MaxFunctionSlots); + +_ (CopyStackIndexToSlot (o, i_tempSlot, checkIndex)); + o->wasmStack [checkIndex] = i_tempSlot; + i_tempSlot += GetTypeNumSlots (c_m3Type_i64); + TouchSlot (o, i_tempSlot - 1); + + // restore this on the way back down + preserveIndex = checkIndex; + collisionSlot = otherSlot1; + + break; + } + + ++checkIndex; + } + +_ (CopyStackIndexToSlot (o, targetSlot, i_stackIndex)); m3log (compile, " copying slot: %d to slot: %d", srcSlot, targetSlot); + o->wasmStack [i_stackIndex] = targetSlot; + + } + +_ (CopyStackSlotsR (o, i_targetSlotStackIndex + 1, i_stackIndex + 1, i_endStackIndex, i_tempSlot)); + + // restore the stack state + o->wasmStack [i_stackIndex] = srcSlot; + o->wasmStack [preserveIndex] = collisionSlot; + } + + _catch: + return result; +} + +static +M3Result ResolveBlockResults (IM3Compilation o, IM3CompilationScope i_targetBlock, bool i_isBranch) +{ + M3Result result = m3Err_none; if (d_m3LogWasmStack) dump_type_stack (o); + + bool isLoop = (i_targetBlock->opcode == c_waOp_loop and i_isBranch); + + u16 numParams = GetFuncTypeNumParams (i_targetBlock->type); + u16 numResults = GetFuncTypeNumResults (i_targetBlock->type); + + u16 slotRecords = i_targetBlock->exitStackIndex; + + u16 numValues; + + if (not isLoop) + { + numValues = numResults; + slotRecords += numParams; + } + else numValues = numParams; + + u16 blockHeight = GetNumBlockValuesOnStack (o); + + _throwif (m3Err_typeCountMismatch, i_isBranch ? (blockHeight < numValues) : (blockHeight != numValues)); + + if (numValues) + { + u16 endIndex = GetStackTopIndex (o) + 1; + u16 numRemValues = numValues; + + // The last result is taken from _fp0. See PushBlockResults. + if (not isLoop and IsFpType (GetStackTopType (o))) + { +_ (CopyStackTopToRegister (o, false)); + --endIndex; + --numRemValues; + } + + // TODO: tempslot affects maxStackSlots, so can grow unnecess each time. + u16 tempSlot = o->maxStackSlots;// GetMaxUsedSlotPlusOne (o); doesn't work cause can collide with slotRecords + AlignSlotToType (& tempSlot, c_m3Type_i64); + +_ (CopyStackSlotsR (o, slotRecords, endIndex - numRemValues, endIndex, tempSlot)); + + if (d_m3LogWasmStack) dump_type_stack (o); + } + + _catch: return result; +} + + +static +M3Result ReturnValues (IM3Compilation o, IM3CompilationScope i_functionBlock, bool i_isBranch) +{ + M3Result result = m3Err_none; if (d_m3LogWasmStack) dump_type_stack (o); + + u16 numReturns = GetFuncTypeNumResults (i_functionBlock->type); // could just o->function too... + u16 blockHeight = GetNumBlockValuesOnStack (o); + + if (not IsStackPolymorphic (o)) + _throwif (m3Err_typeCountMismatch, i_isBranch ? (blockHeight < numReturns) : (blockHeight != numReturns)); + + if (numReturns) + { + // return slots like args are 64-bit aligned + u16 returnSlot = numReturns * c_ioSlotCount; + u16 stackTop = GetStackTopIndex (o); + + for (u16 i = 0; i < numReturns; ++i) + { + u8 returnType = GetFuncTypeResultType (i_functionBlock->type, numReturns - 1 - i); + + u8 stackType = GetStackTypeFromTop (o, i); // using FromTop so that only dynamic items are checked + + if (IsStackPolymorphic (o) and stackType == c_m3Type_none) + stackType = returnType; + + _throwif (m3Err_typeMismatch, returnType != stackType); + + if (not IsStackPolymorphic (o)) + { + returnSlot -= c_ioSlotCount; +_ (CopyStackIndexToSlot (o, returnSlot, stackTop--)); + } + } + + if (not i_isBranch) + { + while (numReturns--) +_ (Pop (o)); + } + } + + _catch: return result; +} + + +//------------------------------------------------------------------------------------------------------------------------- + +static +M3Result Compile_Const_i32 (IM3Compilation o, m3opcode_t i_opcode) +{ + M3Result result; + + i32 value; +_ (ReadLEB_i32 (& value, & o->wasm, o->wasmEnd)); +_ (PushConst (o, value, c_m3Type_i32)); m3log (compile, d_indent " (const i32 = %" PRIi32 ")", get_indention_string (o), value); + _catch: return result; +} + +static +M3Result Compile_Const_i64 (IM3Compilation o, m3opcode_t i_opcode) +{ + M3Result result; + + i64 value; +_ (ReadLEB_i64 (& value, & o->wasm, o->wasmEnd)); +_ (PushConst (o, value, c_m3Type_i64)); m3log (compile, d_indent " (const i64 = %" PRIi64 ")", get_indention_string (o), value); + _catch: return result; +} + + +#if d_m3ImplementFloat +static +M3Result Compile_Const_f32 (IM3Compilation o, m3opcode_t i_opcode) +{ + M3Result result; + + union { u32 u; f32 f; } value = { 0 }; + +_ (Read_f32 (& value.f, & o->wasm, o->wasmEnd)); m3log (compile, d_indent " (const f32 = %" PRIf32 ")", get_indention_string (o), value.f); +_ (PushConst (o, value.u, c_m3Type_f32)); + + _catch: return result; +} + +static +M3Result Compile_Const_f64 (IM3Compilation o, m3opcode_t i_opcode) +{ + M3Result result; + + union { u64 u; f64 f; } value = { 0 }; + +_ (Read_f64 (& value.f, & o->wasm, o->wasmEnd)); m3log (compile, d_indent " (const f64 = %" PRIf64 ")", get_indention_string (o), value.f); +_ (PushConst (o, value.u, c_m3Type_f64)); + + _catch: return result; +} +#endif + +#if d_m3CascadedOpcodes + +static +M3Result Compile_ExtendedOpcode (IM3Compilation o, m3opcode_t i_opcode) +{ +_try { + u8 opcode; +_ (Read_u8 (& opcode, & o->wasm, o->wasmEnd)); m3log (compile, d_indent " (FC: %" PRIi32 ")", get_indention_string (o), opcode); + + i_opcode = (i_opcode << 8) | opcode; + + //printf("Extended opcode: 0x%x\n", i_opcode); + + IM3OpInfo opInfo = GetOpInfo (i_opcode); + _throwif (m3Err_unknownOpcode, not opInfo); + + M3Compiler compiler = opInfo->compiler; + _throwif (m3Err_noCompiler, not compiler); + +_ ((* compiler) (o, i_opcode)); + + o->previousOpcode = i_opcode; + + } _catch: return result; +} +#endif + +static +M3Result Compile_Return (IM3Compilation o, m3opcode_t i_opcode) +{ + M3Result result = m3Err_none; + + if (not IsStackPolymorphic (o)) + { + IM3CompilationScope functionScope; +_ (GetBlockScope (o, & functionScope, o->block.depth)); + +_ (ReturnValues (o, functionScope, true)); + +_ (EmitOp (o, op_Return)); + +_ (SetStackPolymorphic (o)); + } + + _catch: return result; +} + +static +M3Result ValidateBlockEnd (IM3Compilation o) +{ + M3Result result = m3Err_none; + + u16 numResults = GetFuncTypeNumResults (o->block.type); + u16 blockHeight = GetNumBlockValuesOnStack (o); + + if (not IsStackPolymorphic (o)) + { + // Spec: at block end, stack height must match the number of results + _throwif (m3Err_typeCountMismatch, blockHeight != numResults); + + // Spec: result types must match expected types + for (u16 i = 0; i < numResults; ++i) + { + u8 expectedType = GetFuncTypeResultType (o->block.type, numResults - 1 - i); + u8 actualType = GetStackTypeFromTop (o, i); + _throwif (m3Err_typeMismatch, actualType != expectedType); + } + } + + _catch: return result; +} + +static +M3Result Compile_End (IM3Compilation o, m3opcode_t i_opcode) +{ + M3Result result = m3Err_none; //dump_type_stack (o); + + // function end: + if (o->block.depth == 0) + { + ValidateBlockEnd (o); + +// if (not IsStackPolymorphic (o)) + { + if (o->function) + { +_ (ReturnValues (o, & o->block, false)); + } + +_ (EmitOp (o, op_Return)); + } + } + + _catch: return result; +} + + +static +M3Result Compile_SetLocal (IM3Compilation o, m3opcode_t i_opcode) +{ + M3Result result; + + u32 localIndex; +_ (ReadLEB_u32 (& localIndex, & o->wasm, o->wasmEnd)); // printf ("--- set local: %d \n", localSlot); + + if (localIndex < GetFunctionNumArgsAndLocals (o->function)) + { + // Spec: value type must match local type + if (not IsStackPolymorphic (o)) + { + u8 localType = GetStackTypeFromBottom (o, localIndex); + u8 stackTopType = GetStackTopType (o); + _throwif (m3Err_typeMismatch, stackTopType != c_m3Type_none and localType != c_m3Type_none and stackTopType != localType); + } + + u16 localSlot = GetSlotForStackIndex (o, localIndex); + + u16 preserveSlot; +_ (FindReferencedLocalWithinCurrentBlock (o, & preserveSlot, localSlot)); // preserve will be different than local, if referenced + + if (preserveSlot == localSlot) +_ (CopyStackTopToSlot (o, localSlot)) + else +_ (PreservedCopyTopSlot (o, localSlot, preserveSlot)) + + if (i_opcode != c_waOp_teeLocal) +_ (Pop (o)); + } + else _throw ("local index out of bounds"); + + _catch: return result; +} + +static +M3Result Compile_GetLocal (IM3Compilation o, m3opcode_t i_opcode) +{ +_try { + + u32 localIndex; +_ (ReadLEB_u32 (& localIndex, & o->wasm, o->wasmEnd)); + + if (localIndex >= GetFunctionNumArgsAndLocals (o->function)) + _throw ("local index out of bounds"); + + u8 type = GetStackTypeFromBottom (o, localIndex); + u16 slot = GetSlotForStackIndex (o, localIndex); + +_ (Push (o, type, slot)); + + } _catch: return result; +} + +static +M3Result Compile_GetGlobal (IM3Compilation o, M3Global * i_global) +{ + M3Result result; + + IM3Operation op = Is64BitType (i_global->type) ? op_GetGlobal_s64 : op_GetGlobal_s32; +_ (EmitOp (o, op)); + EmitPointer (o, & i_global->i64Value); +_ (PushAllocatedSlotAndEmit (o, i_global->type)); + + _catch: return result; +} + +static +M3Result Compile_SetGlobal (IM3Compilation o, M3Global * i_global) +{ + M3Result result = m3Err_none; + + if (i_global->isMutable) + { + // Spec: value type must match global type + if (not IsStackPolymorphic (o)) + { + u8 stackTopType = GetStackTopType (o); + _throwif (m3Err_typeMismatch, stackTopType != c_m3Type_none and stackTopType != i_global->type); + } + + IM3Operation op; + u8 type = GetStackTopType (o); + + if (IsStackTopInRegister (o)) + { + op = c_setGlobalOps [type]; + } + else op = Is64BitType (type) ? op_SetGlobal_s64 : op_SetGlobal_s32; + +_ (EmitOp (o, op)); + EmitPointer (o, & i_global->i64Value); + + if (IsStackTopInSlot (o)) + EmitSlotOffset (o, GetStackTopSlotNumber (o)); + +_ (Pop (o)); + } + else _throw (m3Err_settingImmutableGlobal); + + _catch: return result; +} + +static +M3Result Compile_GetSetGlobal (IM3Compilation o, m3opcode_t i_opcode) +{ + M3Result result = m3Err_none; + + u32 globalIndex; +_ (ReadLEB_u32 (& globalIndex, & o->wasm, o->wasmEnd)); + + if (globalIndex < o->module->numGlobals) + { + if (o->module->globals) + { + M3Global * global = & o->module->globals [globalIndex]; + + // Spec: a constant expression may only read an imported global. + // The module's own globals are counted before their initializer is + // walked, so a bare index check would let one reference itself. + _throwif (m3Err_globaIndexOutOfBounds, o->isInitExpr and not global->imported); + +_ ((i_opcode == c_waOp_getGlobal) ? Compile_GetGlobal (o, global) : Compile_SetGlobal (o, global)); + } + else _throw (ErrorCompile (m3Err_globalMemoryNotAllocated, o, "module '%s' is missing global memory", o->module->name)); + } + else _throw (m3Err_globaIndexOutOfBounds); + + _catch: return result; +} + +static +void EmitPatchingBranchPointer (IM3Compilation o, IM3CompilationScope i_scope) +{ + pc_t patch = EmitPointer (o, i_scope->patches); m3log (compile, "branch patch required at: %p", patch); + i_scope->patches = patch; +} + +static +M3Result EmitPatchingBranch (IM3Compilation o, IM3CompilationScope i_scope) +{ + M3Result result = m3Err_none; + +_ (EmitOp (o, op_Branch)); + EmitPatchingBranchPointer (o, i_scope); + + _catch: return result; +} + +static +M3Result Compile_Branch (IM3Compilation o, m3opcode_t i_opcode) +{ + M3Result result; + + u32 depth; +_ (ReadLEB_u32 (& depth, & o->wasm, o->wasmEnd)); + + // Spec: br_if condition must be i32 + if (i_opcode == c_waOp_branchIf and not IsStackPolymorphic (o)) + { + u8 condType = GetStackTopType (o); + _throwif (m3Err_typeMismatch, condType != c_m3Type_none and condType != c_m3Type_i32); + } + + IM3CompilationScope scope; +_ (GetBlockScope (o, & scope, depth)); + + // branch target is a loop (continue) + if (scope->opcode == c_waOp_loop) + { + if (i_opcode == c_waOp_branchIf) + { + if (GetFuncTypeNumParams (scope->type)) + { + IM3Operation op = IsStackTopInRegister (o) ? op_BranchIfPrologue_r : op_BranchIfPrologue_s; + +_ (EmitOp (o, op)); +_ (EmitSlotNumOfStackTopAndPop (o)); + + pc_t * jumpTo = (pc_t *) ReservePointer (o); + +_ (ResolveBlockResults (o, scope, /* isBranch: */ true)); + +_ (EmitOp (o, op_ContinueLoop)); + EmitPointer (o, scope->pc); + + * jumpTo = GetPC (o); + } + else + { + // move the condition to a register +_ (CopyStackTopToRegister (o, false)); +_ (PopType (o, c_m3Type_i32)); + +_ (EmitOp (o, op_ContinueLoopIf)); + EmitPointer (o, scope->pc); + } + +// dump_type_stack(o); + } + else // is c_waOp_branch + { + _ (EmitOp (o, op_ContinueLoop)); + EmitPointer (o, scope->pc); + o->block.isPolymorphic = true; + } + } + else // forward branch + { + pc_t * jumpTo = NULL; + + bool isReturn = (scope->depth == 0); + bool targetHasResults = GetFuncTypeNumResults (scope->type); + + if (i_opcode == c_waOp_branchIf) + { + if (targetHasResults or isReturn) + { + IM3Operation op = IsStackTopInRegister (o) ? op_BranchIfPrologue_r : op_BranchIfPrologue_s; + + _ (EmitOp (o, op)); + _ (EmitSlotNumOfStackTopAndPop (o)); // condition + + // this is continuation point, if the branch isn't taken + jumpTo = (pc_t *) ReservePointer (o); + } + else + { + IM3Operation op = IsStackTopInRegister (o) ? op_BranchIf_r : op_BranchIf_s; + + _ (EmitOp (o, op)); + _ (EmitSlotNumOfStackTopAndPop (o)); // condition + + EmitPatchingBranchPointer (o, scope); + goto _catch; + } + } + + if (not IsStackPolymorphic (o)) + { + if (isReturn) + { +_ (ReturnValues (o, scope, true)); +_ (EmitOp (o, op_Return)); + } + else + { +_ (ResolveBlockResults (o, scope, true)); +_ (EmitPatchingBranch (o, scope)); + } + } + + if (jumpTo) + { + * jumpTo = GetPC (o); + } + + if (i_opcode == c_waOp_branch) +_ (SetStackPolymorphic (o)); + } + + _catch: return result; +} + +static +M3Result Compile_BranchTable (IM3Compilation o, m3opcode_t i_opcode) +{ +_try { + u32 targetCount; +_ (ReadLEB_u32 (& targetCount, & o->wasm, o->wasmEnd)); + + // Spec: validate that the branch index operand is i32 + if (not IsStackPolymorphic (o)) + { + u8 indexType = GetStackTopType (o); + _throwif (m3Err_typeMismatch, indexType != c_m3Type_none and indexType != c_m3Type_i32); + } + +_ (PreserveRegisterIfOccupied (o, c_m3Type_i64)); // move branch operand to a slot + u16 slot = GetStackTopSlotNumber (o); +_ (Pop (o)); + + // OPTZ: according to spec: "forward branches that target a control instruction with a non-empty + // result type consume matching operands first and push them back on the operand stack after unwinding" + // So, this move-to-reg is only necessary if the target scopes have a type. + + u32 numCodeLines = targetCount + 4; // 3 => IM3Operation + slot + target_count + default_target +_ (EnsureCodePageNumLines (o, numCodeLines)); + +_ (EmitOp (o, op_BranchTable)); + EmitSlotOffset (o, slot); + EmitConstant32 (o, targetCount); + + IM3CodePage continueOpPage = NULL; + +#if d_m3EnableValidation + // Spec: all br_table labels must have the same arity as the default label + // First pass: read all targets (including default which is the last one) and validate arities + u32 defaultArity = 0; + IM3FuncType defaultType = NULL; + bool defaultIsLoop = false; + + // We need to read all targets, validate arity, then generate code + // Save wasm position to read targets again for code generation + bytes_t targetListStart = o->wasm; + + // Pre-scan to get the default label type (last in the list) + { + bytes_t scanPos = o->wasm; + for (u32 i = 0; i <= targetCount; ++i) + { + u32 target; +_ (ReadLEB_u32 (& target, & scanPos, o->wasmEnd)); + + IM3CompilationScope scope; +_ (GetBlockScope (o, & scope, target)); + + if (i == targetCount) // default label (last) + { + defaultType = scope->type; + defaultIsLoop = (scope->opcode == c_waOp_loop); + defaultArity = defaultIsLoop + ? GetFuncTypeNumParams (scope->type) + : GetFuncTypeNumResults (scope->type); + } + } + } + + // Spec: all labels must have the same label_types as the default + // label_types = params for loop, results for other blocks + { + bytes_t scanPos = o->wasm; + for (u32 i = 0; i <= targetCount; ++i) + { + u32 target; +_ (ReadLEB_u32 (& target, & scanPos, o->wasmEnd)); + + IM3CompilationScope scope; +_ (GetBlockScope (o, & scope, target)); + + bool isLoop = (scope->opcode == c_waOp_loop); + u16 arity = isLoop + ? GetFuncTypeNumParams (scope->type) + : GetFuncTypeNumResults (scope->type); + + _throwif (m3Err_typeCountMismatch, arity != defaultArity); + + // Compare actual types + for (u16 t = 0; t < arity; ++t) + { + u8 labelType = isLoop + ? GetFuncTypeParamType (scope->type, t) + : GetFuncTypeResultType (scope->type, t); + u8 defaultLabelType = defaultIsLoop + ? GetFuncTypeParamType (defaultType, t) + : GetFuncTypeResultType (defaultType, t); + _throwif (m3Err_typeMismatch, labelType != defaultLabelType); + } + } + } +#endif // d_m3EnableValidation + + ++targetCount; // include default + for (u32 i = 0; i < targetCount; ++i) + { + u32 target; +_ (ReadLEB_u32 (& target, & o->wasm, o->wasmEnd)); + + IM3CompilationScope scope; +_ (GetBlockScope (o, & scope, target)); + + // TODO: don't need codepage rigmarole for + // no-param forward-branch targets + +_ (AcquireCompilationCodePage (o, & continueOpPage)); + + pc_t startPC = GetPagePC (continueOpPage); + IM3CodePage savedPage = o->page; + o->page = continueOpPage; + + if (scope->opcode == c_waOp_loop) + { +_ (ResolveBlockResults (o, scope, true)); + +_ (EmitOp (o, op_ContinueLoop)); + EmitPointer (o, scope->pc); + } + else + { + // TODO: this could be fused with equivalent targets + if (not IsStackPolymorphic (o)) + { + if (scope->depth == 0) + { +_ (ReturnValues (o, scope, true)); +_ (EmitOp (o, op_Return)); + } + else + { +_ (ResolveBlockResults (o, scope, true)); + +_ (EmitPatchingBranch (o, scope)); + } + } + } + + ReleaseCompilationCodePage (o); // FIX: continueOpPage can get lost if thrown + o->page = savedPage; + + EmitPointer (o, startPC); + } + +_ (SetStackPolymorphic (o)); + + } + + _catch: return result; +} + +static +M3Result CompileCallArgsAndReturn (IM3Compilation o, u16 * o_stackOffset, IM3FuncType i_type, bool i_isIndirect) +{ +_try { + + u16 topSlot = GetMaxUsedSlotPlusOne (o); + + // force use of at least one stack slot; this is to help ensure + // the m3 stack overflows (and traps) before the native stack can overflow. + // e.g. see Wasm spec test 'runaway' in call.wast + topSlot = M3_MAX (1, topSlot); + + // stack frame is 64-bit aligned + AlignSlotToType (& topSlot, c_m3Type_i64); + + * o_stackOffset = topSlot; + + // wait to pop this here so that topSlot search is correct + if (i_isIndirect) +_ (Pop (o)); + + u16 numArgs = GetFuncTypeNumParams (i_type); + u16 numRets = GetFuncTypeNumResults (i_type); + + u16 argTop = topSlot + (numArgs + numRets) * c_ioSlotCount; + + while (numArgs--) + { +_ (CopyStackTopToSlot (o, argTop -= c_ioSlotCount)); +_ (Pop (o)); + } + + u16 i = 0; + while (numRets--) + { + u8 type = GetFuncTypeResultType (i_type, i++); + +_ (Push (o, type, topSlot)); +_ (MarkSlotsAllocatedByType (o, topSlot, type)); + + topSlot += c_ioSlotCount; + } + + } _catch: return result; +} + +static +M3Result Compile_Call (IM3Compilation o, m3opcode_t i_opcode) +{ +_try { + u32 functionIndex; +_ (ReadLEB_u32 (& functionIndex, & o->wasm, o->wasmEnd)); + + IM3Function function = Module_GetFunction (o->module, functionIndex); + + if (function) + { m3log (compile, d_indent " (func= [%d] '%s'; args= %d)", + get_indention_string (o), functionIndex, m3_GetFunctionName (function), function->funcType->numArgs); + if (function->module) + { + u16 slotTop; +_ (CompileCallArgsAndReturn (o, & slotTop, function->funcType, false)); + + IM3Operation op; + const void * operand; + + if (function->compiled) + { + op = op_Call; + operand = function->compiled; + } + else + { + op = op_Compile; + operand = function; + } + +_ (EmitOp (o, op)); + EmitPointer (o, operand); + EmitSlotOffset (o, slotTop); + } + else + { + _throw (ErrorCompile (m3Err_functionImportMissing, o, "'%s.%s'", GetFunctionImportModuleName (function), m3_GetFunctionName (function))); + } + } + else _throw (m3Err_functionLookupFailed); + + } _catch: return result; +} + +static +M3Result Compile_CallIndirect (IM3Compilation o, m3opcode_t i_opcode) +{ +_try { + u32 typeIndex; +_ (ReadLEB_u32 (& typeIndex, & o->wasm, o->wasmEnd)); + + u32 tableIndex; +_ (ReadLEB_u32 (& tableIndex, & o->wasm, o->wasmEnd)); + + _throwif ("function call type index out of range", typeIndex >= o->module->numFuncTypes); + + if (IsStackTopInRegister (o)) +_ (PreserveRegisterIfOccupied (o, c_m3Type_i32)); + + u16 tableIndexSlot = GetStackTopSlotNumber (o); + + u16 execTop; + IM3FuncType type = o->module->funcTypes [typeIndex]; +_ (CompileCallArgsAndReturn (o, & execTop, type, true)); + +_ (EmitOp (o, op_CallIndirect)); + EmitSlotOffset (o, tableIndexSlot); + EmitPointer (o, o->module); + EmitPointer (o, type); // TODO: unify all types in M3Environment + EmitSlotOffset (o, execTop); + +} _catch: + return result; +} + +static +M3Result Compile_Memory_Size (IM3Compilation o, m3opcode_t i_opcode) +{ + M3Result result; + + i8 reserved; +_ (ReadLEB_i7 (& reserved, & o->wasm, o->wasmEnd)); + +_ (PreserveRegisterIfOccupied (o, c_m3Type_i32)); + +_ (EmitOp (o, op_MemSize)); + +_ (PushRegister (o, c_m3Type_i32)); + + _catch: return result; +} + +static +M3Result Compile_Memory_Grow (IM3Compilation o, m3opcode_t i_opcode) +{ + M3Result result; + + i8 reserved; +_ (ReadLEB_i7 (& reserved, & o->wasm, o->wasmEnd)); + +_ (CopyStackTopToRegister (o, false)); +_ (PopType (o, c_m3Type_i32)); + +_ (EmitOp (o, op_MemGrow)); + +_ (PushRegister (o, c_m3Type_i32)); + + _catch: return result; +} + +static +M3Result Compile_Memory_CopyFill (IM3Compilation o, m3opcode_t i_opcode) +{ + M3Result result = m3Err_none; + + u32 sourceMemoryIdx, targetMemoryIdx; + IM3Operation op; + if (i_opcode == c_waOp_memoryCopy) + { +_ (ReadLEB_u32 (& sourceMemoryIdx, & o->wasm, o->wasmEnd)); + op = op_MemCopy; + } + else op = op_MemFill; + +_ (ReadLEB_u32 (& targetMemoryIdx, & o->wasm, o->wasmEnd)); + +_ (CopyStackTopToRegister (o, false)); + +_ (EmitOp (o, op)); +_ (PopType (o, c_m3Type_i32)); +_ (EmitSlotNumOfStackTopAndPop (o)); +_ (EmitSlotNumOfStackTopAndPop (o)); + + _catch: return result; +} + + +static +M3Result ReadBlockType (IM3Compilation o, IM3FuncType * o_blockType) +{ + M3Result result; + + i64 type; +_ (ReadLebSigned (& type, 33, & o->wasm, o->wasmEnd)); + + if (type < 0) + { + u8 valueType; +_ (NormalizeType (&valueType, type)); m3log (compile, d_indent " (type: %s)", get_indention_string (o), c_waTypes [valueType]); + *o_blockType = o->module->environment->retFuncTypes[valueType]; + } + else + { + _throwif("func type out of bounds", type >= o->module->numFuncTypes); + *o_blockType = o->module->funcTypes[type]; m3log (compile, d_indent " (type: %s)", get_indention_string (o), SPrintFuncTypeSignature (*o_blockType)); + } + _catch: return result; +} + +static +M3Result PreserveArgsAndLocals (IM3Compilation o) +{ + M3Result result = m3Err_none; + + if (o->stackIndex > o->stackFirstDynamicIndex) + { + u32 numArgsAndLocals = GetFunctionNumArgsAndLocals (o->function); + + for (u32 i = 0; i < numArgsAndLocals; ++i) + { + u16 slot = GetSlotForStackIndex (o, i); + + u16 preservedSlotNumber; +_ (FindReferencedLocalWithinCurrentBlock (o, & preservedSlotNumber, slot)); + + if (preservedSlotNumber != slot) + { + u8 type = GetStackTypeFromBottom (o, i); d_m3Assert (type != c_m3Type_none) + IM3Operation op = Is64BitType (type) ? op_CopySlot_64 : op_CopySlot_32; + + EmitOp (o, op); + EmitSlotOffset (o, preservedSlotNumber); + EmitSlotOffset (o, slot); + } + } + } + + _catch: + return result; +} + +static +M3Result Compile_LoopOrBlock (IM3Compilation o, m3opcode_t i_opcode) +{ + M3Result result; + + // TODO: these shouldn't be necessary for non-loop blocks? +_ (PreserveRegisters (o)); +_ (PreserveArgsAndLocals (o)); + + IM3FuncType blockType; +_ (ReadBlockType (o, & blockType)); + + if (i_opcode == c_waOp_loop) + { + u16 numParams = GetFuncTypeNumParams (blockType); + if (numParams) + { + // instantiate constants + u16 numValues = GetNumBlockValuesOnStack (o); // CompileBlock enforces this at comptime + d_m3Assert (numValues >= numParams); + if (numValues >= numParams) + { + u16 stackTop = GetStackTopIndex (o) + 1; + + for (u16 i = stackTop - numParams; i < stackTop; ++i) + { + u16 slot = GetSlotForStackIndex (o, i); + u8 type = GetStackTypeFromBottom (o, i); + + if (IsConstantSlot (o, slot)) + { + u16 newSlot = c_slotUnused; +_ (AllocateSlots (o, & newSlot, type)); +_ (CopyStackIndexToSlot (o, newSlot, i)); + o->wasmStack [i] = newSlot; + } + } + } + } + +_ (EmitOp (o, op_Loop)); + } + else + { + } + +_ (CompileBlock (o, blockType, i_opcode)); + + _catch: return result; +} + +static +M3Result CompileElseBlock (IM3Compilation o, pc_t * o_startPC, IM3FuncType i_blockType) +{ + IM3CodePage savedPage = o->page; +_try { + + IM3CodePage elsePage; +_ (AcquireCompilationCodePage (o, & elsePage)); + + * o_startPC = GetPagePC (elsePage); + + o->page = elsePage; + +_ (CompileBlock (o, i_blockType, c_waOp_else)); + +_ (EmitOp (o, op_Branch)); + EmitPointer (o, GetPagePC (savedPage)); +} _catch: + if(o->page != savedPage) { + ReleaseCompilationCodePage (o); + } + o->page = savedPage; + return result; +} + +static +M3Result Compile_If (IM3Compilation o, m3opcode_t i_opcode) +{ + /* [ op_If ] + [ ] ----> [ ..else.. ] + [ ..if.. ] [ ..block.. ] + [ ..block.. ] [ op_Branch ] + [ end ] <----- [ ] */ + +_try { + + // Spec: if condition must be i32 + if (not IsStackPolymorphic (o)) + { + u8 condType = GetStackTopType (o); + _throwif (m3Err_typeMismatch, condType != c_m3Type_none and condType != c_m3Type_i32); + } + +_ (PreserveNonTopRegisters (o)); +_ (PreserveArgsAndLocals (o)); + + IM3Operation op = IsStackTopInRegister (o) ? op_If_r : op_If_s; + +_ (EmitOp (o, op)); +_ (EmitSlotNumOfStackTopAndPop (o)); + + pc_t * pc = (pc_t *) ReservePointer (o); + + IM3FuncType blockType; +_ (ReadBlockType (o, & blockType)); + +// dump_type_stack (o); + + u16 stackIndex = o->stackIndex; + +_ (CompileBlock (o, blockType, i_opcode)); + + if (o->previousOpcode == c_waOp_else) + { + o->stackIndex = stackIndex; +_ (CompileElseBlock (o, pc, blockType)); + } + else + { + // if block produces values and there isn't a defined else + // case, then we need to make one up so that the pass-through + // results end up in the right place + if (GetFuncTypeNumResults (blockType)) + { + // rewind to the if's end to create a fake else block + o->wasm--; + o->stackIndex = stackIndex; + +// dump_type_stack (o); + +_ (CompileElseBlock (o, pc, blockType)); + } + else * pc = GetPC (o); + } + + } _catch: return result; +} + +static +M3Result Compile_Select (IM3Compilation o, m3opcode_t i_opcode) +{ + M3Result result = m3Err_none; + + u16 slots [3] = { c_slotUnused, c_slotUnused, c_slotUnused }; + + IM3Operation op = NULL; + + u8 type = GetStackTypeFromTop (o, 1); // get type of selection + + if (not IsStackPolymorphic (o)) + { + // Spec: the condition operand (top) must be i32 + u8 condType = GetStackTypeFromTop (o, 0); + _throwif (m3Err_typeMismatch, condType != c_m3Type_none and condType != c_m3Type_i32); + + // Spec: the two value operands (below condition) must have matching types + u8 type2 = GetStackTypeFromTop (o, 2); + _throwif (m3Err_typeMismatch, type != c_m3Type_none and type2 != c_m3Type_none and type != type2); + } + + if (IsFpType (type)) + { +# if d_m3HasFloat + // not consuming a fp reg, so preserve + if (not IsStackTopMinus1InRegister (o) and + not IsStackTopMinus2InRegister (o)) + { +_ (PreserveRegisterIfOccupied (o, type)); + } + + bool selectorInReg = IsStackTopInRegister (o); + slots [0] = GetStackTopSlotNumber (o); +_ (Pop (o)); + + u32 opIndex = 0; + + for (u32 i = 1; i <= 2; ++i) + { + if (IsStackTopInRegister (o)) + opIndex = i; + else + slots [i] = GetStackTopSlotNumber (o); + +_ (Pop (o)); + } + + op = c_fpSelectOps [type - c_m3Type_f32] [selectorInReg] [opIndex]; +# else + _throw (m3Err_unknownOpcode); +# endif + } + else if (IsIntType (type)) + { + // 'sss' operation doesn't consume a register, so might have to protected its contents + if (not IsStackTopInRegister (o) and + not IsStackTopMinus1InRegister (o) and + not IsStackTopMinus2InRegister (o)) + { +_ (PreserveRegisterIfOccupied (o, type)); + } + + u32 opIndex = 3; // op_Select_*_sss + + for (u32 i = 0; i < 3; ++i) + { + if (IsStackTopInRegister (o)) + opIndex = i; + else + slots [i] = GetStackTopSlotNumber (o); + +_ (Pop (o)); + } + + op = c_intSelectOps [type - c_m3Type_i32] [opIndex]; + } + else if (not IsStackPolymorphic (o)) + _throw (m3Err_functionStackUnderrun); + + EmitOp (o, op); + for (u32 i = 0; i < 3; i++) + { + if (IsValidSlot (slots [i])) + EmitSlotOffset (o, slots [i]); + } +_ (PushRegister (o, type)); + + _catch: return result; +} + +static +M3Result Compile_Drop (IM3Compilation o, m3opcode_t i_opcode) +{ + M3Result result = Pop (o); if (d_m3LogWasmStack) dump_type_stack (o); + return result; +} + +static +M3Result Compile_Nop (IM3Compilation o, m3opcode_t i_opcode) +{ + return m3Err_none; +} + +static +M3Result Compile_Unreachable (IM3Compilation o, m3opcode_t i_opcode) +{ + M3Result result; + +_ (AddTrapRecord (o)); + +_ (EmitOp (o, op_Unreachable)); +_ (SetStackPolymorphic (o)); + + _catch: + return result; +} + + +// OPTZ: currently all stack slot indices take up a full word, but +// dual stack source operands could be packed together +static +M3Result Compile_Operator (IM3Compilation o, m3opcode_t i_opcode) +{ + M3Result result; + + IM3OpInfo opInfo = GetOpInfo (i_opcode); + _throwif (m3Err_unknownOpcode, not opInfo); + + // Spec: validate operand types for load/store operations + if (not IsStackPolymorphic (o)) + { + // For load ops (stackOffset == 0, unary), the operand is always i32 (address) + if (i_opcode >= 0x28 and i_opcode <= 0x35) + { + u8 topType = GetStackTopType (o); + _throwif (m3Err_typeMismatch, topType != c_m3Type_none and topType != c_m3Type_i32); + } + + // For store ops (stackOffset == -2), the address operand must be i32 + if (i_opcode >= 0x36 and i_opcode <= 0x3e) + { + u8 addrType = GetStackTypeFromTop (o, 1); + _throwif (m3Err_typeMismatch, addrType != c_m3Type_none and addrType != c_m3Type_i32); + } + } + + IM3Operation op; + + // This preserve is for for FP compare operations. + // either need additional slot destination operations or the + // easy fix, move _r0 out of the way. + // moving out the way might be the optimal solution most often? + // otherwise, the _r0 reg can get buried down in the stack + // and be idle & wasted for a moment. + if (IsFpType (GetStackTopType (o)) and IsIntType (opInfo->type)) + { +_ (PreserveRegisterIfOccupied (o, opInfo->type)); + } + + if (opInfo->stackOffset == 0) + { + if (IsStackTopInRegister (o)) + { + op = opInfo->operations [0]; // _s + } + else + { +_ (PreserveRegisterIfOccupied (o, opInfo->type)); + op = opInfo->operations [1]; // _r + } + } + else + { + if (IsStackTopInRegister (o)) + { + op = opInfo->operations [0]; // _rs + + if (IsStackTopMinus1InRegister (o)) + { d_m3Assert (i_opcode == c_waOp_store_f32 or i_opcode == c_waOp_store_f64); + op = opInfo->operations [3]; // _rr for fp.store + } + } + else if (IsStackTopMinus1InRegister (o)) + { + op = opInfo->operations [1]; // _sr + + if (not op) // must be commutative, then + op = opInfo->operations [0]; + } + else + { +_ (PreserveRegisterIfOccupied (o, opInfo->type)); // _ss + op = opInfo->operations [2]; + } + } + + if (op) + { +_ (EmitOp (o, op)); + +_ (EmitSlotNumOfStackTopAndPop (o)); + + if (opInfo->stackOffset < 0) +_ (EmitSlotNumOfStackTopAndPop (o)); + + if (opInfo->type != c_m3Type_none) +_ (PushRegister (o, opInfo->type)); + } + else + { +# ifdef DEBUG + result = ErrorCompile ("no operation found for opcode", o, "'%s'", opInfo->name); +# else + result = ErrorCompile ("no operation found for opcode", o, "%x", i_opcode); +# endif + _throw (result); + } + + _catch: return result; +} + +static +M3Result Compile_Convert (IM3Compilation o, m3opcode_t i_opcode) +{ +_try { + IM3OpInfo opInfo = GetOpInfo (i_opcode); + _throwif (m3Err_unknownOpcode, not opInfo); + + // Spec: validate source operand type for conversion instructions + if (not IsStackPolymorphic (o)) + { + u8 sourceType = c_m3Type_none; + switch (i_opcode) + { + case 0xa7: // i32.wrap/i64 + sourceType = c_m3Type_i64; break; + case 0xa8: case 0xa9: // i32.trunc_s/f32, i32.trunc_u/f32 + case 0xae: case 0xaf: // i64.trunc_s/f32, i64.trunc_u/f32 + case 0xbb: // f64.promote/f32 + case 0xbc: // i32.reinterpret/f32 + sourceType = c_m3Type_f32; break; + case 0xaa: case 0xab: // i32.trunc_s/f64, i32.trunc_u/f64 + case 0xb0: case 0xb1: // i64.trunc_s/f64, i64.trunc_u/f64 + case 0xb6: // f32.demote/f64 + case 0xbd: // i64.reinterpret/f64 + sourceType = c_m3Type_f64; break; + case 0xac: case 0xad: // i64.extend_s/i32, i64.extend_u/i32 + case 0xb2: case 0xb3: // f32.convert_s/i32, f32.convert_u/i32 + case 0xb7: case 0xb8: // f64.convert_s/i32, f64.convert_u/i32 + case 0xbe: // f32.reinterpret/i32 + sourceType = c_m3Type_i32; break; + case 0xb4: case 0xb5: // f32.convert_s/i64, f32.convert_u/i64 + case 0xb9: case 0xba: // f64.convert_s/i64, f64.convert_u/i64 + case 0xbf: // f64.reinterpret/i64 + sourceType = c_m3Type_i64; break; + default: break; + } + + if (sourceType != c_m3Type_none) + { + u8 topType = GetStackTopType (o); + _throwif (m3Err_typeMismatch, topType != c_m3Type_none and topType != sourceType); + } + } + + bool destInSlot = IsRegisterTypeAllocated (o, opInfo->type); + bool sourceInSlot = IsStackTopInSlot (o); + + IM3Operation op = opInfo->operations [destInSlot * 2 + sourceInSlot]; + +_ (EmitOp (o, op)); +_ (EmitSlotNumOfStackTopAndPop (o)); + + if (destInSlot) +_ (PushAllocatedSlotAndEmit (o, opInfo->type)) + else +_ (PushRegister (o, opInfo->type)) + +} + _catch: return result; +} + +static +M3Result Compile_Load_Store (IM3Compilation o, m3opcode_t i_opcode) +{ +_try { + u32 alignHint, memoryOffset; + +_ (ReadLEB_u32 (& alignHint, & o->wasm, o->wasmEnd)); // checked by the validator +_ (ReadLEB_u32 (& memoryOffset, & o->wasm, o->wasmEnd)); + m3log (compile, d_indent " (offset = %d)", get_indention_string (o), memoryOffset); + IM3OpInfo opInfo = GetOpInfo (i_opcode); + _throwif (m3Err_unknownOpcode, not opInfo); + + if (IsFpType (opInfo->type)) +_ (PreserveRegisterIfOccupied (o, c_m3Type_f64)); + +_ (Compile_Operator (o, i_opcode)); + + EmitConstant32 (o, memoryOffset); +} + _catch: return result; +} + + +M3Result CompileRawFunction (IM3Module io_module, IM3Function io_function, const void * i_function, const void * i_userdata) +{ + d_m3Assert (io_module->runtime); + + IM3CodePage page = AcquireCodePageWithCapacity (io_module->runtime, 4); + + if (page) + { + io_function->compiled = GetPagePC (page); + io_function->module = io_module; + + EmitWord (page, op_CallRawFunction); + EmitWord (page, i_function); + EmitWord (page, io_function); + EmitWord (page, i_userdata); + + ReleaseCodePage (io_module->runtime, page); + return m3Err_none; + } + else { + return m3Err_mallocFailedCodePage; + } +} + + + +// d_logOp, d_logOp2 macros aren't actually used by the compiler, just codepage decoding (d_m3LogCodePages = 1) +#define d_logOp(OP) { op_##OP, NULL, NULL, NULL } +#define d_logOp2(OP1,OP2) { op_##OP1, op_##OP2, NULL, NULL } + +#define d_emptyOpList { NULL, NULL, NULL, NULL } +#define d_unaryOpList(TYPE, NAME) { op_##TYPE##_##NAME##_r, op_##TYPE##_##NAME##_s, NULL, NULL } +#define d_binOpList(TYPE, NAME) { op_##TYPE##_##NAME##_rs, op_##TYPE##_##NAME##_sr, op_##TYPE##_##NAME##_ss, NULL } +#define d_storeFpOpList(TYPE, NAME) { op_##TYPE##_##NAME##_rs, op_##TYPE##_##NAME##_sr, op_##TYPE##_##NAME##_ss, op_##TYPE##_##NAME##_rr } +#define d_commutativeBinOpList(TYPE, NAME) { op_##TYPE##_##NAME##_rs, NULL, op_##TYPE##_##NAME##_ss, NULL } +#define d_convertOpList(OP) { op_##OP##_r_r, op_##OP##_r_s, op_##OP##_s_r, op_##OP##_s_s } + + +const M3OpInfo c_operations [] = +{ + M3OP( "unreachable", 0, none, d_logOp (Unreachable), Compile_Unreachable ), // 0x00 + M3OP( "nop", 0, none, d_emptyOpList, Compile_Nop ), // 0x01 . + M3OP( "block", 0, none, d_emptyOpList, Compile_LoopOrBlock ), // 0x02 + M3OP( "loop", 0, none, d_logOp (Loop), Compile_LoopOrBlock ), // 0x03 + M3OP( "if", -1, none, d_emptyOpList, Compile_If ), // 0x04 + M3OP( "else", 0, none, d_emptyOpList, Compile_Nop ), // 0x05 + + M3OP_RESERVED, M3OP_RESERVED, M3OP_RESERVED, M3OP_RESERVED, M3OP_RESERVED, // 0x06...0x0a + + M3OP( "end", 0, none, d_emptyOpList, Compile_End ), // 0x0b + M3OP( "br", 0, none, d_logOp (Branch), Compile_Branch ), // 0x0c + M3OP( "br_if", -1, none, d_logOp2 (BranchIf_r, BranchIf_s), Compile_Branch ), // 0x0d + M3OP( "br_table", -1, none, d_logOp (BranchTable), Compile_BranchTable ), // 0x0e + M3OP( "return", 0, any, d_logOp (Return), Compile_Return ), // 0x0f + M3OP( "call", 0, any, d_logOp (Call), Compile_Call ), // 0x10 + M3OP( "call_indirect", 0, any, d_logOp (CallIndirect), Compile_CallIndirect ), // 0x11 + M3OP( "return_call", 0, any, d_emptyOpList, Compile_Call ), // 0x12 TODO: Optimize + M3OP( "return_call_indirect",0, any, d_emptyOpList, Compile_CallIndirect ), // 0x13 + + M3OP_RESERVED, M3OP_RESERVED, // 0x14... + M3OP_RESERVED, M3OP_RESERVED, M3OP_RESERVED, M3OP_RESERVED, // ...0x19 + + M3OP( "drop", -1, none, d_emptyOpList, Compile_Drop ), // 0x1a + M3OP( "select", -2, any, d_emptyOpList, Compile_Select ), // 0x1b + + M3OP_RESERVED, M3OP_RESERVED, M3OP_RESERVED, M3OP_RESERVED, // 0x1c...0x1f + + M3OP( "local.get", 1, any, d_emptyOpList, Compile_GetLocal ), // 0x20 + M3OP( "local.set", 1, none, d_emptyOpList, Compile_SetLocal ), // 0x21 + M3OP( "local.tee", 0, any, d_emptyOpList, Compile_SetLocal ), // 0x22 + M3OP( "global.get", 1, none, d_emptyOpList, Compile_GetSetGlobal ), // 0x23 + M3OP( "global.set", 1, none, d_emptyOpList, Compile_GetSetGlobal ), // 0x24 + + M3OP_RESERVED, M3OP_RESERVED, M3OP_RESERVED, // 0x25...0x27 + + M3OP( "i32.load", 0, i_32, d_unaryOpList (i32, Load_i32), Compile_Load_Store ), // 0x28 + M3OP( "i64.load", 0, i_64, d_unaryOpList (i64, Load_i64), Compile_Load_Store ), // 0x29 + M3OP_F( "f32.load", 0, f_32, d_unaryOpList (f32, Load_f32), Compile_Load_Store ), // 0x2a + M3OP_F( "f64.load", 0, f_64, d_unaryOpList (f64, Load_f64), Compile_Load_Store ), // 0x2b + + M3OP( "i32.load8_s", 0, i_32, d_unaryOpList (i32, Load_i8), Compile_Load_Store ), // 0x2c + M3OP( "i32.load8_u", 0, i_32, d_unaryOpList (i32, Load_u8), Compile_Load_Store ), // 0x2d + M3OP( "i32.load16_s", 0, i_32, d_unaryOpList (i32, Load_i16), Compile_Load_Store ), // 0x2e + M3OP( "i32.load16_u", 0, i_32, d_unaryOpList (i32, Load_u16), Compile_Load_Store ), // 0x2f + + M3OP( "i64.load8_s", 0, i_64, d_unaryOpList (i64, Load_i8), Compile_Load_Store ), // 0x30 + M3OP( "i64.load8_u", 0, i_64, d_unaryOpList (i64, Load_u8), Compile_Load_Store ), // 0x31 + M3OP( "i64.load16_s", 0, i_64, d_unaryOpList (i64, Load_i16), Compile_Load_Store ), // 0x32 + M3OP( "i64.load16_u", 0, i_64, d_unaryOpList (i64, Load_u16), Compile_Load_Store ), // 0x33 + M3OP( "i64.load32_s", 0, i_64, d_unaryOpList (i64, Load_i32), Compile_Load_Store ), // 0x34 + M3OP( "i64.load32_u", 0, i_64, d_unaryOpList (i64, Load_u32), Compile_Load_Store ), // 0x35 + + M3OP( "i32.store", -2, none, d_binOpList (i32, Store_i32), Compile_Load_Store ), // 0x36 + M3OP( "i64.store", -2, none, d_binOpList (i64, Store_i64), Compile_Load_Store ), // 0x37 + M3OP_F( "f32.store", -2, none, d_storeFpOpList (f32, Store_f32), Compile_Load_Store ), // 0x38 + M3OP_F( "f64.store", -2, none, d_storeFpOpList (f64, Store_f64), Compile_Load_Store ), // 0x39 + + M3OP( "i32.store8", -2, none, d_binOpList (i32, Store_u8), Compile_Load_Store ), // 0x3a + M3OP( "i32.store16", -2, none, d_binOpList (i32, Store_i16), Compile_Load_Store ), // 0x3b + + M3OP( "i64.store8", -2, none, d_binOpList (i64, Store_u8), Compile_Load_Store ), // 0x3c + M3OP( "i64.store16", -2, none, d_binOpList (i64, Store_i16), Compile_Load_Store ), // 0x3d + M3OP( "i64.store32", -2, none, d_binOpList (i64, Store_i32), Compile_Load_Store ), // 0x3e + + M3OP( "memory.size", 1, i_32, d_logOp (MemSize), Compile_Memory_Size ), // 0x3f + M3OP( "memory.grow", 1, i_32, d_logOp (MemGrow), Compile_Memory_Grow ), // 0x40 + + M3OP( "i32.const", 1, i_32, d_logOp (Const32), Compile_Const_i32 ), // 0x41 + M3OP( "i64.const", 1, i_64, d_logOp (Const64), Compile_Const_i64 ), // 0x42 + M3OP_F( "f32.const", 1, f_32, d_emptyOpList, Compile_Const_f32 ), // 0x43 + M3OP_F( "f64.const", 1, f_64, d_emptyOpList, Compile_Const_f64 ), // 0x44 + + M3OP( "i32.eqz", 0, i_32, d_unaryOpList (i32, EqualToZero) , NULL ), // 0x45 + M3OP( "i32.eq", -1, i_32, d_commutativeBinOpList (i32, Equal) , NULL ), // 0x46 + M3OP( "i32.ne", -1, i_32, d_commutativeBinOpList (i32, NotEqual) , NULL ), // 0x47 + M3OP( "i32.lt_s", -1, i_32, d_binOpList (i32, LessThan) , NULL ), // 0x48 + M3OP( "i32.lt_u", -1, i_32, d_binOpList (u32, LessThan) , NULL ), // 0x49 + M3OP( "i32.gt_s", -1, i_32, d_binOpList (i32, GreaterThan) , NULL ), // 0x4a + M3OP( "i32.gt_u", -1, i_32, d_binOpList (u32, GreaterThan) , NULL ), // 0x4b + M3OP( "i32.le_s", -1, i_32, d_binOpList (i32, LessThanOrEqual) , NULL ), // 0x4c + M3OP( "i32.le_u", -1, i_32, d_binOpList (u32, LessThanOrEqual) , NULL ), // 0x4d + M3OP( "i32.ge_s", -1, i_32, d_binOpList (i32, GreaterThanOrEqual) , NULL ), // 0x4e + M3OP( "i32.ge_u", -1, i_32, d_binOpList (u32, GreaterThanOrEqual) , NULL ), // 0x4f + + M3OP( "i64.eqz", 0, i_32, d_unaryOpList (i64, EqualToZero) , NULL ), // 0x50 + M3OP( "i64.eq", -1, i_32, d_commutativeBinOpList (i64, Equal) , NULL ), // 0x51 + M3OP( "i64.ne", -1, i_32, d_commutativeBinOpList (i64, NotEqual) , NULL ), // 0x52 + M3OP( "i64.lt_s", -1, i_32, d_binOpList (i64, LessThan) , NULL ), // 0x53 + M3OP( "i64.lt_u", -1, i_32, d_binOpList (u64, LessThan) , NULL ), // 0x54 + M3OP( "i64.gt_s", -1, i_32, d_binOpList (i64, GreaterThan) , NULL ), // 0x55 + M3OP( "i64.gt_u", -1, i_32, d_binOpList (u64, GreaterThan) , NULL ), // 0x56 + M3OP( "i64.le_s", -1, i_32, d_binOpList (i64, LessThanOrEqual) , NULL ), // 0x57 + M3OP( "i64.le_u", -1, i_32, d_binOpList (u64, LessThanOrEqual) , NULL ), // 0x58 + M3OP( "i64.ge_s", -1, i_32, d_binOpList (i64, GreaterThanOrEqual) , NULL ), // 0x59 + M3OP( "i64.ge_u", -1, i_32, d_binOpList (u64, GreaterThanOrEqual) , NULL ), // 0x5a + + M3OP_F( "f32.eq", -1, i_32, d_commutativeBinOpList (f32, Equal) , NULL ), // 0x5b + M3OP_F( "f32.ne", -1, i_32, d_commutativeBinOpList (f32, NotEqual) , NULL ), // 0x5c + M3OP_F( "f32.lt", -1, i_32, d_binOpList (f32, LessThan) , NULL ), // 0x5d + M3OP_F( "f32.gt", -1, i_32, d_binOpList (f32, GreaterThan) , NULL ), // 0x5e + M3OP_F( "f32.le", -1, i_32, d_binOpList (f32, LessThanOrEqual) , NULL ), // 0x5f + M3OP_F( "f32.ge", -1, i_32, d_binOpList (f32, GreaterThanOrEqual) , NULL ), // 0x60 + + M3OP_F( "f64.eq", -1, i_32, d_commutativeBinOpList (f64, Equal) , NULL ), // 0x61 + M3OP_F( "f64.ne", -1, i_32, d_commutativeBinOpList (f64, NotEqual) , NULL ), // 0x62 + M3OP_F( "f64.lt", -1, i_32, d_binOpList (f64, LessThan) , NULL ), // 0x63 + M3OP_F( "f64.gt", -1, i_32, d_binOpList (f64, GreaterThan) , NULL ), // 0x64 + M3OP_F( "f64.le", -1, i_32, d_binOpList (f64, LessThanOrEqual) , NULL ), // 0x65 + M3OP_F( "f64.ge", -1, i_32, d_binOpList (f64, GreaterThanOrEqual) , NULL ), // 0x66 + + M3OP( "i32.clz", 0, i_32, d_unaryOpList (u32, Clz) , NULL ), // 0x67 + M3OP( "i32.ctz", 0, i_32, d_unaryOpList (u32, Ctz) , NULL ), // 0x68 + M3OP( "i32.popcnt", 0, i_32, d_unaryOpList (u32, Popcnt) , NULL ), // 0x69 + + M3OP( "i32.add", -1, i_32, d_commutativeBinOpList (i32, Add) , NULL ), // 0x6a + M3OP( "i32.sub", -1, i_32, d_binOpList (i32, Subtract) , NULL ), // 0x6b + M3OP( "i32.mul", -1, i_32, d_commutativeBinOpList (i32, Multiply) , NULL ), // 0x6c + M3OP( "i32.div_s", -1, i_32, d_binOpList (i32, Divide) , NULL ), // 0x6d + M3OP( "i32.div_u", -1, i_32, d_binOpList (u32, Divide) , NULL ), // 0x6e + M3OP( "i32.rem_s", -1, i_32, d_binOpList (i32, Remainder) , NULL ), // 0x6f + M3OP( "i32.rem_u", -1, i_32, d_binOpList (u32, Remainder) , NULL ), // 0x70 + M3OP( "i32.and", -1, i_32, d_commutativeBinOpList (u32, And) , NULL ), // 0x71 + M3OP( "i32.or", -1, i_32, d_commutativeBinOpList (u32, Or) , NULL ), // 0x72 + M3OP( "i32.xor", -1, i_32, d_commutativeBinOpList (u32, Xor) , NULL ), // 0x73 + M3OP( "i32.shl", -1, i_32, d_binOpList (u32, ShiftLeft) , NULL ), // 0x74 + M3OP( "i32.shr_s", -1, i_32, d_binOpList (i32, ShiftRight) , NULL ), // 0x75 + M3OP( "i32.shr_u", -1, i_32, d_binOpList (u32, ShiftRight) , NULL ), // 0x76 + M3OP( "i32.rotl", -1, i_32, d_binOpList (u32, Rotl) , NULL ), // 0x77 + M3OP( "i32.rotr", -1, i_32, d_binOpList (u32, Rotr) , NULL ), // 0x78 + + M3OP( "i64.clz", 0, i_64, d_unaryOpList (u64, Clz) , NULL ), // 0x79 + M3OP( "i64.ctz", 0, i_64, d_unaryOpList (u64, Ctz) , NULL ), // 0x7a + M3OP( "i64.popcnt", 0, i_64, d_unaryOpList (u64, Popcnt) , NULL ), // 0x7b + + M3OP( "i64.add", -1, i_64, d_commutativeBinOpList (i64, Add) , NULL ), // 0x7c + M3OP( "i64.sub", -1, i_64, d_binOpList (i64, Subtract) , NULL ), // 0x7d + M3OP( "i64.mul", -1, i_64, d_commutativeBinOpList (i64, Multiply) , NULL ), // 0x7e + M3OP( "i64.div_s", -1, i_64, d_binOpList (i64, Divide) , NULL ), // 0x7f + M3OP( "i64.div_u", -1, i_64, d_binOpList (u64, Divide) , NULL ), // 0x80 + M3OP( "i64.rem_s", -1, i_64, d_binOpList (i64, Remainder) , NULL ), // 0x81 + M3OP( "i64.rem_u", -1, i_64, d_binOpList (u64, Remainder) , NULL ), // 0x82 + M3OP( "i64.and", -1, i_64, d_commutativeBinOpList (u64, And) , NULL ), // 0x83 + M3OP( "i64.or", -1, i_64, d_commutativeBinOpList (u64, Or) , NULL ), // 0x84 + M3OP( "i64.xor", -1, i_64, d_commutativeBinOpList (u64, Xor) , NULL ), // 0x85 + M3OP( "i64.shl", -1, i_64, d_binOpList (u64, ShiftLeft) , NULL ), // 0x86 + M3OP( "i64.shr_s", -1, i_64, d_binOpList (i64, ShiftRight) , NULL ), // 0x87 + M3OP( "i64.shr_u", -1, i_64, d_binOpList (u64, ShiftRight) , NULL ), // 0x88 + M3OP( "i64.rotl", -1, i_64, d_binOpList (u64, Rotl) , NULL ), // 0x89 + M3OP( "i64.rotr", -1, i_64, d_binOpList (u64, Rotr) , NULL ), // 0x8a + + M3OP_F( "f32.abs", 0, f_32, d_unaryOpList(f32, Abs) , NULL ), // 0x8b + M3OP_F( "f32.neg", 0, f_32, d_unaryOpList(f32, Negate) , NULL ), // 0x8c + M3OP_F( "f32.ceil", 0, f_32, d_unaryOpList(f32, Ceil) , NULL ), // 0x8d + M3OP_F( "f32.floor", 0, f_32, d_unaryOpList(f32, Floor) , NULL ), // 0x8e + M3OP_F( "f32.trunc", 0, f_32, d_unaryOpList(f32, Trunc) , NULL ), // 0x8f + M3OP_F( "f32.nearest", 0, f_32, d_unaryOpList(f32, Nearest) , NULL ), // 0x90 + M3OP_F( "f32.sqrt", 0, f_32, d_unaryOpList(f32, Sqrt) , NULL ), // 0x91 + + M3OP_F( "f32.add", -1, f_32, d_commutativeBinOpList (f32, Add) , NULL ), // 0x92 + M3OP_F( "f32.sub", -1, f_32, d_binOpList (f32, Subtract) , NULL ), // 0x93 + M3OP_F( "f32.mul", -1, f_32, d_commutativeBinOpList (f32, Multiply) , NULL ), // 0x94 + M3OP_F( "f32.div", -1, f_32, d_binOpList (f32, Divide) , NULL ), // 0x95 + M3OP_F( "f32.min", -1, f_32, d_commutativeBinOpList (f32, Min) , NULL ), // 0x96 + M3OP_F( "f32.max", -1, f_32, d_commutativeBinOpList (f32, Max) , NULL ), // 0x97 + M3OP_F( "f32.copysign", -1, f_32, d_binOpList (f32, CopySign) , NULL ), // 0x98 + + M3OP_F( "f64.abs", 0, f_64, d_unaryOpList(f64, Abs) , NULL ), // 0x99 + M3OP_F( "f64.neg", 0, f_64, d_unaryOpList(f64, Negate) , NULL ), // 0x9a + M3OP_F( "f64.ceil", 0, f_64, d_unaryOpList(f64, Ceil) , NULL ), // 0x9b + M3OP_F( "f64.floor", 0, f_64, d_unaryOpList(f64, Floor) , NULL ), // 0x9c + M3OP_F( "f64.trunc", 0, f_64, d_unaryOpList(f64, Trunc) , NULL ), // 0x9d + M3OP_F( "f64.nearest", 0, f_64, d_unaryOpList(f64, Nearest) , NULL ), // 0x9e + M3OP_F( "f64.sqrt", 0, f_64, d_unaryOpList(f64, Sqrt) , NULL ), // 0x9f + + M3OP_F( "f64.add", -1, f_64, d_commutativeBinOpList (f64, Add) , NULL ), // 0xa0 + M3OP_F( "f64.sub", -1, f_64, d_binOpList (f64, Subtract) , NULL ), // 0xa1 + M3OP_F( "f64.mul", -1, f_64, d_commutativeBinOpList (f64, Multiply) , NULL ), // 0xa2 + M3OP_F( "f64.div", -1, f_64, d_binOpList (f64, Divide) , NULL ), // 0xa3 + M3OP_F( "f64.min", -1, f_64, d_commutativeBinOpList (f64, Min) , NULL ), // 0xa4 + M3OP_F( "f64.max", -1, f_64, d_commutativeBinOpList (f64, Max) , NULL ), // 0xa5 + M3OP_F( "f64.copysign", -1, f_64, d_binOpList (f64, CopySign) , NULL ), // 0xa6 + + M3OP( "i32.wrap/i64", 0, i_32, d_unaryOpList (i32, Wrap_i64), NULL ), // 0xa7 + M3OP_F( "i32.trunc_s/f32", 0, i_32, d_convertOpList (i32_Trunc_f32), Compile_Convert ), // 0xa8 + M3OP_F( "i32.trunc_u/f32", 0, i_32, d_convertOpList (u32_Trunc_f32), Compile_Convert ), // 0xa9 + M3OP_F( "i32.trunc_s/f64", 0, i_32, d_convertOpList (i32_Trunc_f64), Compile_Convert ), // 0xaa + M3OP_F( "i32.trunc_u/f64", 0, i_32, d_convertOpList (u32_Trunc_f64), Compile_Convert ), // 0xab + + M3OP( "i64.extend_s/i32", 0, i_64, d_unaryOpList (i64, Extend_i32), NULL ), // 0xac + M3OP( "i64.extend_u/i32", 0, i_64, d_unaryOpList (i64, Extend_u32), NULL ), // 0xad + + M3OP_F( "i64.trunc_s/f32", 0, i_64, d_convertOpList (i64_Trunc_f32), Compile_Convert ), // 0xae + M3OP_F( "i64.trunc_u/f32", 0, i_64, d_convertOpList (u64_Trunc_f32), Compile_Convert ), // 0xaf + M3OP_F( "i64.trunc_s/f64", 0, i_64, d_convertOpList (i64_Trunc_f64), Compile_Convert ), // 0xb0 + M3OP_F( "i64.trunc_u/f64", 0, i_64, d_convertOpList (u64_Trunc_f64), Compile_Convert ), // 0xb1 + + M3OP_F( "f32.convert_s/i32",0, f_32, d_convertOpList (f32_Convert_i32), Compile_Convert ), // 0xb2 + M3OP_F( "f32.convert_u/i32",0, f_32, d_convertOpList (f32_Convert_u32), Compile_Convert ), // 0xb3 + M3OP_F( "f32.convert_s/i64",0, f_32, d_convertOpList (f32_Convert_i64), Compile_Convert ), // 0xb4 + M3OP_F( "f32.convert_u/i64",0, f_32, d_convertOpList (f32_Convert_u64), Compile_Convert ), // 0xb5 + + M3OP_F( "f32.demote/f64", 0, f_32, d_unaryOpList (f32, Demote_f64), NULL ), // 0xb6 + + M3OP_F( "f64.convert_s/i32",0, f_64, d_convertOpList (f64_Convert_i32), Compile_Convert ), // 0xb7 + M3OP_F( "f64.convert_u/i32",0, f_64, d_convertOpList (f64_Convert_u32), Compile_Convert ), // 0xb8 + M3OP_F( "f64.convert_s/i64",0, f_64, d_convertOpList (f64_Convert_i64), Compile_Convert ), // 0xb9 + M3OP_F( "f64.convert_u/i64",0, f_64, d_convertOpList (f64_Convert_u64), Compile_Convert ), // 0xba + + M3OP_F( "f64.promote/f32", 0, f_64, d_unaryOpList (f64, Promote_f32), NULL ), // 0xbb + + M3OP_F( "i32.reinterpret/f32",0,i_32, d_convertOpList (i32_Reinterpret_f32), Compile_Convert ), // 0xbc + M3OP_F( "i64.reinterpret/f64",0,i_64, d_convertOpList (i64_Reinterpret_f64), Compile_Convert ), // 0xbd + M3OP_F( "f32.reinterpret/i32",0,f_32, d_convertOpList (f32_Reinterpret_i32), Compile_Convert ), // 0xbe + M3OP_F( "f64.reinterpret/i64",0,f_64, d_convertOpList (f64_Reinterpret_i64), Compile_Convert ), // 0xbf + + M3OP( "i32.extend8_s", 0, i_32, d_unaryOpList (i32, Extend8_s), NULL ), // 0xc0 + M3OP( "i32.extend16_s", 0, i_32, d_unaryOpList (i32, Extend16_s), NULL ), // 0xc1 + M3OP( "i64.extend8_s", 0, i_64, d_unaryOpList (i64, Extend8_s), NULL ), // 0xc2 + M3OP( "i64.extend16_s", 0, i_64, d_unaryOpList (i64, Extend16_s), NULL ), // 0xc3 + M3OP( "i64.extend32_s", 0, i_64, d_unaryOpList (i64, Extend32_s), NULL ), // 0xc4 + +# ifdef DEBUG // for codepage logging. the order doesn't matter: +# define d_m3DebugOp(OP) M3OP (#OP, 0, none, { op_##OP }) + +# if d_m3HasFloat +# define d_m3DebugTypedOp(OP) M3OP (#OP, 0, none, { op_##OP##_i32, op_##OP##_i64, op_##OP##_f32, op_##OP##_f64, }) +# else +# define d_m3DebugTypedOp(OP) M3OP (#OP, 0, none, { op_##OP##_i32, op_##OP##_i64 }) +# endif + + d_m3DebugOp (Compile), d_m3DebugOp (Entry), d_m3DebugOp (End), + d_m3DebugOp (Unsupported), d_m3DebugOp (CallRawFunction), + + d_m3DebugOp (GetGlobal_s32), d_m3DebugOp (GetGlobal_s64), d_m3DebugOp (ContinueLoop), d_m3DebugOp (ContinueLoopIf), + + d_m3DebugOp (CopySlot_32), d_m3DebugOp (PreserveCopySlot_32), d_m3DebugOp (If_s), d_m3DebugOp (BranchIfPrologue_s), + d_m3DebugOp (CopySlot_64), d_m3DebugOp (PreserveCopySlot_64), d_m3DebugOp (If_r), d_m3DebugOp (BranchIfPrologue_r), + + d_m3DebugOp (Select_i32_rss), d_m3DebugOp (Select_i32_srs), d_m3DebugOp (Select_i32_ssr), d_m3DebugOp (Select_i32_sss), + d_m3DebugOp (Select_i64_rss), d_m3DebugOp (Select_i64_srs), d_m3DebugOp (Select_i64_ssr), d_m3DebugOp (Select_i64_sss), + +# if d_m3HasFloat + d_m3DebugOp (Select_f32_sss), d_m3DebugOp (Select_f32_srs), d_m3DebugOp (Select_f32_ssr), + d_m3DebugOp (Select_f32_rss), d_m3DebugOp (Select_f32_rrs), d_m3DebugOp (Select_f32_rsr), + + d_m3DebugOp (Select_f64_sss), d_m3DebugOp (Select_f64_srs), d_m3DebugOp (Select_f64_ssr), + d_m3DebugOp (Select_f64_rss), d_m3DebugOp (Select_f64_rrs), d_m3DebugOp (Select_f64_rsr), +# endif + + d_m3DebugOp (MemFill), d_m3DebugOp (MemCopy), + + d_m3DebugTypedOp (SetGlobal), d_m3DebugOp (SetGlobal_s32), d_m3DebugOp (SetGlobal_s64), + + d_m3DebugTypedOp (SetRegister), d_m3DebugTypedOp (SetSlot), d_m3DebugTypedOp (PreserveSetSlot), +# endif + +# if d_m3CascadedOpcodes + [c_waOp_extended] = M3OP( "0xFC", 0, c_m3Type_unknown, d_emptyOpList, Compile_ExtendedOpcode ), +# endif + +# ifdef DEBUG + M3OP( "termination", 0, c_m3Type_unknown ) // for find_operation_info +# endif +}; + +const M3OpInfo c_operationsFC [] = +{ + M3OP_F( "i32.trunc_s:sat/f32",0, i_32, d_convertOpList (i32_TruncSat_f32), Compile_Convert ), // 0x00 + M3OP_F( "i32.trunc_u:sat/f32",0, i_32, d_convertOpList (u32_TruncSat_f32), Compile_Convert ), // 0x01 + M3OP_F( "i32.trunc_s:sat/f64",0, i_32, d_convertOpList (i32_TruncSat_f64), Compile_Convert ), // 0x02 + M3OP_F( "i32.trunc_u:sat/f64",0, i_32, d_convertOpList (u32_TruncSat_f64), Compile_Convert ), // 0x03 + M3OP_F( "i64.trunc_s:sat/f32",0, i_64, d_convertOpList (i64_TruncSat_f32), Compile_Convert ), // 0x04 + M3OP_F( "i64.trunc_u:sat/f32",0, i_64, d_convertOpList (u64_TruncSat_f32), Compile_Convert ), // 0x05 + M3OP_F( "i64.trunc_s:sat/f64",0, i_64, d_convertOpList (i64_TruncSat_f64), Compile_Convert ), // 0x06 + M3OP_F( "i64.trunc_u:sat/f64",0, i_64, d_convertOpList (u64_TruncSat_f64), Compile_Convert ), // 0x07 + + M3OP_RESERVED, M3OP_RESERVED, + + M3OP( "memory.copy", 0, none, d_emptyOpList, Compile_Memory_CopyFill ), // 0x0a + M3OP( "memory.fill", 0, none, d_emptyOpList, Compile_Memory_CopyFill ), // 0x0b + + +# ifdef DEBUG + M3OP( "termination", 0, c_m3Type_unknown ) // for find_operation_info +# endif +}; + + +IM3OpInfo GetOpInfo (m3opcode_t opcode) +{ + switch (opcode >> 8) { + case 0x00: + if (M3_LIKELY(opcode < M3_COUNT_OF(c_operations))) { + return &c_operations[opcode]; + } + break; + case c_waOp_extended: + opcode &= 0xFF; + if (M3_LIKELY(opcode < M3_COUNT_OF(c_operationsFC))) { + return &c_operationsFC[opcode]; + } + break; + } + return NULL; +} + +M3Result CompileBlockStatements (IM3Compilation o) +{ + M3Result result = m3Err_none; + bool validEnd = false; + + while (o->wasm < o->wasmEnd) + { +# if d_m3EnableOpTracing + if (o->numEmits) + { + EmitOp (o, op_DumpStack); + EmitConstant32 (o, o->numOpcodes); + EmitConstant32 (o, GetMaxUsedSlotPlusOne(o)); + EmitPointer (o, o->function); + + o->numEmits = 0; + } +# endif + m3opcode_t opcode; + o->lastOpcodeStart = o->wasm; +_ (Read_opcode (& opcode, & o->wasm, o->wasmEnd)); log_opcode (o, opcode); + + // Restrict opcodes when evaluating expressions + if (not o->function) { + switch (opcode) { + case c_waOp_i32_const: case c_waOp_i64_const: + case c_waOp_f32_const: case c_waOp_f64_const: + case c_waOp_getGlobal: case c_waOp_end: + break; + default: + _throw(m3Err_restrictedOpcode); + } + } + + IM3OpInfo opinfo = GetOpInfo (opcode); + + if (opinfo == NULL) + _throw (ErrorCompile (m3Err_unknownOpcode, o, "opcode '%x' not available", opcode)); + + if (opinfo->compiler) { +_ ((* opinfo->compiler) (o, opcode)) + } else { +_ (Compile_Operator (o, opcode)); + } + + o->previousOpcode = opcode; + + if (opcode == c_waOp_else) + { + _throwif (m3Err_wasmMalformed, o->block.opcode != c_waOp_if); + validEnd = true; + break; + } + else if (opcode == c_waOp_end) + { + validEnd = true; + break; + } + } + _throwif(m3Err_wasmMalformed, !(validEnd)); + +_catch: + return result; +} + +static +M3Result PushBlockResults (IM3Compilation o) +{ + M3Result result = m3Err_none; + + u16 numResults = GetFuncTypeNumResults (o->block.type); + + for (u16 i = 0; i < numResults; ++i) + { + u8 type = GetFuncTypeResultType (o->block.type, i); + + if (i == numResults - 1 and IsFpType (type)) + { +_ (PushRegister (o, type)); + } + else +_ (PushAllocatedSlot (o, type)); + } + + _catch: return result; +} + + +M3Result CompileBlock (IM3Compilation o, IM3FuncType i_blockType, m3opcode_t i_blockOpcode) +{ + d_m3Assert (not IsRegisterAllocated (o, 0)); + d_m3Assert (not IsRegisterAllocated (o, 1)); + M3CompilationScope outerScope = o->block; + M3CompilationScope * block = & o->block; + + block->outer = & outerScope; + block->pc = GetPagePC (o->page); + block->patches = NULL; + block->type = i_blockType; + block->depth ++; + block->opcode = i_blockOpcode; + + /* + The block stack frame is a little strange but for good reasons. Because blocks need to be restarted to + compile different pathways (if/else), the incoming params must be saved. The parameters are popped + and validated. But, then the stack top is readjusted so they aren't subsequently overwritten. + Next, the result are preallocated to find destination slots. But again these are immediately popped + (deallocated) and the stack top is readjusted to keep these records in pace. This allows branch instructions + to find their result landing pads. Finally, the params are copied from the "dead" records and pushed back + onto the stack as active stack items for the CompileBlockStatements () call. + + [ block ] + [ params ] + ------------------ + [ result ] <---- blockStackIndex + [ slots ] + ------------------ + [ saved param ] + [ records ] + <----- exitStackIndex + */ + +_try { + // validate and dealloc params ---------------------------- + + u16 stackIndex = o->stackIndex; + + u16 numParams = GetFuncTypeNumParams (i_blockType); + + if (i_blockOpcode != c_waOp_else) + { + for (u16 i = 0; i < numParams; ++i) + { + u8 type = GetFuncTypeParamType (i_blockType, numParams - 1 - i); +_ (PopType (o, type)); + } + } + else { + if (IsStackPolymorphic (o) && o->block.blockStackIndex + numParams > o->stackIndex) { + o->stackIndex = o->block.blockStackIndex; + } else { + o->stackIndex -= numParams; + } + } + + u16 paramIndex = o->stackIndex; + block->exitStackIndex = paramIndex; // consume the params at block exit + + // keep copies of param slots in the stack + o->stackIndex = stackIndex; + + // find slots for the results ---------------------------- + PushBlockResults (o); + + stackIndex = o->stackIndex; + + // dealloc but keep record of the result slots in the stack + u16 numResults = GetFuncTypeNumResults (i_blockType); + while (numResults--) + Pop (o); + + block->blockStackIndex = o->stackIndex = stackIndex; + + // push the params back onto the stack ------------------- + for (u16 i = 0; i < numParams; ++i) + { + u8 type = GetFuncTypeParamType (i_blockType, i); + + u16 slot = GetSlotForStackIndex (o, paramIndex + i); + Push (o, type, slot); + + if (slot >= o->slotFirstDynamicIndex && slot != c_slotUnused) +_ (MarkSlotsAllocatedByType (o, slot, type)); + } + + //-------------------------------------------------------- + +_ (CompileBlockStatements (o)); + +_ (ValidateBlockEnd (o)); + + if (o->function) // skip for expressions + { + if (not IsStackPolymorphic (o)) +_ (ResolveBlockResults (o, & o->block, /* isBranch: */ false)); + +_ (UnwindBlockStack (o)) + + if (not ((i_blockOpcode == c_waOp_if and numResults) or o->previousOpcode == c_waOp_else)) + { + o->stackIndex = o->block.exitStackIndex; +_ (PushBlockResults (o)); + } + } + + PatchBranches (o); + + o->block = outerScope; + +} _catch: return result; +} + +static +M3Result CompileLocals (IM3Compilation o) +{ + M3Result result; + + u32 numLocals = 0; + u32 numLocalBlocks; +_ (ReadLEB_u32 (& numLocalBlocks, & o->wasm, o->wasmEnd)); + + for (u32 l = 0; l < numLocalBlocks; ++l) + { + u32 varCount; + i8 waType; + u8 localType; + +_ (ReadLEB_u32 (& varCount, & o->wasm, o->wasmEnd)); +_ (ReadLEB_i7 (& waType, & o->wasm, o->wasmEnd)); +_ (NormalizeType (& localType, waType)); + numLocals += varCount; m3log (compile, "pushing locals. count: %d; type: %s", varCount, c_waTypes [localType]); + while (varCount--) +_ (PushAllocatedSlot (o, localType)); + } + + if (o->function) + o->function->numLocals = numLocals; + + _catch: return result; +} + +static +M3Result ReserveConstants (IM3Compilation o) +{ + M3Result result = m3Err_none; + + // in the interest of speed, this blindly scans the Wasm code looking for any byte + // that looks like an const opcode. + u16 numConstantSlots = 0; + + bytes_t wa = o->wasm; + while (wa < o->wasmEnd) + { + u8 code = * wa++; + u16 addSlots = 0; + + if (code == c_waOp_i32_const or code == c_waOp_f32_const) + addSlots = 1; + else if (code == c_waOp_i64_const or code == c_waOp_f64_const) + addSlots = GetTypeNumSlots (c_m3Type_i64); + + if (numConstantSlots + addSlots >= d_m3MaxConstantTableSize) + break; + + numConstantSlots += addSlots; + } + + // if constants overflow their reserved stack space, the compiler simply emits op_Const + // operations as needed. Compiled expressions (global inits) don't pass through this + // ReserveConstants function and thus always produce inline constants. + + AlignSlotToType (& numConstantSlots, c_m3Type_i64); m3log (compile, "reserved constant slots: %d", numConstantSlots); + + o->slotFirstDynamicIndex = o->slotFirstConstIndex + numConstantSlots; + + if (o->slotFirstDynamicIndex >= d_m3MaxFunctionSlots) + _throw (m3Err_functionStackOverflow); + + _catch: + return result; +} + + +M3Result CompileFunction (IM3Function io_function) +{ + if (!io_function->wasm) return "function body is missing"; + +#if d_m3EnableValidation + M3Result vr = ValidateFunction(io_function); + if (vr) return vr; +#endif + + IM3FuncType funcType = io_function->funcType; m3log (compile, "compiling: [%d] %s %s; wasm-size: %d", + io_function->index, m3_GetFunctionName (io_function), SPrintFuncTypeSignature (funcType), (u32) (io_function->wasmEnd - io_function->wasm)); + IM3Runtime runtime = io_function->module->runtime; + + IM3Compilation o = & runtime->compilation; d_m3Assert (d_m3MaxFunctionSlots >= d_m3MaxFunctionStackHeight * (d_m3Use32BitSlots + 1)) // need twice as many slots in 32-bit mode + memset (o, 0x0, sizeof (M3Compilation)); + + o->runtime = runtime; + o->module = io_function->module; + o->function = io_function; + o->wasm = io_function->wasm; + o->wasmEnd = io_function->wasmEnd; + o->block.type = funcType; + +_try { + // skip over code size. the end was already calculated during parse phase + u32 size; +_ (ReadLEB_u32 (& size, & o->wasm, o->wasmEnd)); d_m3Assert (size == (o->wasmEnd - o->wasm)) + +_ (AcquireCompilationCodePage (o, & o->page)); + + pc_t pc = GetPagePC (o->page); + + u16 numRetSlots = GetFunctionNumReturns (o->function) * c_ioSlotCount; + + for (u16 i = 0; i < numRetSlots; ++i) + MarkSlotAllocated (o, i); + + o->function->numRetSlots = o->slotFirstDynamicIndex = numRetSlots; + + u16 numArgs = GetFunctionNumArgs (o->function); + + // push the arg types to the type stack + for (u16 i = 0; i < numArgs; ++i) + { + u8 type = GetFunctionArgType (o->function, i); +_ (PushAllocatedSlot (o, type)); + + // prevent allocator fill-in + o->slotFirstDynamicIndex += c_ioSlotCount; + } + + o->slotMaxAllocatedIndexPlusOne = o->function->numRetAndArgSlots = o->slotFirstLocalIndex = o->slotFirstDynamicIndex; + +_ (CompileLocals (o)); + + u16 maxSlot = GetMaxUsedSlotPlusOne (o); + + o->function->numLocalBytes = (maxSlot - o->slotFirstLocalIndex) * sizeof (m3slot_t); + + o->slotFirstConstIndex = o->slotMaxConstIndex = maxSlot; + + // ReserveConstants initializes o->firstDynamicSlotNumber +_ (ReserveConstants (o)); + + // start tracking the max stack used (Push() also updates this value) so that op_Entry can precisely detect stack overflow + o->maxStackSlots = o->slotMaxAllocatedIndexPlusOne = o->slotFirstDynamicIndex; + + o->block.blockStackIndex = o->stackFirstDynamicIndex = o->stackIndex; m3log (compile, "start stack index: %d", + (u32) o->stackFirstDynamicIndex); +_ (EmitOp (o, op_Entry)); + EmitPointer (o, io_function); + +_ (CompileBlockStatements (o)); + + // TODO: validate opcode sequences + _throwif(m3Err_wasmMalformed, o->previousOpcode != c_waOp_end); + + io_function->compiled = pc; + io_function->maxStackSlots = o->maxStackSlots; + + u16 numConstantSlots = o->slotMaxConstIndex - o->slotFirstConstIndex; m3log (compile, "unique constant slots: %d; unused slots: %d", + numConstantSlots, o->slotFirstDynamicIndex - o->slotMaxConstIndex); + io_function->numConstantBytes = numConstantSlots * sizeof (m3slot_t); + + if (numConstantSlots) + { + io_function->constants = m3_CopyMem (o->constants, io_function->numConstantBytes); + _throwifnull(io_function->constants); + } + +} _catch: + + ReleaseCompilationCodePage (o); + + return result; +} diff --git a/driver/wasm3/m3_compile.h b/driver/wasm3/m3_compile.h new file mode 100644 index 0000000..8ab0a92 --- /dev/null +++ b/driver/wasm3/m3_compile.h @@ -0,0 +1,208 @@ +// +// m3_compile.h +// +// Created by Steven Massey on 4/17/19. +// Copyright © 2019 Steven Massey. All rights reserved. +// + +#ifndef m3_compile_h +#define m3_compile_h + +#include "m3_code.h" +#include "m3_exec_defs.h" +#include "m3_function.h" + +d_m3BeginExternC + +enum +{ + c_waOp_block = 0x02, + c_waOp_loop = 0x03, + c_waOp_if = 0x04, + c_waOp_else = 0x05, + c_waOp_end = 0x0b, + c_waOp_branch = 0x0c, + c_waOp_branchTable = 0x0e, + c_waOp_branchIf = 0x0d, + c_waOp_call = 0x10, + c_waOp_getLocal = 0x20, + c_waOp_setLocal = 0x21, + c_waOp_teeLocal = 0x22, + + c_waOp_getGlobal = 0x23, + + c_waOp_store_f32 = 0x38, + c_waOp_store_f64 = 0x39, + + c_waOp_i32_const = 0x41, + c_waOp_i64_const = 0x42, + c_waOp_f32_const = 0x43, + c_waOp_f64_const = 0x44, + + c_waOp_extended = 0xfc, + + c_waOp_memoryCopy = 0xfc0a, + c_waOp_memoryFill = 0xfc0b +}; + + +#define d_FuncRetType(ftype,i) ((ftype)->types[(i)]) +#define d_FuncArgType(ftype,i) ((ftype)->types[(ftype)->numRets + (i)]) + +//----------------------------------------------------------------------------------------------------------------------------------- + +typedef struct M3CompilationScope +{ + struct M3CompilationScope * outer; + + pc_t pc; // used by ContinueLoop's + pc_t patches; + i32 depth; + u16 exitStackIndex; + u16 blockStackIndex; +// u16 topSlot; + IM3FuncType type; + m3opcode_t opcode; + bool isPolymorphic; +} +M3CompilationScope; + +typedef M3CompilationScope * IM3CompilationScope; + +typedef struct +{ + IM3Runtime runtime; + IM3Module module; + + bytes_t wasm; + bytes_t wasmEnd; + bytes_t lastOpcodeStart; + + M3CompilationScope block; + + IM3Function function; + + IM3CodePage page; + +#ifdef DEBUG + u32 numEmits; + u32 numOpcodes; +#endif + + u16 stackFirstDynamicIndex; // args and locals are pushed to the stack so that their slot locations can be tracked. the wasm model itself doesn't + // treat these values as being on the stack, so stackFirstDynamicIndex marks the start of the real Wasm stack + u16 stackIndex; // current stack top + + u16 slotFirstConstIndex; + u16 slotMaxConstIndex; // as const's are encountered during compilation this tracks their location in the "real" stack + + u16 slotFirstLocalIndex; + u16 slotFirstDynamicIndex; // numArgs + numLocals + numReservedConstants. the first mutable slot available to the compiler. + + u16 maxStackSlots; + + m3slot_t constants [d_m3MaxConstantTableSize]; + + // 'wasmStack' holds slot locations + u16 wasmStack [d_m3MaxFunctionStackHeight]; + u8 typeStack [d_m3MaxFunctionStackHeight]; + + // 'm3Slots' contains allocation usage counts + u8 m3Slots [d_m3MaxFunctionSlots]; + + u16 slotMaxAllocatedIndexPlusOne; + + u16 regStackIndexPlusOne [2]; + + m3opcode_t previousOpcode; + + bool isInitExpr; // walking a constant expression, not a function body +} +M3Compilation; + +typedef M3Compilation * IM3Compilation; + +typedef M3Result (* M3Compiler) (IM3Compilation, m3opcode_t); + + +//----------------------------------------------------------------------------------------------------------------------------------- + + +typedef struct M3OpInfo +{ +#ifdef DEBUG + const char * const name; +#endif + + i8 stackOffset; + u8 type; + + // for most operations: + // [0]= top operand in register, [1]= top operand in stack, [2]= both operands in stack + IM3Operation operations [4]; + + M3Compiler compiler; +} +M3OpInfo; + +typedef const M3OpInfo * IM3OpInfo; + +IM3OpInfo GetOpInfo (m3opcode_t opcode); + +// TODO: This helper should be removed, when MultiValue is implemented +static inline +u8 GetSingleRetType(IM3FuncType ftype) { + return (ftype && ftype->numRets) ? ftype->types[0] : (u8)c_m3Type_none; +} + +static const u16 c_m3RegisterUnallocated = 0; +static const u16 c_slotUnused = 0xffff; + +static inline +bool IsRegisterAllocated (IM3Compilation o, u32 i_register) +{ + return (o->regStackIndexPlusOne [i_register] != c_m3RegisterUnallocated); +} + +static inline +bool IsStackPolymorphic (IM3Compilation o) +{ + return o->block.isPolymorphic; +} + +static inline bool IsRegisterSlotAlias (u16 i_slot) { return (i_slot >= d_m3Reg0SlotAlias and i_slot != c_slotUnused); } +static inline bool IsFpRegisterSlotAlias (u16 i_slot) { return (i_slot == d_m3Fp0SlotAlias); } +static inline bool IsIntRegisterSlotAlias (u16 i_slot) { return (i_slot == d_m3Reg0SlotAlias); } + + +#ifdef DEBUG + #define M3OP(...) { __VA_ARGS__ } + #define M3OP_RESERVED { "reserved" } +#else + // Strip-off name + #define M3OP(name, ...) { __VA_ARGS__ } + #define M3OP_RESERVED { 0 } +#endif + +#if d_m3HasFloat + #define M3OP_F M3OP +#elif d_m3NoFloatDynamic + #define M3OP_F(n,o,t,op,...) M3OP(n, o, t, { op_Unsupported, op_Unsupported, op_Unsupported, op_Unsupported }, __VA_ARGS__) +#else + #define M3OP_F(...) { 0 } +#endif + +//----------------------------------------------------------------------------------------------------------------------------------- + +u16 GetMaxUsedSlotPlusOne (IM3Compilation o); + +M3Result CompileBlock (IM3Compilation io, IM3FuncType i_blockType, m3opcode_t i_blockOpcode); + +M3Result CompileBlockStatements (IM3Compilation io); +M3Result CompileFunction (IM3Function io_function); + +M3Result CompileRawFunction (IM3Module io_module, IM3Function io_function, const void * i_function, const void * i_userdata); + +d_m3EndExternC + +#endif // m3_compile_h diff --git a/driver/wasm3/m3_config.h b/driver/wasm3/m3_config.h new file mode 100644 index 0000000..05bc5d6 --- /dev/null +++ b/driver/wasm3/m3_config.h @@ -0,0 +1,168 @@ +// +// m3_config.h +// +// Created by Steven Massey on 5/4/19. +// Copyright © 2019 Steven Massey. All rights reserved. +// + +#ifndef m3_config_h +#define m3_config_h + +#include "m3_config_platforms.h" + +// general -------------------------------------------------------------------- + +# ifndef d_m3CodePageAlignSize +# define d_m3CodePageAlignSize 32*1024 +# endif + +# ifndef d_m3MaxFunctionStackHeight +# define d_m3MaxFunctionStackHeight 2000 // max: 32768 +# endif + +# ifndef d_m3MaxLinearMemoryPages +# define d_m3MaxLinearMemoryPages 65536 +# endif + +# ifndef d_m3MaxFunctionSlots +# define d_m3MaxFunctionSlots ((d_m3MaxFunctionStackHeight)*2) +# endif + +# ifndef d_m3ValStack // validator operand and local type stacks: +# define d_m3ValStack (d_m3MaxFunctionStackHeight) // the same operand stack the compiler bounds +# endif + +# ifndef d_m3ValCtrlDepth // validator block nesting depth. Each frame is bigger than an +# define d_m3ValCtrlDepth ((d_m3MaxFunctionStackHeight)/8)// operand entry, so this dominates the validator's stack usage +# endif + +# ifndef d_m3MaxConstantTableSize +# define d_m3MaxConstantTableSize 120 +# endif + +# ifndef d_m3MaxDuplicateFunctionImpl +# define d_m3MaxDuplicateFunctionImpl 3 +# endif + +# ifndef d_m3CascadedOpcodes // Cascaded opcodes are slightly faster at the expense of some memory +# define d_m3CascadedOpcodes 1 // Adds ~3Kb to operations table in m3_compile.c +# endif + +# ifndef d_m3VerboseErrorMessages +# define d_m3VerboseErrorMessages 1 +# endif + +# ifndef d_m3FixedHeap +# define d_m3FixedHeap false +//# define d_m3FixedHeap (32*1024) +# endif + +# ifndef d_m3FixedHeapAlign +# define d_m3FixedHeapAlign 16 +# endif + +# ifndef d_m3Use32BitSlots +# define d_m3Use32BitSlots 1 +# endif + +# ifndef d_m3ProfilerSlotMask +# define d_m3ProfilerSlotMask 0xFFFF +# endif + +# ifndef d_m3RecordBacktraces +# define d_m3RecordBacktraces 0 +# endif + +# ifndef d_m3EnableExceptionBreakpoint +# define d_m3EnableExceptionBreakpoint 0 // see m3_exception.h +# endif + + +// profiling and tracing ------------------------------------------------------ + +# ifndef d_m3EnableOpProfiling +# define d_m3EnableOpProfiling 0 // opcode usage counters +# endif + +# ifndef d_m3EnableOpTracing +# define d_m3EnableOpTracing 0 // only works with DEBUG +# endif + +# ifndef d_m3EnableWasiTracing +# define d_m3EnableWasiTracing 0 +# endif + +# ifndef d_m3EnableStrace +# define d_m3EnableStrace 0 // 1 - trace exported function calls + // 2 - trace all calls (structured) + // 3 - all calls + loops + memory operations +# endif + + +// logging -------------------------------------------------------------------- + +# ifndef d_m3LogParse +# define d_m3LogParse 0 // .wasm binary decoding info +# endif + +# ifndef d_m3LogModule +# define d_m3LogModule 0 // wasm module info +# endif + +# ifndef d_m3LogCompile +# define d_m3LogCompile 0 // wasm -> metacode generation phase +# endif + +# ifndef d_m3LogWasmStack +# define d_m3LogWasmStack 0 // dump the wasm stack when pushed or popped +# endif + +# ifndef d_m3LogEmit +# define d_m3LogEmit 0 // metacode generation info +# endif + +# ifndef d_m3LogCodePages +# define d_m3LogCodePages 0 // dump metacode pages when released +# endif + +# ifndef d_m3LogRuntime +# define d_m3LogRuntime 0 // higher-level runtime information +# endif + +# ifndef d_m3LogNativeStack +# define d_m3LogNativeStack 0 // track the memory usage of the C-stack +# endif + +# ifndef d_m3LogHeapOps +# define d_m3LogHeapOps 0 // track heap usage +# endif + +# ifndef d_m3LogTimestamps +# define d_m3LogTimestamps 0 // track timestamps on heap logs +# endif + +// other ---------------------------------------------------------------------- + +# ifndef d_m3HasFloat +# define d_m3HasFloat 1 // implement floating point ops +# endif + +#if !d_m3HasFloat && !defined(d_m3NoFloatDynamic) +# define d_m3NoFloatDynamic 1 // if no floats, do not fail until flops are actually executed +#endif + +# ifndef d_m3EnableValidation +# define d_m3EnableValidation 1 // pre-pass bytecode type validation +# endif + +# ifndef d_m3SkipStackCheck +# define d_m3SkipStackCheck 0 // skip stack overrun checks +# endif + +# ifndef d_m3SkipMemoryBoundsCheck +# define d_m3SkipMemoryBoundsCheck 0 // skip memory bounds checks +# endif + +#define d_m3EnableCodePageRefCounting 0 // not supported currently + +#endif // m3_config_h diff --git a/driver/wasm3/m3_config_platforms.h b/driver/wasm3/m3_config_platforms.h new file mode 100644 index 0000000..bd66630 --- /dev/null +++ b/driver/wasm3/m3_config_platforms.h @@ -0,0 +1,216 @@ +// +// m3_config_platforms.h +// +// Created by Volodymyr Shymanskyy on 11/20/19. +// Copyright © 2019 Volodymyr Shymanskyy. All rights reserved. +// + +#ifndef m3_config_platforms_h +#define m3_config_platforms_h + +#include "wasm3_defs.h" + +/* + * Internal helpers + */ + +# if !defined(__cplusplus) || defined(_MSC_VER) +# define not ! +# define and && +# define or || +# endif + +/* + * Detect/define features + */ + +# if defined(M3_COMPILER_MSVC) +# include +# if UINTPTR_MAX == 0xFFFFFFFF +# define M3_SIZEOF_PTR 4 +# elif UINTPTR_MAX == 0xFFFFFFFFFFFFFFFFu +# define M3_SIZEOF_PTR 8 +# else +# error "Pointer size not supported" +# endif +# elif defined(__SIZEOF_POINTER__) +# define M3_SIZEOF_PTR __SIZEOF_POINTER__ +#else +# error "Pointer size not detected" +# endif + +# if defined(M3_BIG_ENDIAN) +# define M3_BSWAP_u8(X) {} +# define M3_BSWAP_u16(X) { (X)=m3_bswap16((X)); } +# define M3_BSWAP_u32(X) { (X)=m3_bswap32((X)); } +# define M3_BSWAP_u64(X) { (X)=m3_bswap64((X)); } +# define M3_BSWAP_i8(X) {} +# define M3_BSWAP_i16(X) M3_BSWAP_u16(X) +# define M3_BSWAP_i32(X) M3_BSWAP_u32(X) +# define M3_BSWAP_i64(X) M3_BSWAP_u64(X) +# define M3_BSWAP_f32(X) { union { f32 f; u32 i; } u; u.f = (X); M3_BSWAP_u32(u.i); (X) = u.f; } +# define M3_BSWAP_f64(X) { union { f64 f; u64 i; } u; u.f = (X); M3_BSWAP_u64(u.i); (X) = u.f; } +# else +# define M3_BSWAP_u8(X) {} +# define M3_BSWAP_u16(x) {} +# define M3_BSWAP_u32(x) {} +# define M3_BSWAP_u64(x) {} +# define M3_BSWAP_i8(X) {} +# define M3_BSWAP_i16(X) {} +# define M3_BSWAP_i32(X) {} +# define M3_BSWAP_i64(X) {} +# define M3_BSWAP_f32(X) {} +# define M3_BSWAP_f64(X) {} +# endif + +# if defined(M3_COMPILER_MSVC) +# define M3_WEAK //__declspec(selectany) +# define M3_NO_UBSAN +# define M3_NOINLINE +# elif defined(__MINGW32__) || defined(__CYGWIN__) +# define M3_WEAK //__attribute__((selectany)) +# define M3_NO_UBSAN +# define M3_NOINLINE __attribute__((noinline)) +# else +# define M3_WEAK __attribute__((weak)) +# define M3_NO_UBSAN //__attribute__((no_sanitize("undefined"))) +// Workaround for Cosmopolitan noinline conflict: https://github.com/jart/cosmopolitan/issues/310 +# if defined(noinline) +# define M3_NOINLINE noinline +# else +# define M3_NOINLINE __attribute__((noinline)) +# endif +# endif + +# if !defined(M3_HAS_TAIL_CALL) +# if defined(__EMSCRIPTEN__) +# define M3_HAS_TAIL_CALL 0 +# else +# define M3_HAS_TAIL_CALL 1 +# endif +# endif + +# if M3_HAS_TAIL_CALL && M3_COMPILER_HAS_ATTRIBUTE(musttail) +# define M3_MUSTTAIL __attribute__((musttail)) +# else +# define M3_MUSTTAIL +# endif + +# ifndef M3_MIN +# define M3_MIN(A,B) (((A) < (B)) ? (A) : (B)) +# endif +# ifndef M3_MAX +# define M3_MAX(A,B) (((A) > (B)) ? (A) : (B)) +# endif + +#define M3_INIT(field) memset(&field, 0, sizeof(field)) + +#define M3_COUNT_OF(x) ((sizeof(x)/sizeof(0[x])) / ((size_t)(!(sizeof(x) % sizeof(0[x]))))) + +#if defined(__AVR__) + +#include + +# define PRIu64 "llu" +# define PRIi64 "lli" + +# define d_m3ShortTypesDefined +typedef double f64; +typedef float f32; +typedef uint64_t u64; +typedef int64_t i64; +typedef uint32_t u32; +typedef int32_t i32; +typedef short unsigned u16; +typedef short i16; +typedef uint8_t u8; +typedef int8_t i8; + +#endif + +/* + * Apply settings + */ + +# if defined (M3_COMPILER_MSVC) +# define vectorcall // For MSVC, better not to specify any call convention +# elif defined(__x86_64__) +# define vectorcall +//# elif defined(__riscv) && (__riscv_xlen == 64) +//# define vectorcall +# elif defined(__MINGW32__) +# define vectorcall +# elif defined(WIN32) +# define vectorcall __vectorcall +# elif defined (ESP8266) +# include +# define vectorcall //ICACHE_FLASH_ATTR +# elif defined (ESP32) +# if defined(M3_IN_IRAM) // the interpreter is in IRAM, attribute not needed +# define vectorcall +# else +# include "esp_system.h" +# define vectorcall IRAM_ATTR +# endif +# elif defined (FOMU) +# define vectorcall __attribute__((section(".ramtext"))) +# endif + +#ifndef vectorcall +#define vectorcall +#endif + + +/* + * Device-specific defaults + */ + +# ifndef d_m3MaxFunctionStackHeight +# if defined(ESP8266) || defined(ESP32) || defined(ARDUINO_AMEBA) || defined(TEENSYDUINO) +# define d_m3MaxFunctionStackHeight 256 +# endif +# endif + +# ifndef d_m3FixedHeap +# if defined(ARDUINO_AMEBA) +# define d_m3FixedHeap (128*1024) +# elif defined(BLUE_PILL) || defined(FOMU) +# define d_m3FixedHeap (12*1024) +# elif defined(ARDUINO_ARCH_ARC32) // Arduino 101 +# define d_m3FixedHeap (10*1024) +# endif +# endif + +/* + * Platform-specific defaults + */ + +# if defined(ARDUINO) || defined(PARTICLE) || defined(PLATFORMIO) || defined(__MBED__) || \ + defined(ESP8266) || defined(ESP32) || defined(BLUE_PILL) || defined(WM_W600) || defined(FOMU) +# ifndef d_m3CascadedOpcodes +# define d_m3CascadedOpcodes 0 +# endif +# ifndef d_m3VerboseErrorMessages +# define d_m3VerboseErrorMessages 0 +# endif +# ifndef d_m3MaxConstantTableSize +# define d_m3MaxConstantTableSize 64 +# endif +# ifndef d_m3MaxFunctionStackHeight +# define d_m3MaxFunctionStackHeight 128 +# endif +# ifndef d_m3CodePageAlignSize +# define d_m3CodePageAlignSize 1024 +# endif +# endif + +/* + * Arch-specific defaults + */ +#if defined(__riscv) && (__riscv_xlen == 64) +# ifndef d_m3Use32BitSlots +# define d_m3Use32BitSlots 0 +# endif +#endif + +#endif // m3_config_platforms_h diff --git a/driver/wasm3/m3_core.c b/driver/wasm3/m3_core.c new file mode 100644 index 0000000..4e686b2 --- /dev/null +++ b/driver/wasm3/m3_core.c @@ -0,0 +1,717 @@ +// +// m3_core.c +// +// Created by Steven Massey on 4/15/19. +// Copyright © 2019 Steven Massey. All rights reserved. +// + +#define M3_IMPLEMENT_ERROR_STRINGS +#include "m3_config.h" +#include "wasm3.h" + +#include "m3_core.h" +#include "m3_env.h" + +void m3_Abort(const char* message) { +#ifdef DEBUG + fprintf(stderr, "Error: %s\n", message); +#endif + abort(); +} + +M3_WEAK +M3Result m3_Yield () +{ + return m3Err_none; +} + +#if d_m3LogTimestamps + +#include + +#define SEC_TO_US(sec) ((sec)*1000000) +#define NS_TO_US(ns) ((ns)/1000) + +static uint64_t initial_ts = -1; + +uint64_t m3_GetTimestamp() +{ + if (initial_ts == -1) { + initial_ts = 0; + initial_ts = m3_GetTimestamp(); + } + struct timespec ts; + timespec_get(&ts, TIME_UTC); + uint64_t us = SEC_TO_US((uint64_t)ts.tv_sec) + NS_TO_US((uint64_t)ts.tv_nsec); + return us - initial_ts; +} + +#endif + +#if d_m3FixedHeap + +static u8 fixedHeap[d_m3FixedHeap]; +static u8* fixedHeapPtr = fixedHeap; +static u8* const fixedHeapEnd = fixedHeap + d_m3FixedHeap; +static u8* fixedHeapLast = NULL; + +#if d_m3FixedHeapAlign > 1 +# define HEAP_ALIGN_PTR(P) P = (u8*)(((size_t)(P)+(d_m3FixedHeapAlign-1)) & ~ (d_m3FixedHeapAlign-1)); +#else +# define HEAP_ALIGN_PTR(P) +#endif + +void * m3_Malloc_Impl (size_t i_size) +{ + u8 * ptr = fixedHeapPtr; + + fixedHeapPtr += i_size; + HEAP_ALIGN_PTR(fixedHeapPtr); + + if (fixedHeapPtr >= fixedHeapEnd) + { + return NULL; + } + + memset (ptr, 0x0, i_size); + fixedHeapLast = ptr; + + return ptr; +} + +void m3_Free_Impl (void * i_ptr) +{ + // Handle the last chunk + if (i_ptr && i_ptr == fixedHeapLast) { + fixedHeapPtr = fixedHeapLast; + fixedHeapLast = NULL; + } else { + //printf("== free %p [failed]\n", io_ptr); + } +} + +void * m3_Realloc_Impl (void * i_ptr, size_t i_newSize, size_t i_oldSize) +{ + if (M3_UNLIKELY(i_newSize == i_oldSize)) return i_ptr; + + void * newPtr; + + // Handle the last chunk + if (i_ptr && i_ptr == fixedHeapLast) { + fixedHeapPtr = fixedHeapLast + i_newSize; + HEAP_ALIGN_PTR(fixedHeapPtr); + if (fixedHeapPtr >= fixedHeapEnd) + { + return NULL; + } + newPtr = i_ptr; + } else { + newPtr = m3_Malloc_Impl(i_newSize); + if (!newPtr) { + return NULL; + } + if (i_ptr) { + memcpy(newPtr, i_ptr, i_oldSize); + } + } + + if (i_newSize > i_oldSize) { + memset ((u8 *) newPtr + i_oldSize, 0x0, i_newSize - i_oldSize); + } + + return newPtr; +} + +#else + +void * m3_Malloc_Impl (size_t i_size) +{ + return calloc (i_size, 1); +} + +void m3_Free_Impl (void * io_ptr) +{ + free (io_ptr); +} + +void * m3_Realloc_Impl (void * i_ptr, size_t i_newSize, size_t i_oldSize) +{ + if (M3_UNLIKELY(i_newSize == i_oldSize)) return i_ptr; + + void * newPtr = realloc (i_ptr, i_newSize); + + if (M3_LIKELY(newPtr)) + { + if (i_newSize > i_oldSize) { + memset ((u8 *) newPtr + i_oldSize, 0x0, i_newSize - i_oldSize); + } + return newPtr; + } + return NULL; +} + +#endif + +void * m3_CopyMem (const void * i_from, size_t i_size) +{ + void * ptr = m3_Malloc("CopyMem", i_size); + if (ptr) { + memcpy (ptr, i_from, i_size); + } + return ptr; +} + +//-------------------------------------------------------------------------------------------- + +#if d_m3LogNativeStack + +static size_t stack_start; +static size_t stack_end; + +void m3StackCheckInit () +{ + char stack; + stack_end = stack_start = (size_t)&stack; +} + +void m3StackCheck () +{ + char stack; + size_t addr = (size_t)&stack; + + size_t stackEnd = stack_end; + stack_end = M3_MIN (stack_end, addr); + +// if (stackEnd != stack_end) +// printf ("maxStack: %ld\n", m3StackGetMax ()); +} + +int m3StackGetMax () +{ + return stack_start - stack_end; +} + +#endif + +//-------------------------------------------------------------------------------------------- + +M3Result NormalizeType (u8 * o_type, i8 i_convolutedWasmType) +{ + M3Result result = m3Err_none; + + u8 type = -i_convolutedWasmType; + + if (type == 0x40) + type = c_m3Type_none; + // Accept v128 (wasm-encoded as 0x7b → -i_convolutedWasmType == 5) + // as an opaque slot so modules with v128 in signatures or local + // declarations parse. Actual v128 opcodes still hit + // m3Err_unknownOpcode at compile time - we just stop refusing + // unused SIMD slots that auto-vectorization emits. + else if (type < c_m3Type_i32 or type > c_m3Type_v128) + result = m3Err_invalidTypeId; + + * o_type = type; + + return result; +} + + +bool IsFpType (u8 i_m3Type) +{ + return (i_m3Type == c_m3Type_f32 or i_m3Type == c_m3Type_f64); +} + + +bool IsIntType (u8 i_m3Type) +{ + return (i_m3Type == c_m3Type_i32 or i_m3Type == c_m3Type_i64); +} + + +bool Is64BitType (u8 i_m3Type) +{ + if (i_m3Type == c_m3Type_i64 or i_m3Type == c_m3Type_f64) + return true; + else if (i_m3Type == c_m3Type_i32 or i_m3Type == c_m3Type_f32 or i_m3Type == c_m3Type_none) + return false; + else + return (sizeof (voidptr_t) == 8); // all other cases are pointers +} + +u32 SizeOfType (u8 i_m3Type) +{ + if (i_m3Type == c_m3Type_i32 or i_m3Type == c_m3Type_f32) + return sizeof (i32); + + return sizeof (i64); +} + + +//-- Binary Wasm parsing utils ------------------------------------------------------------------------------------------ + + +M3Result Read_u64 (u64 * o_value, bytes_t * io_bytes, cbytes_t i_end) +{ + const u8 * ptr = * io_bytes; + ptr += sizeof (u64); + + if (ptr <= i_end) + { + memcpy(o_value, * io_bytes, sizeof(u64)); + M3_BSWAP_u64(*o_value); + * io_bytes = ptr; + return m3Err_none; + } + else return m3Err_wasmUnderrun; +} + + +M3Result Read_u32 (u32 * o_value, bytes_t * io_bytes, cbytes_t i_end) +{ + const u8 * ptr = * io_bytes; + ptr += sizeof (u32); + + if (ptr <= i_end) + { + memcpy(o_value, * io_bytes, sizeof(u32)); + M3_BSWAP_u32(*o_value); + * io_bytes = ptr; + return m3Err_none; + } + else return m3Err_wasmUnderrun; +} + +#if d_m3ImplementFloat + +M3Result Read_f64 (f64 * o_value, bytes_t * io_bytes, cbytes_t i_end) +{ + const u8 * ptr = * io_bytes; + ptr += sizeof (f64); + + if (ptr <= i_end) + { + memcpy(o_value, * io_bytes, sizeof(f64)); + M3_BSWAP_f64(*o_value); + * io_bytes = ptr; + return m3Err_none; + } + else return m3Err_wasmUnderrun; +} + + +M3Result Read_f32 (f32 * o_value, bytes_t * io_bytes, cbytes_t i_end) +{ + const u8 * ptr = * io_bytes; + ptr += sizeof (f32); + + if (ptr <= i_end) + { + memcpy(o_value, * io_bytes, sizeof(f32)); + M3_BSWAP_f32(*o_value); + * io_bytes = ptr; + return m3Err_none; + } + else return m3Err_wasmUnderrun; +} + +#endif + +M3Result Read_u8 (u8 * o_value, bytes_t * io_bytes, cbytes_t i_end) +{ + const u8 * ptr = * io_bytes; + + if (ptr < i_end) + { + * o_value = * ptr; + * io_bytes = ptr + 1; + + return m3Err_none; + } + else return m3Err_wasmUnderrun; +} + +M3Result Read_opcode (m3opcode_t * o_value, bytes_t * io_bytes, cbytes_t i_end) +{ + const u8 * ptr = * io_bytes; + + if (ptr < i_end) + { + m3opcode_t opcode = * ptr++; + +#if d_m3CascadedOpcodes == 0 + if (M3_UNLIKELY(opcode == c_waOp_extended)) + { + if (ptr < i_end) + { + opcode = (opcode << 8) | (* ptr++); + } + else return m3Err_wasmUnderrun; + } +#endif + * o_value = opcode; + * io_bytes = ptr; + + return m3Err_none; + } + else return m3Err_wasmUnderrun; +} + + +M3Result ReadLebUnsigned (u64 * o_value, u32 i_maxNumBits, bytes_t * io_bytes, cbytes_t i_end) +{ + M3Result result = m3Err_wasmUnderrun; + + u64 value = 0; + + u32 shift = 0; + const u8 * ptr = * io_bytes; + + while (ptr < i_end) + { + u64 byte = * (ptr++); + + value |= ((byte & 0x7f) << shift); + shift += 7; + + if ((byte & 0x80) == 0) + { + result = m3Err_none; + +#if d_m3EnableValidation + // The last byte must not carry bits past i_maxNumBits + if (shift > i_maxNumBits) + { + u32 numUsedBits = i_maxNumBits + 7 - shift; + + if (byte >> numUsedBits) + result = m3Err_lebOverflow; + } +#endif + break; + } + + if (shift >= i_maxNumBits) + { + result = m3Err_lebOverflow; + break; + } + } + + * o_value = value; + * io_bytes = ptr; + + return result; +} + + +M3Result ReadLebSigned (i64 * o_value, u32 i_maxNumBits, bytes_t * io_bytes, cbytes_t i_end) +{ + M3Result result = m3Err_wasmUnderrun; + + i64 value = 0; + + u32 shift = 0; + const u8 * ptr = * io_bytes; + + while (ptr < i_end) + { + u64 byte = * (ptr++); + + value |= ((byte & 0x7f) << shift); + shift += 7; + + if ((byte & 0x80) == 0) + { + result = m3Err_none; + +#if d_m3EnableValidation + // The bits of the last byte past i_maxNumBits must all repeat the + // sign bit, otherwise the value doesn't fit + if (shift > i_maxNumBits) + { + u32 numUsedBits = i_maxNumBits + 7 - shift; + u8 signBits = (u8) ((0x7f << (numUsedBits - 1)) & 0x7f); + u8 bits = (u8) (byte & signBits); + + if (bits != 0 and bits != signBits) + result = m3Err_lebOverflow; + } +#endif + if ((byte & 0x40) and (shift < 64)) // do sign extension + { + u64 extend = 0; + value |= (~extend << shift); + } + + break; + } + + if (shift >= i_maxNumBits) + { + result = m3Err_lebOverflow; + break; + } + } + + * o_value = value; + * io_bytes = ptr; + + return result; +} + + +M3Result ReadLEB_u32 (u32 * o_value, bytes_t * io_bytes, cbytes_t i_end) +{ + u64 value; + M3Result result = ReadLebUnsigned (& value, 32, io_bytes, i_end); + * o_value = (u32) value; + + return result; +} + + +M3Result ReadLEB_u7 (u8 * o_value, bytes_t * io_bytes, cbytes_t i_end) +{ + u64 value; + M3Result result = ReadLebUnsigned (& value, 7, io_bytes, i_end); + * o_value = (u8) value; + + return result; +} + + +M3Result ReadLEB_i7 (i8 * o_value, bytes_t * io_bytes, cbytes_t i_end) +{ + i64 value; + M3Result result = ReadLebSigned (& value, 7, io_bytes, i_end); + * o_value = (i8) value; + + return result; +} + + +M3Result ReadLEB_i32 (i32 * o_value, bytes_t * io_bytes, cbytes_t i_end) +{ + i64 value; + M3Result result = ReadLebSigned (& value, 32, io_bytes, i_end); + * o_value = (i32) value; + + return result; +} + + +M3Result ReadLEB_i64 (i64 * o_value, bytes_t * io_bytes, cbytes_t i_end) +{ + i64 value; + M3Result result = ReadLebSigned (& value, 64, io_bytes, i_end); + * o_value = value; + + return result; +} + +#if d_m3EnableValidation +// Validate that a byte sequence is well-formed UTF-8 per the Unicode spec. +// Returns true if valid, false otherwise. +static bool IsValidUtf8 (const u8 * i_data, u32 i_length) +{ + const u8 * ptr = i_data; + const u8 * end = i_data + i_length; + + while (ptr < end) + { + u8 b0 = *ptr++; + + if (b0 < 0x80) + { + // single-byte: 0xxxxxxx + continue; + } + else if ((b0 & 0xE0) == 0xC0) + { + // two-byte: 110xxxxx 10xxxxxx + if (b0 < 0xC2) return false; // overlong + if (ptr >= end) return false; + u8 b1 = *ptr++; + if ((b1 & 0xC0) != 0x80) return false; + } + else if ((b0 & 0xF0) == 0xE0) + { + // three-byte: 1110xxxx 10xxxxxx 10xxxxxx + if (ptr + 1 >= end) return false; + u8 b1 = *ptr++; + u8 b2 = *ptr++; + if ((b1 & 0xC0) != 0x80) return false; + if ((b2 & 0xC0) != 0x80) return false; + // reject overlong + if (b0 == 0xE0 && b1 < 0xA0) return false; + // reject surrogates U+D800..U+DFFF + if (b0 == 0xED && b1 >= 0xA0) return false; + } + else if ((b0 & 0xF8) == 0xF0) + { + // four-byte: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + if (b0 > 0xF4) return false; // above U+10FFFF + if (ptr + 2 >= end) return false; + u8 b1 = *ptr++; + u8 b2 = *ptr++; + u8 b3 = *ptr++; + if ((b1 & 0xC0) != 0x80) return false; + if ((b2 & 0xC0) != 0x80) return false; + if ((b3 & 0xC0) != 0x80) return false; + // reject overlong + if (b0 == 0xF0 && b1 < 0x90) return false; + // reject above U+10FFFF + if (b0 == 0xF4 && b1 > 0x8F) return false; + } + else + { + // invalid leading byte (0x80..0xBF or 0xF5..0xFF) + return false; + } + } + + return true; +} +#endif // d_m3EnableValidation + + +M3Result Read_utf8 (cstr_t * o_utf8, bytes_t * io_bytes, cbytes_t i_end) +{ + *o_utf8 = NULL; + + u32 utf8Length; + M3Result result = ReadLEB_u32 (& utf8Length, io_bytes, i_end); + + if (not result) + { + if (utf8Length <= d_m3MaxSaneUtf8Length) + { + const u8 * ptr = * io_bytes; + const u8 * end = ptr + utf8Length; + + if (end <= i_end) + { +#if d_m3EnableValidation + if (not IsValidUtf8 (ptr, utf8Length)) + { + * io_bytes = end; + return m3Err_wasmMalformed; + } +#endif // d_m3EnableValidation + + char * utf8 = (char *)m3_Malloc ("UTF8", utf8Length + 1); + + if (utf8) + { + memcpy (utf8, ptr, utf8Length); + utf8 [utf8Length] = 0; + * o_utf8 = utf8; + } + + * io_bytes = end; + } + else result = m3Err_wasmUnderrun; + } + else result = m3Err_missingUTF8; + } + + return result; +} + +#if d_m3RecordBacktraces +u32 FindModuleOffset (IM3Runtime i_runtime, pc_t i_pc) +{ + // walk the code pages + IM3CodePage curr = i_runtime->pagesOpen; + bool pageFound = false; + + while (curr) + { + if (ContainsPC (curr, i_pc)) + { + pageFound = true; + break; + } + curr = curr->info.next; + } + + if (!pageFound) + { + curr = i_runtime->pagesFull; + while (curr) + { + if (ContainsPC (curr, i_pc)) + { + pageFound = true; + break; + } + curr = curr->info.next; + } + } + + if (pageFound) + { + u32 result = 0; + + bool pcFound = MapPCToOffset (curr, i_pc, & result); + d_m3Assert (pcFound); + + return result; + } + else return 0; +} + + +void PushBacktraceFrame (IM3Runtime io_runtime, pc_t i_pc) +{ + // don't try to push any more frames if we've already had an alloc failure + if (M3_UNLIKELY (io_runtime->backtrace.lastFrame == M3_BACKTRACE_TRUNCATED)) + return; + + M3BacktraceFrame * newFrame = m3_AllocStruct(M3BacktraceFrame); + + if (!newFrame) + { + io_runtime->backtrace.lastFrame = M3_BACKTRACE_TRUNCATED; + return; + } + + newFrame->moduleOffset = FindModuleOffset (io_runtime, i_pc); + + if (!io_runtime->backtrace.frames || !io_runtime->backtrace.lastFrame) + io_runtime->backtrace.frames = newFrame; + else + io_runtime->backtrace.lastFrame->next = newFrame; + io_runtime->backtrace.lastFrame = newFrame; +} + + +void FillBacktraceFunctionInfo (IM3Runtime io_runtime, IM3Function i_function) +{ + // If we've had an alloc failure then the last frame doesn't refer to the + // frame we want to fill in the function info for. + if (M3_UNLIKELY (io_runtime->backtrace.lastFrame == M3_BACKTRACE_TRUNCATED)) + return; + + if (!io_runtime->backtrace.lastFrame) + return; + + io_runtime->backtrace.lastFrame->function = i_function; +} + + +void ClearBacktrace (IM3Runtime io_runtime) +{ + M3BacktraceFrame * currentFrame = io_runtime->backtrace.frames; + while (currentFrame) + { + M3BacktraceFrame * nextFrame = currentFrame->next; + m3_Free (currentFrame); + currentFrame = nextFrame; + } + + io_runtime->backtrace.frames = NULL; + io_runtime->backtrace.lastFrame = NULL; +} +#endif // d_m3RecordBacktraces diff --git a/driver/wasm3/m3_core.h b/driver/wasm3/m3_core.h new file mode 100644 index 0000000..daf37cc --- /dev/null +++ b/driver/wasm3/m3_core.h @@ -0,0 +1,311 @@ +// +// m3_core.h +// +// Created by Steven Massey on 4/15/19. +// Copyright © 2019 Steven Massey. All rights reserved. +// + +#ifndef m3_core_h +#define m3_core_h + +#include +#include +#include +#include +#include + +#include "wasm3.h" +#include "m3_config.h" + +# if defined(__cplusplus) +# define d_m3BeginExternC extern "C" { +# define d_m3EndExternC } +# else +# define d_m3BeginExternC +# define d_m3EndExternC +# endif + +d_m3BeginExternC + +#define d_m3ImplementFloat (d_m3HasFloat || d_m3NoFloatDynamic) + +#if !defined(d_m3ShortTypesDefined) + +typedef uint64_t u64; +typedef int64_t i64; +typedef uint32_t u32; +typedef int32_t i32; +typedef uint16_t u16; +typedef int16_t i16; +typedef uint8_t u8; +typedef int8_t i8; + +#if d_m3ImplementFloat +typedef double f64; +typedef float f32; +#endif + +#endif // d_m3ShortTypesDefined + +#define PRIf32 "f" +#define PRIf64 "lf" + +typedef const void * m3ret_t; +typedef const void * voidptr_t; +typedef const char * cstr_t; +typedef const char * const ccstr_t; +typedef const u8 * bytes_t; +typedef const u8 * const cbytes_t; + +typedef u16 m3opcode_t; + +typedef i64 m3reg_t; + +# if d_m3Use32BitSlots +typedef u32 m3slot_t; +# else +typedef u64 m3slot_t; +# endif + +typedef m3slot_t * m3stack_t; + +typedef +const void * const cvptr_t; + +# if defined (DEBUG) + +# define d_m3Log(CATEGORY, FMT, ...) printf (" %8s | " FMT, #CATEGORY, ##__VA_ARGS__); + +# if d_m3LogParse +# define m3log_parse(CATEGORY, FMT, ...) d_m3Log(CATEGORY, FMT, ##__VA_ARGS__) +# else +# define m3log_parse(...) {} +# endif + +# if d_m3LogCompile +# define m3log_compile(CATEGORY, FMT, ...) d_m3Log(CATEGORY, FMT, ##__VA_ARGS__) +# else +# define m3log_compile(...) {} +# endif + +# if d_m3LogEmit +# define m3log_emit(CATEGORY, FMT, ...) d_m3Log(CATEGORY, FMT, ##__VA_ARGS__) +# else +# define m3log_emit(...) {} +# endif + +# if d_m3LogCodePages +# define m3log_code(CATEGORY, FMT, ...) d_m3Log(CATEGORY, FMT, ##__VA_ARGS__) +# else +# define m3log_code(...) {} +# endif + +# if d_m3LogModule +# define m3log_module(CATEGORY, FMT, ...) d_m3Log(CATEGORY, FMT, ##__VA_ARGS__) +# else +# define m3log_module(...) {} +# endif + +# if d_m3LogRuntime +# define m3log_runtime(CATEGORY, FMT, ...) d_m3Log(CATEGORY, FMT, ##__VA_ARGS__) +# else +# define m3log_runtime(...) {} +# endif + +# define m3log(CATEGORY, FMT, ...) m3log_##CATEGORY (CATEGORY, FMT "\n", ##__VA_ARGS__) +# else +# define d_m3Log(CATEGORY, FMT, ...) {} +# define m3log(CATEGORY, FMT, ...) {} +# endif + + +# if defined(ASSERTS) || (defined(DEBUG) && !defined(NASSERTS)) +# define d_m3Assert(ASS) if (!(ASS)) { printf("Assertion failed at %s:%d : %s\n", __FILE__, __LINE__, #ASS); abort(); } +# else +# define d_m3Assert(ASS) +# endif + +typedef void /*const*/ * code_t; +typedef code_t const * /*__restrict__*/ pc_t; + + +typedef struct M3MemoryHeader +{ + IM3Runtime runtime; + void * maxStack; + size_t length; +} +M3MemoryHeader; + +struct M3CodeMappingPage; + +typedef struct M3CodePageHeader +{ + struct M3CodePage * next; + + u32 lineIndex; + u32 numLines; + u32 sequence; // this is just used for debugging; could be removed + u32 usageCount; + +# if d_m3RecordBacktraces + struct M3CodeMappingPage * mapping; +# endif // d_m3RecordBacktraces +} +M3CodePageHeader; + + +#define d_m3CodePageFreeLinesThreshold 4+2 // max is: select _sss & CallIndirect + 2 for bridge + +#define d_m3DefaultMemPageSize 65536 + +#define d_m3Reg0SlotAlias 60000 +#define d_m3Fp0SlotAlias (d_m3Reg0SlotAlias + 2) + +#define d_m3MaxSaneTypesCount 1000000 +#define d_m3MaxSaneFunctionsCount 1000000 +#define d_m3MaxSaneImportsCount 100000 +#define d_m3MaxSaneExportsCount 100000 +#define d_m3MaxSaneGlobalsCount 1000000 +#define d_m3MaxSaneElementSegments 10000000 +#define d_m3MaxSaneDataSegments 100000 +#define d_m3MaxSaneTableSize 10000000 +#define d_m3MaxSaneUtf8Length 10000 +#define d_m3MaxSaneFunctionArgRetCount 1000 // still insane, but whatever + +#define d_externalKind_function 0 +#define d_externalKind_table 1 +#define d_externalKind_memory 2 +#define d_externalKind_global 3 + +static const char * const c_waTypes [] = { "nil", "i32", "i64", "f32", "f64", "unknown" }; +static const char * const c_waCompactTypes [] = { "_", "i", "I", "f", "F", "?" }; + + +# if d_m3VerboseErrorMessages + +M3Result m3Error (M3Result i_result, IM3Runtime i_runtime, IM3Module i_module, IM3Function i_function, + const char * const i_file, u32 i_lineNum, const char * const i_errorMessage, ...); + +# define _m3Error(RESULT, RT, MOD, FUN, FILE, LINE, FORMAT, ...) \ + m3Error (RESULT, RT, MOD, FUN, FILE, LINE, FORMAT, ##__VA_ARGS__) + +# else +# define _m3Error(RESULT, RT, MOD, FUN, FILE, LINE, FORMAT, ...) (RESULT) +# endif + +#define ErrorRuntime(RESULT, RUNTIME, FORMAT, ...) _m3Error (RESULT, RUNTIME, NULL, NULL, __FILE__, __LINE__, FORMAT, ##__VA_ARGS__) +#define ErrorModule(RESULT, MOD, FORMAT, ...) _m3Error (RESULT, MOD->runtime, MOD, NULL, __FILE__, __LINE__, FORMAT, ##__VA_ARGS__) +#define ErrorCompile(RESULT, COMP, FORMAT, ...) _m3Error (RESULT, COMP->runtime, COMP->module, NULL, __FILE__, __LINE__, FORMAT, ##__VA_ARGS__) + +#if d_m3LogNativeStack +void m3StackCheckInit (); +void m3StackCheck (); +int m3StackGetMax (); +#else +#define m3StackCheckInit() +#define m3StackCheck() +#define m3StackGetMax() 0 +#endif + +#if d_m3LogTimestamps +#define PRIts "%llu" +uint64_t m3_GetTimestamp (); +#else +#define PRIts "%s" +#define m3_GetTimestamp() "" +#endif + +void m3_Abort (const char* message); +void * m3_Malloc_Impl (size_t i_size); +void * m3_Realloc_Impl (void * i_ptr, size_t i_newSize, size_t i_oldSize); +void m3_Free_Impl (void * i_ptr); +void * m3_CopyMem (const void * i_from, size_t i_size); + +#if d_m3LogHeapOps + +// Tracing format: timestamp;heap:OpCode;name;size(bytes);new items;new ptr;old items;old ptr + +static inline void * m3_AllocStruct_Impl(ccstr_t name, size_t i_size) { + void * result = m3_Malloc_Impl(i_size); + fprintf(stderr, PRIts ";heap:AllocStruct;%s;%zu;;%p;;\n", m3_GetTimestamp(), name, i_size, result); + return result; +} + +static inline void * m3_AllocArray_Impl(ccstr_t name, size_t i_num, size_t i_size) { + void * result = m3_Malloc_Impl(i_size * i_num); + fprintf(stderr, PRIts ";heap:AllocArr;%s;%zu;%zu;%p;;\n", m3_GetTimestamp(), name, i_size, i_num, result); + return result; +} + +static inline void * m3_ReallocArray_Impl(ccstr_t name, void * i_ptr_old, size_t i_num_new, size_t i_num_old, size_t i_size) { + void * result = m3_Realloc_Impl (i_ptr_old, i_size * i_num_new, i_size * i_num_old); + fprintf(stderr, PRIts ";heap:ReallocArr;%s;%zu;%zu;%p;%zu;%p\n", m3_GetTimestamp(), name, i_size, i_num_new, result, i_num_old, i_ptr_old); + return result; +} + +static inline void * m3_Malloc (ccstr_t name, size_t i_size) { + void * result = m3_Malloc_Impl (i_size); + fprintf(stderr, PRIts ";heap:AllocMem;%s;%zu;;%p;;\n", m3_GetTimestamp(), name, i_size, result); + return result; +} +static inline void * m3_Realloc (ccstr_t name, void * i_ptr, size_t i_newSize, size_t i_oldSize) { + void * result = m3_Realloc_Impl (i_ptr, i_newSize, i_oldSize); + fprintf(stderr, PRIts ";heap:ReallocMem;%s;;%zu;%p;%zu;%p\n", m3_GetTimestamp(), name, i_newSize, result, i_oldSize, i_ptr); + return result; +} + +#define m3_AllocStruct(STRUCT) (STRUCT *)m3_AllocStruct_Impl (#STRUCT, sizeof (STRUCT)) +#define m3_AllocArray(STRUCT, NUM) (STRUCT *)m3_AllocArray_Impl (#STRUCT, NUM, sizeof (STRUCT)) +#define m3_ReallocArray(STRUCT, PTR, NEW, OLD) (STRUCT *)m3_ReallocArray_Impl (#STRUCT, (void *)(PTR), (NEW), (OLD), sizeof (STRUCT)) +#define m3_Free(P) do { void* p = (void*)(P); \ + if (p) { fprintf(stderr, PRIts ";heap:FreeMem;;;;%p;\n", m3_GetTimestamp(), p); } \ + m3_Free_Impl (p); (P) = NULL; } while(0) +#else +#define m3_Malloc(NAME, SIZE) m3_Malloc_Impl(SIZE) +#define m3_Realloc(NAME, PTR, NEW, OLD) m3_Realloc_Impl(PTR, NEW, OLD) +#define m3_AllocStruct(STRUCT) (STRUCT *)m3_Malloc_Impl (sizeof (STRUCT)) +#define m3_AllocArray(STRUCT, NUM) (STRUCT *)m3_Malloc_Impl (sizeof (STRUCT) * (NUM)) +#define m3_ReallocArray(STRUCT, PTR, NEW, OLD) (STRUCT *)m3_Realloc_Impl ((void *)(PTR), sizeof (STRUCT) * (NEW), sizeof (STRUCT) * (OLD)) +#define m3_Free(P) do { m3_Free_Impl ((void*)(P)); (P) = NULL; } while(0) +#endif + +M3Result NormalizeType (u8 * o_type, i8 i_convolutedWasmType); + +bool IsIntType (u8 i_wasmType); +bool IsFpType (u8 i_wasmType); +bool Is64BitType (u8 i_m3Type); +u32 SizeOfType (u8 i_m3Type); + +M3Result Read_u64 (u64 * o_value, bytes_t * io_bytes, cbytes_t i_end); +M3Result Read_u32 (u32 * o_value, bytes_t * io_bytes, cbytes_t i_end); +#if d_m3ImplementFloat +M3Result Read_f64 (f64 * o_value, bytes_t * io_bytes, cbytes_t i_end); +M3Result Read_f32 (f32 * o_value, bytes_t * io_bytes, cbytes_t i_end); +#endif +M3Result Read_u8 (u8 * o_value, bytes_t * io_bytes, cbytes_t i_end); +M3Result Read_opcode (m3opcode_t * o_value, bytes_t * io_bytes, cbytes_t i_end); + +M3Result ReadLebUnsigned (u64 * o_value, u32 i_maxNumBits, bytes_t * io_bytes, cbytes_t i_end); +M3Result ReadLebSigned (i64 * o_value, u32 i_maxNumBits, bytes_t * io_bytes, cbytes_t i_end); +M3Result ReadLEB_u32 (u32 * o_value, bytes_t * io_bytes, cbytes_t i_end); +M3Result ReadLEB_u7 (u8 * o_value, bytes_t * io_bytes, cbytes_t i_end); +M3Result ReadLEB_i7 (i8 * o_value, bytes_t * io_bytes, cbytes_t i_end); +M3Result ReadLEB_i32 (i32 * o_value, bytes_t * io_bytes, cbytes_t i_end); +M3Result ReadLEB_i64 (i64 * o_value, bytes_t * io_bytes, cbytes_t i_end); +M3Result Read_utf8 (cstr_t * o_utf8, bytes_t * io_bytes, cbytes_t i_end); + +cstr_t SPrintValue (void * i_value, u8 i_type); +size_t SPrintArg (char * o_string, size_t i_stringBufferSize, voidptr_t i_sp, u8 i_type); + +void ReportError (IM3Runtime io_runtime, IM3Module i_module, IM3Function i_function, ccstr_t i_errorMessage, ccstr_t i_file, u32 i_lineNum); + +# if d_m3RecordBacktraces +void PushBacktraceFrame (IM3Runtime io_runtime, pc_t i_pc); +void FillBacktraceFunctionInfo (IM3Runtime io_runtime, IM3Function i_function); +void ClearBacktrace (IM3Runtime io_runtime); +# endif + +d_m3EndExternC + +#endif // m3_core_h diff --git a/driver/wasm3/m3_env.c b/driver/wasm3/m3_env.c new file mode 100644 index 0000000..83f4976 --- /dev/null +++ b/driver/wasm3/m3_env.c @@ -0,0 +1,1239 @@ +// +// m3_env.c +// +// Created by Steven Massey on 4/19/19. +// Copyright © 2019 Steven Massey. All rights reserved. +// + +#include +#include + +#include "m3_env.h" +#include "m3_compile.h" +#include "m3_exception.h" +#include "m3_info.h" + + +IM3Environment m3_NewEnvironment () +{ + IM3Environment env = m3_AllocStruct (M3Environment); + + if (env) + { + _try + { + // create FuncTypes for all simple block return ValueTypes + for (u8 t = c_m3Type_none; t <= c_m3Type_f64; t++) + { + IM3FuncType ftype; +_ (AllocFuncType (& ftype, 1)); + + ftype->numArgs = 0; + ftype->numRets = (t == c_m3Type_none) ? 0 : 1; + ftype->types [0] = t; + + Environment_AddFuncType (env, & ftype); + + d_m3Assert (t < 5); + env->retFuncTypes [t] = ftype; + } + } + + _catch: + if (result) + { + m3_FreeEnvironment (env); + env = NULL; + } + } + + return env; +} + + +void Environment_Release (IM3Environment i_environment) +{ + IM3FuncType ftype = i_environment->funcTypes; + + while (ftype) + { + IM3FuncType next = ftype->next; + m3_Free (ftype); + ftype = next; + } + + m3log (runtime, "freeing %d pages from environment", CountCodePages (i_environment->pagesReleased)); + FreeCodePages (& i_environment->pagesReleased); +} + + +void m3_FreeEnvironment (IM3Environment i_environment) +{ + if (i_environment) + { + Environment_Release (i_environment); + m3_Free (i_environment); + } +} + + +void m3_SetCustomSectionHandler (IM3Environment i_environment, M3SectionHandler i_handler) +{ + if (i_environment) i_environment->customSectionHandler = i_handler; +} + + +// returns the same io_funcType or replaces it with an equivalent that's already in the type linked list +void Environment_AddFuncType (IM3Environment i_environment, IM3FuncType * io_funcType) +{ + IM3FuncType addType = * io_funcType; + IM3FuncType newType = i_environment->funcTypes; + + while (newType) + { + if (AreFuncTypesEqual (newType, addType)) + { + m3_Free (addType); + break; + } + + newType = newType->next; + } + + if (newType == NULL) + { + newType = addType; + newType->next = i_environment->funcTypes; + i_environment->funcTypes = newType; + } + + * io_funcType = newType; +} + + +IM3CodePage RemoveCodePageOfCapacity (M3CodePage ** io_list, u32 i_minimumLineCount) +{ + IM3CodePage prev = NULL; + IM3CodePage page = * io_list; + + while (page) + { + if (NumFreeLines (page) >= i_minimumLineCount) + { d_m3Assert (page->info.usageCount == 0); + IM3CodePage next = page->info.next; + if (prev) + prev->info.next = next; // mid-list + else + * io_list = next; // front of list + + break; + } + + prev = page; + page = page->info.next; + } + + return page; +} + + +IM3CodePage Environment_AcquireCodePage (IM3Environment i_environment, u32 i_minimumLineCount) +{ + return RemoveCodePageOfCapacity (& i_environment->pagesReleased, i_minimumLineCount); +} + + +void Environment_ReleaseCodePages (IM3Environment i_environment, IM3CodePage i_codePageList) +{ + IM3CodePage end = i_codePageList; + + while (end) + { + end->info.lineIndex = 0; // reset page +#if d_m3RecordBacktraces + end->info.mapping->size = 0; +#endif // d_m3RecordBacktraces + + IM3CodePage next = end->info.next; + if (not next) + break; + + end = next; + } + + if (end) + { + // push list to front + end->info.next = i_environment->pagesReleased; + i_environment->pagesReleased = i_codePageList; + } +} + + +IM3Runtime m3_NewRuntime (IM3Environment i_environment, u32 i_stackSizeInBytes, void * i_userdata) +{ + IM3Runtime runtime = m3_AllocStruct (M3Runtime); + + if (runtime) + { + m3_ResetErrorInfo(runtime); + + runtime->environment = i_environment; + runtime->userdata = i_userdata; + + runtime->originStack = m3_Malloc ("Wasm Stack", i_stackSizeInBytes + 4*sizeof (m3slot_t)); // TODO: more precise stack checks + + if (runtime->originStack) + { + runtime->stack = runtime->originStack; + runtime->numStackSlots = i_stackSizeInBytes / sizeof (m3slot_t); m3log (runtime, "new stack: %p", runtime->originStack); + } + else m3_Free (runtime); + } + + return runtime; +} + +void * m3_GetUserData (IM3Runtime i_runtime) +{ + return i_runtime ? i_runtime->userdata : NULL; +} + + +void * ForEachModule (IM3Runtime i_runtime, ModuleVisitor i_visitor, void * i_info) +{ + void * r = NULL; + + IM3Module module = i_runtime->modules; + + while (module) + { + IM3Module next = module->next; + r = i_visitor (module, i_info); + if (r) + break; + + module = next; + } + + return r; +} + + +void * _FreeModule (IM3Module i_module, void * i_info) +{ + m3_FreeModule (i_module); + return NULL; +} + + +void Runtime_Release (IM3Runtime i_runtime) +{ + ForEachModule (i_runtime, _FreeModule, NULL); d_m3Assert (i_runtime->numActiveCodePages == 0); + + Environment_ReleaseCodePages (i_runtime->environment, i_runtime->pagesOpen); + Environment_ReleaseCodePages (i_runtime->environment, i_runtime->pagesFull); + + m3_Free (i_runtime->originStack); + m3_Free (i_runtime->memory.mallocated); +} + + +void m3_FreeRuntime (IM3Runtime i_runtime) +{ + if (i_runtime) + { + m3_PrintProfilerInfo (); + + Runtime_Release (i_runtime); + m3_Free (i_runtime); + } +} + +M3Result EvaluateExpression (IM3Module i_module, void * o_expressed, u8 i_type, bytes_t * io_bytes, cbytes_t i_end) +{ + M3Result result = m3Err_none; + + // OPTZ: use a simplified interpreter for expressions + + // create a temporary runtime context +#if defined(d_m3PreferStaticAlloc) + static M3Runtime runtime; +#else + M3Runtime runtime; +#endif + M3_INIT (runtime); + + runtime.environment = i_module->runtime->environment; + runtime.numStackSlots = i_module->runtime->numStackSlots; + runtime.stack = i_module->runtime->stack; + + m3stack_t stack = (m3stack_t)runtime.stack; + + IM3Runtime savedRuntime = i_module->runtime; + i_module->runtime = & runtime; + + IM3Compilation o = & runtime.compilation; + o->runtime = & runtime; + o->module = i_module; + o->wasm = * io_bytes; + o->wasmEnd = i_end; + o->lastOpcodeStart = o->wasm; + + o->block.depth = -1; // so that root compilation depth = 0 + + // OPTZ: this code page could be erased after use. maybe have 'empty' list in addition to full and open? + o->page = AcquireCodePage (& runtime); // AcquireUnusedCodePage (...) + + if (o->page) + { + IM3FuncType ftype = runtime.environment->retFuncTypes[i_type]; + + pc_t m3code = GetPagePC (o->page); + result = CompileBlock (o, ftype, c_waOp_block); + + if (not result && o->maxStackSlots >= runtime.numStackSlots) { + result = m3Err_trapStackOverflow; + } + + if (not result) + { +# if (d_m3EnableOpProfiling || d_m3EnableOpTracing) + m3ret_t r = RunCode (m3code, stack, NULL, d_m3OpDefaultArgs, d_m3BaseCstr); +# else + m3ret_t r = RunCode (m3code, stack, NULL, d_m3OpDefaultArgs); +# endif + + if (r == 0) + { m3log (runtime, "expression result: %s", SPrintValue (stack, i_type)); + if (SizeOfType (i_type) == sizeof (u32)) + { + * (u32 *) o_expressed = * ((u32 *) stack); + } + else + { + * (u64 *) o_expressed = * ((u64 *) stack); + } + } + } + + // TODO: EraseCodePage (...) see OPTZ above + ReleaseCodePage (& runtime, o->page); + } + else result = m3Err_mallocFailedCodePage; + + runtime.originStack = NULL; // prevent free(stack) in ReleaseRuntime + Runtime_Release (& runtime); + i_module->runtime = savedRuntime; + + * io_bytes = o->wasm; + + return result; +} + + +M3Result InitMemory (IM3Runtime io_runtime, IM3Module i_module) +{ + M3Result result = m3Err_none; //d_m3Assert (not io_runtime->memory.wasmPages); + + if (not i_module->memoryImported) + { + u32 maxPages = i_module->memoryInfo.maxPages; + u32 pageSize = i_module->memoryInfo.pageSize; + io_runtime->memory.maxPages = maxPages ? maxPages : 65536; + io_runtime->memory.pageSize = pageSize ? pageSize : d_m3DefaultMemPageSize; + + result = ResizeMemory (io_runtime, i_module->memoryInfo.initPages); + } + + return result; +} + + +M3Result ResizeMemory (IM3Runtime io_runtime, u32 i_numPages) +{ + M3Result result = m3Err_none; + + u32 numPagesToAlloc = i_numPages; + + M3Memory * memory = & io_runtime->memory; + +#if 0 // Temporary fix for memory allocation + if (memory->mallocated) { + memory->numPages = i_numPages; + memory->mallocated->end = memory->wasmPages + (memory->numPages * io_runtime->memory.pageSize); + return result; + } + + i_numPagesToAlloc = 256; +#endif + + if (numPagesToAlloc <= memory->maxPages) + { + size_t numPageBytes = numPagesToAlloc * io_runtime->memory.pageSize; + +#if d_m3MaxLinearMemoryPages > 0 + _throwif("linear memory limitation exceeded", numPagesToAlloc > d_m3MaxLinearMemoryPages); +#endif + + // Limit the amount of memory that gets actually allocated + if (io_runtime->memoryLimit) { + numPageBytes = M3_MIN (numPageBytes, io_runtime->memoryLimit); + } + + size_t numBytes = numPageBytes + sizeof (M3MemoryHeader); + + size_t numPreviousBytes = memory->numPages * io_runtime->memory.pageSize; + if (numPreviousBytes) + numPreviousBytes += sizeof (M3MemoryHeader); + + void* newMem = m3_Realloc ("Wasm Linear Memory", memory->mallocated, numBytes, numPreviousBytes); + _throwifnull(newMem); + + memory->mallocated = (M3MemoryHeader*)newMem; + +# if d_m3LogRuntime + M3MemoryHeader * oldMallocated = memory->mallocated; +# endif + + memory->numPages = numPagesToAlloc; + + memory->mallocated->length = numPageBytes; + memory->mallocated->runtime = io_runtime; + + memory->mallocated->maxStack = (m3slot_t *) io_runtime->stack + io_runtime->numStackSlots; + + m3log (runtime, "resized old: %p; mem: %p; length: %zu; pages: %d", oldMallocated, memory->mallocated, memory->mallocated->length, memory->numPages); + } + else result = m3Err_wasmMemoryOverflow; + + _catch: return result; +} + + +M3Result InitGlobals (IM3Module io_module) +{ + M3Result result = m3Err_none; + + if (io_module->numGlobals) + { + // placing the globals in their structs isn't good for cache locality, but i don't really know what the global + // access patterns typically look like yet. + + // io_module->globalMemory = m3Alloc (m3reg_t, io_module->numGlobals); + + // if (io_module->globalMemory) + { + for (u32 i = 0; i < io_module->numGlobals; ++i) + { + M3Global * g = & io_module->globals [i]; m3log (runtime, "initializing global: %d", i); + + if (g->initExpr) + { + bytes_t start = g->initExpr; + + result = EvaluateExpression (io_module, & g->i64Value, g->type, & start, g->initExpr + g->initExprSize); + + if (not result) + { + // io_module->globalMemory [i] = initValue; + } + else break; + } + else + { m3log (runtime, "importing global"); + + } + } + } + // else result = ErrorModule (m3Err_mallocFailed, io_module, "could allocate globals for module: '%s", io_module->name); + } + + return result; +} + + +M3Result InitDataSegments (M3Memory * io_memory, IM3Module io_module) +{ + M3Result result = m3Err_none; + + _throwif ("unallocated linear memory", !(io_memory->mallocated)); + + for (u32 i = 0; i < io_module->numDataSegments; ++i) + { + M3DataSegment * segment = & io_module->dataSegments [i]; + + i32 segmentOffset; + bytes_t start = segment->initExpr; +_ (EvaluateExpression (io_module, & segmentOffset, c_m3Type_i32, & start, segment->initExpr + segment->initExprSize)); + + m3log (runtime, "loading data segment: %d; size: %d; offset: %d", i, segment->size, segmentOffset); + + if (segmentOffset >= 0 && (size_t)(segmentOffset) + segment->size <= io_memory->mallocated->length) + { + u8 * dest = m3MemData (io_memory->mallocated) + segmentOffset; + memcpy (dest, segment->data, segment->size); + } else { + _throw ("data segment out of bounds"); + } + } + + _catch: return result; +} + + +M3Result InitElements (IM3Module io_module) +{ + M3Result result = m3Err_none; + + bytes_t bytes = io_module->elementSection; + cbytes_t end = io_module->elementSectionEnd; + + for (u32 i = 0; i < io_module->numElementSegments; ++i) + { + u32 index; +_ (ReadLEB_u32 (& index, & bytes, end)); + + if (index == 0) + { + i32 offset; +_ (EvaluateExpression (io_module, & offset, c_m3Type_i32, & bytes, end)); + _throwif ("table underflow", offset < 0); + + u32 numElements; +_ (ReadLEB_u32 (& numElements, & bytes, end)); + + size_t endElement = (size_t) numElements + offset; + _throwif ("table overflow", endElement > d_m3MaxSaneTableSize); + + // is there any requirement that elements must be in increasing sequence? + // make sure the table isn't shrunk. + if (endElement > io_module->table0Size) + { + io_module->table0 = m3_ReallocArray (IM3Function, io_module->table0, endElement, io_module->table0Size); + io_module->table0Size = (u32) endElement; + } + _throwifnull(io_module->table0); + + for (u32 e = 0; e < numElements; ++e) + { + u32 functionIndex; +_ (ReadLEB_u32 (& functionIndex, & bytes, end)); + _throwif ("function index out of range", functionIndex >= io_module->numFunctions); + IM3Function function = & io_module->functions [functionIndex]; d_m3Assert (function); //printf ("table: %s\n", m3_GetFunctionName(function)); + io_module->table0 [e + offset] = function; + } + } + else _throw ("element table index must be zero for MVP"); + } + + _catch: return result; +} + +M3Result m3_CompileModule (IM3Module io_module) +{ + M3Result result = m3Err_none; + + for (u32 i = 0; i < io_module->numFunctions; ++i) + { + IM3Function f = & io_module->functions [i]; + if (f->wasm and not f->compiled) + { +_ (CompileFunction (f)); + } + } + + _catch: return result; +} + +M3Result m3_RunStart (IM3Module io_module) +{ +#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION + // Execution disabled for fuzzing builds + return m3Err_none; +#endif + + M3Result result = m3Err_none; + i32 startFunctionTmp = -1; + + if (io_module and io_module->startFunction >= 0) + { + IM3Function function = & io_module->functions [io_module->startFunction]; + + if (not function->compiled) + { +_ (CompileFunction (function)); + } + + IM3FuncType ftype = function->funcType; + if (ftype->numArgs != 0 || ftype->numRets != 0) + _throw (m3Err_argumentCountMismatch); + + IM3Module module = function->module; + IM3Runtime runtime = module->runtime; + + startFunctionTmp = io_module->startFunction; + io_module->startFunction = -1; + +# if (d_m3EnableOpProfiling || d_m3EnableOpTracing) + result = (M3Result) RunCode (function->compiled, (m3stack_t) runtime->stack, runtime->memory.mallocated, d_m3OpDefaultArgs, d_m3BaseCstr); +# else + result = (M3Result) RunCode (function->compiled, (m3stack_t) runtime->stack, runtime->memory.mallocated, d_m3OpDefaultArgs); +# endif + + if (result) + { + io_module->startFunction = startFunctionTmp; + EXCEPTION_PRINT(result); + goto _catch; + } + } + + _catch: return result; +} + +// TODO: deal with main + side-modules loading efforcement +M3Result m3_LoadModule (IM3Runtime io_runtime, IM3Module io_module) +{ + M3Result result = m3Err_none; + + if (M3_UNLIKELY(io_module->runtime)) { + return m3Err_moduleAlreadyLinked; + } + + io_module->runtime = io_runtime; + M3Memory * memory = & io_runtime->memory; + +_ (InitMemory (io_runtime, io_module)); +_ (InitGlobals (io_module)); +_ (InitDataSegments (memory, io_module)); +_ (InitElements (io_module)); + + // Start func might use imported functions, which are not liked here yet, + // so it will be called before a function call is attempted (in m3_FindFunction) + +#ifdef DEBUG + Module_GenerateNames(io_module); +#endif + + io_module->next = io_runtime->modules; + io_runtime->modules = io_module; + return result; // ok + +_catch: + io_module->runtime = NULL; + return result; +} + +IM3Global m3_FindGlobal (IM3Module io_module, + const char * const i_globalName) +{ + // Search exports + for (u32 i = 0; i < io_module->numGlobals; ++i) + { + IM3Global g = & io_module->globals [i]; + if (g->name and strcmp (g->name, i_globalName) == 0) + { + return g; + } + } + + // Search imports + for (u32 i = 0; i < io_module->numGlobals; ++i) + { + IM3Global g = & io_module->globals [i]; + + if (g->import.moduleUtf8 and g->import.fieldUtf8) + { + if (strcmp (g->import.fieldUtf8, i_globalName) == 0) + { + return g; + } + } + } + return NULL; +} + +M3Result m3_GetGlobal (IM3Global i_global, + IM3TaggedValue o_value) +{ + if (not i_global) return m3Err_globalLookupFailed; + + switch (i_global->type) { + case c_m3Type_i32: o_value->value.i32 = i_global->i32Value; break; + case c_m3Type_i64: o_value->value.i64 = i_global->i64Value; break; +# if d_m3HasFloat + case c_m3Type_f32: o_value->value.f32 = i_global->f32Value; break; + case c_m3Type_f64: o_value->value.f64 = i_global->f64Value; break; +# endif + default: return m3Err_invalidTypeId; + } + + o_value->type = (M3ValueType)(i_global->type); + return m3Err_none; +} + +M3Result m3_SetGlobal (IM3Global i_global, + const IM3TaggedValue i_value) +{ + if (not i_global) return m3Err_globalLookupFailed; + if (not i_global->isMutable) return m3Err_globalNotMutable; + if (i_global->type != i_value->type) return m3Err_globalTypeMismatch; + + switch (i_value->type) { + case c_m3Type_i32: i_global->i32Value = i_value->value.i32; break; + case c_m3Type_i64: i_global->i64Value = i_value->value.i64; break; +# if d_m3HasFloat + case c_m3Type_f32: i_global->f32Value = i_value->value.f32; break; + case c_m3Type_f64: i_global->f64Value = i_value->value.f64; break; +# endif + default: return m3Err_invalidTypeId; + } + + return m3Err_none; +} + +M3ValueType m3_GetGlobalType (IM3Global i_global) +{ + return (i_global) ? (M3ValueType)(i_global->type) : c_m3Type_none; +} + + +void * v_FindFunction (IM3Module i_module, const char * const i_name) +{ + + // Prefer exported functions + for (u32 i = 0; i < i_module->numFunctions; ++i) + { + IM3Function f = & i_module->functions [i]; + if (f->export_name and strcmp (f->export_name, i_name) == 0) + return f; + } + + // Search internal functions + for (u32 i = 0; i < i_module->numFunctions; ++i) + { + IM3Function f = & i_module->functions [i]; + + bool isImported = f->import.moduleUtf8 or f->import.fieldUtf8; + + if (isImported) + continue; + + for (int j = 0; j < f->numNames; j++) + { + if (f->names [j] and strcmp (f->names [j], i_name) == 0) + return f; + } + } + + return NULL; +} + + +M3Result m3_FindFunction (IM3Function * o_function, IM3Runtime i_runtime, const char * const i_functionName) +{ + M3Result result = m3Err_none; d_m3Assert (o_function and i_runtime and i_functionName); + + IM3Function function = NULL; + + if (not i_runtime->modules) { + _throw ("no modules loaded"); + } + + function = (IM3Function) ForEachModule (i_runtime, (ModuleVisitor) v_FindFunction, (void *) i_functionName); + + if (function) + { + if (not function->compiled) + { +_ (CompileFunction (function)) + } + } + else _throw (ErrorModule (m3Err_functionLookupFailed, i_runtime->modules, "'%s'", i_functionName)); + + _catch: + if (result) + function = NULL; + + * o_function = function; + + return result; +} + + +M3Result m3_GetTableFunction (IM3Function * o_function, IM3Module i_module, uint32_t i_index) +{ +_try { + if (i_index >= i_module->table0Size) + { + _throw ("function index out of range"); + } + + IM3Function function = i_module->table0[i_index]; + + if (function) + { + if (not function->compiled) + { +_ (CompileFunction (function)) + } + } + + * o_function = function; +} _catch: + return result; +} + + +static +M3Result checkStartFunction(IM3Module i_module) +{ + M3Result result = m3Err_none; d_m3Assert(i_module); + + // Check if start function needs to be called + if (i_module->startFunction >= 0) + { + result = m3_RunStart (i_module); + } + + return result; +} + +uint32_t m3_GetArgCount (IM3Function i_function) +{ + if (i_function) { + IM3FuncType ft = i_function->funcType; + if (ft) { + return ft->numArgs; + } + } + return 0; +} + +uint32_t m3_GetRetCount (IM3Function i_function) +{ + if (i_function) { + IM3FuncType ft = i_function->funcType; + if (ft) { + return ft->numRets; + } + } + return 0; +} + + +M3ValueType m3_GetArgType (IM3Function i_function, uint32_t index) +{ + if (i_function) { + IM3FuncType ft = i_function->funcType; + if (ft and index < ft->numArgs) { + return (M3ValueType)d_FuncArgType(ft, index); + } + } + return c_m3Type_none; +} + +M3ValueType m3_GetRetType (IM3Function i_function, uint32_t index) +{ + if (i_function) { + IM3FuncType ft = i_function->funcType; + if (ft and index < ft->numRets) { + return (M3ValueType) d_FuncRetType (ft, index); + } + } + return c_m3Type_none; +} + + +u8 * GetStackPointerForArgs (IM3Function i_function) +{ + u64 * stack = (u64 *) i_function->module->runtime->stack; + IM3FuncType ftype = i_function->funcType; + + stack += ftype->numRets; + + return (u8 *) stack; +} + + +M3Result m3_CallV (IM3Function i_function, ...) +{ + va_list ap; + va_start(ap, i_function); + M3Result r = m3_CallVL(i_function, ap); + va_end(ap); + return r; +} + +static +void ReportNativeStackUsage () +{ +# if d_m3LogNativeStack + int stackUsed = m3StackGetMax(); + fprintf (stderr, "Native stack used: %d\n", stackUsed); +# endif +} + + +M3Result m3_CallVL (IM3Function i_function, va_list i_args) +{ + IM3Runtime runtime = i_function->module->runtime; + IM3FuncType ftype = i_function->funcType; + M3Result result = m3Err_none; + u8* s = NULL; + + if (!i_function->compiled) { + return m3Err_missingCompiledCode; + } + +# if d_m3RecordBacktraces + ClearBacktrace (runtime); +# endif + + m3StackCheckInit(); + +_ (checkStartFunction(i_function->module)) + + s = GetStackPointerForArgs (i_function); + + for (u32 i = 0; i < ftype->numArgs; ++i) + { + switch (d_FuncArgType(ftype, i)) { + case c_m3Type_i32: *(i32*)(s) = va_arg(i_args, i32); s += 8; break; + case c_m3Type_i64: *(i64*)(s) = va_arg(i_args, i64); s += 8; break; +# if d_m3HasFloat + case c_m3Type_f32: *(f32*)(s) = va_arg(i_args, f64); s += 8; break; // f32 is passed as f64 + case c_m3Type_f64: *(f64*)(s) = va_arg(i_args, f64); s += 8; break; +# endif + default: return "unknown argument type"; + } + } + +# if (d_m3EnableOpProfiling || d_m3EnableOpTracing) + result = (M3Result) RunCode (i_function->compiled, (m3stack_t)(runtime->stack), runtime->memory.mallocated, d_m3OpDefaultArgs, d_m3BaseCstr); +# else + result = (M3Result) RunCode (i_function->compiled, (m3stack_t)(runtime->stack), runtime->memory.mallocated, d_m3OpDefaultArgs); +# endif + ReportNativeStackUsage (); + + runtime->lastCalled = result ? NULL : i_function; + + _catch: return result; +} + +M3Result m3_Call (IM3Function i_function, uint32_t i_argc, const void * i_argptrs[]) +{ + IM3Runtime runtime = i_function->module->runtime; + IM3FuncType ftype = i_function->funcType; + M3Result result = m3Err_none; + u8* s = NULL; + + if (i_argc != ftype->numArgs) { + return m3Err_argumentCountMismatch; + } + if (!i_function->compiled) { + return m3Err_missingCompiledCode; + } + +# if d_m3RecordBacktraces + ClearBacktrace (runtime); +# endif + + m3StackCheckInit(); + +_ (checkStartFunction(i_function->module)) + + s = GetStackPointerForArgs (i_function); + + for (u32 i = 0; i < ftype->numArgs; ++i) + { + switch (d_FuncArgType(ftype, i)) { + case c_m3Type_i32: *(i32*)(s) = *(i32*)i_argptrs[i]; s += 8; break; + case c_m3Type_i64: *(i64*)(s) = *(i64*)i_argptrs[i]; s += 8; break; +# if d_m3HasFloat + case c_m3Type_f32: *(f32*)(s) = *(f32*)i_argptrs[i]; s += 8; break; + case c_m3Type_f64: *(f64*)(s) = *(f64*)i_argptrs[i]; s += 8; break; +# endif + default: return "unknown argument type"; + } + } + +# if (d_m3EnableOpProfiling || d_m3EnableOpTracing) + result = (M3Result) RunCode (i_function->compiled, (m3stack_t)(runtime->stack), runtime->memory.mallocated, d_m3OpDefaultArgs, d_m3BaseCstr); +# else + result = (M3Result) RunCode (i_function->compiled, (m3stack_t)(runtime->stack), runtime->memory.mallocated, d_m3OpDefaultArgs); +# endif + + ReportNativeStackUsage (); + + runtime->lastCalled = result ? NULL : i_function; + + _catch: return result; +} + +M3Result m3_CallArgv (IM3Function i_function, uint32_t i_argc, const char * i_argv[]) +{ + IM3FuncType ftype = i_function->funcType; + IM3Runtime runtime = i_function->module->runtime; + M3Result result = m3Err_none; + u8* s = NULL; + + if (i_argc != ftype->numArgs) { + return m3Err_argumentCountMismatch; + } + if (!i_function->compiled) { + return m3Err_missingCompiledCode; + } + +# if d_m3RecordBacktraces + ClearBacktrace (runtime); +# endif + + m3StackCheckInit(); + +_ (checkStartFunction(i_function->module)) + + s = GetStackPointerForArgs (i_function); + + for (u32 i = 0; i < ftype->numArgs; ++i) + { + switch (d_FuncArgType(ftype, i)) { + case c_m3Type_i32: *(i32*)(s) = strtoul(i_argv[i], NULL, 10); s += 8; break; + case c_m3Type_i64: *(i64*)(s) = strtoull(i_argv[i], NULL, 10); s += 8; break; +# if d_m3HasFloat + case c_m3Type_f32: *(f32*)(s) = strtod(i_argv[i], NULL); s += 8; break; // strtof would be less portable + case c_m3Type_f64: *(f64*)(s) = strtod(i_argv[i], NULL); s += 8; break; +# endif + default: return "unknown argument type"; + } + } + +# if (d_m3EnableOpProfiling || d_m3EnableOpTracing) + result = (M3Result) RunCode (i_function->compiled, (m3stack_t)(runtime->stack), runtime->memory.mallocated, d_m3OpDefaultArgs, d_m3BaseCstr); +# else + result = (M3Result) RunCode (i_function->compiled, (m3stack_t)(runtime->stack), runtime->memory.mallocated, d_m3OpDefaultArgs); +# endif + + ReportNativeStackUsage (); + + runtime->lastCalled = result ? NULL : i_function; + + _catch: return result; +} + + +//u8 * AlignStackPointerTo64Bits (const u8 * i_stack) +//{ +// uintptr_t ptr = (uintptr_t) i_stack; +// return (u8 *) ((ptr + 7) & ~7); +//} + + +M3Result m3_GetResults (IM3Function i_function, uint32_t i_retc, const void * o_retptrs[]) +{ + IM3FuncType ftype = i_function->funcType; + IM3Runtime runtime = i_function->module->runtime; + + if (i_retc != ftype->numRets) { + return m3Err_argumentCountMismatch; + } + if (i_function != runtime->lastCalled) { + return "function not called"; + } + + u8* s = (u8*) runtime->stack; + + for (u32 i = 0; i < ftype->numRets; ++i) + { + switch (d_FuncRetType(ftype, i)) { + case c_m3Type_i32: *(i32*)o_retptrs[i] = *(i32*)(s); s += 8; break; + case c_m3Type_i64: *(i64*)o_retptrs[i] = *(i64*)(s); s += 8; break; +# if d_m3HasFloat + case c_m3Type_f32: *(f32*)o_retptrs[i] = *(f32*)(s); s += 8; break; + case c_m3Type_f64: *(f64*)o_retptrs[i] = *(f64*)(s); s += 8; break; +# endif + default: return "unknown return type"; + } + } + return m3Err_none; +} + +M3Result m3_GetResultsV (IM3Function i_function, ...) +{ + va_list ap; + va_start(ap, i_function); + M3Result r = m3_GetResultsVL(i_function, ap); + va_end(ap); + return r; +} + +M3Result m3_GetResultsVL (IM3Function i_function, va_list o_rets) +{ + IM3Runtime runtime = i_function->module->runtime; + IM3FuncType ftype = i_function->funcType; + + if (i_function != runtime->lastCalled) { + return "function not called"; + } + + u8* s = (u8*) runtime->stack; + for (u32 i = 0; i < ftype->numRets; ++i) + { + switch (d_FuncRetType(ftype, i)) { + case c_m3Type_i32: *va_arg(o_rets, i32*) = *(i32*)(s); s += 8; break; + case c_m3Type_i64: *va_arg(o_rets, i64*) = *(i64*)(s); s += 8; break; +# if d_m3HasFloat + case c_m3Type_f32: *va_arg(o_rets, f32*) = *(f32*)(s); s += 8; break; + case c_m3Type_f64: *va_arg(o_rets, f64*) = *(f64*)(s); s += 8; break; +# endif + default: return "unknown argument type"; + } + } + return m3Err_none; +} + +void ReleaseCodePageNoTrack (IM3Runtime i_runtime, IM3CodePage i_codePage) +{ + if (i_codePage) + { + IM3CodePage * list; + + bool pageFull = (NumFreeLines (i_codePage) < d_m3CodePageFreeLinesThreshold); + if (pageFull) + list = & i_runtime->pagesFull; + else + list = & i_runtime->pagesOpen; + + PushCodePage (list, i_codePage); m3log (emit, "release page: %d to queue: '%s'", i_codePage->info.sequence, pageFull ? "full" : "open") + } +} + + +IM3CodePage AcquireCodePageWithCapacity (IM3Runtime i_runtime, u32 i_minLineCount) +{ + IM3CodePage page = RemoveCodePageOfCapacity (& i_runtime->pagesOpen, i_minLineCount); + + if (not page) + { + page = Environment_AcquireCodePage (i_runtime->environment, i_minLineCount); + + if (not page) + page = NewCodePage (i_runtime, i_minLineCount); + + if (page) + i_runtime->numCodePages++; + } + + if (page) + { m3log (emit, "acquire page: %d", page->info.sequence); + i_runtime->numActiveCodePages++; + } + + return page; +} + + +IM3CodePage AcquireCodePage (IM3Runtime i_runtime) +{ + return AcquireCodePageWithCapacity (i_runtime, d_m3CodePageFreeLinesThreshold); +} + + +void ReleaseCodePage (IM3Runtime i_runtime, IM3CodePage i_codePage) +{ + if (i_codePage) + { + ReleaseCodePageNoTrack (i_runtime, i_codePage); + i_runtime->numActiveCodePages--; + +# if defined (DEBUG) + u32 numOpen = CountCodePages (i_runtime->pagesOpen); + u32 numFull = CountCodePages (i_runtime->pagesFull); + + m3log (runtime, "runtime: %p; open-pages: %d; full-pages: %d; active: %d; total: %d", i_runtime, numOpen, numFull, i_runtime->numActiveCodePages, i_runtime->numCodePages); + + d_m3Assert (numOpen + numFull + i_runtime->numActiveCodePages == i_runtime->numCodePages); + +# if d_m3LogCodePages + dump_code_page (i_codePage, /* startPC: */ NULL); +# endif +# endif + } +} + + +#if d_m3VerboseErrorMessages +M3Result m3Error (M3Result i_result, IM3Runtime i_runtime, IM3Module i_module, IM3Function i_function, + const char * const i_file, u32 i_lineNum, const char * const i_errorMessage, ...) +{ + if (i_runtime) + { + i_runtime->error = (M3ErrorInfo){ .result = i_result, .runtime = i_runtime, .module = i_module, + .function = i_function, .file = i_file, .line = i_lineNum }; + i_runtime->error.message = i_runtime->error_message; + + va_list args; + va_start (args, i_errorMessage); + vsnprintf (i_runtime->error_message, sizeof(i_runtime->error_message), i_errorMessage, args); + va_end (args); + } + + return i_result; +} +#endif + + +void m3_GetErrorInfo (IM3Runtime i_runtime, M3ErrorInfo* o_info) +{ + if (i_runtime) + { + *o_info = i_runtime->error; + m3_ResetErrorInfo (i_runtime); + } +} + + +void m3_ResetErrorInfo (IM3Runtime i_runtime) +{ + if (i_runtime) + { + M3_INIT(i_runtime->error); + i_runtime->error.message = ""; + } +} + +uint8_t * m3_GetMemory (IM3Runtime i_runtime, uint32_t * o_memorySizeInBytes, uint32_t i_memoryIndex) +{ + uint8_t * memory = NULL; d_m3Assert (i_memoryIndex == 0); + + if (i_runtime) + { + u32 size = (u32) i_runtime->memory.mallocated->length; + + if (o_memorySizeInBytes) + * o_memorySizeInBytes = size; + + if (size) + memory = m3MemData (i_runtime->memory.mallocated); + } + + return memory; +} + + +uint32_t m3_GetMemorySize (IM3Runtime i_runtime) +{ + return i_runtime->memory.mallocated->length; +} + + +M3BacktraceInfo * m3_GetBacktrace (IM3Runtime i_runtime) +{ +# if d_m3RecordBacktraces + return & i_runtime->backtrace; +# else + return NULL; +# endif +} + diff --git a/driver/wasm3/m3_env.h b/driver/wasm3/m3_env.h new file mode 100644 index 0000000..59599b4 --- /dev/null +++ b/driver/wasm3/m3_env.h @@ -0,0 +1,221 @@ +// +// m3_env.h +// +// Created by Steven Massey on 4/19/19. +// Copyright © 2019 Steven Massey. All rights reserved. +// + +#ifndef m3_env_h +#define m3_env_h + +#include "wasm3.h" +#include "m3_code.h" +#include "m3_compile.h" + +d_m3BeginExternC + + +//--------------------------------------------------------------------------------------------------------------------------------- + +typedef struct M3MemoryInfo +{ + u32 initPages; + u32 maxPages; + u32 pageSize; +} +M3MemoryInfo; + + +typedef struct M3Memory +{ + M3MemoryHeader * mallocated; + + u32 numPages; + u32 maxPages; + u32 pageSize; +} +M3Memory; + +typedef M3Memory * IM3Memory; + + +//--------------------------------------------------------------------------------------------------------------------------------- + +typedef struct M3DataSegment +{ + const u8 * initExpr; // wasm code + const u8 * data; + + u32 initExprSize; + u32 memoryRegion; + u32 size; +} +M3DataSegment; + +//--------------------------------------------------------------------------------------------------------------------------------- + +typedef struct M3Global +{ + M3ImportInfo import; + + union + { + i32 i32Value; + i64 i64Value; +#if d_m3HasFloat + f64 f64Value; + f32 f32Value; +#endif + }; + + cstr_t name; + bytes_t initExpr; // wasm code + u32 initExprSize; + u8 type; + bool imported; + bool isMutable; +} +M3Global; + + +//--------------------------------------------------------------------------------------------------------------------------------- +typedef struct M3Module +{ + struct M3Runtime * runtime; + struct M3Environment * environment; + + bytes_t wasmStart; + bytes_t wasmEnd; + + cstr_t name; + + u32 numFuncTypes; + IM3FuncType * funcTypes; // array of pointers to list of FuncTypes + + u32 numFuncImports; + u32 numFunctions; + u32 allFunctions; // allocated functions count + M3Function * functions; + + i32 startFunction; + + u32 numDataSegments; + M3DataSegment * dataSegments; + + //u32 importedGlobals; + u32 numGlobals; + M3Global * globals; + + u32 numElementSegments; + bytes_t elementSection; + bytes_t elementSectionEnd; + + IM3Function * table0; + u32 table0Size; + const char* table0ExportName; + bool hasTable; + + M3MemoryInfo memoryInfo; + M3ImportInfo memoryImport; + bool memoryImported; + bool memoryDeclared; // has a memory section entry + const char* memoryExportName; + + //bool hasWasmCodeCopy; + + struct M3Module * next; +} +M3Module; + +M3Result Module_AddGlobal (IM3Module io_module, IM3Global * o_global, u8 i_type, bool i_mutable, bool i_isImported); + +M3Result Module_PreallocFunctions (IM3Module io_module, u32 i_totalFunctions); +M3Result Module_AddFunction (IM3Module io_module, u32 i_typeIndex, IM3ImportInfo i_importInfo /* can be null */); +IM3Function Module_GetFunction (IM3Module i_module, u32 i_functionIndex); + +void Module_GenerateNames (IM3Module i_module); + +void FreeImportInfo (M3ImportInfo * i_info); + +//--------------------------------------------------------------------------------------------------------------------------------- + +typedef struct M3Environment +{ +// struct M3Runtime * runtimes; + + IM3FuncType funcTypes; // linked list of unique M3FuncType structs that can be compared using pointer-equivalence + + IM3FuncType retFuncTypes [c_m3Type_unknown]; // these 'point' to elements in the linked list above. + // the number of elements must match the basic types as per M3ValueType + M3CodePage * pagesReleased; + + M3SectionHandler customSectionHandler; +} +M3Environment; + +void Environment_Release (IM3Environment i_environment); + +// takes ownership of io_funcType and returns a pointer to the persistent version (could be same or different) +void Environment_AddFuncType (IM3Environment i_environment, IM3FuncType * io_funcType); + +//--------------------------------------------------------------------------------------------------------------------------------- + +typedef struct M3Runtime +{ + M3Compilation compilation; + + IM3Environment environment; + + M3CodePage * pagesOpen; // linked list of code pages with writable space on them + M3CodePage * pagesFull; // linked list of at-capacity pages + + u32 numCodePages; + u32 numActiveCodePages; + + IM3Module modules; // linked list of imported modules + + void * stack; + void * originStack; + u32 stackSize; + u32 numStackSlots; + IM3Function lastCalled; // last function that successfully executed + + void * userdata; + + M3Memory memory; + u32 memoryLimit; + +#if d_m3EnableStrace >= 2 + u32 callDepth; +#endif + + M3ErrorInfo error; +#if d_m3VerboseErrorMessages + char error_message[256]; // the actual buffer. M3ErrorInfo can point to this +#endif + +#if d_m3RecordBacktraces + M3BacktraceInfo backtrace; +#endif + + u32 newCodePageSequence; +} +M3Runtime; + +void InitRuntime (IM3Runtime io_runtime, u32 i_stackSizeInBytes); +void Runtime_Release (IM3Runtime io_runtime); + +M3Result ResizeMemory (IM3Runtime io_runtime, u32 i_numPages); + +typedef void * (* ModuleVisitor) (IM3Module i_module, void * i_info); +void * ForEachModule (IM3Runtime i_runtime, ModuleVisitor i_visitor, void * i_info); + +void * v_FindFunction (IM3Module i_module, const char * const i_name); + +IM3CodePage AcquireCodePage (IM3Runtime io_runtime); +IM3CodePage AcquireCodePageWithCapacity (IM3Runtime io_runtime, u32 i_lineCount); +void ReleaseCodePage (IM3Runtime io_runtime, IM3CodePage i_codePage); + +d_m3EndExternC + +#endif // m3_env_h diff --git a/driver/wasm3/m3_exception.h b/driver/wasm3/m3_exception.h new file mode 100644 index 0000000..258f6f3 --- /dev/null +++ b/driver/wasm3/m3_exception.h @@ -0,0 +1,33 @@ +// +// m3_exception.h +// +// Created by Steven Massey on 7/5/19. +// Copyright © 2019 Steven Massey. All rights reserved. +// +// some macros to emulate try/catch + +#ifndef m3_exception_h +#define m3_exception_h + +#include "m3_config.h" + +# if d_m3EnableExceptionBreakpoint + +// declared in m3_info.c +void ExceptionBreakpoint (cstr_t i_exception, cstr_t i_message); + +# define EXCEPTION_PRINT(ERROR) ExceptionBreakpoint (ERROR, (__FILE__ ":" M3_STR(__LINE__))) + +# else +# define EXCEPTION_PRINT(...) +# endif + + +#define _try M3Result result = m3Err_none; +#define _(TRY) { result = TRY; if (M3_UNLIKELY(result)) { EXCEPTION_PRINT (result); goto _catch; } } +#define _throw(ERROR) { result = ERROR; EXCEPTION_PRINT (result); goto _catch; } +#define _throwif(ERROR, COND) if (M3_UNLIKELY(COND)) { _throw(ERROR); } + +#define _throwifnull(PTR) _throwif (m3Err_mallocFailed, !(PTR)) + +#endif // m3_exception_h diff --git a/driver/wasm3/m3_exec.c b/driver/wasm3/m3_exec.c new file mode 100644 index 0000000..718e447 --- /dev/null +++ b/driver/wasm3/m3_exec.c @@ -0,0 +1,8 @@ +// +// m3_exec.c +// +// Created by Steven Massey on 4/17/19. +// Copyright © 2019 Steven Massey. All rights reserved. +// + +// EMPTY FOR NOW diff --git a/driver/wasm3/m3_exec.h b/driver/wasm3/m3_exec.h new file mode 100644 index 0000000..7fa7f46 --- /dev/null +++ b/driver/wasm3/m3_exec.h @@ -0,0 +1,1527 @@ +// +// m3_exec.h +// +// Created by Steven Massey on 4/17/19. +// Copyright © 2019 Steven Massey. All rights reserved. + + +#ifndef m3_exec_h +#define m3_exec_h + +// TODO: all these functions could move over to the .c at some point. normally, I'd say screw it, +// but it might prove useful to be able to compile m3_exec alone w/ optimizations while the remaining +// code is at debug O0 + + +// About the naming convention of these operations/macros (_rs, _sr_, _ss, _srs, etc.) +//------------------------------------------------------------------------------------------------------ +// - 'r' means register and 's' means slot +// - the first letter is the top of the stack +// +// so, for example, _rs means the first operand (the first thing pushed to the stack) is in a slot +// and the second operand (the top of the stack) is in a register +//------------------------------------------------------------------------------------------------------ + +#ifndef M3_COMPILE_OPCODES +# error "Opcodes should only be included in one compilation unit" +#endif + +#include "m3_math_utils.h" +#include "m3_compile.h" +#include "m3_env.h" +#include "m3_info.h" +#include "m3_exec_defs.h" + +#include + +d_m3BeginExternC + +# define rewrite_op(OP) * ((void **) (_pc-1)) = (void*)(OP) + +# define immediate(TYPE) * ((TYPE *) _pc++) +# define skip_immediate(TYPE) (_pc++) + +# define slot(TYPE) * (TYPE *) (_sp + immediate (i32)) +# define slot_ptr(TYPE) (TYPE *) (_sp + immediate (i32)) + + +# if d_m3EnableOpProfiling + d_m3RetSig profileOp (d_m3OpSig, cstr_t i_operationName); +# define nextOp() M3_MUSTTAIL return profileOp (d_m3OpAllArgs, __FUNCTION__) +# elif d_m3EnableOpTracing + d_m3RetSig debugOp (d_m3OpSig, cstr_t i_operationName); +# define nextOp() M3_MUSTTAIL return debugOp (d_m3OpAllArgs, __FUNCTION__) +# else +# define nextOp() nextOpDirect() +# endif + +#define jumpOp(PC) jumpOpDirect(PC) + +#if d_m3RecordBacktraces + #define pushBacktraceFrame() (PushBacktraceFrame (_mem->runtime, _pc - 1)) + #define fillBacktraceFrame(FUNCTION) (FillBacktraceFunctionInfo (_mem->runtime, function)) + + #define newTrap(err) return (pushBacktraceFrame (), err) + #define forwardTrap(err) return err +#else + #define pushBacktraceFrame() do {} while (0) + #define fillBacktraceFrame(FUNCTION) do {} while (0) + + #define newTrap(err) return err + #define forwardTrap(err) return err +#endif + + +#if d_m3EnableStrace == 1 + // Flat trace + #define d_m3TracePrepare + #define d_m3TracePrint(fmt, ...) fprintf(stderr, fmt "\n", ##__VA_ARGS__) +#elif d_m3EnableStrace >= 2 + // Structured trace + #define d_m3TracePrepare const IM3Runtime trace_rt = m3MemRuntime(_mem); + #define d_m3TracePrint(fmt, ...) fprintf(stderr, "%*s" fmt "\n", (trace_rt->callDepth)*2, "", ##__VA_ARGS__) +#else + #define d_m3TracePrepare + #define d_m3TracePrint(fmt, ...) +#endif + +#if d_m3EnableStrace >= 3 + #define d_m3TraceLoad(TYPE,offset,val) d_m3TracePrint("load." #TYPE " 0x%x = %" PRI##TYPE, offset, val) + #define d_m3TraceStore(TYPE,offset,val) d_m3TracePrint("store." #TYPE " 0x%x , %" PRI##TYPE, offset, val) +#else + #define d_m3TraceLoad(TYPE,offset,val) + #define d_m3TraceStore(TYPE,offset,val) +#endif + +#ifdef DEBUG + #define d_outOfBounds newTrap (ErrorRuntime (m3Err_trapOutOfBoundsMemoryAccess, \ + _mem->runtime, "memory size: %zu; access offset: %zu", \ + _mem->length, operand)) + +# define d_outOfBoundsMemOp(OFFSET, SIZE) newTrap (ErrorRuntime (m3Err_trapOutOfBoundsMemoryAccess, \ + _mem->runtime, "memory size: %zu; access offset: %zu; size: %u", \ + _mem->length, OFFSET, SIZE)) +#else + #define d_outOfBounds newTrap (m3Err_trapOutOfBoundsMemoryAccess) + +# define d_outOfBoundsMemOp(OFFSET, SIZE) newTrap (m3Err_trapOutOfBoundsMemoryAccess) + +#endif + +# if (d_m3EnableOpProfiling || d_m3EnableOpTracing) +d_m3RetSig Call (d_m3OpSig, cstr_t i_operationName) +# else +d_m3RetSig Call (d_m3OpSig) +# endif +{ + m3ret_t possible_trap = m3_Yield (); + if (M3_UNLIKELY(possible_trap)) return possible_trap; + + nextOpDirect(); +} + +// TODO: OK, this needs some explanation here ;0 + +#define d_m3CommutativeOpMacro(RES, REG, TYPE, NAME, OP, ...) \ +d_m3Op(TYPE##_##NAME##_rs) \ +{ \ + TYPE operand = slot (TYPE); \ + OP((RES), operand, ((TYPE) REG), ##__VA_ARGS__); \ + nextOp (); \ +} \ +d_m3Op(TYPE##_##NAME##_ss) \ +{ \ + TYPE operand2 = slot (TYPE); \ + TYPE operand1 = slot (TYPE); \ + OP((RES), operand1, operand2, ##__VA_ARGS__); \ + nextOp (); \ +} + +#define d_m3OpMacro(RES, REG, TYPE, NAME, OP, ...) \ +d_m3Op(TYPE##_##NAME##_sr) \ +{ \ + TYPE operand = slot (TYPE); \ + OP((RES), ((TYPE) REG), operand, ##__VA_ARGS__); \ + nextOp (); \ +} \ +d_m3CommutativeOpMacro(RES, REG, TYPE,NAME, OP, ##__VA_ARGS__) + +// Accept macros +#define d_m3CommutativeOpMacro_i(TYPE, NAME, MACRO, ...) d_m3CommutativeOpMacro ( _r0, _r0, TYPE, NAME, MACRO, ##__VA_ARGS__) +#define d_m3OpMacro_i(TYPE, NAME, MACRO, ...) d_m3OpMacro ( _r0, _r0, TYPE, NAME, MACRO, ##__VA_ARGS__) +#define d_m3CommutativeOpMacro_f(TYPE, NAME, MACRO, ...) d_m3CommutativeOpMacro (_fp0, _fp0, TYPE, NAME, MACRO, ##__VA_ARGS__) +#define d_m3OpMacro_f(TYPE, NAME, MACRO, ...) d_m3OpMacro (_fp0, _fp0, TYPE, NAME, MACRO, ##__VA_ARGS__) + +#define M3_FUNC(RES, A, B, OP) (RES) = OP((A), (B)) // Accept functions: res = OP(a,b) +#define M3_OPER(RES, A, B, OP) (RES) = ((A) OP (B)) // Accept operators: res = a OP b + +#define d_m3CommutativeOpFunc_i(TYPE, NAME, OP) d_m3CommutativeOpMacro_i (TYPE, NAME, M3_FUNC, OP) +#define d_m3OpFunc_i(TYPE, NAME, OP) d_m3OpMacro_i (TYPE, NAME, M3_FUNC, OP) +#define d_m3CommutativeOpFunc_f(TYPE, NAME, OP) d_m3CommutativeOpMacro_f (TYPE, NAME, M3_FUNC, OP) +#define d_m3OpFunc_f(TYPE, NAME, OP) d_m3OpMacro_f (TYPE, NAME, M3_FUNC, OP) + +#define d_m3CommutativeOp_i(TYPE, NAME, OP) d_m3CommutativeOpMacro_i (TYPE, NAME, M3_OPER, OP) +#define d_m3Op_i(TYPE, NAME, OP) d_m3OpMacro_i (TYPE, NAME, M3_OPER, OP) +#define d_m3CommutativeOp_f(TYPE, NAME, OP) d_m3CommutativeOpMacro_f (TYPE, NAME, M3_OPER, OP) +#define d_m3Op_f(TYPE, NAME, OP) d_m3OpMacro_f (TYPE, NAME, M3_OPER, OP) + +// compare needs to be distinct for fp 'cause the result must be _r0 +#define d_m3CompareOp_f(TYPE, NAME, OP) d_m3OpMacro (_r0, _fp0, TYPE, NAME, M3_OPER, OP) +#define d_m3CommutativeCmpOp_f(TYPE, NAME, OP) d_m3CommutativeOpMacro (_r0, _fp0, TYPE, NAME, M3_OPER, OP) + + +//----------------------- + +// signed +d_m3CommutativeOp_i (i32, Equal, ==) d_m3CommutativeOp_i (i64, Equal, ==) +d_m3CommutativeOp_i (i32, NotEqual, !=) d_m3CommutativeOp_i (i64, NotEqual, !=) + +d_m3Op_i (i32, LessThan, < ) d_m3Op_i (i64, LessThan, < ) +d_m3Op_i (i32, GreaterThan, > ) d_m3Op_i (i64, GreaterThan, > ) +d_m3Op_i (i32, LessThanOrEqual, <=) d_m3Op_i (i64, LessThanOrEqual, <=) +d_m3Op_i (i32, GreaterThanOrEqual, >=) d_m3Op_i (i64, GreaterThanOrEqual, >=) + +// unsigned +d_m3Op_i (u32, LessThan, < ) d_m3Op_i (u64, LessThan, < ) +d_m3Op_i (u32, GreaterThan, > ) d_m3Op_i (u64, GreaterThan, > ) +d_m3Op_i (u32, LessThanOrEqual, <=) d_m3Op_i (u64, LessThanOrEqual, <=) +d_m3Op_i (u32, GreaterThanOrEqual, >=) d_m3Op_i (u64, GreaterThanOrEqual, >=) + +#if d_m3HasFloat +d_m3CommutativeCmpOp_f (f32, Equal, ==) d_m3CommutativeCmpOp_f (f64, Equal, ==) +d_m3CommutativeCmpOp_f (f32, NotEqual, !=) d_m3CommutativeCmpOp_f (f64, NotEqual, !=) +d_m3CompareOp_f (f32, LessThan, < ) d_m3CompareOp_f (f64, LessThan, < ) +d_m3CompareOp_f (f32, GreaterThan, > ) d_m3CompareOp_f (f64, GreaterThan, > ) +d_m3CompareOp_f (f32, LessThanOrEqual, <=) d_m3CompareOp_f (f64, LessThanOrEqual, <=) +d_m3CompareOp_f (f32, GreaterThanOrEqual, >=) d_m3CompareOp_f (f64, GreaterThanOrEqual, >=) +#endif + +#define OP_ADD_32(A,B) (i32)((u32)(A) + (u32)(B)) +#define OP_ADD_64(A,B) (i64)((u64)(A) + (u64)(B)) +#define OP_SUB_32(A,B) (i32)((u32)(A) - (u32)(B)) +#define OP_SUB_64(A,B) (i64)((u64)(A) - (u64)(B)) +#define OP_MUL_32(A,B) (i32)((u32)(A) * (u32)(B)) +#define OP_MUL_64(A,B) (i64)((u64)(A) * (u64)(B)) + +d_m3CommutativeOpFunc_i (i32, Add, OP_ADD_32) d_m3CommutativeOpFunc_i (i64, Add, OP_ADD_64) +d_m3CommutativeOpFunc_i (i32, Multiply, OP_MUL_32) d_m3CommutativeOpFunc_i (i64, Multiply, OP_MUL_64) + +d_m3OpFunc_i (i32, Subtract, OP_SUB_32) d_m3OpFunc_i (i64, Subtract, OP_SUB_64) + +#define OP_SHL_32(X,N) ((X) << ((u32)(N) % 32)) +#define OP_SHL_64(X,N) ((X) << ((u64)(N) % 64)) +#define OP_SHR_32(X,N) ((X) >> ((u32)(N) % 32)) +#define OP_SHR_64(X,N) ((X) >> ((u64)(N) % 64)) + +d_m3OpFunc_i (u32, ShiftLeft, OP_SHL_32) d_m3OpFunc_i (u64, ShiftLeft, OP_SHL_64) +d_m3OpFunc_i (i32, ShiftRight, OP_SHR_32) d_m3OpFunc_i (i64, ShiftRight, OP_SHR_64) +d_m3OpFunc_i (u32, ShiftRight, OP_SHR_32) d_m3OpFunc_i (u64, ShiftRight, OP_SHR_64) + +d_m3CommutativeOp_i (u32, And, &) +d_m3CommutativeOp_i (u32, Or, |) +d_m3CommutativeOp_i (u32, Xor, ^) + +d_m3CommutativeOp_i (u64, And, &) +d_m3CommutativeOp_i (u64, Or, |) +d_m3CommutativeOp_i (u64, Xor, ^) + +#if d_m3HasFloat +d_m3CommutativeOp_f (f32, Add, +) d_m3CommutativeOp_f (f64, Add, +) +d_m3CommutativeOp_f (f32, Multiply, *) d_m3CommutativeOp_f (f64, Multiply, *) +d_m3Op_f (f32, Subtract, -) d_m3Op_f (f64, Subtract, -) +d_m3Op_f (f32, Divide, /) d_m3Op_f (f64, Divide, /) +#endif + +d_m3OpFunc_i(u32, Rotl, rotl32) +d_m3OpFunc_i(u32, Rotr, rotr32) +d_m3OpFunc_i(u64, Rotl, rotl64) +d_m3OpFunc_i(u64, Rotr, rotr64) + +d_m3OpMacro_i(u32, Divide, OP_DIV_U); +d_m3OpMacro_i(i32, Divide, OP_DIV_S, INT32_MIN); +d_m3OpMacro_i(u64, Divide, OP_DIV_U); +d_m3OpMacro_i(i64, Divide, OP_DIV_S, INT64_MIN); + +d_m3OpMacro_i(u32, Remainder, OP_REM_U); +d_m3OpMacro_i(i32, Remainder, OP_REM_S, INT32_MIN); +d_m3OpMacro_i(u64, Remainder, OP_REM_U); +d_m3OpMacro_i(i64, Remainder, OP_REM_S, INT64_MIN); + +#if d_m3HasFloat +d_m3OpFunc_f(f32, Min, min_f32); +d_m3OpFunc_f(f32, Max, max_f32); +d_m3OpFunc_f(f64, Min, min_f64); +d_m3OpFunc_f(f64, Max, max_f64); + +d_m3OpFunc_f(f32, CopySign, copysignf); +d_m3OpFunc_f(f64, CopySign, copysign); +#endif + +// Unary operations +// Note: This macro follows the principle of d_m3OpMacro + +#define d_m3UnaryMacro(RES, REG, TYPE, NAME, OP, ...) \ +d_m3Op(TYPE##_##NAME##_r) \ +{ \ + OP((RES), (TYPE) REG, ##__VA_ARGS__); \ + nextOp (); \ +} \ +d_m3Op(TYPE##_##NAME##_s) \ +{ \ + TYPE operand = slot (TYPE); \ + OP((RES), operand, ##__VA_ARGS__); \ + nextOp (); \ +} + +#define M3_UNARY(RES, X, OP) (RES) = OP(X) +#define d_m3UnaryOp_i(TYPE, NAME, OPERATION) d_m3UnaryMacro( _r0, _r0, TYPE, NAME, M3_UNARY, OPERATION) +#define d_m3UnaryOp_f(TYPE, NAME, OPERATION) d_m3UnaryMacro(_fp0, _fp0, TYPE, NAME, M3_UNARY, OPERATION) + +#if d_m3HasFloat +d_m3UnaryOp_f (f32, Abs, fabsf); d_m3UnaryOp_f (f64, Abs, fabs); +d_m3UnaryOp_f (f32, Ceil, ceilf); d_m3UnaryOp_f (f64, Ceil, ceil); +d_m3UnaryOp_f (f32, Floor, floorf); d_m3UnaryOp_f (f64, Floor, floor); +d_m3UnaryOp_f (f32, Trunc, truncf); d_m3UnaryOp_f (f64, Trunc, trunc); +d_m3UnaryOp_f (f32, Sqrt, sqrtf); d_m3UnaryOp_f (f64, Sqrt, sqrt); +d_m3UnaryOp_f (f32, Nearest, rintf); d_m3UnaryOp_f (f64, Nearest, rint); +d_m3UnaryOp_f (f32, Negate, -); d_m3UnaryOp_f (f64, Negate, -); +#endif + +#define OP_EQZ(x) ((x) == 0) + +d_m3UnaryOp_i (i32, EqualToZero, OP_EQZ) +d_m3UnaryOp_i (i64, EqualToZero, OP_EQZ) + +// clz(0), ctz(0) results are undefined for rest platforms, fix it +#if (defined(__i386__) || defined(__x86_64__)) && !(defined(__AVX2__) || (defined(__ABM__) && defined(__BMI__))) + #define OP_CLZ_32(x) (M3_UNLIKELY((x) == 0) ? 32 : __builtin_clz(x)) + #define OP_CTZ_32(x) (M3_UNLIKELY((x) == 0) ? 32 : __builtin_ctz(x)) + // for 64-bit instructions branchless approach more preferable + #define OP_CLZ_64(x) (__builtin_clzll((x) | (1LL << 0)) + OP_EQZ(x)) + #define OP_CTZ_64(x) (__builtin_ctzll((x) | (1LL << 63)) + OP_EQZ(x)) +#elif defined(__ppc__) || defined(__ppc64__) +// PowerPC is defined for __builtin_clz(0) and __builtin_ctz(0). +// See (https://github.com/aquynh/capstone/blob/master/MathExtras.h#L99) + #define OP_CLZ_32(x) __builtin_clz(x) + #define OP_CTZ_32(x) __builtin_ctz(x) + #define OP_CLZ_64(x) __builtin_clzll(x) + #define OP_CTZ_64(x) __builtin_ctzll(x) +#else + #define OP_CLZ_32(x) (M3_UNLIKELY((x) == 0) ? 32 : __builtin_clz(x)) + #define OP_CTZ_32(x) (M3_UNLIKELY((x) == 0) ? 32 : __builtin_ctz(x)) + #define OP_CLZ_64(x) (M3_UNLIKELY((x) == 0) ? 64 : __builtin_clzll(x)) + #define OP_CTZ_64(x) (M3_UNLIKELY((x) == 0) ? 64 : __builtin_ctzll(x)) +#endif + +d_m3UnaryOp_i (u32, Clz, OP_CLZ_32) +d_m3UnaryOp_i (u64, Clz, OP_CLZ_64) + +d_m3UnaryOp_i (u32, Ctz, OP_CTZ_32) +d_m3UnaryOp_i (u64, Ctz, OP_CTZ_64) + +d_m3UnaryOp_i (u32, Popcnt, __builtin_popcount) +d_m3UnaryOp_i (u64, Popcnt, __builtin_popcountll) + +#define OP_WRAP_I64(X) ((X) & 0x00000000ffffffff) + +d_m3Op(i32_Wrap_i64_r) +{ + _r0 = OP_WRAP_I64((i64) _r0); + nextOp (); +} + +d_m3Op(i32_Wrap_i64_s) +{ + i64 operand = slot (i64); + _r0 = OP_WRAP_I64(operand); + nextOp (); +} + +// Integer sign extension operations +#define OP_EXTEND8_S_I32(X) ((int32_t)(int8_t)(X)) +#define OP_EXTEND16_S_I32(X) ((int32_t)(int16_t)(X)) +#define OP_EXTEND8_S_I64(X) ((int64_t)(int8_t)(X)) +#define OP_EXTEND16_S_I64(X) ((int64_t)(int16_t)(X)) +#define OP_EXTEND32_S_I64(X) ((int64_t)(int32_t)(X)) + +d_m3UnaryOp_i (i32, Extend8_s, OP_EXTEND8_S_I32) +d_m3UnaryOp_i (i32, Extend16_s, OP_EXTEND16_S_I32) +d_m3UnaryOp_i (i64, Extend8_s, OP_EXTEND8_S_I64) +d_m3UnaryOp_i (i64, Extend16_s, OP_EXTEND16_S_I64) +d_m3UnaryOp_i (i64, Extend32_s, OP_EXTEND32_S_I64) + +#define d_m3TruncMacro(DEST, SRC, TYPE, NAME, FROM, OP, ...) \ +d_m3Op(TYPE##_##NAME##_##FROM##_r_r) \ +{ \ + OP((DEST), (FROM) SRC, ##__VA_ARGS__); \ + nextOp (); \ +} \ +d_m3Op(TYPE##_##NAME##_##FROM##_r_s) \ +{ \ + FROM * stack = slot_ptr (FROM); \ + OP((DEST), (* stack), ##__VA_ARGS__); \ + nextOp (); \ +} \ +d_m3Op(TYPE##_##NAME##_##FROM##_s_r) \ +{ \ + TYPE * dest = slot_ptr (TYPE); \ + OP((* dest), (FROM) SRC, ##__VA_ARGS__); \ + nextOp (); \ +} \ +d_m3Op(TYPE##_##NAME##_##FROM##_s_s) \ +{ \ + FROM * stack = slot_ptr (FROM); \ + TYPE * dest = slot_ptr (TYPE); \ + OP((* dest), (* stack), ##__VA_ARGS__); \ + nextOp (); \ +} + +#if d_m3HasFloat +d_m3TruncMacro(_r0, _fp0, i32, Trunc, f32, OP_I32_TRUNC_F32) +d_m3TruncMacro(_r0, _fp0, u32, Trunc, f32, OP_U32_TRUNC_F32) +d_m3TruncMacro(_r0, _fp0, i32, Trunc, f64, OP_I32_TRUNC_F64) +d_m3TruncMacro(_r0, _fp0, u32, Trunc, f64, OP_U32_TRUNC_F64) + +d_m3TruncMacro(_r0, _fp0, i64, Trunc, f32, OP_I64_TRUNC_F32) +d_m3TruncMacro(_r0, _fp0, u64, Trunc, f32, OP_U64_TRUNC_F32) +d_m3TruncMacro(_r0, _fp0, i64, Trunc, f64, OP_I64_TRUNC_F64) +d_m3TruncMacro(_r0, _fp0, u64, Trunc, f64, OP_U64_TRUNC_F64) + +d_m3TruncMacro(_r0, _fp0, i32, TruncSat, f32, OP_I32_TRUNC_SAT_F32) +d_m3TruncMacro(_r0, _fp0, u32, TruncSat, f32, OP_U32_TRUNC_SAT_F32) +d_m3TruncMacro(_r0, _fp0, i32, TruncSat, f64, OP_I32_TRUNC_SAT_F64) +d_m3TruncMacro(_r0, _fp0, u32, TruncSat, f64, OP_U32_TRUNC_SAT_F64) + +d_m3TruncMacro(_r0, _fp0, i64, TruncSat, f32, OP_I64_TRUNC_SAT_F32) +d_m3TruncMacro(_r0, _fp0, u64, TruncSat, f32, OP_U64_TRUNC_SAT_F32) +d_m3TruncMacro(_r0, _fp0, i64, TruncSat, f64, OP_I64_TRUNC_SAT_F64) +d_m3TruncMacro(_r0, _fp0, u64, TruncSat, f64, OP_U64_TRUNC_SAT_F64) +#endif + +#define d_m3TypeModifyOp(REG_TO, REG_FROM, TO, NAME, FROM) \ +d_m3Op(TO##_##NAME##_##FROM##_r) \ +{ \ + REG_TO = (TO) ((FROM) REG_FROM); \ + nextOp (); \ +} \ + \ +d_m3Op(TO##_##NAME##_##FROM##_s) \ +{ \ + FROM from = slot (FROM); \ + REG_TO = (TO) (from); \ + nextOp (); \ +} + +// Int to int +d_m3TypeModifyOp (_r0, _r0, i64, Extend, i32); +d_m3TypeModifyOp (_r0, _r0, i64, Extend, u32); + +// Float to float +#if d_m3HasFloat +d_m3TypeModifyOp (_fp0, _fp0, f32, Demote, f64); +d_m3TypeModifyOp (_fp0, _fp0, f64, Promote, f32); +#endif + +#define d_m3TypeConvertOp(REG_TO, REG_FROM, TO, NAME, FROM) \ +d_m3Op(TO##_##NAME##_##FROM##_r_r) \ +{ \ + REG_TO = (TO) ((FROM) REG_FROM); \ + nextOp (); \ +} \ + \ +d_m3Op(TO##_##NAME##_##FROM##_s_r) \ +{ \ + slot (TO) = (TO) ((FROM) REG_FROM); \ + nextOp (); \ +} \ + \ +d_m3Op(TO##_##NAME##_##FROM##_r_s) \ +{ \ + FROM from = slot (FROM); \ + REG_TO = (TO) (from); \ + nextOp (); \ +} \ + \ +d_m3Op(TO##_##NAME##_##FROM##_s_s) \ +{ \ + FROM from = slot (FROM); \ + slot (TO) = (TO) (from); \ + nextOp (); \ +} + +// Int to float +#if d_m3HasFloat +d_m3TypeConvertOp (_fp0, _r0, f64, Convert, i32); +d_m3TypeConvertOp (_fp0, _r0, f64, Convert, u32); +d_m3TypeConvertOp (_fp0, _r0, f64, Convert, i64); +d_m3TypeConvertOp (_fp0, _r0, f64, Convert, u64); + +d_m3TypeConvertOp (_fp0, _r0, f32, Convert, i32); +d_m3TypeConvertOp (_fp0, _r0, f32, Convert, u32); +d_m3TypeConvertOp (_fp0, _r0, f32, Convert, i64); +d_m3TypeConvertOp (_fp0, _r0, f32, Convert, u64); +#endif + +#define d_m3ReinterpretOp(REG, TO, SRC, FROM) \ +d_m3Op(TO##_Reinterpret_##FROM##_r_r) \ +{ \ + union { FROM c; TO t; } u; \ + u.c = (FROM) SRC; \ + REG = u.t; \ + nextOp (); \ +} \ + \ +d_m3Op(TO##_Reinterpret_##FROM##_r_s) \ +{ \ + union { FROM c; TO t; } u; \ + u.c = slot (FROM); \ + REG = u.t; \ + nextOp (); \ +} \ + \ +d_m3Op(TO##_Reinterpret_##FROM##_s_r) \ +{ \ + union { FROM c; TO t; } u; \ + u.c = (FROM) SRC; \ + slot (TO) = u.t; \ + nextOp (); \ +} \ + \ +d_m3Op(TO##_Reinterpret_##FROM##_s_s) \ +{ \ + union { FROM c; TO t; } u; \ + u.c = slot (FROM); \ + slot (TO) = u.t; \ + nextOp (); \ +} + +#if d_m3HasFloat +d_m3ReinterpretOp (_r0, i32, _fp0, f32) +d_m3ReinterpretOp (_r0, i64, _fp0, f64) +d_m3ReinterpretOp (_fp0, f32, _r0, i32) +d_m3ReinterpretOp (_fp0, f64, _r0, i64) +#endif + + +d_m3Op (GetGlobal_s32) +{ + u32 * global = immediate (u32 *); + slot (u32) = * global; // printf ("get global: %p %" PRIi64 "\n", global, *global); + + nextOp (); +} + + +d_m3Op (GetGlobal_s64) +{ + u64 * global = immediate (u64 *); + slot (u64) = * global; // printf ("get global: %p %" PRIi64 "\n", global, *global); + + nextOp (); +} + + +d_m3Op (SetGlobal_i32) +{ + u32 * global = immediate (u32 *); + * global = (u32) _r0; // printf ("set global: %p %" PRIi64 "\n", global, _r0); + + nextOp (); +} + + +d_m3Op (SetGlobal_i64) +{ + u64 * global = immediate (u64 *); + * global = (u64) _r0; // printf ("set global: %p %" PRIi64 "\n", global, _r0); + + nextOp (); +} + + +d_m3Op (Call) +{ + pc_t callPC = immediate (pc_t); + i32 stackOffset = immediate (i32); + IM3Memory memory = m3MemInfo (_mem); + + m3stack_t sp = _sp + stackOffset; + +# if (d_m3EnableOpProfiling || d_m3EnableOpTracing) + m3ret_t r = Call (callPC, sp, _mem, d_m3OpDefaultArgs, d_m3BaseCstr); +# else + m3ret_t r = Call (callPC, sp, _mem, d_m3OpDefaultArgs); +# endif + + _mem = memory->mallocated; + + if (M3_LIKELY(not r)) + nextOp (); + else + { + pushBacktraceFrame (); + forwardTrap (r); + } +} + + +d_m3Op (CallIndirect) +{ + u32 tableIndex = slot (u32); + IM3Module module = immediate (IM3Module); + IM3FuncType type = immediate (IM3FuncType); + i32 stackOffset = immediate (i32); + IM3Memory memory = m3MemInfo (_mem); + + m3stack_t sp = _sp + stackOffset; + + m3ret_t r = m3Err_none; + + if (M3_LIKELY(tableIndex < module->table0Size)) + { + IM3Function function = module->table0 [tableIndex]; + + if (M3_LIKELY(function)) + { + if (M3_LIKELY(type == function->funcType)) + { + if (M3_UNLIKELY(not function->compiled)) + r = CompileFunction (function); + + if (M3_LIKELY(not r)) + { + +# if (d_m3EnableOpProfiling || d_m3EnableOpTracing) + r = Call (function->compiled, sp, _mem, d_m3OpDefaultArgs, d_m3BaseCstr); +# else + r = Call (function->compiled, sp, _mem, d_m3OpDefaultArgs); +# endif + + _mem = memory->mallocated; + + if (M3_LIKELY(not r)) + nextOpDirect (); + else + { + pushBacktraceFrame (); + forwardTrap (r); + } + } + } + else r = m3Err_trapIndirectCallTypeMismatch; + } + else r = m3Err_trapTableElementIsNull; + } + else r = m3Err_trapTableIndexOutOfRange; + + if (M3_UNLIKELY(r)) + newTrap (r); + else forwardTrap (r); +} + + +d_m3Op (CallRawFunction) +{ + d_m3TracePrepare + + M3ImportContext ctx; + + M3RawCall call = (M3RawCall) (* _pc++); + ctx.function = immediate (IM3Function); + ctx.userdata = immediate (void *); + u64* const sp = ((u64*)_sp); + IM3Memory memory = m3MemInfo (_mem); + + IM3Runtime runtime = m3MemRuntime(_mem); + +#if d_m3EnableStrace + IM3FuncType ftype = ctx.function->funcType; + + FILE* out = stderr; + char outbuff[1024]; + char* outp = outbuff; + char* oute = outbuff+1024; + + outp += snprintf(outp, oute-outp, "%s!%s(", ctx.function->import.moduleUtf8, ctx.function->import.fieldUtf8); + + const int nArgs = ftype->numArgs; + const int nRets = ftype->numRets; + u64 * args = sp + nRets; + for (int i=0; itypes[nRets + i]; + switch (type) { + case c_m3Type_i32: outp += snprintf(outp, oute-outp, "%" PRIi32, *(i32*)(args+i)); break; + case c_m3Type_i64: outp += snprintf(outp, oute-outp, "%" PRIi64, *(i64*)(args+i)); break; + case c_m3Type_f32: outp += snprintf(outp, oute-outp, "%" PRIf32, *(f32*)(args+i)); break; + case c_m3Type_f64: outp += snprintf(outp, oute-outp, "%" PRIf64, *(f64*)(args+i)); break; + default: outp += snprintf(outp, oute-outp, "", type); break; + } + outp += snprintf(outp, oute-outp, (i < nArgs-1) ? ", " : ")"); + } +# if d_m3EnableStrace >= 2 + outp += snprintf(outp, oute-outp, " { }"); +# endif +#endif + + // m3_Call uses runtime->stack to set-up initial exported function stack. + // Reconfigure the stack to enable recursive invocations of m3_Call. + // I.e. exported/table function can be called from an impoted function. + void* stack_backup = runtime->stack; + runtime->stack = sp; + m3ret_t possible_trap = call (runtime, &ctx, sp, m3MemData(_mem)); + runtime->stack = stack_backup; + +#if d_m3EnableStrace + if (M3_UNLIKELY(possible_trap)) { + d_m3TracePrint("%s -> %s", outbuff, (char*)possible_trap); + } else { + switch (GetSingleRetType(ftype)) { + case c_m3Type_none: d_m3TracePrint("%s", outbuff); break; + case c_m3Type_i32: d_m3TracePrint("%s = %" PRIi32, outbuff, *(i32*)sp); break; + case c_m3Type_i64: d_m3TracePrint("%s = %" PRIi64, outbuff, *(i64*)sp); break; + case c_m3Type_f32: d_m3TracePrint("%s = %" PRIf32, outbuff, *(f32*)sp); break; + case c_m3Type_f64: d_m3TracePrint("%s = %" PRIf64, outbuff, *(f64*)sp); break; + } + } +#endif + + if (M3_UNLIKELY(possible_trap)) { + _mem = memory->mallocated; + pushBacktraceFrame (); + } + forwardTrap (possible_trap); +} + + +d_m3Op (MemSize) +{ + IM3Memory memory = m3MemInfo (_mem); + + _r0 = memory->numPages; + + nextOp (); +} + + +d_m3Op (MemGrow) +{ + IM3Runtime runtime = m3MemRuntime(_mem); + IM3Memory memory = & runtime->memory; + + i32 numPagesToGrow = _r0; + if (numPagesToGrow >= 0) { + _r0 = memory->numPages; + + if (M3_LIKELY(numPagesToGrow)) + { + u32 requiredPages = memory->numPages + numPagesToGrow; + + M3Result r = ResizeMemory (runtime, requiredPages); + if (r) + _r0 = -1; + + _mem = memory->mallocated; + } + } + else + { + _r0 = -1; + } + + nextOp (); +} + + +d_m3Op (MemCopy) +{ + u32 size = (u32) _r0; + u64 source = slot (u32); + u64 destination = slot (u32); + + if (M3_LIKELY(destination + size <= _mem->length)) + { + if (M3_LIKELY(source + size <= _mem->length)) + { + u8 * dst = m3MemData (_mem) + destination; + u8 * src = m3MemData (_mem) + source; + memmove (dst, src, size); + + nextOp (); + } + else d_outOfBoundsMemOp (source, size); + } + else d_outOfBoundsMemOp (destination, size); +} + + +d_m3Op (MemFill) +{ + u32 size = (u32) _r0; + u32 byte = slot (u32); + u64 destination = slot (u32); + + if (M3_LIKELY(destination + size <= _mem->length)) + { + u8 * mem8 = m3MemData (_mem) + destination; + memset (mem8, (u8) byte, size); + nextOp (); + } + else d_outOfBoundsMemOp (destination, size); +} + + +// it's a debate: should the compilation be trigger be the caller or callee page. +// it's a much easier to put it in the caller pager. if it's in the callee, either the entire page +// has be left dangling or it's just a stub that jumps to a newly acquired page. In Gestalt, I opted +// for the stub approach. Stubbing makes it easier to dynamically free the compilation. You can also +// do both. +d_m3Op (Compile) +{ + rewrite_op (op_Call); + + IM3Function function = immediate (IM3Function); + + m3ret_t result = m3Err_none; + + if (M3_UNLIKELY(not function->compiled)) // check to see if function was compiled since this operation was emitted. + result = CompileFunction (function); + + if (not result) + { + // patch up compiled pc and call rewritten op_Call + * ((void**) --_pc) = (void*) (function->compiled); + --_pc; + nextOpDirect (); + } + + newTrap (result); +} + + + +d_m3Op (Entry) +{ + d_m3ClearRegisters + + d_m3TracePrepare + + IM3Function function = immediate (IM3Function); + IM3Memory memory = m3MemInfo (_mem); + +#if d_m3SkipStackCheck + if (true) +#else + if (M3_LIKELY ((void *) (_sp + function->maxStackSlots) < _mem->maxStack)) +#endif + { +#if defined(DEBUG) + function->hits++; +#endif + u8 * stack = (u8 *) ((m3slot_t *) _sp + function->numRetAndArgSlots); + + memset (stack, 0x0, function->numLocalBytes); + stack += function->numLocalBytes; + + if (function->constants) + { + memcpy (stack, function->constants, function->numConstantBytes); + } + +#if d_m3EnableStrace >= 2 + d_m3TracePrint("%s %s {", m3_GetFunctionName(function), SPrintFunctionArgList (function, _sp + function->numRetSlots)); + trace_rt->callDepth++; +#endif + + m3ret_t r = nextOpImpl (); + +#if d_m3EnableStrace >= 2 + trace_rt->callDepth--; + + if (r) { + d_m3TracePrint("} !trap = %s", (char*)r); + } else { + int rettype = GetSingleRetType(function->funcType); + if (rettype != c_m3Type_none) { + char str [128] = { 0 }; + SPrintArg (str, 127, _sp, rettype); + d_m3TracePrint("} = %s", str); + } else { + d_m3TracePrint("}"); + } + } +#endif + + if (M3_UNLIKELY(r)) { + _mem = memory->mallocated; + fillBacktraceFrame (); + } + forwardTrap (r); + } + else newTrap (m3Err_trapStackOverflow); +} + + +d_m3Op (Loop) +{ + d_m3TracePrepare + + // regs are unused coming into a loop anyway + // this reduces code size & stack usage + d_m3ClearRegisters + + m3ret_t r; + + IM3Memory memory = m3MemInfo (_mem); + + do + { +#if d_m3EnableStrace >= 3 + d_m3TracePrint("iter {"); + trace_rt->callDepth++; +#endif + r = nextOpImpl (); + +#if d_m3EnableStrace >= 3 + trace_rt->callDepth--; + d_m3TracePrint("}"); +#endif + // linear memory pointer needs refreshed here because the block it's looping over + // can potentially invoke the grow operation. + _mem = memory->mallocated; + } + while (r == _pc); + + forwardTrap (r); +} + + +d_m3Op (Branch) +{ + jumpOp (* _pc); +} + + +d_m3Op (If_r) +{ + i32 condition = (i32) _r0; + + pc_t elsePC = immediate (pc_t); + + if (condition) + nextOp (); + else + jumpOp (elsePC); +} + + +d_m3Op (If_s) +{ + i32 condition = slot (i32); + + pc_t elsePC = immediate (pc_t); + + if (condition) + nextOp (); + else + jumpOp (elsePC); +} + + +d_m3Op (BranchTable) +{ + u32 branchIndex = slot (u32); // branch index is always in a slot + u32 numTargets = immediate (u32); + + pc_t * branches = (pc_t *) _pc; + + if (branchIndex > numTargets) + branchIndex = numTargets; // the default index + + jumpOp (branches [branchIndex]); +} + + +#define d_m3SetRegisterSetSlot(TYPE, REG) \ +d_m3Op (SetRegister_##TYPE) \ +{ \ + REG = slot (TYPE); \ + nextOp (); \ +} \ + \ +d_m3Op (SetSlot_##TYPE) \ +{ \ + slot (TYPE) = (TYPE) REG; \ + nextOp (); \ +} \ + \ +d_m3Op (PreserveSetSlot_##TYPE) \ +{ \ + TYPE * stack = slot_ptr (TYPE); \ + TYPE * preserve = slot_ptr (TYPE); \ + \ + * preserve = * stack; \ + * stack = (TYPE) REG; \ + \ + nextOp (); \ +} + +d_m3SetRegisterSetSlot (i32, _r0) +d_m3SetRegisterSetSlot (i64, _r0) +#if d_m3HasFloat +d_m3SetRegisterSetSlot (f32, _fp0) +d_m3SetRegisterSetSlot (f64, _fp0) +#endif + +d_m3Op (CopySlot_32) +{ + u32 * dst = slot_ptr (u32); + u32 * src = slot_ptr (u32); + + * dst = * src; + + nextOp (); +} + + +d_m3Op (PreserveCopySlot_32) +{ + u32 * dest = slot_ptr (u32); + u32 * src = slot_ptr (u32); + u32 * preserve = slot_ptr (u32); + + * preserve = * dest; + * dest = * src; + + nextOp (); +} + + +d_m3Op (CopySlot_64) +{ + u64 * dst = slot_ptr (u64); + u64 * src = slot_ptr (u64); + + * dst = * src; // printf ("copy: %p <- %" PRIi64 " <- %p\n", dst, * dst, src); + + nextOp (); +} + + +d_m3Op (PreserveCopySlot_64) +{ + u64 * dest = slot_ptr (u64); + u64 * src = slot_ptr (u64); + u64 * preserve = slot_ptr (u64); + + * preserve = * dest; + * dest = * src; + + nextOp (); +} + + +#if d_m3EnableOpTracing +//-------------------------------------------------------------------------------------------------------- +d_m3Op (DumpStack) +{ + u32 opcodeIndex = immediate (u32); + u32 stackHeight = immediate (u32); + IM3Function function = immediate (IM3Function); + + cstr_t funcName = (function) ? m3_GetFunctionName(function) : ""; + + printf (" %4d ", opcodeIndex); + printf (" %-25s r0: 0x%016" PRIx64 " i:%" PRIi64 " u:%" PRIu64 "\n", funcName, _r0, _r0, _r0); +#if d_m3HasFloat + printf (" fp0: %" PRIf64 "\n", _fp0); +#endif + m3stack_t sp = _sp; + + for (u32 i = 0; i < stackHeight; ++i) + { + cstr_t kind = ""; + + printf ("%p %5s %2d: 0x%" PRIx64 " i:%" PRIi64 "\n", sp, kind, i, (u64) *(sp), (i64) *(sp)); + + ++sp; + } + printf ("---------------------------------------------------------------------------------------------------------\n"); + + nextOpDirect(); +} +#endif + + +#define d_m3Select_i(TYPE, REG) \ +d_m3Op (Select_##TYPE##_rss) \ +{ \ + i32 condition = (i32) _r0; \ + \ + TYPE operand2 = slot (TYPE); \ + TYPE operand1 = slot (TYPE); \ + \ + REG = (condition) ? operand1 : operand2; \ + \ + nextOp (); \ +} \ + \ +d_m3Op (Select_##TYPE##_srs) \ +{ \ + i32 condition = slot (i32); \ + \ + TYPE operand2 = (TYPE) REG; \ + TYPE operand1 = slot (TYPE); \ + \ + REG = (condition) ? operand1 : operand2; \ + \ + nextOp (); \ +} \ + \ +d_m3Op (Select_##TYPE##_ssr) \ +{ \ + i32 condition = slot (i32); \ + \ + TYPE operand2 = slot (TYPE); \ + TYPE operand1 = (TYPE) REG; \ + \ + REG = (condition) ? operand1 : operand2; \ + \ + nextOp (); \ +} \ + \ +d_m3Op (Select_##TYPE##_sss) \ +{ \ + i32 condition = slot (i32); \ + \ + TYPE operand2 = slot (TYPE); \ + TYPE operand1 = slot (TYPE); \ + \ + REG = (condition) ? operand1 : operand2; \ + \ + nextOp (); \ +} + + +d_m3Select_i (i32, _r0) +d_m3Select_i (i64, _r0) + + +#define d_m3Select_f(TYPE, REG, LABEL, SELECTOR) \ +d_m3Op (Select_##TYPE##_##LABEL##ss) \ +{ \ + i32 condition = (i32) SELECTOR; \ + \ + TYPE operand2 = slot (TYPE); \ + TYPE operand1 = slot (TYPE); \ + \ + REG = (condition) ? operand1 : operand2; \ + \ + nextOp (); \ +} \ + \ +d_m3Op (Select_##TYPE##_##LABEL##rs) \ +{ \ + i32 condition = (i32) SELECTOR; \ + \ + TYPE operand2 = (TYPE) REG; \ + TYPE operand1 = slot (TYPE); \ + \ + REG = (condition) ? operand1 : operand2; \ + \ + nextOp (); \ +} \ + \ +d_m3Op (Select_##TYPE##_##LABEL##sr) \ +{ \ + i32 condition = (i32) SELECTOR; \ + \ + TYPE operand2 = slot (TYPE); \ + TYPE operand1 = (TYPE) REG; \ + \ + REG = (condition) ? operand1 : operand2; \ + \ + nextOp (); \ +} + +#if d_m3HasFloat +d_m3Select_f (f32, _fp0, r, _r0) +d_m3Select_f (f32, _fp0, s, slot (i32)) + +d_m3Select_f (f64, _fp0, r, _r0) +d_m3Select_f (f64, _fp0, s, slot (i32)) +#endif + +d_m3Op (Return) +{ + m3StackCheck(); + return m3Err_none; +} + + +d_m3Op (BranchIf_r) +{ + i32 condition = (i32) _r0; + pc_t branch = immediate (pc_t); + + if (condition) + { + jumpOp (branch); + } + else nextOp (); +} + + +d_m3Op (BranchIf_s) +{ + i32 condition = slot (i32); + pc_t branch = immediate (pc_t); + + if (condition) + { + jumpOp (branch); + } + else nextOp (); +} + + +d_m3Op (BranchIfPrologue_r) +{ + i32 condition = (i32) _r0; + pc_t branch = immediate (pc_t); + + if (condition) + { + // this is the "prologue" that ends with + // a plain branch to the actual target + nextOp (); + } + else jumpOp (branch); // jump over the prologue +} + + +d_m3Op (BranchIfPrologue_s) +{ + i32 condition = slot (i32); + pc_t branch = immediate (pc_t); + + if (condition) + { + nextOp (); + } + else jumpOp (branch); +} + + +d_m3Op (ContinueLoop) +{ + m3StackCheck(); + + // TODO: this is where execution can "escape" the M3 code and callback to the client / fiber switch + // OR it can go in the Loop operation. I think it's best to do here. adding code to the loop operation + // has the potential to increase its native-stack usage. (don't forget ContinueLoopIf too.) + + void * loopId = immediate (void *); + return loopId; +} + + +d_m3Op (ContinueLoopIf) +{ + i32 condition = (i32) _r0; + void * loopId = immediate (void *); + + if (condition) + { + return loopId; + } + else nextOp (); +} + + +d_m3Op (Const32) +{ + u32 value = * (u32 *)_pc++; + slot (u32) = value; + nextOp (); +} + + +d_m3Op (Const64) +{ + u64 value = * (u64 *)_pc; + _pc += (M3_SIZEOF_PTR == 4) ? 2 : 1; + slot (u64) = value; + nextOp (); +} + +d_m3Op (Unsupported) +{ + newTrap ("unsupported instruction executed"); +} + +d_m3Op (Unreachable) +{ + m3StackCheck(); + newTrap (m3Err_trapUnreachable); +} + + +d_m3Op (End) +{ + m3StackCheck(); + return m3Err_none; +} + + +d_m3Op (SetGlobal_s32) +{ + u32 * global = immediate (u32 *); + * global = slot (u32); + + nextOp (); +} + + +d_m3Op (SetGlobal_s64) +{ + u64 * global = immediate (u64 *); + * global = slot (u64); + + nextOp (); +} + +#if d_m3HasFloat +d_m3Op (SetGlobal_f32) +{ + f32 * global = immediate (f32 *); + * global = _fp0; + + nextOp (); +} + + +d_m3Op (SetGlobal_f64) +{ + f64 * global = immediate (f64 *); + * global = _fp0; + + nextOp (); +} +#endif + + +#if d_m3SkipMemoryBoundsCheck +# define m3MemCheck(x) true +#else +# define m3MemCheck(x) M3_LIKELY(x) +#endif + +// memcpy here is to support non-aligned access on some platforms. + +#define d_m3Load(REG,DEST_TYPE,SRC_TYPE) \ +d_m3Op(DEST_TYPE##_Load_##SRC_TYPE##_r) \ +{ \ + d_m3TracePrepare \ + u32 offset = immediate (u32); \ + u64 operand = (u32) _r0; \ + operand += offset; \ + \ + if (m3MemCheck( \ + operand + sizeof (SRC_TYPE) <= _mem->length \ + )) { \ + { \ + u8* src8 = m3MemData(_mem) + operand; \ + SRC_TYPE value; \ + memcpy(&value, src8, sizeof(value)); \ + M3_BSWAP_##SRC_TYPE(value); \ + REG = (DEST_TYPE)value; \ + d_m3TraceLoad(DEST_TYPE, operand, REG); \ + } \ + nextOp (); \ + } else d_outOfBounds; \ +} \ +d_m3Op(DEST_TYPE##_Load_##SRC_TYPE##_s) \ +{ \ + d_m3TracePrepare \ + u64 operand = slot (u32); \ + u32 offset = immediate (u32); \ + operand += offset; \ + \ + if (m3MemCheck( \ + operand + sizeof (SRC_TYPE) <= _mem->length \ + )) { \ + { \ + u8* src8 = m3MemData(_mem) + operand; \ + SRC_TYPE value; \ + memcpy(&value, src8, sizeof(value)); \ + M3_BSWAP_##SRC_TYPE(value); \ + REG = (DEST_TYPE)value; \ + d_m3TraceLoad(DEST_TYPE, operand, REG); \ + } \ + nextOp (); \ + } else d_outOfBounds; \ +} + +// printf ("get: %d -> %d\n", operand + offset, (i64) REG); + + +#define d_m3Load_i(DEST_TYPE, SRC_TYPE) d_m3Load(_r0, DEST_TYPE, SRC_TYPE) +#define d_m3Load_f(DEST_TYPE, SRC_TYPE) d_m3Load(_fp0, DEST_TYPE, SRC_TYPE) + +#if d_m3HasFloat +d_m3Load_f (f32, f32); +d_m3Load_f (f64, f64); +#endif + +d_m3Load_i (i32, i8); +d_m3Load_i (i32, u8); +d_m3Load_i (i32, i16); +d_m3Load_i (i32, u16); +d_m3Load_i (i32, i32); + +d_m3Load_i (i64, i8); +d_m3Load_i (i64, u8); +d_m3Load_i (i64, i16); +d_m3Load_i (i64, u16); +d_m3Load_i (i64, i32); +d_m3Load_i (i64, u32); +d_m3Load_i (i64, i64); + +#define d_m3Store(REG, SRC_TYPE, DEST_TYPE) \ +d_m3Op (SRC_TYPE##_Store_##DEST_TYPE##_rs) \ +{ \ + d_m3TracePrepare \ + u64 operand = slot (u32); \ + u32 offset = immediate (u32); \ + operand += offset; \ + \ + if (m3MemCheck( \ + operand + sizeof (DEST_TYPE) <= _mem->length \ + )) { \ + { \ + d_m3TraceStore(SRC_TYPE, operand, REG); \ + u8* mem8 = m3MemData(_mem) + operand; \ + DEST_TYPE val = (DEST_TYPE) REG; \ + M3_BSWAP_##DEST_TYPE(val); \ + memcpy(mem8, &val, sizeof(val)); \ + } \ + nextOp (); \ + } else d_outOfBounds; \ +} \ +d_m3Op (SRC_TYPE##_Store_##DEST_TYPE##_sr) \ +{ \ + d_m3TracePrepare \ + const SRC_TYPE value = slot (SRC_TYPE); \ + u64 operand = (u32) _r0; \ + u32 offset = immediate (u32); \ + operand += offset; \ + \ + if (m3MemCheck( \ + operand + sizeof (DEST_TYPE) <= _mem->length \ + )) { \ + { \ + d_m3TraceStore(SRC_TYPE, operand, value); \ + u8* mem8 = m3MemData(_mem) + operand; \ + DEST_TYPE val = (DEST_TYPE) value; \ + M3_BSWAP_##DEST_TYPE(val); \ + memcpy(mem8, &val, sizeof(val)); \ + } \ + nextOp (); \ + } else d_outOfBounds; \ +} \ +d_m3Op (SRC_TYPE##_Store_##DEST_TYPE##_ss) \ +{ \ + d_m3TracePrepare \ + const SRC_TYPE value = slot (SRC_TYPE); \ + u64 operand = slot (u32); \ + u32 offset = immediate (u32); \ + operand += offset; \ + \ + if (m3MemCheck( \ + operand + sizeof (DEST_TYPE) <= _mem->length \ + )) { \ + { \ + d_m3TraceStore(SRC_TYPE, operand, value); \ + u8* mem8 = m3MemData(_mem) + operand; \ + DEST_TYPE val = (DEST_TYPE) value; \ + M3_BSWAP_##DEST_TYPE(val); \ + memcpy(mem8, &val, sizeof(val)); \ + } \ + nextOp (); \ + } else d_outOfBounds; \ +} + +// both operands can be in regs when storing a float +#define d_m3StoreFp(REG, TYPE) \ +d_m3Op (TYPE##_Store_##TYPE##_rr) \ +{ \ + d_m3TracePrepare \ + u64 operand = (u32) _r0; \ + u32 offset = immediate (u32); \ + operand += offset; \ + \ + if (m3MemCheck( \ + operand + sizeof (TYPE) <= _mem->length \ + )) { \ + { \ + d_m3TraceStore(TYPE, operand, REG); \ + u8* mem8 = m3MemData(_mem) + operand; \ + TYPE val = (TYPE) REG; \ + M3_BSWAP_##TYPE(val); \ + memcpy(mem8, &val, sizeof(val)); \ + } \ + nextOp (); \ + } else d_outOfBounds; \ +} + + +#define d_m3Store_i(SRC_TYPE, DEST_TYPE) d_m3Store(_r0, SRC_TYPE, DEST_TYPE) +#define d_m3Store_f(SRC_TYPE, DEST_TYPE) d_m3Store(_fp0, SRC_TYPE, DEST_TYPE) d_m3StoreFp (_fp0, SRC_TYPE); + +#if d_m3HasFloat +d_m3Store_f (f32, f32) +d_m3Store_f (f64, f64) +#endif + +d_m3Store_i (i32, u8) +d_m3Store_i (i32, i16) +d_m3Store_i (i32, i32) + +d_m3Store_i (i64, u8) +d_m3Store_i (i64, i16) +d_m3Store_i (i64, i32) +d_m3Store_i (i64, i64) + +#undef m3MemCheck + + +//--------------------------------------------------------------------------------------------------------------------- +// debug/profiling +//--------------------------------------------------------------------------------------------------------------------- +#if d_m3EnableOpTracing +d_m3RetSig debugOp (d_m3OpSig, cstr_t i_opcode) +{ + char name [100]; + strcpy (name, strstr (i_opcode, "op_") + 3); + char * bracket = strstr (name, "("); + if (bracket) { + *bracket = 0; + } + + puts (name); + nextOpDirect(); +} +# endif + +# if d_m3EnableOpProfiling +d_m3RetSig profileOp (d_m3OpSig, cstr_t i_operationName) +{ + ProfileHit (i_operationName); + + nextOpDirect(); +} +# endif + +d_m3EndExternC + +#endif // m3_exec_h diff --git a/driver/wasm3/m3_exec_defs.h b/driver/wasm3/m3_exec_defs.h new file mode 100644 index 0000000..1c0dc72 --- /dev/null +++ b/driver/wasm3/m3_exec_defs.h @@ -0,0 +1,76 @@ +// +// m3_exec_defs.h +// +// Created by Steven Massey on 5/1/19. +// Copyright © 2019 Steven Massey. All rights reserved. +// + +#ifndef m3_exec_defs_h +#define m3_exec_defs_h + +#include "m3_core.h" + +d_m3BeginExternC + +# define m3MemData(mem) (u8*)(((M3MemoryHeader*)(mem))+1) +# define m3MemRuntime(mem) (((M3MemoryHeader*)(mem))->runtime) +# define m3MemInfo(mem) (&(((M3MemoryHeader*)(mem))->runtime->memory)) + +# define d_m3BaseOpSig pc_t _pc, m3stack_t _sp, M3MemoryHeader * _mem, m3reg_t _r0 +# define d_m3BaseOpArgs _sp, _mem, _r0 +# define d_m3BaseOpAllArgs _pc, _sp, _mem, _r0 +# define d_m3BaseOpDefaultArgs 0 +# define d_m3BaseClearRegisters _r0 = 0; +# define d_m3BaseCstr "" + +# define d_m3ExpOpSig(...) d_m3BaseOpSig, __VA_ARGS__ +# define d_m3ExpOpArgs(...) d_m3BaseOpArgs, __VA_ARGS__ +# define d_m3ExpOpAllArgs(...) d_m3BaseOpAllArgs, __VA_ARGS__ +# define d_m3ExpOpDefaultArgs(...) d_m3BaseOpDefaultArgs, __VA_ARGS__ +# define d_m3ExpClearRegisters(...) d_m3BaseClearRegisters; __VA_ARGS__ + +# if d_m3HasFloat +# define d_m3OpSig d_m3ExpOpSig (f64 _fp0) +# define d_m3OpArgs d_m3ExpOpArgs (_fp0) +# define d_m3OpAllArgs d_m3ExpOpAllArgs (_fp0) +# define d_m3OpDefaultArgs d_m3ExpOpDefaultArgs (0.) +# define d_m3ClearRegisters d_m3ExpClearRegisters (_fp0 = 0.;) +# else +# define d_m3OpSig d_m3BaseOpSig +# define d_m3OpArgs d_m3BaseOpArgs +# define d_m3OpAllArgs d_m3BaseOpAllArgs +# define d_m3OpDefaultArgs d_m3BaseOpDefaultArgs +# define d_m3ClearRegisters d_m3BaseClearRegisters +# endif + + +#define d_m3RetSig static inline m3ret_t vectorcall +# if (d_m3EnableOpProfiling || d_m3EnableOpTracing) + typedef m3ret_t (vectorcall * IM3Operation) (d_m3OpSig, cstr_t i_operationName); +# define d_m3Op(NAME) M3_NO_UBSAN d_m3RetSig op_##NAME (d_m3OpSig, cstr_t i_operationName) + +# define nextOpImpl() ((IM3Operation)(* _pc))(_pc + 1, d_m3OpArgs, __FUNCTION__) +# define jumpOpImpl(PC) ((IM3Operation)(* PC))( PC + 1, d_m3OpArgs, __FUNCTION__) +# else + typedef m3ret_t (vectorcall * IM3Operation) (d_m3OpSig); +# define d_m3Op(NAME) M3_NO_UBSAN d_m3RetSig op_##NAME (d_m3OpSig) + +# define nextOpImpl() ((IM3Operation)(* _pc))(_pc + 1, d_m3OpArgs) +# define jumpOpImpl(PC) ((IM3Operation)(* PC))( PC + 1, d_m3OpArgs) +# endif + +#define nextOpDirect() M3_MUSTTAIL return nextOpImpl() +#define jumpOpDirect(PC) M3_MUSTTAIL return jumpOpImpl((pc_t)(PC)) + +# if (d_m3EnableOpProfiling || d_m3EnableOpTracing) +d_m3RetSig RunCode (d_m3OpSig, cstr_t i_operationName) +# else +d_m3RetSig RunCode (d_m3OpSig) +# endif +{ + nextOpDirect(); +} + +d_m3EndExternC + +#endif // m3_exec_defs_h diff --git a/driver/wasm3/m3_function.c b/driver/wasm3/m3_function.c new file mode 100644 index 0000000..b100cae --- /dev/null +++ b/driver/wasm3/m3_function.c @@ -0,0 +1,233 @@ +// +// m3_function.c +// +// Created by Steven Massey on 4/7/21. +// Copyright © 2021 Steven Massey. All rights reserved. +// + +#include "m3_function.h" +#include "m3_env.h" + + +M3Result AllocFuncType (IM3FuncType * o_functionType, u32 i_numTypes) +{ + *o_functionType = (IM3FuncType) m3_Malloc ("M3FuncType", sizeof (M3FuncType) + i_numTypes); + return (*o_functionType) ? m3Err_none : m3Err_mallocFailed; +} + + +bool AreFuncTypesEqual (const IM3FuncType i_typeA, const IM3FuncType i_typeB) +{ + if (i_typeA->numRets == i_typeB->numRets && i_typeA->numArgs == i_typeB->numArgs) + { + return (memcmp (i_typeA->types, i_typeB->types, i_typeA->numRets + i_typeA->numArgs) == 0); + } + + return false; +} + +u16 GetFuncTypeNumParams (const IM3FuncType i_funcType) +{ + return i_funcType ? i_funcType->numArgs : 0; +} + + +u8 GetFuncTypeParamType (const IM3FuncType i_funcType, u16 i_index) +{ + u8 type = c_m3Type_unknown; + + if (i_funcType) + { + if (i_index < i_funcType->numArgs) + { + type = i_funcType->types [i_funcType->numRets + i_index]; + } + } + + return type; +} + + + +u16 GetFuncTypeNumResults (const IM3FuncType i_funcType) +{ + return i_funcType ? i_funcType->numRets : 0; +} + + +u8 GetFuncTypeResultType (const IM3FuncType i_funcType, u16 i_index) +{ + u8 type = c_m3Type_unknown; + + if (i_funcType) + { + if (i_index < i_funcType->numRets) + { + type = i_funcType->types [i_index]; + } + } + + return type; +} + + +//--------------------------------------------------------------------------------------------------------------- + + +void FreeImportInfo (M3ImportInfo * i_info) +{ + m3_Free (i_info->moduleUtf8); + m3_Free (i_info->fieldUtf8); +} + + +void Function_Release (IM3Function i_function) +{ + m3_Free (i_function->constants); + + for (int i = 0; i < i_function->numNames; i++) + { + // name can be an alias of fieldUtf8 + if (i_function->names[i] != i_function->import.fieldUtf8) + { + m3_Free (i_function->names[i]); + } + } + + FreeImportInfo (& i_function->import); + + if (i_function->ownsWasmCode) + m3_Free (i_function->wasm); + + // Function_FreeCompiledCode (func); + +# if (d_m3EnableCodePageRefCounting) + { + m3_Free (i_function->codePageRefs); + i_function->numCodePageRefs = 0; + } +# endif +} + + +void Function_FreeCompiledCode (IM3Function i_function) +{ +# if (d_m3EnableCodePageRefCounting) + { + i_function->compiled = NULL; + + while (i_function->numCodePageRefs--) + { + IM3CodePage page = i_function->codePageRefs [i_function->numCodePageRefs]; + + if (--(page->info.usageCount) == 0) + { +// printf ("free %p\n", page); + } + } + + m3_Free (i_function->codePageRefs); + + Runtime_ReleaseCodePages (i_function->module->runtime); + } +# endif +} + + +cstr_t m3_GetFunctionName (IM3Function i_function) +{ + u16 numNames = 0; + cstr_t *names = GetFunctionNames(i_function, &numNames); + if (numNames > 0) + return names[0]; + else + return ""; +} + + +IM3Module m3_GetFunctionModule (IM3Function i_function) +{ + return i_function ? i_function->module : NULL; +} + + +cstr_t * GetFunctionNames (IM3Function i_function, u16 * o_numNames) +{ + if (!i_function || !o_numNames) + return NULL; + + if (i_function->import.fieldUtf8) + { + *o_numNames = 1; + return &i_function->import.fieldUtf8; + } + else + { + *o_numNames = i_function->numNames; + return i_function->names; + } +} + + +cstr_t GetFunctionImportModuleName (IM3Function i_function) +{ + return (i_function->import.moduleUtf8) ? i_function->import.moduleUtf8 : ""; +} + + +u16 GetFunctionNumArgs (IM3Function i_function) +{ + u16 numArgs = 0; + + if (i_function) + { + if (i_function->funcType) + numArgs = i_function->funcType->numArgs; + } + + return numArgs; +} + +u8 GetFunctionArgType (IM3Function i_function, u32 i_index) +{ + u8 type = c_m3Type_none; + + if (i_index < GetFunctionNumArgs (i_function)) + { + u32 numReturns = i_function->funcType->numRets; + + type = i_function->funcType->types [numReturns + i_index]; + } + + return type; +} + + +u16 GetFunctionNumReturns (IM3Function i_function) +{ + u16 numReturns = 0; + + if (i_function) + { + if (i_function->funcType) + numReturns = i_function->funcType->numRets; + } + + return numReturns; +} + + +u8 GetFunctionReturnType (const IM3Function i_function, u16 i_index) +{ + return i_function ? GetFuncTypeResultType (i_function->funcType, i_index) : c_m3Type_unknown; +} + + +u32 GetFunctionNumArgsAndLocals (IM3Function i_function) +{ + if (i_function) + return i_function->numLocals + GetFunctionNumArgs (i_function); + else + return 0; +} + diff --git a/driver/wasm3/m3_function.h b/driver/wasm3/m3_function.h new file mode 100644 index 0000000..4a006a2 --- /dev/null +++ b/driver/wasm3/m3_function.h @@ -0,0 +1,103 @@ +// +// m3_function.h +// +// Created by Steven Massey on 4/7/21. +// Copyright © 2021 Steven Massey. All rights reserved. +// + +#ifndef m3_function_h +#define m3_function_h + +#include "m3_core.h" + +d_m3BeginExternC + +//--------------------------------------------------------------------------------------------------------------------------------- + +typedef struct M3FuncType +{ + struct M3FuncType * next; + + u16 numRets; + u16 numArgs; + u8 types []; // returns, then args +} +M3FuncType; + +typedef M3FuncType * IM3FuncType; + + +M3Result AllocFuncType (IM3FuncType * o_functionType, u32 i_numTypes); +bool AreFuncTypesEqual (const IM3FuncType i_typeA, const IM3FuncType i_typeB); + +u16 GetFuncTypeNumParams (const IM3FuncType i_funcType); +u8 GetFuncTypeParamType (const IM3FuncType i_funcType, u16 i_index); + +u16 GetFuncTypeNumResults (const IM3FuncType i_funcType); +u8 GetFuncTypeResultType (const IM3FuncType i_funcType, u16 i_index); + +//--------------------------------------------------------------------------------------------------------------------------------- + +typedef struct M3Function +{ + struct M3Module * module; + + M3ImportInfo import; + + bytes_t wasm; + bytes_t wasmEnd; + + cstr_t names[d_m3MaxDuplicateFunctionImpl]; + cstr_t export_name; // should be a part of "names" + u16 numNames; // maximum of d_m3MaxDuplicateFunctionImpl + + IM3FuncType funcType; + + pc_t compiled; + +# if (d_m3EnableCodePageRefCounting) + IM3CodePage * codePageRefs; // array of all pages used + u32 numCodePageRefs; +# endif + +# if defined (DEBUG) + u32 hits; + u32 index; +# endif + + u16 maxStackSlots; + + u16 numRetSlots; + u16 numRetAndArgSlots; + + u16 numLocals; // not including args + u16 numLocalBytes; + + bool ownsWasmCode; + + u16 numConstantBytes; + void * constants; +} +M3Function; + +void Function_Release (IM3Function i_function); +void Function_FreeCompiledCode (IM3Function i_function); + +cstr_t GetFunctionImportModuleName (IM3Function i_function); +cstr_t * GetFunctionNames (IM3Function i_function, u16 * o_numNames); +u16 GetFunctionNumArgs (IM3Function i_function); +u8 GetFunctionArgType (IM3Function i_function, u32 i_index); + +u16 GetFunctionNumReturns (IM3Function i_function); +u8 GetFunctionReturnType (const IM3Function i_function, u16 i_index); + +u32 GetFunctionNumArgsAndLocals (IM3Function i_function); + +cstr_t SPrintFunctionArgList (IM3Function i_function, m3stack_t i_sp); + +//--------------------------------------------------------------------------------------------------------------------------------- + + +d_m3EndExternC + +#endif /* m3_function_h */ diff --git a/driver/wasm3/m3_info.c b/driver/wasm3/m3_info.c new file mode 100644 index 0000000..42ef919 --- /dev/null +++ b/driver/wasm3/m3_info.c @@ -0,0 +1,564 @@ +// +// m3_info.c +// +// Created by Steven Massey on 4/27/19. +// Copyright © 2019 Steven Massey. All rights reserved. +// + +#include "m3_env.h" +#include "m3_info.h" +#include "m3_compile.h" + +#if defined(DEBUG) || (d_m3EnableStrace >= 2) + +size_t SPrintArg (char * o_string, size_t i_stringBufferSize, voidptr_t i_sp, u8 i_type) +{ + int len = 0; + + * o_string = 0; + + if (i_type == c_m3Type_i32) + len = snprintf (o_string, i_stringBufferSize, "%" PRIi32, * (i32 *) i_sp); + else if (i_type == c_m3Type_i64) + len = snprintf (o_string, i_stringBufferSize, "%" PRIi64, * (i64 *) i_sp); +#if d_m3HasFloat + else if (i_type == c_m3Type_f32) + len = snprintf (o_string, i_stringBufferSize, "%" PRIf32, * (f32 *) i_sp); + else if (i_type == c_m3Type_f64) + len = snprintf (o_string, i_stringBufferSize, "%" PRIf64, * (f64 *) i_sp); +#endif + + len = M3_MAX (0, len); + + return len; +} + + +cstr_t SPrintFunctionArgList (IM3Function i_function, m3stack_t i_sp) +{ + int ret; + static char string [256]; + + char * s = string; + ccstr_t e = string + sizeof(string) - 1; + + ret = snprintf (s, e-s, "("); + s += M3_MAX (0, ret); + + u64 * argSp = (u64 *) i_sp; + + IM3FuncType funcType = i_function->funcType; + if (funcType) + { + u32 numArgs = funcType->numArgs; + + for (u32 i = 0; i < numArgs; ++i) + { + u8 type = d_FuncArgType(funcType, i); + + ret = snprintf (s, e-s, "%s: ", c_waTypes [type]); + s += M3_MAX (0, ret); + + s += SPrintArg (s, e-s, argSp + i, type); + + if (i != numArgs - 1) { + ret = snprintf (s, e-s, ", "); + s += M3_MAX (0, ret); + } + } + } + else printf ("null signature"); + + ret = snprintf (s, e-s, ")"); + s += M3_MAX (0, ret); + + return string; +} + +#endif + +#ifdef DEBUG + +// a central function you can be breakpoint: +void ExceptionBreakpoint (cstr_t i_exception, cstr_t i_message) +{ + printf ("\nexception: '%s' @ %s\n", i_exception, i_message); + return; +} + + +typedef struct OpInfo +{ + IM3OpInfo info; + m3opcode_t opcode; +} +OpInfo; + +void m3_PrintM3Info () +{ + printf ("\n-- m3 configuration --------------------------------------------\n"); +// printf (" sizeof M3CodePage : %zu bytes (%d slots) \n", sizeof (M3CodePage), c_m3CodePageNumSlots); + printf (" sizeof M3MemPage : %u bytes \n", d_m3DefaultMemPageSize); + printf (" sizeof M3Compilation : %zu bytes \n", sizeof (M3Compilation)); + printf (" sizeof M3Function : %zu bytes \n", sizeof (M3Function)); + printf ("----------------------------------------------------------------\n\n"); +} + + +void * v_PrintEnvModuleInfo (IM3Module i_module, u32 * io_index) +{ + printf (" module [%u] name: '%s'; funcs: %d \n", * io_index++, i_module->name, i_module->numFunctions); + + return NULL; +} + + +void m3_PrintRuntimeInfo (IM3Runtime i_runtime) +{ + printf ("\n-- m3 runtime -------------------------------------------------\n"); + + printf (" stack-size: %zu \n\n", i_runtime->numStackSlots * sizeof (m3slot_t)); + + u32 moduleIndex = 0; + ForEachModule (i_runtime, (ModuleVisitor) v_PrintEnvModuleInfo, & moduleIndex); + + printf ("----------------------------------------------------------------\n\n"); +} + + +cstr_t GetTypeName (u8 i_m3Type) +{ + if (i_m3Type < 5) + return c_waTypes [i_m3Type]; + else + return "?"; +} + + +// TODO: these 'static char string []' aren't thread-friendly. though these functions are +// mainly for simple diagnostics during development, it'd be nice if they were fully reliable. + +cstr_t SPrintFuncTypeSignature (IM3FuncType i_funcType) +{ + static char string [256]; + + sprintf (string, "("); + + for (u32 i = 0; i < i_funcType->numArgs; ++i) + { + if (i != 0) + strcat (string, ", "); + + strcat (string, GetTypeName (d_FuncArgType(i_funcType, i))); + } + + strcat (string, ") -> "); + + for (u32 i = 0; i < i_funcType->numRets; ++i) + { + if (i != 0) + strcat (string, ", "); + + strcat (string, GetTypeName (d_FuncRetType(i_funcType, i))); + } + + return string; +} + + +cstr_t SPrintValue (void * i_value, u8 i_type) +{ + static char string [100]; + SPrintArg (string, 100, (m3stack_t) i_value, i_type); + return string; +} + +static +OpInfo find_operation_info (IM3Operation i_operation) +{ + OpInfo opInfo = { NULL, 0 }; + + if (!i_operation) return opInfo; + + // TODO: find also extended opcodes + for (u32 i = 0; i <= 0xff; ++i) + { + IM3OpInfo oi = GetOpInfo (i); + + if (oi->type != c_m3Type_unknown) + { + for (u32 o = 0; o < 4; ++o) + { + if (oi->operations [o] == i_operation) + { + opInfo.info = oi; + opInfo.opcode = i; + break; + } + } + } + else break; + } + + return opInfo; +} + + +#undef fetch +#define fetch(TYPE) (* (TYPE *) ((*o_pc)++)) + +#define d_m3Decoder(FUNC) void Decode_##FUNC (char * o_string, u8 i_opcode, IM3Operation i_operation, IM3OpInfo i_opInfo, pc_t * o_pc) + +d_m3Decoder (Call) +{ + void * function = fetch (void *); + i32 stackOffset = fetch (i32); + + sprintf (o_string, "%p; stack-offset: %d", function, stackOffset); +} + + +d_m3Decoder (Entry) +{ + IM3Function function = fetch (IM3Function); + + // only prints out the first registered name for the function + sprintf (o_string, "%s", m3_GetFunctionName(function)); +} + + +d_m3Decoder (f64_Store) +{ + if (i_operation == i_opInfo->operations [0]) + { + u32 operand = fetch (u32); + u32 offset = fetch (u32); + + sprintf (o_string, "offset= slot:%d + immediate:%d", operand, offset); + } + +// sprintf (o_string, "%s", function->name); +} + + +d_m3Decoder (Branch) +{ + void * target = fetch (void *); + sprintf (o_string, "%p", target); +} + +d_m3Decoder (BranchTable) +{ + u32 slot = fetch (u32); + + o_string += sprintf (o_string, "slot: %" PRIu32 "; targets: ", slot); + +// IM3Function function = fetch2 (IM3Function); + + i32 targets = fetch (i32); + + for (i32 i = 0; i < targets; ++i) + { + pc_t addr = fetch (pc_t); + o_string += sprintf (o_string, "%" PRIi32 "=%p, ", i, addr); + } + + pc_t addr = fetch (pc_t); + sprintf (o_string, "def=%p ", addr); +} + + +d_m3Decoder (Const) +{ + u64 value = fetch (u64); i32 offset = fetch (i32); + sprintf (o_string, " slot [%d] = %" PRIu64, offset, value); +} + + +#undef fetch + +void DecodeOperation (char * o_string, u8 i_opcode, IM3Operation i_operation, IM3OpInfo i_opInfo, pc_t * o_pc) +{ + #define d_m3Decode(OPCODE, FUNC) case OPCODE: Decode_##FUNC (o_string, i_opcode, i_operation, i_opInfo, o_pc); break; + + switch (i_opcode) + { +// d_m3Decode (0xc0, Const) + d_m3Decode (0xc5, Entry) + d_m3Decode (c_waOp_call, Call) + d_m3Decode (c_waOp_branch, Branch) + d_m3Decode (c_waOp_branchTable, BranchTable) + d_m3Decode (0x39, f64_Store) + } +} + +// WARNING/TODO: this isn't fully implemented. it blindly assumes each word is a Operation pointer +// and, if an operation happens to missing from the c_operations table it won't be recognized here +void dump_code_page (IM3CodePage i_codePage, pc_t i_startPC) +{ + m3log (code, "code page seq: %d", i_codePage->info.sequence); + + pc_t pc = i_startPC ? i_startPC : GetPageStartPC (i_codePage); + pc_t end = GetPagePC (i_codePage); + + m3log (code, "---------------------------------------------------------------------------------------"); + + while (pc < end) + { + pc_t operationPC = pc; + IM3Operation op = (IM3Operation) (* pc++); + + OpInfo i = find_operation_info (op); + + if (i.info) + { + char infoString [8*1024] = { 0 }; + + DecodeOperation (infoString, i.opcode, op, i.info, & pc); + + m3log (code, "%p | %20s %s", operationPC, i.info->name, infoString); + } + else + m3log (code, "%p | %p", operationPC, op); + + } + + m3log (code, "---------------------------------------------------------------------------------------"); + + m3log (code, "free-lines: %d", i_codePage->info.numLines - i_codePage->info.lineIndex); +} + + +void dump_type_stack (IM3Compilation o) +{ + /* Reminders about how the stack works! :) + -- args & locals remain on the type stack for duration of the function. Denoted with a constant 'A' and 'L' in this dump. + -- the initial stack dumps originate from the CompileLocals () function, so these identifiers won't/can't be + applied until this compilation stage is finished + -- constants are not statically represented in the type stack (like args & constants) since they don't have/need + write counts + + -- the number shown for static args and locals (value in wasmStack [i]) represents the write count for the variable + + -- (does Wasm ever write to an arg? I dunno/don't remember.) + -- the number for the dynamic stack values represents the slot number. + -- if the slot index points to arg, local or constant it's denoted with a lowercase 'a', 'l' or 'c' + + */ + + // for the assert at end of dump: + i32 regAllocated [2] = { (i32) IsRegisterAllocated (o, 0), (i32) IsRegisterAllocated (o, 1) }; + + // display whether r0 or fp0 is allocated. these should then also be reflected somewhere in the stack too. + d_m3Log(stack, "\n"); + d_m3Log(stack, " "); + printf ("%s %s ", regAllocated [0] ? "(r0)" : " ", regAllocated [1] ? "(fp0)" : " "); + printf("\n"); + + for (u32 p = 1; p <= 2; ++p) + { + d_m3Log(stack, " "); + + for (u16 i = 0; i < o->stackIndex; ++i) + { + if (i > 0 and i == o->stackFirstDynamicIndex) + printf ("#"); + + if (i == o->block.blockStackIndex) + printf (">"); + + const char * type = c_waCompactTypes [o->typeStack [i]]; + + const char * location = ""; + + i32 slot = o->wasmStack [i]; + + if (IsRegisterSlotAlias (slot)) + { + bool isFp = IsFpRegisterSlotAlias (slot); + location = isFp ? "/f" : "/r"; + + regAllocated [isFp]--; + slot = -1; + } + else + { + if (slot < o->slotFirstDynamicIndex) + { + if (slot >= o->slotFirstConstIndex) + location = "c"; + else if (slot >= o->function->numRetAndArgSlots) + location = "L"; + else + location = "a"; + } + } + + char item [100]; + + if (slot >= 0) + sprintf (item, "%s%s%d", type, location, slot); + else + sprintf (item, "%s%s", type, location); + + if (p == 1) + { + size_t s = strlen (item); + + sprintf (item, "%d", i); + + while (strlen (item) < s) + strcat (item, " "); + } + + printf ("|%s ", item); + + } + printf ("\n"); + } + +// for (u32 r = 0; r < 2; ++r) +// d_m3Assert (regAllocated [r] == 0); // reg allocation & stack out of sync + + u16 maxSlot = GetMaxUsedSlotPlusOne (o); + + if (maxSlot > o->slotFirstDynamicIndex) + { + d_m3Log (stack, " -"); + + for (u16 i = o->slotFirstDynamicIndex; i < maxSlot; ++i) + printf ("----"); + + printf ("\n"); + + d_m3Log (stack, " slot |"); + for (u16 i = o->slotFirstDynamicIndex; i < maxSlot; ++i) + printf ("%3d|", i); + + printf ("\n"); + d_m3Log (stack, " alloc |"); + + for (u16 i = o->slotFirstDynamicIndex; i < maxSlot; ++i) + { + printf ("%3d|", o->m3Slots [i]); + } + + printf ("\n"); + } + d_m3Log(stack, "\n"); +} + + +static const char * GetOpcodeIndentionString (i32 blockDepth) +{ + blockDepth += 1; + + if (blockDepth < 0) + blockDepth = 0; + + static const char * s_spaces = "......................................................................................."; + const char * indent = s_spaces + strlen (s_spaces); + indent -= (blockDepth * 2); + if (indent < s_spaces) + indent = s_spaces; + + return indent; +} + + +const char * get_indention_string (IM3Compilation o) +{ + return GetOpcodeIndentionString (o->block.depth+4); +} + + +void log_opcode (IM3Compilation o, m3opcode_t i_opcode) +{ + i32 depth = o->block.depth; + if (i_opcode == c_waOp_end or i_opcode == c_waOp_else) + depth--; + + m3log (compile, "%4d | 0x%02x %s %s", o->numOpcodes++, i_opcode, GetOpcodeIndentionString (depth), GetOpInfo(i_opcode)->name); +} + + +void log_emit (IM3Compilation o, IM3Operation i_operation) +{ + OpInfo i = find_operation_info (i_operation); + + d_m3Log(emit, ""); + if (i.info) + { + printf ("%p: %s\n", GetPagePC (o->page), i.info->name); + } + else printf ("not found: %p\n", i_operation); +} + +#endif // DEBUG + + +# if d_m3EnableOpProfiling + +typedef struct M3ProfilerSlot +{ + cstr_t opName; + u64 hitCount; +} +M3ProfilerSlot; + +static M3ProfilerSlot s_opProfilerCounts [d_m3ProfilerSlotMask + 1] = {}; + +void ProfileHit (cstr_t i_operationName) +{ + u64 ptr = (u64) i_operationName; + + M3ProfilerSlot * slot = & s_opProfilerCounts [ptr & d_m3ProfilerSlotMask]; + + if (slot->opName) + { + if (slot->opName != i_operationName) + { + m3_Abort ("profiler slot collision; increase d_m3ProfilerSlotMask"); + } + } + + slot->opName = i_operationName; + slot->hitCount++; +} + + +void m3_PrintProfilerInfo () +{ + M3ProfilerSlot dummy; + M3ProfilerSlot * maxSlot = & dummy; + + do + { + maxSlot->hitCount = 0; + + for (u32 i = 0; i <= d_m3ProfilerSlotMask; ++i) + { + M3ProfilerSlot * slot = & s_opProfilerCounts [i]; + + if (slot->opName) + { + if (slot->hitCount > maxSlot->hitCount) + maxSlot = slot; + } + } + + if (maxSlot->opName) + { + fprintf (stderr, "%13llu %s\n", maxSlot->hitCount, maxSlot->opName); + maxSlot->opName = NULL; + } + } + while (maxSlot->hitCount); +} + +# else + +void m3_PrintProfilerInfo () {} + +# endif + diff --git a/driver/wasm3/m3_info.h b/driver/wasm3/m3_info.h new file mode 100644 index 0000000..228e93f --- /dev/null +++ b/driver/wasm3/m3_info.h @@ -0,0 +1,38 @@ +// +// m3_info.h +// +// Created by Steven Massey on 12/6/19. +// Copyright © 2019 Steven Massey. All rights reserved. +// + +#ifndef m3_info_h +#define m3_info_h + +#include "m3_compile.h" + +d_m3BeginExternC + +void ProfileHit (cstr_t i_operationName); + +#ifdef DEBUG + +void dump_type_stack (IM3Compilation o); +void log_opcode (IM3Compilation o, m3opcode_t i_opcode); +const char * get_indention_string (IM3Compilation o); +void log_emit (IM3Compilation o, IM3Operation i_operation); + +cstr_t SPrintFuncTypeSignature (IM3FuncType i_funcType); + +#else // DEBUG + +#define dump_type_stack(...) {} +#define log_opcode(...) {} +#define get_indention_string(...) "" +#define emit_stack_dump(...) {} +#define log_emit(...) {} + +#endif // DEBUG + +d_m3EndExternC + +#endif // m3_info_h diff --git a/driver/wasm3/m3_math_utils.h b/driver/wasm3/m3_math_utils.h new file mode 100644 index 0000000..351aa92 --- /dev/null +++ b/driver/wasm3/m3_math_utils.h @@ -0,0 +1,316 @@ +// +// m3_math_utils.h +// +// Created by Volodymyr Shymanksyy on 8/10/19. +// Copyright © 2019 Volodymyr Shymanskyy. All rights reserved. +// + +#ifndef m3_math_utils_h +#define m3_math_utils_h + +#include "m3_core.h" + +#include + +#if defined(M3_COMPILER_MSVC) + +#include + +#define __builtin_popcount __popcnt + +static inline +int __builtin_ctz(uint32_t x) { + unsigned long ret; + _BitScanForward(&ret, x); + return (int)ret; +} + +static inline +int __builtin_clz(uint32_t x) { + unsigned long ret; + _BitScanReverse(&ret, x); + return (int)(31 ^ ret); +} + + + +#ifdef _WIN64 + +#define __builtin_popcountll __popcnt64 + +static inline +int __builtin_ctzll(uint64_t value) { + unsigned long ret; + _BitScanForward64(&ret, value); + return (int)ret; +} + +static inline +int __builtin_clzll(uint64_t value) { + unsigned long ret; + _BitScanReverse64(&ret, value); + return (int)(63 ^ ret); +} + +#else // _WIN64 + +#define __builtin_popcountll(x) (__popcnt((x) & 0xFFFFFFFF) + __popcnt((x) >> 32)) + +static inline +int __builtin_ctzll(uint64_t value) { + //if (value == 0) return 64; // Note: ctz(0) result is undefined anyway + uint32_t msh = (uint32_t)(value >> 32); + uint32_t lsh = (uint32_t)(value & 0xFFFFFFFF); + if (lsh != 0) return __builtin_ctz(lsh); + return 32 + __builtin_ctz(msh); +} + +static inline +int __builtin_clzll(uint64_t value) { + //if (value == 0) return 64; // Note: clz(0) result is undefined anyway + uint32_t msh = (uint32_t)(value >> 32); + uint32_t lsh = (uint32_t)(value & 0xFFFFFFFF); + if (msh != 0) return __builtin_clz(msh); + return 32 + __builtin_clz(lsh); +} + +#endif // _WIN64 + +#endif // defined(M3_COMPILER_MSVC) + + +// TODO: not sure why, signbit is actually defined in math.h +#if (defined(ESP8266) || defined(ESP32)) && !defined(signbit) + #define signbit(__x) \ + ((sizeof(__x) == sizeof(float)) ? __signbitf(__x) : __signbitd(__x)) +#endif + +#if defined(__AVR__) + +static inline +float rintf( float arg ) { + union { float f; uint32_t i; } u; + u.f = arg; + uint32_t ux = u.i & 0x7FFFFFFF; + if (M3_UNLIKELY(ux == 0 || ux > 0x5A000000)) { + return arg; + } + return (float)lrint(arg); +} + +static inline +double rint( double arg ) { + union { double f; uint32_t i[2]; } u; + u.f = arg; + uint32_t ux = u.i[1] & 0x7FFFFFFF; + if (M3_UNLIKELY((ux == 0 && u.i[0] == 0) || ux > 0x433FFFFF)) { + return arg; + } + return (double)lrint(arg); +} + +static inline +uint64_t strtoull(const char* str, char** endptr, int base) { + uint64_t result = 0; + const char* p = str; + + while (*p == ' ' || *p == '\t') p++; + + if (base == 0) { + if (*p == '0' && (*(p+1) == 'x' || *(p+1) == 'X')) { + base = 16; p += 2; + } else if (*p == '0') { + base = 8; p++; + } else { + base = 10; + } + } else if (base == 16 && *p == '0' && (*(p+1) == 'x' || *(p+1) == 'X')) { + p += 2; + } + + while (*p) { + int digit; + if (*p >= '0' && *p <= '9') digit = *p - '0'; + else if (*p >= 'a' && *p <= 'f') digit = *p - 'a' + 10; + else if (*p >= 'A' && *p <= 'F') digit = *p - 'A' + 10; + else break; + if (digit >= base) break; + result = result * base + digit; + p++; + } + + if (endptr) *endptr = (char*)p; + return result; +} + +#endif + +/* + * Rotr, Rotl + */ + +static inline +u32 rotl32(u32 n, unsigned c) { + const unsigned mask = CHAR_BIT * sizeof(n) - 1; + c &= mask & 31; + return (n << c) | (n >> ((-c) & mask)); +} + +static inline +u32 rotr32(u32 n, unsigned c) { + const unsigned mask = CHAR_BIT * sizeof(n) - 1; + c &= mask & 31; + return (n >> c) | (n << ((-c) & mask)); +} + +static inline +u64 rotl64(u64 n, unsigned c) { + const unsigned mask = CHAR_BIT * sizeof(n) - 1; + c &= mask & 63; + return (n << c) | (n >> ((-c) & mask)); +} + +static inline +u64 rotr64(u64 n, unsigned c) { + const unsigned mask = CHAR_BIT * sizeof(n) - 1; + c &= mask & 63; + return (n >> c) | (n << ((-c) & mask)); +} + +/* + * Integer Div, Rem + */ + +#define OP_DIV_U(RES, A, B) \ + if (M3_UNLIKELY(B == 0)) newTrap (m3Err_trapDivisionByZero); \ + RES = A / B; + +#define OP_REM_U(RES, A, B) \ + if (M3_UNLIKELY(B == 0)) newTrap (m3Err_trapDivisionByZero); \ + RES = A % B; + +// 2's complement detection +#if (INT_MIN != -INT_MAX) + + #define OP_DIV_S(RES, A, B, TYPE_MIN) \ + if (M3_UNLIKELY(B == 0)) newTrap (m3Err_trapDivisionByZero); \ + if (M3_UNLIKELY(B == -1 and A == TYPE_MIN)) { \ + newTrap (m3Err_trapIntegerOverflow); \ + } \ + RES = A / B; + + #define OP_REM_S(RES, A, B, TYPE_MIN) \ + if (M3_UNLIKELY(B == 0)) newTrap (m3Err_trapDivisionByZero); \ + if (M3_UNLIKELY(B == -1 and A == TYPE_MIN)) RES = 0; \ + else RES = A % B; + +#else + + #define OP_DIV_S(RES, A, B, TYPE_MIN) OP_DIV_U(RES, A, B) + #define OP_REM_S(RES, A, B, TYPE_MIN) OP_REM_U(RES, A, B) + +#endif + +/* + * Trunc + */ + +#define OP_TRUNC(RES, A, TYPE, RMIN, RMAX) \ + if (M3_UNLIKELY(isnan(A))) { \ + newTrap (m3Err_trapIntegerConversion); \ + } \ + if (M3_UNLIKELY(A <= RMIN or A >= RMAX)) { \ + newTrap (m3Err_trapIntegerOverflow); \ + } \ + RES = (TYPE)A; + + +#define OP_I32_TRUNC_F32(RES, A) OP_TRUNC(RES, A, i32, -2147483904.0f, 2147483648.0f) +#define OP_U32_TRUNC_F32(RES, A) OP_TRUNC(RES, A, u32, -1.0f, 4294967296.0f) +#define OP_I32_TRUNC_F64(RES, A) OP_TRUNC(RES, A, i32, -2147483649.0 , 2147483648.0 ) +#define OP_U32_TRUNC_F64(RES, A) OP_TRUNC(RES, A, u32, -1.0 , 4294967296.0 ) + +#define OP_I64_TRUNC_F32(RES, A) OP_TRUNC(RES, A, i64, -9223373136366403584.0f, 9223372036854775808.0f) +#define OP_U64_TRUNC_F32(RES, A) OP_TRUNC(RES, A, u64, -1.0f, 18446744073709551616.0f) +#define OP_I64_TRUNC_F64(RES, A) OP_TRUNC(RES, A, i64, -9223372036854777856.0 , 9223372036854775808.0 ) +#define OP_U64_TRUNC_F64(RES, A) OP_TRUNC(RES, A, u64, -1.0 , 18446744073709551616.0 ) + +#define OP_TRUNC_SAT(RES, A, TYPE, RMIN, RMAX, IMIN, IMAX) \ + if (M3_UNLIKELY(isnan(A))) { \ + RES = 0; \ + } else if (M3_UNLIKELY(A <= RMIN)) { \ + RES = IMIN; \ + } else if (M3_UNLIKELY(A >= RMAX)) { \ + RES = IMAX; \ + } else { \ + RES = (TYPE)A; \ + } + +#define OP_I32_TRUNC_SAT_F32(RES, A) OP_TRUNC_SAT(RES, A, i32, -2147483904.0f, 2147483648.0f, INT32_MIN, INT32_MAX) +#define OP_U32_TRUNC_SAT_F32(RES, A) OP_TRUNC_SAT(RES, A, u32, -1.0f, 4294967296.0f, 0UL, UINT32_MAX) +#define OP_I32_TRUNC_SAT_F64(RES, A) OP_TRUNC_SAT(RES, A, i32, -2147483649.0 , 2147483648.0, INT32_MIN, INT32_MAX) +#define OP_U32_TRUNC_SAT_F64(RES, A) OP_TRUNC_SAT(RES, A, u32, -1.0 , 4294967296.0, 0UL, UINT32_MAX) + +#define OP_I64_TRUNC_SAT_F32(RES, A) OP_TRUNC_SAT(RES, A, i64, -9223373136366403584.0f, 9223372036854775808.0f, INT64_MIN, INT64_MAX) +#define OP_U64_TRUNC_SAT_F32(RES, A) OP_TRUNC_SAT(RES, A, u64, -1.0f, 18446744073709551616.0f, 0ULL, UINT64_MAX) +#define OP_I64_TRUNC_SAT_F64(RES, A) OP_TRUNC_SAT(RES, A, i64, -9223372036854777856.0 , 9223372036854775808.0, INT64_MIN, INT64_MAX) +#define OP_U64_TRUNC_SAT_F64(RES, A) OP_TRUNC_SAT(RES, A, u64, -1.0 , 18446744073709551616.0, 0ULL, UINT64_MAX) + +/* + * Min, Max + */ + +#if d_m3HasFloat + +#include + +// Propagate a NaN operand the way the arithmetic ops do. +// keep its sign and payload, but force the quiet bit, since the spec requires +// min/max to produce an arithmetic NaN. + +static inline +f32 quiet_nan_f32(f32 arg) { + union { f32 f; u32 i; } u; + u.f = arg; + u.i |= 0x00400000; + return u.f; +} + +static inline +f64 quiet_nan_f64(f64 arg) { + union { f64 f; u64 i; } u; + u.f = arg; + u.i |= 0x0008000000000000ULL; + return u.f; +} + +static inline +f32 min_f32(f32 a, f32 b) { + if (M3_UNLIKELY(isnan(a) or isnan(b))) return quiet_nan_f32(isnan(a) ? a : b); + if (M3_UNLIKELY(a == 0 and a == b)) return signbit(a) ? a : b; + return a > b ? b : a; +} + +static inline +f32 max_f32(f32 a, f32 b) { + if (M3_UNLIKELY(isnan(a) or isnan(b))) return quiet_nan_f32(isnan(a) ? a : b); + if (M3_UNLIKELY(a == 0 and a == b)) return signbit(a) ? b : a; + return a > b ? a : b; +} + +static inline +f64 min_f64(f64 a, f64 b) { + if (M3_UNLIKELY(isnan(a) or isnan(b))) return quiet_nan_f64(isnan(a) ? a : b); + if (M3_UNLIKELY(a == 0 and a == b)) return signbit(a) ? a : b; + return a > b ? b : a; +} + +static inline +f64 max_f64(f64 a, f64 b) { + if (M3_UNLIKELY(isnan(a) or isnan(b))) return quiet_nan_f64(isnan(a) ? a : b); + if (M3_UNLIKELY(a == 0 and a == b)) return signbit(a) ? b : a; + return a > b ? a : b; +} +#endif + +#endif // m3_math_utils_h diff --git a/driver/wasm3/m3_module.c b/driver/wasm3/m3_module.c new file mode 100644 index 0000000..3cb6866 --- /dev/null +++ b/driver/wasm3/m3_module.c @@ -0,0 +1,175 @@ +// +// m3_module.c +// +// Created by Steven Massey on 5/7/19. +// Copyright © 2019 Steven Massey. All rights reserved. +// + +#include "m3_env.h" +#include "m3_exception.h" + + +void Module_FreeFunctions (IM3Module i_module) +{ + for (u32 i = 0; i < i_module->numFunctions; ++i) + { + IM3Function func = & i_module->functions [i]; + Function_Release (func); + } +} + + +void m3_FreeModule (IM3Module i_module) +{ + if (i_module) + { + m3log (module, "freeing module: %s (funcs: %d; segments: %d)", + i_module->name, i_module->numFunctions, i_module->numDataSegments); + + Module_FreeFunctions (i_module); + + m3_Free (i_module->functions); + //m3_Free (i_module->imports); + m3_Free (i_module->funcTypes); + m3_Free (i_module->dataSegments); + m3_Free (i_module->table0); + + for (u32 i = 0; i < i_module->numGlobals; ++i) + { + m3_Free (i_module->globals[i].name); + FreeImportInfo(&(i_module->globals[i].import)); + } + m3_Free (i_module->globals); + m3_Free (i_module->memoryExportName); + m3_Free (i_module->table0ExportName); + + FreeImportInfo(&i_module->memoryImport); + + m3_Free (i_module); + } +} + + +M3Result Module_AddGlobal (IM3Module io_module, IM3Global * o_global, u8 i_type, bool i_mutable, bool i_isImported) +{ +_try { + u32 index = io_module->numGlobals++; + io_module->globals = m3_ReallocArray (M3Global, io_module->globals, io_module->numGlobals, index); + _throwifnull (io_module->globals); + M3Global * global = & io_module->globals [index]; + + global->type = i_type; + global->imported = i_isImported; + global->isMutable = i_mutable; + + if (o_global) + * o_global = global; + +} _catch: + return result; +} + +M3Result Module_PreallocFunctions (IM3Module io_module, u32 i_totalFunctions) +{ +_try { + if (i_totalFunctions > io_module->allFunctions) { + io_module->functions = m3_ReallocArray (M3Function, io_module->functions, i_totalFunctions, io_module->allFunctions); + io_module->allFunctions = i_totalFunctions; + _throwifnull (io_module->functions); + } +} _catch: + return result; +} + +M3Result Module_AddFunction (IM3Module io_module, u32 i_typeIndex, IM3ImportInfo i_importInfo) +{ +_try { + + u32 index = io_module->numFunctions++; +_ (Module_PreallocFunctions(io_module, io_module->numFunctions)); + + _throwif ("type sig index out of bounds", i_typeIndex >= io_module->numFuncTypes); + + IM3FuncType ft = io_module->funcTypes [i_typeIndex]; + + IM3Function func = Module_GetFunction (io_module, index); + func->funcType = ft; + +# ifdef DEBUG + func->index = index; +# endif + + if (i_importInfo and func->numNames == 0) + { + func->import = * i_importInfo; + func->names[0] = i_importInfo->fieldUtf8; + func->numNames = 1; + } + + m3log (module, " added function: %3d; sig: %d", index, i_typeIndex); + +} _catch: + return result; +} + +#ifdef DEBUG +void Module_GenerateNames (IM3Module i_module) +{ + for (u32 i = 0; i < i_module->numFunctions; ++i) + { + IM3Function func = & i_module->functions [i]; + + if (func->numNames == 0) + { + char* buff = m3_AllocArray(char, 16); + snprintf(buff, 16, "$func%d", i); + func->names[0] = buff; + func->numNames = 1; + } + } + for (u32 i = 0; i < i_module->numGlobals; ++i) + { + IM3Global global = & i_module->globals [i]; + + if (global->name == NULL) + { + char* buff = m3_AllocArray(char, 16); + snprintf(buff, 16, "$global%d", i); + global->name = buff; + } + } +} +#endif + +IM3Function Module_GetFunction (IM3Module i_module, u32 i_functionIndex) +{ + IM3Function func = NULL; + + if (i_functionIndex < i_module->numFunctions) + { + func = & i_module->functions [i_functionIndex]; + //func->module = i_module; + } + + return func; +} + + +const char* m3_GetModuleName (IM3Module i_module) +{ + if (!i_module || !i_module->name) + return ".unnamed"; + + return i_module->name; +} + +void m3_SetModuleName (IM3Module i_module, const char* name) +{ + if (i_module) i_module->name = name; +} + +IM3Runtime m3_GetModuleRuntime (IM3Module i_module) +{ + return i_module ? i_module->runtime : NULL; +} + diff --git a/driver/wasm3/m3_parse.c b/driver/wasm3/m3_parse.c new file mode 100644 index 0000000..8201487 --- /dev/null +++ b/driver/wasm3/m3_parse.c @@ -0,0 +1,846 @@ +// +// m3_parse.c +// +// Created by Steven Massey on 4/19/19. +// Copyright © 2019 Steven Massey. All rights reserved. +// + +#include "m3_env.h" +#include "m3_compile.h" +#include "m3_exception.h" +#include "m3_info.h" + + +M3Result ParseType_Table (IM3Module io_module, bytes_t i_bytes, cbytes_t i_end) +{ + M3Result result = m3Err_none; + + u32 numTables; +_ (ReadLEB_u32 (& numTables, & i_bytes, i_end)); m3log (parse, "** Table [%d]", numTables); + + // MVP: at most one table, counting any that was already imported + _throwif (m3Err_wasmMalformed, numTables > 1); + _throwif (m3Err_wasmMalformed, numTables and io_module->hasTable); + + for (u32 i = 0; i < numTables; ++i) + { + u8 elemType; +_ (Read_u8 (& elemType, & i_bytes, i_end)); + // Spec: element type must be funcref (0x70) + _throwif (m3Err_wasmMalformed, elemType != 0x70); + + u8 flag; +_ (ReadLEB_u7 (& flag, & i_bytes, i_end)); + u32 initSize; +_ (ReadLEB_u32 (& initSize, & i_bytes, i_end)); + if (flag & 1) { + u32 maxSize; +_ (ReadLEB_u32 (& maxSize, & i_bytes, i_end)); + _throwif (m3Err_wasmMalformed, maxSize < initSize); + } + io_module->hasTable = true; + } + + _throwif (m3Err_wasmMalformed, i_bytes != i_end); // section size mismatch + + _catch: return result; +} + + +M3Result ParseType_Memory (M3MemoryInfo * o_memory, bytes_t * io_bytes, cbytes_t i_end) +{ + M3Result result = m3Err_none; + + u8 flag; + +_ (ReadLEB_u7 (& flag, io_bytes, i_end)); // really a u1 +_ (ReadLEB_u32 (& o_memory->initPages, io_bytes, i_end)); + + o_memory->maxPages = 0; + if (flag & (1u << 0)) + { +_ (ReadLEB_u32 (& o_memory->maxPages, io_bytes, i_end)); + + // Spec: memory limits validation - max must not be less than init + _throwif (m3Err_wasmMalformed, o_memory->maxPages < o_memory->initPages); + } + + o_memory->pageSize = 0; + if (flag & (1u << 3)) { + u32 logPageSize; +_ (ReadLEB_u32 (& logPageSize, io_bytes, i_end)); + o_memory->pageSize = 1u << logPageSize; + } + + // Spec: memory limits must be valid within range 2^16 (65536 pages) + // Only enforce for standard page size (no custom page size flag) + if (!(flag & (1u << 3))) + { + _throwif (m3Err_wasmMalformed, o_memory->initPages > 65536); + if (flag & (1u << 0)) + _throwif (m3Err_wasmMalformed, o_memory->maxPages > 65536); + } + + _catch: return result; +} + + +M3Result ParseSection_Type (IM3Module io_module, bytes_t i_bytes, cbytes_t i_end) +{ + IM3FuncType ftype = NULL; + +_try { + u32 numTypes; +_ (ReadLEB_u32 (& numTypes, & i_bytes, i_end)); m3log (parse, "** Type [%d]", numTypes); + + _throwif("too many types", numTypes > d_m3MaxSaneTypesCount); + + if (numTypes) + { + // table of IM3FuncType (that point to the actual M3FuncType struct in the Environment) + io_module->funcTypes = m3_AllocArray (IM3FuncType, numTypes); + _throwifnull (io_module->funcTypes); + io_module->numFuncTypes = numTypes; + + for (u32 i = 0; i < numTypes; ++i) + { + i8 form; +_ (ReadLEB_i7 (& form, & i_bytes, i_end)); + _throwif (m3Err_wasmMalformed, form != -32); // for Wasm MVP + + u32 numArgs; +_ (ReadLEB_u32 (& numArgs, & i_bytes, i_end)); + + _throwif (m3Err_tooManyArgsRets, numArgs > d_m3MaxSaneFunctionArgRetCount); +#if defined(M3_COMPILER_MSVC) + u8 argTypes [d_m3MaxSaneFunctionArgRetCount]; +#else + u8 argTypes[numArgs+1]; // make ubsan happy +#endif + for (u32 a = 0; a < numArgs; ++a) + { + i8 wasmType; + u8 argType; +_ (ReadLEB_i7 (& wasmType, & i_bytes, i_end)); +_ (NormalizeType (& argType, wasmType)); + + argTypes[a] = argType; + } + + u32 numRets; +_ (ReadLEB_u32 (& numRets, & i_bytes, i_end)); + _throwif (m3Err_tooManyArgsRets, (u64)(numRets) + numArgs > d_m3MaxSaneFunctionArgRetCount); + +_ (AllocFuncType (& ftype, numRets + numArgs)); + ftype->numArgs = numArgs; + ftype->numRets = numRets; + + for (u32 r = 0; r < numRets; ++r) + { + i8 wasmType; + u8 retType; +_ (ReadLEB_i7 (& wasmType, & i_bytes, i_end)); +_ (NormalizeType (& retType, wasmType)); + + ftype->types[r] = retType; + } + memcpy (ftype->types + numRets, argTypes, numArgs); m3log (parse, " type %2d: %s", i, SPrintFuncTypeSignature (ftype)); + + Environment_AddFuncType (io_module->environment, & ftype); + io_module->funcTypes [i] = ftype; + ftype = NULL; // ownership transferred to environment + } + } + + _throwif (m3Err_wasmMalformed, i_bytes != i_end); // section size mismatch + +} _catch: + + if (result) + { + m3_Free (ftype); + // FIX: M3FuncTypes in the table are leaked + m3_Free (io_module->funcTypes); + io_module->numFuncTypes = 0; + } + + return result; +} + + +M3Result ParseSection_Function (IM3Module io_module, bytes_t i_bytes, cbytes_t i_end) +{ + M3Result result = m3Err_none; + + u32 numFunctions; +_ (ReadLEB_u32 (& numFunctions, & i_bytes, i_end)); m3log (parse, "** Function [%d]", numFunctions); + + _throwif("too many functions", numFunctions > d_m3MaxSaneFunctionsCount); + +_ (Module_PreallocFunctions(io_module, io_module->numFunctions + numFunctions)); + + for (u32 i = 0; i < numFunctions; ++i) + { + u32 funcTypeIndex; +_ (ReadLEB_u32 (& funcTypeIndex, & i_bytes, i_end)); + +_ (Module_AddFunction (io_module, funcTypeIndex, NULL /* import info */)); + } + + _throwif (m3Err_wasmMalformed, i_bytes != i_end); // section size mismatch + + _catch: return result; +} + + +M3Result ParseSection_Import (IM3Module io_module, bytes_t i_bytes, cbytes_t i_end) +{ + M3Result result = m3Err_none; + + M3ImportInfo import = { NULL, NULL }, clearImport = { NULL, NULL }; + + u32 numImports; +_ (ReadLEB_u32 (& numImports, & i_bytes, i_end)); m3log (parse, "** Import [%d]", numImports); + + _throwif("too many imports", numImports > d_m3MaxSaneImportsCount); + + // Most imports are functions, so we won't waste much space anyway (if any) +_ (Module_PreallocFunctions(io_module, numImports)); + + for (u32 i = 0; i < numImports; ++i) + { + u8 importKind; + +_ (Read_utf8 (& import.moduleUtf8, & i_bytes, i_end)); +_ (Read_utf8 (& import.fieldUtf8, & i_bytes, i_end)); +_ (Read_u8 (& importKind, & i_bytes, i_end)); m3log (parse, " kind: %d '%s.%s' ", + (u32) importKind, import.moduleUtf8, import.fieldUtf8); + switch (importKind) + { + case d_externalKind_function: + { + u32 typeIndex; +_ (ReadLEB_u32 (& typeIndex, & i_bytes, i_end)) + +_ (Module_AddFunction (io_module, typeIndex, & import)) + import = clearImport; + + io_module->numFuncImports++; + } + break; + + case d_externalKind_table: + { + // Parse and validate table type (elem type + limits) + u8 elemType; +_ (Read_u8 (& elemType, & i_bytes, i_end)); + _throwif (m3Err_wasmMalformed, elemType != 0x70); // must be funcref + u8 flag; +_ (ReadLEB_u7 (& flag, & i_bytes, i_end)); + u32 initSize; +_ (ReadLEB_u32 (& initSize, & i_bytes, i_end)); + if (flag & 1) { + u32 maxSize; +_ (ReadLEB_u32 (& maxSize, & i_bytes, i_end)); + } + io_module->hasTable = true; + } + break; + + case d_externalKind_memory: + { +_ (ParseType_Memory (& io_module->memoryInfo, & i_bytes, i_end)); + io_module->memoryImported = true; + io_module->memoryImport = import; + import = clearImport; + } + break; + + case d_externalKind_global: + { + i8 waType; + u8 type, isMutable; + +_ (ReadLEB_i7 (& waType, & i_bytes, i_end)); +_ (NormalizeType (& type, waType)); +_ (ReadLEB_u7 (& isMutable, & i_bytes, i_end)); m3log (parse, " global: %s mutable=%d", c_waTypes [type], (u32) isMutable); + _throwif (m3Err_wasmMalformed, isMutable > 1); + + IM3Global global; +_ (Module_AddGlobal (io_module, & global, type, isMutable, true /* isImport */)); + global->import = import; + import = clearImport; + } + break; + + default: + _throw (m3Err_wasmMalformed); + } + + FreeImportInfo (& import); + } + + _throwif (m3Err_wasmMalformed, i_bytes != i_end); // section size mismatch + + _catch: + + FreeImportInfo (& import); + + return result; +} + + +M3Result ParseSection_Export (IM3Module io_module, bytes_t i_bytes, cbytes_t i_end) +{ + M3Result result = m3Err_none; + const char * utf8 = NULL; +#if d_m3EnableValidation + // We store name pointers + lengths to handle embedded NUL bytes correctly + typedef struct { const u8 * ptr; u16 len; } ExportName; + ExportName * exportNames = NULL; +#endif + + u32 numExports; +_ (ReadLEB_u32 (& numExports, & i_bytes, i_end)); m3log (parse, "** Export [%d]", numExports); + + _throwif("too many exports", numExports > d_m3MaxSaneExportsCount); + +#if d_m3EnableValidation + // Spec: all export names must be different + if (numExports > 1) + { + exportNames = (ExportName *) m3_Malloc ("exportNames", sizeof(ExportName) * numExports); + } +#endif + + for (u32 i = 0; i < numExports; ++i) + { + u8 exportKind; + u32 index; + + // Read name length and remember raw position for uniqueness check +#if d_m3EnableValidation + const u8 * nameStart = i_bytes; + u32 nameLen = 0; + { + bytes_t tmp = i_bytes; + M3Result rl = ReadLEB_u32 (& nameLen, & tmp, i_end); + if (rl) { m3_Free(exportNames); _throw(rl); } + nameStart = tmp; // points to the raw name bytes + } +#endif + +_ (Read_utf8 (& utf8, & i_bytes, i_end)); +_ (Read_u8 (& exportKind, & i_bytes, i_end)); +_ (ReadLEB_u32 (& index, & i_bytes, i_end)); m3log (parse, " index: %3d; kind: %d; export: '%s'; ", index, (u32) exportKind, utf8); + +#if d_m3EnableValidation + if (exportNames) + { + for (u32 j = 0; j < i; ++j) + { + if (exportNames[j].len == nameLen && + memcmp (exportNames[j].ptr, nameStart, nameLen) == 0) + { + m3_Free (exportNames); + _throw (m3Err_wasmMalformed); // duplicate export name + } + } + exportNames[i].ptr = nameStart; + exportNames[i].len = (u16)nameLen; + } +#endif + + if (exportKind == d_externalKind_function) + { + _throwif(m3Err_wasmMalformed, index >= io_module->numFunctions); + IM3Function func = &(io_module->functions [index]); + if (func->numNames < d_m3MaxDuplicateFunctionImpl) + { + func->names[func->numNames++] = utf8; + func->export_name = utf8; + utf8 = NULL; // ownership transferred to M3Function + } + } + else if (exportKind == d_externalKind_global) + { + _throwif(m3Err_wasmMalformed, index >= io_module->numGlobals); + IM3Global global = &(io_module->globals [index]); + m3_Free (global->name); + global->name = utf8; + utf8 = NULL; // ownership transferred to M3Global + } + else if (exportKind == d_externalKind_memory) + { + _throwif(m3Err_wasmMalformed, index != 0); + _throwif(m3Err_wasmMalformed, not (io_module->memoryImported or io_module->memoryDeclared)); + m3_Free (io_module->memoryExportName); + io_module->memoryExportName = utf8; + utf8 = NULL; // ownership transferred to M3Module + } + else if (exportKind == d_externalKind_table) + { + _throwif(m3Err_wasmMalformed, index != 0); + _throwif(m3Err_wasmMalformed, not io_module->hasTable); + m3_Free (io_module->table0ExportName); + io_module->table0ExportName = utf8; + utf8 = NULL; // ownership transferred to M3Module + } + + m3_Free (utf8); + } + + _throwif (m3Err_wasmMalformed, i_bytes != i_end); // section size mismatch + +_catch: + m3_Free (utf8); +#if d_m3EnableValidation + m3_Free (exportNames); +#endif + return result; +} + + +M3Result ParseSection_Start (IM3Module io_module, bytes_t i_bytes, cbytes_t i_end) +{ + M3Result result = m3Err_none; + + u32 startFuncIndex; +_ (ReadLEB_u32 (& startFuncIndex, & i_bytes, i_end)); m3log (parse, "** Start Function: %d", startFuncIndex); + + if (startFuncIndex < io_module->numFunctions) + { + // Spec: start function type must be [] -> [] + IM3Function func = & io_module->functions [startFuncIndex]; + if (func->funcType) + { + _throwif (m3Err_wasmMalformed, + func->funcType->numArgs != 0 || func->funcType->numRets != 0); + } + + io_module->startFunction = startFuncIndex; + } + else result = "start function index out of bounds"; + + _throwif (m3Err_wasmMalformed, i_bytes != i_end); // section size mismatch + + _catch: return result; +} + + +M3Result Parse_InitExpr (M3Module * io_module, bytes_t * io_bytes, cbytes_t i_end) +{ + M3Result result = m3Err_none; + + // this doesn't generate code pages. just walks the wasm bytecode to find the end + +#if defined(d_m3PreferStaticAlloc) + static M3Compilation compilation; +#else + M3Compilation compilation; +#endif + compilation = (M3Compilation){ .runtime = NULL, .module = io_module, .wasm = * io_bytes, .wasmEnd = i_end, .isInitExpr = true }; + + result = CompileBlockStatements (& compilation); + + * io_bytes = compilation.wasm; + + return result; +} + + +M3Result ParseSection_Element (IM3Module io_module, bytes_t i_bytes, cbytes_t i_end) +{ + M3Result result = m3Err_none; + + u32 numSegments; + bytes_t pos; +_ (ReadLEB_u32 (& numSegments, & i_bytes, i_end)); m3log (parse, "** Element [%d]", numSegments); + + _throwif ("too many element segments", numSegments > d_m3MaxSaneElementSegments); + + // Element segments need a table to populate + _throwif (m3Err_wasmMalformed, numSegments and not io_module->hasTable); + + io_module->elementSection = i_bytes; + io_module->elementSectionEnd = i_end; + io_module->numElementSegments = numSegments; + + // Walk the section to validate structure and detect section size mismatch. + // The actual element initialization happens later in InitElements. + pos = i_bytes; + for (u32 i = 0; i < numSegments; ++i) + { + u32 tableIndex; +_ (ReadLEB_u32 (& tableIndex, & pos, i_end)); + + // Walk the init expression (offset) to find its end +_ (Parse_InitExpr (io_module, & pos, i_end)); + + u32 numElements; +_ (ReadLEB_u32 (& numElements, & pos, i_end)); + + for (u32 e = 0; e < numElements; ++e) + { + u32 funcIndex; +_ (ReadLEB_u32 (& funcIndex, & pos, i_end)); + } + } + + _throwif (m3Err_wasmMalformed, pos != i_end); // section size mismatch + + _catch: return result; +} + + +M3Result ParseSection_Code (M3Module * io_module, bytes_t i_bytes, cbytes_t i_end) +{ + M3Result result; + + u32 numFunctions; +_ (ReadLEB_u32 (& numFunctions, & i_bytes, i_end)); m3log (parse, "** Code [%d]", numFunctions); + + if (numFunctions != io_module->numFunctions - io_module->numFuncImports) + { + _throw ("mismatched function count in code section"); + } + + for (u32 f = 0; f < numFunctions; ++f) + { + const u8 * start = i_bytes; + + u32 size; +_ (ReadLEB_u32 (& size, & i_bytes, i_end)); + + if (size) + { + const u8 * ptr = i_bytes; + i_bytes += size; + + if (i_bytes <= i_end) + { + /* + u32 numLocalBlocks; +_ (ReadLEB_u32 (& numLocalBlocks, & ptr, i_end)); m3log (parse, " code size: %-4d", size); + + u32 numLocals = 0; + + for (u32 l = 0; l < numLocalBlocks; ++l) + { + u32 varCount; + i8 wasmType; + u8 normalType; + +_ (ReadLEB_u32 (& varCount, & ptr, i_end)); +_ (ReadLEB_i7 (& wasmType, & ptr, i_end)); +_ (NormalizeType (& normalType, wasmType)); + + numLocals += varCount; m3log (parse, " %2d locals; type: '%s'", varCount, c_waTypes [normalType]); + } + */ + + IM3Function func = Module_GetFunction (io_module, f + io_module->numFuncImports); + + func->module = io_module; + func->wasm = start; + func->wasmEnd = i_bytes; + //func->ownsWasmCode = io_module->hasWasmCodeCopy; +// func->numLocals = numLocals; + } + else _throw (m3Err_wasmSectionOverrun); + } + } + + _catch: + + if (not result and i_bytes != i_end) + result = m3Err_wasmSectionUnderrun; + + return result; +} + + +M3Result ParseSection_Data (M3Module * io_module, bytes_t i_bytes, cbytes_t i_end) +{ + M3Result result = m3Err_none; + + u32 numDataSegments; +_ (ReadLEB_u32 (& numDataSegments, & i_bytes, i_end)); m3log (parse, "** Data [%d]", numDataSegments); + + _throwif("too many data segments", numDataSegments > d_m3MaxSaneDataSegments); + + io_module->dataSegments = m3_AllocArray (M3DataSegment, numDataSegments); + _throwifnull(io_module->dataSegments); + io_module->numDataSegments = numDataSegments; + + for (u32 i = 0; i < numDataSegments; ++i) + { + M3DataSegment * segment = & io_module->dataSegments [i]; + +_ (ReadLEB_u32 (& segment->memoryRegion, & i_bytes, i_end)); + + // Spec: MVP only supports memory index 0, and it has to exist + _throwif (m3Err_wasmMalformed, segment->memoryRegion != 0); + _throwif (m3Err_wasmMalformed, not (io_module->memoryImported or io_module->memoryDeclared)); + + segment->initExpr = i_bytes; +_ (Parse_InitExpr (io_module, & i_bytes, i_end)); + segment->initExprSize = (u32) (i_bytes - segment->initExpr); + + _throwif (m3Err_wasmMissingInitExpr, segment->initExprSize <= 1); + +_ (ReadLEB_u32 (& segment->size, & i_bytes, i_end)); + segment->data = i_bytes; m3log (parse, " segment [%u] memory: %u; expr-size: %d; size: %d", + i, segment->memoryRegion, segment->initExprSize, segment->size); + i_bytes += segment->size; + + _throwif("data segment underflow", i_bytes > i_end); + } + + _throwif (m3Err_wasmMalformed, i_bytes != i_end); // section size mismatch + + _catch: + + return result; +} + + +M3Result ParseSection_Memory (M3Module * io_module, bytes_t i_bytes, cbytes_t i_end) +{ + M3Result result = m3Err_none; + + // TODO: MVP; assert no memory imported + + u32 numMemories; +_ (ReadLEB_u32 (& numMemories, & i_bytes, i_end)); m3log (parse, "** Memory [%d]", numMemories); + + _throwif (m3Err_tooManyMemorySections, numMemories > 1); + + if (numMemories) + { +_ (ParseType_Memory (& io_module->memoryInfo, & i_bytes, i_end)); + io_module->memoryDeclared = true; + } + + _throwif (m3Err_wasmMalformed, i_bytes != i_end); // section size mismatch + + _catch: return result; +} + + +M3Result ParseSection_Global (M3Module * io_module, bytes_t i_bytes, cbytes_t i_end) +{ + M3Result result = m3Err_none; + + u32 numGlobals; +_ (ReadLEB_u32 (& numGlobals, & i_bytes, i_end)); m3log (parse, "** Global [%d]", numGlobals); + + _throwif("too many globals", numGlobals > d_m3MaxSaneGlobalsCount); + + for (u32 i = 0; i < numGlobals; ++i) + { + i8 waType; + u8 type, isMutable; + +_ (ReadLEB_i7 (& waType, & i_bytes, i_end)); +_ (NormalizeType (& type, waType)); +_ (ReadLEB_u7 (& isMutable, & i_bytes, i_end)); m3log (parse, " global: [%d] %s mutable: %d", i, c_waTypes [type], (u32) isMutable); + _throwif (m3Err_wasmMalformed, isMutable > 1); + + IM3Global global; +_ (Module_AddGlobal (io_module, & global, type, isMutable, false /* isImport */)); + + global->initExpr = i_bytes; +_ (Parse_InitExpr (io_module, & i_bytes, i_end)); + global->initExprSize = (u32) (i_bytes - global->initExpr); + + _throwif (m3Err_wasmMissingInitExpr, global->initExprSize <= 1); + } + + _throwif (m3Err_wasmMalformed, i_bytes != i_end); // section size mismatch + + _catch: return result; +} + + +M3Result ParseSection_Name (M3Module * io_module, bytes_t i_bytes, cbytes_t i_end) +{ + M3Result result = m3Err_none; + + cstr_t name; + + while (i_bytes < i_end) + { + u8 nameType; + u32 payloadLength; + +_ (ReadLEB_u7 (& nameType, & i_bytes, i_end)); +_ (ReadLEB_u32 (& payloadLength, & i_bytes, i_end)); + + bytes_t start = i_bytes; + if (nameType == 1) + { + u32 numNames; +_ (ReadLEB_u32 (& numNames, & i_bytes, i_end)); + + _throwif("too many names", numNames > d_m3MaxSaneFunctionsCount); + + for (u32 i = 0; i < numNames; ++i) + { + u32 index; +_ (ReadLEB_u32 (& index, & i_bytes, i_end)); +_ (Read_utf8 (& name, & i_bytes, i_end)); + + if (index < io_module->numFunctions) + { + IM3Function func = &(io_module->functions [index]); + if (func->numNames == 0) + { + func->names[0] = name; m3log (parse, " naming function%5d: %s", index, name); + func->numNames = 1; + name = NULL; // transfer ownership + } +// else m3log (parse, "prenamed: %s", io_module->functions [index].name); + } + + m3_Free (name); + } + } + + i_bytes = start + payloadLength; + } + + _catch: return result; +} + + +M3Result ParseSection_Custom (M3Module * io_module, bytes_t i_bytes, cbytes_t i_end) +{ + M3Result result; + + cstr_t name; +_ (Read_utf8 (& name, & i_bytes, i_end)); + m3log (parse, "** Custom: '%s'", name); + if (strcmp (name, "name") == 0) { +_ (ParseSection_Name(io_module, i_bytes, i_end)); + } else if (io_module->environment->customSectionHandler) { +_ (io_module->environment->customSectionHandler(io_module, name, i_bytes, i_end)); + } + + m3_Free (name); + + _catch: return result; +} + + +M3Result ParseModuleSection (M3Module * o_module, u8 i_sectionType, bytes_t i_bytes, u32 i_numBytes) +{ + M3Result result = m3Err_none; + + typedef M3Result (* M3Parser) (M3Module *, bytes_t, cbytes_t); + + static M3Parser s_parsers [] = + { + ParseSection_Custom, // 0 + ParseSection_Type, // 1 + ParseSection_Import, // 2 + ParseSection_Function, // 3 + ParseType_Table, // 4 + ParseSection_Memory, // 5 + ParseSection_Global, // 6 + ParseSection_Export, // 7 + ParseSection_Start, // 8 + ParseSection_Element, // 9 + ParseSection_Code, // 10 + ParseSection_Data, // 11 + NULL, // 12: TODO DataCount + }; + + M3Parser parser = NULL; + + if (i_sectionType <= 12) + parser = s_parsers [i_sectionType]; + + if (parser) + { + cbytes_t end = i_bytes + i_numBytes; + result = parser (o_module, i_bytes, end); + } + else + { + m3log (parse, " skipped section type: %d", (u32) i_sectionType); + } + + return result; +} + + +M3Result m3_ParseModule (IM3Environment i_environment, IM3Module * o_module, cbytes_t i_bytes, u32 i_numBytes) +{ + IM3Module module; m3log (parse, "load module: %d bytes", i_numBytes); +_try { + module = m3_AllocStruct (M3Module); + _throwifnull (module); + module->name = ".unnamed"; m3log (parse, "load module: %d bytes", i_numBytes); + module->startFunction = -1; + //module->hasWasmCodeCopy = false; + module->environment = i_environment; + + const u8 * pos = i_bytes; + const u8 * end = pos + i_numBytes; + + module->wasmStart = pos; + module->wasmEnd = end; + + u32 magic, version; +_ (Read_u32 (& magic, & pos, end)); +_ (Read_u32 (& version, & pos, end)); + + _throwif (m3Err_wasmMalformed, magic != 0x6d736100); + _throwif (m3Err_incompatibleWasmVersion, version != 1); + + static const u8 sectionsOrder[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 12, 10, 11, 0 }; // 0 is a placeholder + u8 expectedSection = 0; + + while (pos < end) + { + u8 section; +_ (ReadLEB_u7 (& section, & pos, end)); + + if (section != 0) { + // Ensure sections appear only once and in order + while (sectionsOrder[expectedSection++] != section) { + _throwif(m3Err_misorderedWasmSection, expectedSection >= 12); + } + } + + u32 sectionLength; +_ (ReadLEB_u32 (& sectionLength, & pos, end)); + _throwif(m3Err_wasmMalformed, pos + sectionLength > end); + +_ (ParseModuleSection (module, section, pos, sectionLength)); + + pos += sectionLength; + } + + // Spec: if a function section exists, a code section must also exist with + // matching count (and vice versa). ParseSection_Code checks the other + // direction; this covers the case where the code section is missing entirely. + if (module->numFunctions > module->numFuncImports) + { + IM3Function firstNonImport = & module->functions [module->numFuncImports]; + _throwif (m3Err_wasmMalformed, firstNonImport->wasm == NULL); + } + +} _catch: + + if (result) + { + m3_FreeModule (module); + module = NULL; + } + + * o_module = module; + + return result; +} diff --git a/driver/wasm3/m3_validate.c b/driver/wasm3/m3_validate.c new file mode 100644 index 0000000..48ecb9d --- /dev/null +++ b/driver/wasm3/m3_validate.c @@ -0,0 +1,881 @@ +// +// m3_validate.c +// +// Pre-pass WebAssembly bytecode validator. +// Implements the spec's type-checking algorithm with operand/control stacks. +// + +#include "m3_validate.h" +#include "m3_exception.h" +#include "m3_info.h" + +#if d_m3EnableValidation + +// Sentinel type for polymorphic (unknown) operands +#define c_valUnknown 0xFF + +// ---------- Control frame ---------- + +typedef struct { + m3opcode_t opcode; + u16 height; // operand stack height at block entry + u16 param_count; + u16 result_count; + IM3FuncType type; // block type (for params/results) + bool is_unreachable; +} ValCtrlFrame; + +// ---------- Validator context ---------- + +typedef struct { + bytes_t wasm; + bytes_t wasmEnd; + IM3Module module; + IM3Function function; + + u8 opd [d_m3ValStack]; + u16 opdTop; + + ValCtrlFrame ctrl [d_m3ValCtrlDepth]; + u16 ctrlTop; + + u8 localTypes [d_m3ValStack]; + u16 numLocals; +} ValCtx; + +// A memory op is only valid if the module defines or imports one +static bool v_has_memory (ValCtx * v) +{ + return v->module and (v->module->memoryImported or v->module->memoryDeclared); +} + +// Spec: the alignment immediate of a memory access must not be larger than the +// natural alignment of the operation. Natural alignment: 8-bit=0, 16-bit=1, +// 32-bit=2, 64-bit=3. +static u32 v_max_align (m3opcode_t opcode) +{ + switch (opcode) { + case 0x2c: case 0x2d: // i32.load8_s, i32.load8_u + case 0x30: case 0x31: // i64.load8_s, i64.load8_u + case 0x3a: // i32.store8 + case 0x3c: // i64.store8 + return 0; + case 0x2e: case 0x2f: // i32.load16_s, i32.load16_u + case 0x32: case 0x33: // i64.load16_s, i64.load16_u + case 0x3b: // i32.store16 + case 0x3d: // i64.store16 + return 1; + case 0x29: // i64.load + case 0x2b: // f64.load + case 0x37: // i64.store + case 0x39: // f64.store + return 3; + default: // 32-bit accesses, and a safe fallback + return 2; + } +} + +// ---------- Operand stack ---------- + +static M3Result v_push (ValCtx * v, u8 type) +{ + if (v->opdTop >= d_m3ValStack) + return m3Err_functionStackOverflow; + v->opd[v->opdTop++] = type; + return m3Err_none; +} + +static M3Result v_pop (ValCtx * v, u8 * o_type) +{ + ValCtrlFrame * f = &v->ctrl[v->ctrlTop - 1]; + if (v->opdTop == f->height) { + if (f->is_unreachable) { *o_type = c_valUnknown; return m3Err_none; } + return m3Err_functionStackUnderrun; + } + *o_type = v->opd[--v->opdTop]; + return m3Err_none; +} + +static M3Result v_pop_expect (ValCtx * v, u8 expect, u8 * o_actual) +{ + u8 actual; + M3Result r = v_pop(v, &actual); + if (r) return r; + if (expect != c_valUnknown && actual != c_valUnknown && actual != expect) + return m3Err_typeMismatch; + *o_actual = (actual == c_valUnknown) ? expect : actual; + return m3Err_none; +} + +// ---------- Control stack ---------- + +static M3Result v_push_ctrl (ValCtx * v, m3opcode_t op, IM3FuncType type) +{ + if (v->ctrlTop >= d_m3ValCtrlDepth) + return m3Err_functionStackOverflow; + ValCtrlFrame * f = &v->ctrl[v->ctrlTop++]; + f->opcode = op; + f->type = type; + f->param_count = type ? type->numArgs : 0; + f->result_count = type ? type->numRets : 0; + f->height = v->opdTop; + f->is_unreachable = false; + return m3Err_none; +} + +static M3Result v_pop_ctrl (ValCtx * v, ValCtrlFrame * o_frame) +{ + if (v->ctrlTop == 0) + return m3Err_wasmMalformed; + ValCtrlFrame * f = &v->ctrl[v->ctrlTop - 1]; + // pop result types + if (f->type) { + for (u16 i = f->result_count; i > 0; i--) { + u8 a; + M3Result r = v_pop_expect(v, f->type->types[i - 1], &a); + if (r) return r; + } + } + if (v->opdTop != f->height) + return m3Err_typeCountMismatch; + if (o_frame) *o_frame = *f; + v->ctrlTop--; + return m3Err_none; +} + +static void v_unreachable (ValCtx * v) +{ + ValCtrlFrame * f = &v->ctrl[v->ctrlTop - 1]; + v->opdTop = f->height; + f->is_unreachable = true; +} + +// Label types: loop -> params, block/if/else/func -> results +static u16 v_label_n (ValCtrlFrame * f) +{ + return (f->opcode == 0x03) ? f->param_count : f->result_count; +} + +static u8 v_label_t (ValCtrlFrame * f, u16 i) +{ + if (!f->type) return c_m3Type_none; + if (f->opcode == 0x03) + return f->type->types[f->type->numRets + i]; // params + return f->type->types[i]; // results +} + +// Pop label types for branch target +static M3Result v_pop_labels (ValCtx * v, ValCtrlFrame * tgt) +{ + u16 n = v_label_n(tgt); + for (u16 i = n; i > 0; i--) { + u8 a; + M3Result r = v_pop_expect(v, v_label_t(tgt, i - 1), &a); + if (r) return r; + } + return m3Err_none; +} + +// Push label types back +static M3Result v_push_labels (ValCtx * v, ValCtrlFrame * tgt) +{ + u16 n = v_label_n(tgt); + for (u16 i = 0; i < n; i++) { + M3Result r = v_push(v, v_label_t(tgt, i)); + if (r) return r; + } + return m3Err_none; +} + +// ---------- Block type resolution ---------- + +static M3Result v_read_blocktype (ValCtx * v, IM3FuncType * o_type) +{ + if (v->wasm >= v->wasmEnd) + return m3Err_wasmUnderrun; + + i64 type; + M3Result r = ReadLebSigned(&type, 33, &v->wasm, v->wasmEnd); + if (r) return r; + + if (type < 0) { + u8 valtype; + r = NormalizeType(&valtype, (i8)type); + if (r) return r; + IM3Environment env = v->module->environment; + *o_type = env->retFuncTypes[valtype]; + } else { + if ((u32)type >= v->module->numFuncTypes) return m3Err_wasmMalformed; + *o_type = v->module->funcTypes[(u32)type]; + } + return m3Err_none; +} + +// ---------- Convenience ---------- + +static M3Result v_unop (ValCtx * v, u8 in, u8 out) +{ + u8 a; M3Result r = v_pop_expect(v, in, &a); + if (r) return r; + return v_push(v, out); +} + +static M3Result v_binop (ValCtx * v, u8 t) +{ + u8 a; M3Result r; + r = v_pop_expect(v, t, &a); if (r) return r; + r = v_pop_expect(v, t, &a); if (r) return r; + return v_push(v, t); +} + +static M3Result v_relop (ValCtx * v, u8 t) +{ + u8 a; M3Result r; + r = v_pop_expect(v, t, &a); if (r) return r; + r = v_pop_expect(v, t, &a); if (r) return r; + return v_push(v, c_m3Type_i32); +} + +static M3Result v_testop (ValCtx * v, u8 t) +{ + return v_unop(v, t, c_m3Type_i32); +} + +static M3Result v_cvtop (ValCtx * v, u8 in, u8 out) +{ + return v_unop(v, in, out); +} + + +// ---------- Main validation loop ---------- + +static M3Result v_validate_body (ValCtx * v) +{ + M3Result r = m3Err_none; + u8 a; + + while (v->wasm < v->wasmEnd) + { + m3opcode_t opcode; + r = Read_opcode(&opcode, &v->wasm, v->wasmEnd); + if (r) return r; + + switch (opcode) + { + // ---- Control ---- + case 0x00: // unreachable + v_unreachable(v); + break; + + case 0x01: // nop + break; + + case 0x02: // block + case 0x03: // loop + case 0x04: // if + { + IM3FuncType bt; + r = v_read_blocktype(v, &bt); + if (r) return r; + if (opcode == 0x04) { + r = v_pop_expect(v, c_m3Type_i32, &a); + if (r) return r; + } + // Pop block params from caller stack + if (bt) { + for (u16 i = bt->numArgs; i > 0; i--) { + r = v_pop_expect(v, bt->types[bt->numRets + i - 1], &a); + if (r) return r; + } + } + r = v_push_ctrl(v, opcode, bt); + if (r) return r; + // Push params inside block + if (bt) { + for (u16 i = 0; i < bt->numArgs; i++) { + r = v_push(v, bt->types[bt->numRets + i]); + if (r) return r; + } + } + break; + } + + case 0x05: // else + { + ValCtrlFrame frame; + r = v_pop_ctrl(v, &frame); + if (r) return r; + if (frame.opcode != 0x04) + return m3Err_wasmMalformed; + r = v_push_ctrl(v, 0x05, frame.type); + if (r) return r; + if (frame.type) { + for (u16 i = 0; i < frame.type->numArgs; i++) { + r = v_push(v, frame.type->types[frame.type->numRets + i]); + if (r) return r; + } + } + break; + } + + case 0x0b: // end + { + ValCtrlFrame frame; + r = v_pop_ctrl(v, &frame); + if (r) return r; + // Push results + if (frame.type) { + for (u16 i = 0; i < frame.result_count; i++) { + r = v_push(v, frame.type->types[i]); + if (r) return r; + } + } + // If this was the outermost frame, we're done + if (v->ctrlTop == 0) + return m3Err_none; + break; + } + + case 0x0c: // br + { + u32 depth; + r = ReadLEB_u32(&depth, &v->wasm, v->wasmEnd); + if (r) return r; + if (depth >= v->ctrlTop) return m3Err_wasmMalformed; + ValCtrlFrame * tgt = &v->ctrl[v->ctrlTop - 1 - depth]; + r = v_pop_labels(v, tgt); + if (r) return r; + v_unreachable(v); + break; + } + + case 0x0d: // br_if + { + u32 depth; + r = ReadLEB_u32(&depth, &v->wasm, v->wasmEnd); + if (r) return r; + if (depth >= v->ctrlTop) return m3Err_wasmMalformed; + r = v_pop_expect(v, c_m3Type_i32, &a); + if (r) return r; + ValCtrlFrame * tgt = &v->ctrl[v->ctrlTop - 1 - depth]; + r = v_pop_labels(v, tgt); + if (r) return r; + r = v_push_labels(v, tgt); + if (r) return r; + break; + } + + case 0x0e: // br_table + { + u32 count; + r = ReadLEB_u32(&count, &v->wasm, v->wasmEnd); + if (r) return r; + u32 defDepth = 0; + u16 arity = 0; + // First pass: read all depths and validate arity + types match default + bytes_t savedPos = v->wasm; + // Read all targets to find the default (last one) + for (u32 i = 0; i <= count; i++) { + u32 d; + r = ReadLEB_u32(&d, &v->wasm, v->wasmEnd); + if (r) return r; + if (d >= v->ctrlTop) return m3Err_wasmMalformed; + if (i == count) defDepth = d; + } + // Now validate all labels match the default's types + ValCtrlFrame * defTgt = &v->ctrl[v->ctrlTop - 1 - defDepth]; + arity = v_label_n(defTgt); + v->wasm = savedPos; + for (u32 i = 0; i <= count; i++) { + u32 d; + r = ReadLEB_u32(&d, &v->wasm, v->wasmEnd); + if (r) return r; + ValCtrlFrame * t = &v->ctrl[v->ctrlTop - 1 - d]; + u16 n = v_label_n(t); + if (n != arity) return m3Err_typeCountMismatch; + // Spec: label types must be identical, not just same arity + for (u16 j = 0; j < n; j++) { + if (v_label_t(t, j) != v_label_t(defTgt, j)) + return m3Err_typeMismatch; + } + } + r = v_pop_expect(v, c_m3Type_i32, &a); + if (r) return r; + ValCtrlFrame * dt = &v->ctrl[v->ctrlTop - 1 - defDepth]; + r = v_pop_labels(v, dt); + if (r) return r; + v_unreachable(v); + break; + } + + case 0x0f: // return + { + IM3FuncType ft = v->function->funcType; + if (ft) { + for (u16 i = ft->numRets; i > 0; i--) { + r = v_pop_expect(v, ft->types[i - 1], &a); + if (r) return r; + } + } + v_unreachable(v); + break; + } + + // ---- Call ---- + case 0x10: // call + { + u32 idx; + r = ReadLEB_u32(&idx, &v->wasm, v->wasmEnd); + if (r) return r; + if (idx >= v->module->numFunctions) return m3Err_wasmMalformed; + IM3FuncType ft = v->module->functions[idx].funcType; + if (ft) { + for (u16 i = ft->numArgs; i > 0; i--) { + r = v_pop_expect(v, ft->types[ft->numRets + i - 1], &a); + if (r) return r; + } + for (u16 i = 0; i < ft->numRets; i++) { + r = v_push(v, ft->types[i]); + if (r) return r; + } + } + break; + } + + case 0x11: // call_indirect + { + u32 typeIdx; + r = ReadLEB_u32(&typeIdx, &v->wasm, v->wasmEnd); + if (r) return r; + u32 tableIdx; + r = ReadLEB_u32(&tableIdx, &v->wasm, v->wasmEnd); + if (r) return r; + if (typeIdx >= v->module->numFuncTypes) return m3Err_wasmMalformed; + // Spec: table must exist (MVP requires table index 0 and table must be defined) + if (tableIdx != 0) return m3Err_wasmMalformed; + if (!v->module->hasTable) return m3Err_wasmMalformed; + IM3FuncType ft = v->module->funcTypes[typeIdx]; + r = v_pop_expect(v, c_m3Type_i32, &a); // table index operand + if (r) return r; + if (ft) { + for (u16 i = ft->numArgs; i > 0; i--) { + r = v_pop_expect(v, ft->types[ft->numRets + i - 1], &a); + if (r) return r; + } + for (u16 i = 0; i < ft->numRets; i++) { + r = v_push(v, ft->types[i]); + if (r) return r; + } + } + break; + } + + // ---- Parametric ---- + case 0x1a: // drop + r = v_pop(v, &a); + if (r) return r; + break; + + case 0x1b: // select + { + r = v_pop_expect(v, c_m3Type_i32, &a); + if (r) return r; + u8 t2; + r = v_pop(v, &t2); + if (r) return r; + u8 t1; + r = v_pop_expect(v, t2, &t1); + if (r) return r; + r = v_push(v, (t2 == c_valUnknown) ? t1 : t2); + if (r) return r; + break; + } + + // ---- Variable ---- + case 0x20: // local.get + { + u32 idx; + r = ReadLEB_u32(&idx, &v->wasm, v->wasmEnd); + if (r) return r; + if (idx >= v->numLocals) return m3Err_wasmMalformed; + r = v_push(v, v->localTypes[idx]); + if (r) return r; + break; + } + + case 0x21: // local.set + { + u32 idx; + r = ReadLEB_u32(&idx, &v->wasm, v->wasmEnd); + if (r) return r; + if (idx >= v->numLocals) return m3Err_wasmMalformed; + r = v_pop_expect(v, v->localTypes[idx], &a); + if (r) return r; + break; + } + + case 0x22: // local.tee + { + u32 idx; + r = ReadLEB_u32(&idx, &v->wasm, v->wasmEnd); + if (r) return r; + if (idx >= v->numLocals) return m3Err_wasmMalformed; + r = v_pop_expect(v, v->localTypes[idx], &a); + if (r) return r; + r = v_push(v, v->localTypes[idx]); + if (r) return r; + break; + } + + case 0x23: // global.get + { + u32 idx; + r = ReadLEB_u32(&idx, &v->wasm, v->wasmEnd); + if (r) return r; + if (idx >= v->module->numGlobals) return m3Err_wasmMalformed; + r = v_push(v, v->module->globals[idx].type); + if (r) return r; + break; + } + + case 0x24: // global.set + { + u32 idx; + r = ReadLEB_u32(&idx, &v->wasm, v->wasmEnd); + if (r) return r; + if (idx >= v->module->numGlobals) return m3Err_wasmMalformed; + r = v_pop_expect(v, v->module->globals[idx].type, &a); + if (r) return r; + break; + } + + // ---- Memory load ---- + case 0x28: case 0x29: case 0x2a: case 0x2b: // i32/i64/f32/f64.load + case 0x2c: case 0x2d: case 0x2e: case 0x2f: // i32.load8/16 s/u + case 0x30: case 0x31: case 0x32: case 0x33: // i64.load8/16 s/u + case 0x34: case 0x35: // i64.load32 s/u + { + u32 align, offset; + r = ReadLEB_u32(&align, &v->wasm, v->wasmEnd); if (r) return r; + r = ReadLEB_u32(&offset, &v->wasm, v->wasmEnd); if (r) return r; + if (align > v_max_align(opcode)) return m3Err_wasmMalformed; + if (not v_has_memory(v)) return m3Err_wasmMalformed; + r = v_pop_expect(v, c_m3Type_i32, &a); if (r) return r; + u8 result; + if (opcode == 0x28) result = c_m3Type_i32; + else if (opcode == 0x29) result = c_m3Type_i64; + else if (opcode == 0x2a) result = c_m3Type_f32; + else if (opcode == 0x2b) result = c_m3Type_f64; + else if (opcode <= 0x2f) result = c_m3Type_i32; + else result = c_m3Type_i64; + r = v_push(v, result); + if (r) return r; + break; + } + + // ---- Memory store ---- + case 0x36: case 0x37: case 0x38: case 0x39: // i32/i64/f32/f64.store + case 0x3a: case 0x3b: // i32.store8/16 + case 0x3c: case 0x3d: case 0x3e: // i64.store8/16/32 + { + u32 align, offset; + r = ReadLEB_u32(&align, &v->wasm, v->wasmEnd); if (r) return r; + r = ReadLEB_u32(&offset, &v->wasm, v->wasmEnd); if (r) return r; + if (align > v_max_align(opcode)) return m3Err_wasmMalformed; + if (not v_has_memory(v)) return m3Err_wasmMalformed; + u8 valtype; + if (opcode == 0x36) valtype = c_m3Type_i32; + else if (opcode == 0x37) valtype = c_m3Type_i64; + else if (opcode == 0x38) valtype = c_m3Type_f32; + else if (opcode == 0x39) valtype = c_m3Type_f64; + else if (opcode <= 0x3b) valtype = c_m3Type_i32; + else valtype = c_m3Type_i64; + r = v_pop_expect(v, valtype, &a); if (r) return r; + r = v_pop_expect(v, c_m3Type_i32, &a); if (r) return r; + break; + } + + // ---- Memory size/grow ---- + case 0x3f: // memory.size + { + u32 memidx; + r = ReadLEB_u32(&memidx, &v->wasm, v->wasmEnd); if (r) return r; + if (memidx != 0 or not v_has_memory(v)) return m3Err_wasmMalformed; + r = v_push(v, c_m3Type_i32); if (r) return r; + break; + } + case 0x40: // memory.grow + { + u32 memidx; + r = ReadLEB_u32(&memidx, &v->wasm, v->wasmEnd); if (r) return r; + if (memidx != 0 or not v_has_memory(v)) return m3Err_wasmMalformed; + r = v_pop_expect(v, c_m3Type_i32, &a); if (r) return r; + r = v_push(v, c_m3Type_i32); if (r) return r; + break; + } + + // ---- Constants ---- + case 0x41: { // i32.const + i32 val; + r = ReadLEB_i32(&val, &v->wasm, v->wasmEnd); if (r) return r; + r = v_push(v, c_m3Type_i32); if (r) return r; + break; + } + case 0x42: { // i64.const + i64 val; + r = ReadLEB_i64(&val, &v->wasm, v->wasmEnd); if (r) return r; + r = v_push(v, c_m3Type_i64); if (r) return r; + break; + } + case 0x43: { // f32.const + if (v->wasm + 4 > v->wasmEnd) return m3Err_wasmUnderrun; + v->wasm += 4; + r = v_push(v, c_m3Type_f32); if (r) return r; + break; + } + case 0x44: { // f64.const + if (v->wasm + 8 > v->wasmEnd) return m3Err_wasmUnderrun; + v->wasm += 8; + r = v_push(v, c_m3Type_f64); if (r) return r; + break; + } + + + // ---- i32 comparison ---- + case 0x45: r = v_testop(v, c_m3Type_i32); break; // i32.eqz + case 0x46: case 0x47: case 0x48: case 0x49: case 0x4a: + case 0x4b: case 0x4c: case 0x4d: case 0x4e: case 0x4f: + r = v_relop(v, c_m3Type_i32); break; + + // ---- i64 comparison ---- + case 0x50: r = v_testop(v, c_m3Type_i64); break; // i64.eqz + case 0x51: case 0x52: case 0x53: case 0x54: case 0x55: + case 0x56: case 0x57: case 0x58: case 0x59: case 0x5a: + r = v_relop(v, c_m3Type_i64); break; + + // ---- f32 comparison ---- + case 0x5b: case 0x5c: case 0x5d: case 0x5e: case 0x5f: case 0x60: + r = v_relop(v, c_m3Type_f32); break; + + // ---- f64 comparison ---- + case 0x61: case 0x62: case 0x63: case 0x64: case 0x65: case 0x66: + r = v_relop(v, c_m3Type_f64); break; + + // ---- i32 unary ---- + case 0x67: case 0x68: case 0x69: // clz, ctz, popcnt + r = v_unop(v, c_m3Type_i32, c_m3Type_i32); break; + + // ---- i32 binary ---- + case 0x6a: case 0x6b: case 0x6c: case 0x6d: case 0x6e: case 0x6f: + case 0x70: case 0x71: case 0x72: case 0x73: case 0x74: case 0x75: + case 0x76: case 0x77: case 0x78: // add..rotr + r = v_binop(v, c_m3Type_i32); break; + + // ---- i64 unary ---- + case 0x79: case 0x7a: case 0x7b: // clz, ctz, popcnt + r = v_unop(v, c_m3Type_i64, c_m3Type_i64); break; + + // ---- i64 binary ---- + case 0x7c: case 0x7d: case 0x7e: case 0x7f: case 0x80: case 0x81: + case 0x82: case 0x83: case 0x84: case 0x85: case 0x86: case 0x87: + case 0x88: case 0x89: case 0x8a: // add..rotr + r = v_binop(v, c_m3Type_i64); break; + + // ---- f32 unary ---- + case 0x8b: case 0x8c: case 0x8d: case 0x8e: case 0x8f: + case 0x90: case 0x91: // abs, neg, ceil, floor, trunc, nearest, sqrt + r = v_unop(v, c_m3Type_f32, c_m3Type_f32); break; + + // ---- f32 binary ---- + case 0x92: case 0x93: case 0x94: case 0x95: case 0x96: + case 0x97: case 0x98: // add, sub, mul, div, min, max, copysign + r = v_binop(v, c_m3Type_f32); break; + + // ---- f64 unary ---- + case 0x99: case 0x9a: case 0x9b: case 0x9c: case 0x9d: + case 0x9e: case 0x9f: // abs, neg, ceil, floor, trunc, nearest, sqrt + r = v_unop(v, c_m3Type_f64, c_m3Type_f64); break; + + // ---- f64 binary ---- + case 0xa0: case 0xa1: case 0xa2: case 0xa3: case 0xa4: + case 0xa5: case 0xa6: // add, sub, mul, div, min, max, copysign + r = v_binop(v, c_m3Type_f64); break; + + // ---- Conversions ---- + case 0xa7: r = v_cvtop(v, c_m3Type_i64, c_m3Type_i32); break; // i32.wrap/i64 + case 0xa8: case 0xa9: // i32.trunc_s/f32, i32.trunc_u/f32 + r = v_cvtop(v, c_m3Type_f32, c_m3Type_i32); break; + case 0xaa: case 0xab: // i32.trunc_s/f64, i32.trunc_u/f64 + r = v_cvtop(v, c_m3Type_f64, c_m3Type_i32); break; + case 0xac: case 0xad: // i64.extend_s/i32, i64.extend_u/i32 + r = v_cvtop(v, c_m3Type_i32, c_m3Type_i64); break; + case 0xae: case 0xaf: // i64.trunc_s/f32, i64.trunc_u/f32 + r = v_cvtop(v, c_m3Type_f32, c_m3Type_i64); break; + case 0xb0: case 0xb1: // i64.trunc_s/f64, i64.trunc_u/f64 + r = v_cvtop(v, c_m3Type_f64, c_m3Type_i64); break; + case 0xb2: case 0xb3: // f32.convert_s/i32, f32.convert_u/i32 + r = v_cvtop(v, c_m3Type_i32, c_m3Type_f32); break; + case 0xb4: case 0xb5: // f32.convert_s/i64, f32.convert_u/i64 + r = v_cvtop(v, c_m3Type_i64, c_m3Type_f32); break; + case 0xb6: // f32.demote/f64 + r = v_cvtop(v, c_m3Type_f64, c_m3Type_f32); break; + case 0xb7: case 0xb8: // f64.convert_s/i32, f64.convert_u/i32 + r = v_cvtop(v, c_m3Type_i32, c_m3Type_f64); break; + case 0xb9: case 0xba: // f64.convert_s/i64, f64.convert_u/i64 + r = v_cvtop(v, c_m3Type_i64, c_m3Type_f64); break; + case 0xbb: // f64.promote/f32 + r = v_cvtop(v, c_m3Type_f32, c_m3Type_f64); break; + case 0xbc: // i32.reinterpret/f32 + r = v_cvtop(v, c_m3Type_f32, c_m3Type_i32); break; + case 0xbd: // i64.reinterpret/f64 + r = v_cvtop(v, c_m3Type_f64, c_m3Type_i64); break; + case 0xbe: // f32.reinterpret/i32 + r = v_cvtop(v, c_m3Type_i32, c_m3Type_f32); break; + case 0xbf: // f64.reinterpret/i64 + r = v_cvtop(v, c_m3Type_i64, c_m3Type_f64); break; + + // ---- Sign-extension (MVP post) ---- + case 0xc0: case 0xc1: // i32.extend8_s, i32.extend16_s + r = v_unop(v, c_m3Type_i32, c_m3Type_i32); break; + case 0xc2: case 0xc3: case 0xc4: // i64.extend8/16/32_s + r = v_unop(v, c_m3Type_i64, c_m3Type_i64); break; + + // ---- 0xFC prefix (saturating truncations + bulk memory) ---- + case 0xfc: + { + u32 sub; + r = ReadLEB_u32(&sub, &v->wasm, v->wasmEnd); + if (r) return r; + switch (sub) { + case 0x00: case 0x01: // i32.trunc_sat_f32_s/u + r = v_cvtop(v, c_m3Type_f32, c_m3Type_i32); break; + case 0x02: case 0x03: // i32.trunc_sat_f64_s/u + r = v_cvtop(v, c_m3Type_f64, c_m3Type_i32); break; + case 0x04: case 0x05: // i64.trunc_sat_f32_s/u + r = v_cvtop(v, c_m3Type_f32, c_m3Type_i64); break; + case 0x06: case 0x07: // i64.trunc_sat_f64_s/u + r = v_cvtop(v, c_m3Type_f64, c_m3Type_i64); break; + case 0x0a: // memory.copy + { + u32 dst, src; + r = ReadLEB_u32(&dst, &v->wasm, v->wasmEnd); if (r) return r; + r = ReadLEB_u32(&src, &v->wasm, v->wasmEnd); if (r) return r; + r = v_pop_expect(v, c_m3Type_i32, &a); if (r) return r; // n + r = v_pop_expect(v, c_m3Type_i32, &a); if (r) return r; // src + r = v_pop_expect(v, c_m3Type_i32, &a); if (r) return r; // dst + break; + } + case 0x0b: // memory.fill + { + u32 memidx; + r = ReadLEB_u32(&memidx, &v->wasm, v->wasmEnd); if (r) return r; + r = v_pop_expect(v, c_m3Type_i32, &a); if (r) return r; // n + r = v_pop_expect(v, c_m3Type_i32, &a); if (r) return r; // val + r = v_pop_expect(v, c_m3Type_i32, &a); if (r) return r; // dst + break; + } + default: + // Unknown FC sub-opcode: skip validation (allow forward compat) + break; + } + break; + } + + default: + // Unknown opcode - skip rather than fail for forward compat + // (the compiler will reject truly unsupported ops later) + break; + + } // switch + + if (r) return r; + + } // while + + // If we ran out of bytes without hitting the final end + return m3Err_wasmMalformed; +} + +// ---------- Public entry point ---------- + +M3Result ValidateFunction (IM3Function i_function) +{ + if (!i_function->wasm) return m3Err_none; + + IM3FuncType funcType = i_function->funcType; + IM3Module module = i_function->module; + + // Set up context on stack + ValCtx v; + memset(&v, 0, sizeof(v)); + v.module = module; + v.function = i_function; + v.wasm = i_function->wasm; + v.wasmEnd = i_function->wasmEnd; + + // Skip code size LEB + u32 size; + M3Result r = ReadLEB_u32(&size, &v.wasm, v.wasmEnd); + if (r) return r; + + // Parse locals + u32 numLocalBlocks; + r = ReadLEB_u32(&numLocalBlocks, &v.wasm, v.wasmEnd); + if (r) return r; + + // First: params. Running out of room has to be an error, not a truncation: + // a short localTypes would make later local.get indices read as unknown + u16 numParams = funcType ? funcType->numArgs : 0; + if (numParams > d_m3ValStack) return m3Err_functionStackOverflow; + for (u16 i = 0; i < numParams; i++) { + v.localTypes[v.numLocals++] = funcType->types[funcType->numRets + i]; + } + + // Then: declared locals + for (u32 b = 0; b < numLocalBlocks; b++) { + u32 count; + r = ReadLEB_u32(&count, &v.wasm, v.wasmEnd); + if (r) return r; + i8 waType; + r = ReadLEB_i7(&waType, &v.wasm, v.wasmEnd); + if (r) return r; + u8 normalized; + r = NormalizeType(&normalized, waType); + if (r) return r; + if (count > (u32) (d_m3ValStack - v.numLocals)) return m3Err_functionStackOverflow; + for (u32 c = 0; c < count; c++) { + v.localTypes[v.numLocals++] = normalized; + } + } + + // Push the function-level control frame + r = v_push_ctrl(&v, 0x00, funcType); // opcode 0x00 marks function frame + if (r) return r; + + // Push params onto operand stack (they're part of the function body's initial stack) + // Actually per the spec, locals are indexed but not on the operand stack. + // The function frame's params are NOT pushed to the operand stack. + // Only block params would be pushed (and for the function frame there are no block params + // since the function body's "block type" has results = function returns, params = 0). + // The function frame's label_types = results (since it's not a loop). + + // Validate the body + r = v_validate_body(&v); + if (r) return r; + + // After validation, control stack should be empty + if (v.ctrlTop != 0) + return m3Err_wasmMalformed; + + return m3Err_none; +} + +#else // !d_m3EnableValidation + +M3Result ValidateFunction (IM3Function i_function) +{ + (void)i_function; + return m3Err_none; +} + +#endif // d_m3EnableValidation diff --git a/driver/wasm3/m3_validate.h b/driver/wasm3/m3_validate.h new file mode 100644 index 0000000..6348c37 --- /dev/null +++ b/driver/wasm3/m3_validate.h @@ -0,0 +1,25 @@ +// +// m3_validate.h +// +// Pre-pass WebAssembly bytecode validator using the spec's type-checking algorithm. +// Runs before compilation to catch type errors early. +// + +#ifndef m3_validate_h +#define m3_validate_h + +#include "m3_core.h" +#include "m3_compile.h" +#include "m3_env.h" + +d_m3BeginExternC + +// Validate a function's bytecode before compilation. +// Performs full type-checking per the WebAssembly spec algorithm: +// operand type stack + control stack with polymorphic handling. +// Returns m3Err_none on success or a validation error. +M3Result ValidateFunction (IM3Function i_function); + +d_m3EndExternC + +#endif // m3_validate_h diff --git a/driver/wasm3/wasm3.h b/driver/wasm3/wasm3.h new file mode 100644 index 0000000..7c85661 --- /dev/null +++ b/driver/wasm3/wasm3.h @@ -0,0 +1,391 @@ +// +// Wasm3, high performance WebAssembly interpreter +// +// Copyright © 2019 Steven Massey, Volodymyr Shymanskyy. +// All rights reserved. +// + +#ifndef wasm3_h +#define wasm3_h + +#define M3_VERSION_MAJOR 0 +#define M3_VERSION_MINOR 5 +#define M3_VERSION_REV 2 +#define M3_VERSION "0.5.2" + +#include +#include +#include +#include +#include +#include + +#include "wasm3_defs.h" + +// Constants +#define M3_BACKTRACE_TRUNCATED (IM3BacktraceFrame)(SIZE_MAX) + +#if defined(__cplusplus) +extern "C" { +#endif + +typedef const char * M3Result; + +struct M3Environment; typedef struct M3Environment * IM3Environment; +struct M3Runtime; typedef struct M3Runtime * IM3Runtime; +struct M3Module; typedef struct M3Module * IM3Module; +struct M3Function; typedef struct M3Function * IM3Function; +struct M3Global; typedef struct M3Global * IM3Global; + +typedef struct M3ErrorInfo +{ + M3Result result; + + IM3Runtime runtime; + IM3Module module; + IM3Function function; + + const char * file; + uint32_t line; + + const char * message; +} M3ErrorInfo; + +typedef struct M3BacktraceFrame +{ + uint32_t moduleOffset; + IM3Function function; + + struct M3BacktraceFrame * next; +} +M3BacktraceFrame, * IM3BacktraceFrame; + +typedef struct M3BacktraceInfo +{ + IM3BacktraceFrame frames; + IM3BacktraceFrame lastFrame; // can be M3_BACKTRACE_TRUNCATED +} +M3BacktraceInfo, * IM3BacktraceInfo; + + +typedef enum M3ValueType +{ + c_m3Type_none = 0, + c_m3Type_i32 = 1, + c_m3Type_i64 = 2, + c_m3Type_f32 = 3, + c_m3Type_f64 = 4, + + // Opaque 16-byte slot used purely so wasm3 can PARSE modules + // whose function signatures or local-variable declarations + // mention v128 (the SIMD value type, wasm-encoded as 0x7B). + // Actual v128 OPCODES still error at compile-time with + // m3Err_unknownOpcode - we only avoid the parse-time + // m3Err_invalidTypeId rejection. LLVM's auto-vectorizer emits + // unused v128 locals into many `+simd128` modules even when no + // SIMD op executes; without this slot wasm3 rejects every such + // module before it ever sees a function body. + c_m3Type_v128 = 5, + + c_m3Type_unknown +} M3ValueType; + +typedef struct M3TaggedValue +{ + M3ValueType type; + union M3ValueUnion + { + uint32_t i32; + uint64_t i64; + float f32; + double f64; + } value; +} +M3TaggedValue, * IM3TaggedValue; + +typedef struct M3ImportInfo +{ + const char * moduleUtf8; + const char * fieldUtf8; +} +M3ImportInfo, * IM3ImportInfo; + + +typedef struct M3ImportContext +{ + void * userdata; + IM3Function function; +} +M3ImportContext, * IM3ImportContext; + +// ------------------------------------------------------------------------------------------------------------------------------- +// error codes +// ------------------------------------------------------------------------------------------------------------------------------- + +# if defined(M3_IMPLEMENT_ERROR_STRINGS) +# if defined(__cplusplus) +# define d_m3ErrorConst(LABEL, STRING) extern const M3Result m3Err_##LABEL = { STRING }; +# else +# define d_m3ErrorConst(LABEL, STRING) const M3Result m3Err_##LABEL = { STRING }; +# endif +# else +# define d_m3ErrorConst(LABEL, STRING) extern const M3Result m3Err_##LABEL; +# endif + +// ------------------------------------------------------------------------------------------------------------------------------- + +d_m3ErrorConst (none, NULL) + +// general errors +d_m3ErrorConst (mallocFailed, "memory allocation failed") + +// parse errors +d_m3ErrorConst (incompatibleWasmVersion, "incompatible Wasm binary version") +d_m3ErrorConst (wasmMalformed, "malformed Wasm binary") +d_m3ErrorConst (misorderedWasmSection, "out of order Wasm section") +d_m3ErrorConst (wasmUnderrun, "underrun while parsing Wasm binary") +d_m3ErrorConst (wasmOverrun, "overrun while parsing Wasm binary") +d_m3ErrorConst (wasmMissingInitExpr, "missing init_expr in Wasm binary") +d_m3ErrorConst (lebOverflow, "LEB encoded value overflow") +d_m3ErrorConst (missingUTF8, "invalid length UTF-8 string") +d_m3ErrorConst (wasmSectionUnderrun, "section underrun while parsing Wasm binary") +d_m3ErrorConst (wasmSectionOverrun, "section overrun while parsing Wasm binary") +d_m3ErrorConst (invalidTypeId, "unknown value_type") +d_m3ErrorConst (tooManyMemorySections, "only one memory per module is supported") +d_m3ErrorConst (tooManyArgsRets, "too many arguments or return values") + +// link errors +d_m3ErrorConst (moduleNotLinked, "attempting to use module that is not loaded") +d_m3ErrorConst (moduleAlreadyLinked, "attempting to bind module to multiple runtimes") +d_m3ErrorConst (functionLookupFailed, "function lookup failed") +d_m3ErrorConst (functionImportMissing, "missing imported function") + +d_m3ErrorConst (malformedFunctionSignature, "malformed function signature") + +// compilation errors +d_m3ErrorConst (noCompiler, "no compiler found for opcode") +d_m3ErrorConst (unknownOpcode, "unknown opcode") +d_m3ErrorConst (restrictedOpcode, "restricted opcode") +d_m3ErrorConst (functionStackOverflow, "compiling function overran its stack height limit") +d_m3ErrorConst (functionStackUnderrun, "compiling function underran the stack") +d_m3ErrorConst (mallocFailedCodePage, "memory allocation failed when acquiring a new M3 code page") +d_m3ErrorConst (settingImmutableGlobal, "attempting to set an immutable global") +d_m3ErrorConst (typeMismatch, "incorrect type on stack") +d_m3ErrorConst (typeCountMismatch, "incorrect value count on stack") + +// runtime errors +d_m3ErrorConst (missingCompiledCode, "function is missing compiled m3 code") +d_m3ErrorConst (wasmMemoryOverflow, "runtime ran out of memory") +d_m3ErrorConst (globalMemoryNotAllocated, "global memory is missing from a module") +d_m3ErrorConst (globaIndexOutOfBounds, "global index is too large") +d_m3ErrorConst (argumentCountMismatch, "argument count mismatch") +d_m3ErrorConst (argumentTypeMismatch, "argument type mismatch") +d_m3ErrorConst (globalLookupFailed, "global lookup failed") +d_m3ErrorConst (globalTypeMismatch, "global type mismatch") +d_m3ErrorConst (globalNotMutable, "global is not mutable") + +// traps +d_m3ErrorConst (trapOutOfBoundsMemoryAccess, "[trap] out of bounds memory access") +d_m3ErrorConst (trapDivisionByZero, "[trap] integer divide by zero") +d_m3ErrorConst (trapIntegerOverflow, "[trap] integer overflow") +d_m3ErrorConst (trapIntegerConversion, "[trap] invalid conversion to integer") +d_m3ErrorConst (trapIndirectCallTypeMismatch, "[trap] indirect call type mismatch") +d_m3ErrorConst (trapTableIndexOutOfRange, "[trap] undefined element") +d_m3ErrorConst (trapTableElementIsNull, "[trap] null table element") +d_m3ErrorConst (trapExit, "[trap] program called exit") +d_m3ErrorConst (trapAbort, "[trap] program called abort") +d_m3ErrorConst (trapUnreachable, "[trap] unreachable executed") +d_m3ErrorConst (trapStackOverflow, "[trap] stack overflow") + + +//------------------------------------------------------------------------------------------------------------------------------- +// configuration, can be found in m3_config.h, m3_config_platforms.h, m3_core.h) +//------------------------------------------------------------------------------------------------------------------------------- + +//------------------------------------------------------------------------------------------------------------------------------- +// global environment than can host multiple runtimes +//------------------------------------------------------------------------------------------------------------------------------- + IM3Environment m3_NewEnvironment (void); + + void m3_FreeEnvironment (IM3Environment i_environment); + + typedef M3Result (* M3SectionHandler) (IM3Module i_module, const char* name, const uint8_t * start, const uint8_t * end); + + void m3_SetCustomSectionHandler (IM3Environment i_environment, M3SectionHandler i_handler); + + +//------------------------------------------------------------------------------------------------------------------------------- +// execution context +//------------------------------------------------------------------------------------------------------------------------------- + + IM3Runtime m3_NewRuntime (IM3Environment io_environment, + uint32_t i_stackSizeInBytes, + void * i_userdata); + + void m3_FreeRuntime (IM3Runtime i_runtime); + + // Wasm currently only supports one memory region. i_memoryIndex should be zero. + uint8_t * m3_GetMemory (IM3Runtime i_runtime, + uint32_t * o_memorySizeInBytes, + uint32_t i_memoryIndex); + + // This is used internally by Raw Function helpers + uint32_t m3_GetMemorySize (IM3Runtime i_runtime); + + void * m3_GetUserData (IM3Runtime i_runtime); + + +//------------------------------------------------------------------------------------------------------------------------------- +// modules +//------------------------------------------------------------------------------------------------------------------------------- + + // i_wasmBytes data must be persistent during the lifetime of the module + M3Result m3_ParseModule (IM3Environment i_environment, + IM3Module * o_module, + const uint8_t * const i_wasmBytes, + uint32_t i_numWasmBytes); + + // Only modules not loaded into a M3Runtime need to be freed. A module is considered unloaded if + // a. m3_LoadModule has not yet been called on that module. Or, + // b. m3_LoadModule returned a result. + void m3_FreeModule (IM3Module i_module); + + // LoadModule transfers ownership of a module to the runtime. Do not free modules once successfully loaded into the runtime + M3Result m3_LoadModule (IM3Runtime io_runtime, IM3Module io_module); + + // Optional, compiles all functions in the module + M3Result m3_CompileModule (IM3Module io_module); + + // Calling m3_RunStart is optional + M3Result m3_RunStart (IM3Module i_module); + + // Arguments and return values are passed in and out through the stack pointer _sp. + // Placeholder return value slots are first and arguments after. So, the first argument is at _sp [numReturns] + // Return values should be written into _sp [0] to _sp [num_returns - 1] + typedef const void * (* M3RawCall) (IM3Runtime runtime, IM3ImportContext _ctx, uint64_t * _sp, void * _mem); + + M3Result m3_LinkRawFunction (IM3Module io_module, + const char * const i_moduleName, + const char * const i_functionName, + const char * const i_signature, + M3RawCall i_function); + + M3Result m3_LinkRawFunctionEx (IM3Module io_module, + const char * const i_moduleName, + const char * const i_functionName, + const char * const i_signature, + M3RawCall i_function, + const void * i_userdata); + + const char* m3_GetModuleName (IM3Module i_module); + void m3_SetModuleName (IM3Module i_module, const char* name); + IM3Runtime m3_GetModuleRuntime (IM3Module i_module); + +//------------------------------------------------------------------------------------------------------------------------------- +// globals +//------------------------------------------------------------------------------------------------------------------------------- + IM3Global m3_FindGlobal (IM3Module io_module, + const char * const i_globalName); + + M3Result m3_GetGlobal (IM3Global i_global, + IM3TaggedValue o_value); + + M3Result m3_SetGlobal (IM3Global i_global, + const IM3TaggedValue i_value); + + M3ValueType m3_GetGlobalType (IM3Global i_global); + +//------------------------------------------------------------------------------------------------------------------------------- +// functions +//------------------------------------------------------------------------------------------------------------------------------- + M3Result m3_Yield (void); + + // o_function is valid during the lifetime of the originating runtime + M3Result m3_FindFunction (IM3Function * o_function, + IM3Runtime i_runtime, + const char * const i_functionName); + M3Result m3_GetTableFunction (IM3Function * o_function, + IM3Module i_module, + uint32_t i_index); + + uint32_t m3_GetArgCount (IM3Function i_function); + uint32_t m3_GetRetCount (IM3Function i_function); + M3ValueType m3_GetArgType (IM3Function i_function, uint32_t i_index); + M3ValueType m3_GetRetType (IM3Function i_function, uint32_t i_index); + + M3Result m3_CallV (IM3Function i_function, ...); + M3Result m3_CallVL (IM3Function i_function, va_list i_args); + M3Result m3_Call (IM3Function i_function, uint32_t i_argc, const void * i_argptrs[]); + M3Result m3_CallArgv (IM3Function i_function, uint32_t i_argc, const char * i_argv[]); + + M3Result m3_GetResultsV (IM3Function i_function, ...); + M3Result m3_GetResultsVL (IM3Function i_function, va_list o_rets); + M3Result m3_GetResults (IM3Function i_function, uint32_t i_retc, const void * o_retptrs[]); + + + void m3_GetErrorInfo (IM3Runtime i_runtime, M3ErrorInfo* o_info); + void m3_ResetErrorInfo (IM3Runtime i_runtime); + + const char* m3_GetFunctionName (IM3Function i_function); + IM3Module m3_GetFunctionModule (IM3Function i_function); + +//------------------------------------------------------------------------------------------------------------------------------- +// debug info +//------------------------------------------------------------------------------------------------------------------------------- + + void m3_PrintRuntimeInfo (IM3Runtime i_runtime); + void m3_PrintM3Info (void); + void m3_PrintProfilerInfo (void); + + // The runtime owns the backtrace, do not free the backtrace you obtain. Returns NULL if there's no backtrace. + IM3BacktraceInfo m3_GetBacktrace (IM3Runtime i_runtime); + +//------------------------------------------------------------------------------------------------------------------------------- +// raw function definition helpers +//------------------------------------------------------------------------------------------------------------------------------- + +# define m3ApiOffsetToPtr(offset) (void*)((uint8_t*)_mem + (uint32_t)(offset)) +# define m3ApiPtrToOffset(ptr) (uint32_t)((uint8_t*)ptr - (uint8_t*)_mem) + +# define m3ApiReturnType(TYPE) TYPE* raw_return = ((TYPE*) (_sp++)); +# define m3ApiMultiValueReturnType(TYPE, NAME) TYPE* NAME = ((TYPE*) (_sp++)); +# define m3ApiGetArg(TYPE, NAME) TYPE NAME = \ + (sizeof(TYPE) >= sizeof(uint32_t)) ? \ + (*((TYPE *)(_sp++))) : \ + ((TYPE)(*((uint32_t *)(_sp++)))); +# define m3ApiGetArgMem(TYPE, NAME) TYPE NAME = (TYPE)m3ApiOffsetToPtr(* ((uint32_t *) (_sp++))); + +# define m3ApiIsNullPtr(addr) ((void*)(addr) <= _mem) +# define m3ApiCheckMem(addr, len) { if (M3_UNLIKELY(((void*)(addr) < _mem) || ((uint64_t)(uintptr_t)(addr) + (len)) > ((uint64_t)(uintptr_t)(_mem)+m3_GetMemorySize(runtime)))) m3ApiTrap(m3Err_trapOutOfBoundsMemoryAccess); } + +# define m3ApiRawFunction(NAME) const void * NAME (IM3Runtime runtime, IM3ImportContext _ctx, uint64_t * _sp, void * _mem) +# define m3ApiReturn(VALUE) { *raw_return = (VALUE); return m3Err_none;} +# define m3ApiMultiValueReturn(NAME, VALUE) { *NAME = (VALUE); } +# define m3ApiTrap(VALUE) { return VALUE; } +# define m3ApiSuccess() { return m3Err_none; } + +# if defined(M3_BIG_ENDIAN) +# define m3ApiReadMem8(ptr) (* (uint8_t *)(ptr)) +# define m3ApiReadMem16(ptr) m3_bswap16((* (uint16_t *)(ptr))) +# define m3ApiReadMem32(ptr) m3_bswap32((* (uint32_t *)(ptr))) +# define m3ApiReadMem64(ptr) m3_bswap64((* (uint64_t *)(ptr))) +# define m3ApiWriteMem8(ptr, val) { * (uint8_t *)(ptr) = (val); } +# define m3ApiWriteMem16(ptr, val) { * (uint16_t *)(ptr) = m3_bswap16((val)); } +# define m3ApiWriteMem32(ptr, val) { * (uint32_t *)(ptr) = m3_bswap32((val)); } +# define m3ApiWriteMem64(ptr, val) { * (uint64_t *)(ptr) = m3_bswap64((val)); } +# else +# define m3ApiReadMem8(ptr) (* (uint8_t *)(ptr)) +# define m3ApiReadMem16(ptr) (* (uint16_t *)(ptr)) +# define m3ApiReadMem32(ptr) (* (uint32_t *)(ptr)) +# define m3ApiReadMem64(ptr) (* (uint64_t *)(ptr)) +# define m3ApiWriteMem8(ptr, val) { * (uint8_t *)(ptr) = (val); } +# define m3ApiWriteMem16(ptr, val) { * (uint16_t *)(ptr) = (val); } +# define m3ApiWriteMem32(ptr, val) { * (uint32_t *)(ptr) = (val); } +# define m3ApiWriteMem64(ptr, val) { * (uint64_t *)(ptr) = (val); } +# endif + +#if defined(__cplusplus) +} +#endif + +#endif // wasm3_h diff --git a/driver/wasm3/wasm3_defs.h b/driver/wasm3/wasm3_defs.h new file mode 100644 index 0000000..7e13525 --- /dev/null +++ b/driver/wasm3/wasm3_defs.h @@ -0,0 +1,293 @@ +// +// wasm3_defs.h +// +// Created by Volodymyr Shymanskyy on 11/20/19. +// Copyright © 2019 Volodymyr Shymanskyy. All rights reserved. +// + +#ifndef wasm3_defs_h +#define wasm3_defs_h + +#define M3_STR__(x) #x +#define M3_STR(x) M3_STR__(x) + +#define M3_CONCAT__(a,b) a##b +#define M3_CONCAT(a,b) M3_CONCAT__(a,b) + +/* + * Detect compiler + */ + +# if defined(__clang__) +# define M3_COMPILER_CLANG 1 +# elif defined(__INTEL_COMPILER) +# define M3_COMPILER_ICC 1 +# elif defined(__GNUC__) || defined(__GNUG__) +# define M3_COMPILER_GCC 1 +# elif defined(_MSC_VER) +# define M3_COMPILER_MSVC 1 +# else +# warning "Compiler not detected" +# endif + +# if defined(M3_COMPILER_CLANG) +# if defined(WIN32) +# define M3_COMPILER_VER __VERSION__ " for Windows" +# else +# define M3_COMPILER_VER __VERSION__ +# endif +# elif defined(M3_COMPILER_GCC) +# define M3_COMPILER_VER "GCC " __VERSION__ +# elif defined(M3_COMPILER_ICC) +# define M3_COMPILER_VER __VERSION__ +# elif defined(M3_COMPILER_MSVC) +# define M3_COMPILER_VER "MSVC " M3_STR(_MSC_VER) +# else +# define M3_COMPILER_VER "unknown" +# endif + +# ifdef __has_feature +# define M3_COMPILER_HAS_FEATURE(x) __has_feature(x) +# else +# define M3_COMPILER_HAS_FEATURE(x) 0 +# endif + +# ifdef __has_builtin +# define M3_COMPILER_HAS_BUILTIN(x) __has_builtin(x) +# else +# define M3_COMPILER_HAS_BUILTIN(x) 0 +# endif + +# ifdef __has_attribute +# define M3_COMPILER_HAS_ATTRIBUTE(x) __has_attribute(x) +# else +# define M3_COMPILER_HAS_ATTRIBUTE(x) 0 +# endif + +/* + * Detect endianness + */ + +# if defined(M3_COMPILER_MSVC) +# define M3_LITTLE_ENDIAN +# elif defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ +# define M3_LITTLE_ENDIAN +# elif defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ +# define M3_BIG_ENDIAN +# else +# error "Byte order not detected" +# endif + +/* + * Detect platform + */ + +# if defined(M3_COMPILER_CLANG) || defined(M3_COMPILER_GCC) || defined(M3_COMPILER_ICC) +# if defined(__wasm__) +# define M3_ARCH "wasm" + +# elif defined(__x86_64__) +# define M3_ARCH "x86_64" + +# elif defined(__i386__) +# define M3_ARCH "i386" + +# elif defined(__aarch64__) +# define M3_ARCH "arm64-v8a" + +# elif defined(__arm__) +# if defined(__ARM_ARCH_7A__) +# if defined(__ARM_NEON__) +# if defined(__ARM_PCS_VFP) +# define M3_ARCH "arm-v7a/NEON hard-float" +# else +# define M3_ARCH "arm-v7a/NEON" +# endif +# else +# if defined(__ARM_PCS_VFP) +# define M3_ARCH "arm-v7a hard-float" +# else +# define M3_ARCH "arm-v7a" +# endif +# endif +# else +# define M3_ARCH "arm" +# endif + +# elif defined(__riscv) +# if defined(__riscv_32e) +# define _M3_ARCH_RV "rv32e" +# elif __riscv_xlen == 128 +# define _M3_ARCH_RV "rv128i" +# elif __riscv_xlen == 64 +# define _M3_ARCH_RV "rv64i" +# elif __riscv_xlen == 32 +# define _M3_ARCH_RV "rv32i" +# endif +# if defined(__riscv_muldiv) +# define _M3_ARCH_RV_M _M3_ARCH_RV "m" +# else +# define _M3_ARCH_RV_M _M3_ARCH_RV +# endif +# if defined(__riscv_atomic) +# define _M3_ARCH_RV_A _M3_ARCH_RV_M "a" +# else +# define _M3_ARCH_RV_A _M3_ARCH_RV_M +# endif +# if defined(__riscv_flen) +# define _M3_ARCH_RV_F _M3_ARCH_RV_A "f" +# else +# define _M3_ARCH_RV_F _M3_ARCH_RV_A +# endif +# if defined(__riscv_flen) && __riscv_flen >= 64 +# define _M3_ARCH_RV_D _M3_ARCH_RV_F "d" +# else +# define _M3_ARCH_RV_D _M3_ARCH_RV_F +# endif +# if defined(__riscv_compressed) +# define _M3_ARCH_RV_C _M3_ARCH_RV_D "c" +# else +# define _M3_ARCH_RV_C _M3_ARCH_RV_D +# endif +# define M3_ARCH _M3_ARCH_RV_C + +# elif defined(__mips__) +# if defined(__MIPSEB__) && defined(__mips64) +# define M3_ARCH "mips64 " _MIPS_ARCH +# elif defined(__MIPSEL__) && defined(__mips64) +# define M3_ARCH "mips64el " _MIPS_ARCH +# elif defined(__MIPSEB__) +# define M3_ARCH "mips " _MIPS_ARCH +# elif defined(__MIPSEL__) +# define M3_ARCH "mipsel " _MIPS_ARCH +# endif + +# elif defined(__PPC__) +# if defined(__PPC64__) && defined(__LITTLE_ENDIAN__) +# define M3_ARCH "ppc64le" +# elif defined(__PPC64__) +# define M3_ARCH "ppc64" +# else +# define M3_ARCH "ppc" +# endif + +# elif defined(__sparc__) +# if defined(__arch64__) +# define M3_ARCH "sparc64" +# else +# define M3_ARCH "sparc" +# endif + +# elif defined(__s390x__) +# define M3_ARCH "s390x" + +# elif defined(__alpha__) +# define M3_ARCH "alpha" + +# elif defined(__m68k__) +# define M3_ARCH "m68k" + +# elif defined(__xtensa__) +# define M3_ARCH "xtensa" + +# elif defined(__arc__) +# define M3_ARCH "arc32" + +# elif defined(__AVR__) +# define M3_ARCH "avr" +# endif +# endif + +# if defined(M3_COMPILER_MSVC) +# if defined(_M_X64) +# define M3_ARCH "x86_64" +# elif defined(_M_IX86) +# define M3_ARCH "i386" +# elif defined(_M_ARM64) +# define M3_ARCH "arm64" +# elif defined(_M_ARM) +# define M3_ARCH "arm" +# endif +# endif + +# if !defined(M3_ARCH) +# warning "Architecture not detected" +# define M3_ARCH "unknown" +# endif + +/* + * Byte swapping (for Big-Endian systems only) + */ + +# if defined(M3_COMPILER_MSVC) +# define m3_bswap16(x) _byteswap_ushort((x)) +# define m3_bswap32(x) _byteswap_ulong((x)) +# define m3_bswap64(x) _byteswap_uint64((x)) +# elif defined(M3_COMPILER_GCC) && ((__GNUC__ > 4) || (__GNUC__ == 4 && __GNUC_MINOR__ >= 8)) +// __builtin_bswap32/64 added in gcc 4.3, __builtin_bswap16 added in gcc 4.8 +# define m3_bswap16(x) __builtin_bswap16((x)) +# define m3_bswap32(x) __builtin_bswap32((x)) +# define m3_bswap64(x) __builtin_bswap64((x)) +# elif defined(M3_COMPILER_CLANG) && M3_COMPILER_HAS_BUILTIN(__builtin_bswap16) +# define m3_bswap16(x) __builtin_bswap16((x)) +# define m3_bswap32(x) __builtin_bswap32((x)) +# define m3_bswap64(x) __builtin_bswap64((x)) +# elif defined(M3_COMPILER_ICC) +# define m3_bswap16(x) __builtin_bswap16((x)) +# define m3_bswap32(x) __builtin_bswap32((x)) +# define m3_bswap64(x) __builtin_bswap64((x)) +# else +# ifdef __linux__ +# include +# else +# include +# endif +# if defined(__bswap_16) +# define m3_bswap16(x) __bswap_16((x)) +# define m3_bswap32(x) __bswap_32((x)) +# define m3_bswap64(x) __bswap_64((x)) +# else +# warning "Using naive (probably slow) bswap operations" + static inline + uint16_t m3_bswap16(uint16_t x) { + return ((( x >> 8 ) & 0xffu ) | (( x & 0xffu ) << 8 )); + } + static inline + uint32_t m3_bswap32(uint32_t x) { + return ((( x & 0xff000000u ) >> 24 ) | + (( x & 0x00ff0000u ) >> 8 ) | + (( x & 0x0000ff00u ) << 8 ) | + (( x & 0x000000ffu ) << 24 )); + } + static inline + uint64_t m3_bswap64(uint64_t x) { + return ((( x & 0xff00000000000000ull ) >> 56 ) | + (( x & 0x00ff000000000000ull ) >> 40 ) | + (( x & 0x0000ff0000000000ull ) >> 24 ) | + (( x & 0x000000ff00000000ull ) >> 8 ) | + (( x & 0x00000000ff000000ull ) << 8 ) | + (( x & 0x0000000000ff0000ull ) << 24 ) | + (( x & 0x000000000000ff00ull ) << 40 ) | + (( x & 0x00000000000000ffull ) << 56 )); + } +# endif +# endif + +/* + * Bit ops + */ +#define m3_isBitSet(val, pos) ((val & (1 << pos)) != 0) + +/* + * Other + */ + +# if defined(M3_COMPILER_GCC) || defined(M3_COMPILER_CLANG) || defined(M3_COMPILER_ICC) +# define M3_UNLIKELY(x) __builtin_expect(!!(x), 0) +# define M3_LIKELY(x) __builtin_expect(!!(x), 1) +# else +# define M3_UNLIKELY(x) (x) +# define M3_LIKELY(x) (x) +# endif + +#endif // wasm3_defs_h diff --git a/driver/wasm_call.c b/driver/wasm_call.c new file mode 100644 index 0000000..08cf742 --- /dev/null +++ b/driver/wasm_call.c @@ -0,0 +1,103 @@ +/* wasm_call.c - serialized wasm3 call under a per-module mutex on a fresh 64KB + * kernel stack. one entry point everyone in the driver uses to invoke guests. */ +#include "inc/gvm.h" +#include "../shared/goodmans_ioctl.h" +#include "wasm3/m3_function.h" +#include "wasm3/wasm3.h" + +typedef struct { + IM3Function fn; + uint32_t argc; + const void** argp; + M3Result result; +} gvm_call_ctx; + +static VOID gvm_call_callout(_In_ PVOID p) +{ + gvm_call_ctx* c = (gvm_call_ctx*)p; + DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, + "[goodmans] pre-m3Call fn=%p name=%s\n", + c->fn, (c->fn && c->fn->export_name) ? c->fn->export_name : "?"); + __try { + c->result = m3_Call(c->fn, c->argc, c->argp); + } __except (EXCEPTION_EXECUTE_HANDLER) { + c->result = "wasm3 dispatch raised kernel exception"; + } + DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, + "[goodmans] post-m3Call result=%s\n", c->result ? c->result : "ok"); +} + +M3Result +gvm_call_locked(gvm_module* mod, IM3Function fn, unsigned int argc, const void** argp) +{ + if (!mod || !fn) return "invalid module or function"; + if (mod->poisoned) return "module poisoned by watchdog"; + + KeWaitForSingleObject(&mod->call_mutex, Executive, KernelMode, FALSE, NULL); + + if (mod->poisoned) { KeReleaseMutex(&mod->call_mutex, FALSE); return "module poisoned"; } + + // stamp mutex acquire for watchdog visibility + LARGE_INTEGER _f; + mod->mutex_hold_qpc = (ULONG64)KeQueryPerformanceCounter(&_f).QuadPart; + + // trace: entering export + if (gvm_trace_enabled_for(mod->id)) { + unsigned long long targv[4] = {0,0,0,0}; + unsigned int tac = argc > 4 ? 4 : argc; + for (unsigned int i = 0; i < tac; i++) targv[i] = argp[i] ? *(uint64_t*)argp[i] : 0; + gvm_trace_push(mod->id, GVM_TRK_CALL, fn->export_name ? fn->export_name : "?", tac, targv, 0); + } + + gvm_call_ctx c = { fn, argc, argp, m3Err_none }; + NTSTATUS s = KeExpandKernelStackAndCalloutEx( + gvm_call_callout, &c, + 64 * 1024, + FALSE, + NULL); + + // clear deadline after every call so it doesn't leak across invocations + mod->exec_deadline_qpc = 0; + + // trace: return or trap + if (gvm_trace_enabled_for(mod->id)) { + if (!NT_SUCCESS(s)) { + unsigned long long argv[1] = { (unsigned long long)s }; + gvm_trace_push(mod->id, GVM_TRK_TRAP, "kstack_callout_failed", 1, argv, 0); + } else if (c.result) { + // push the trap reason itself + gvm_trace_push(mod->id, GVM_TRK_TRAP, c.result, 0, NULL, 0); + // and each frame of the wasm backtrace as its own entry + IM3BacktraceInfo bt = m3_GetBacktrace(mod->runtime); + if (bt) { + unsigned int depth = 0; + for (IM3BacktraceFrame f = bt->frames; f && f != M3_BACKTRACE_TRUNCATED && depth < 16; + f = f->next, depth++) { + unsigned long long a[2] = { (unsigned long long)f->moduleOffset, depth }; + const char* fname = (f->function && f->function->export_name) + ? f->function->export_name : ""; + gvm_trace_push(mod->id, GVM_TRK_TRAP, fname, 2, a, 0); + } + } + } else { + gvm_trace_push(mod->id, GVM_TRK_RETURN, fn->export_name ? fn->export_name : "?", 0, NULL, 0); + } + } + + mod->mutex_hold_qpc = 0; + KeReleaseMutex(&mod->call_mutex, FALSE); + + if (!NT_SUCCESS(s)) + return "KeExpandKernelStackAndCalloutEx failed"; + return c.result; +} + +// caller sets timeout_ms > 0 before invoking gvm_call_locked to arm the deadline +void gvm_set_deadline_ms(gvm_module* mod, unsigned int timeout_ms) +{ + if (!mod || timeout_ms == 0) { if (mod) mod->exec_deadline_qpc = 0; return; } + LARGE_INTEGER freq; + LARGE_INTEGER now = KeQueryPerformanceCounter(&freq); + ULONG64 ticks = ((ULONG64)freq.QuadPart * timeout_ms) / 1000ULL; + mod->exec_deadline_qpc = (ULONG64)now.QuadPart + ticks; +} diff --git a/driver/watchdog.c b/driver/watchdog.c new file mode 100644 index 0000000..55c903d --- /dev/null +++ b/driver/watchdog.c @@ -0,0 +1,103 @@ +/* watchdog.c - system thread that patrols loaded modules for stuck guests. + * a module holding call_mutex longer than GVM_WATCHDOG_MAX_MS is flagged + * "poisoned" so no more calls dispatch, and the incident is logged. + * force-unload IOCTL bypasses the standard mutex wait for these cases. + */ +#include "inc/gvm.h" +#include "../shared/goodmans_ioctl.h" + +#define GVM_WATCHDOG_MAX_MS 5000 // max wall-clock a single call may hold +#define GVM_WATCHDOG_PERIOD_MS 500 // scan cadence + +static PETHREAD g_thread = NULL; +static KEVENT g_shutdown; +static BOOLEAN g_running = FALSE; + +static VOID gvm_watchdog_body(PVOID ctx) +{ + UNREFERENCED_PARAMETER(ctx); + LARGE_INTEGER wait; wait.QuadPart = -((LONGLONG)GVM_WATCHDOG_PERIOD_MS * 10 * 1000); + LARGE_INTEGER freq; + ULONG64 max_ticks = 0; + (void)KeQueryPerformanceCounter(&freq); + if (freq.QuadPart) max_ticks = ((ULONG64)freq.QuadPart * GVM_WATCHDOG_MAX_MS) / 1000ULL; + + for (;;) { + NTSTATUS s = KeWaitForSingleObject(&g_shutdown, Executive, KernelMode, FALSE, &wait); + if (s == STATUS_SUCCESS) break; // shutdown signaled + + if (!max_ticks) continue; + + ULONG64 now = (ULONG64)KeQueryPerformanceCounter(NULL).QuadPart; + for (unsigned int i = 0; i < 32; i++) { + gvm_module* m = gvm_modtab_iter(i); + if (!m || !m->used) continue; + ULONG64 h = m->mutex_hold_qpc; + if (!h) continue; + if (now <= h) continue; + if ((now - h) < max_ticks) continue; + if (m->poisoned) continue; + + InterlockedExchange(&m->poisoned, 1); + gvm_log("watchdog: module %u '%s' stuck >%u ms - marked poisoned", + m->id, m->name, GVM_WATCHDOG_MAX_MS); + } + } + + PsTerminateSystemThread(STATUS_SUCCESS); +} + +void gvm_watchdog_start(void) +{ + if (g_running) return; + KeInitializeEvent(&g_shutdown, NotificationEvent, FALSE); + HANDLE h; + NTSTATUS s = PsCreateSystemThread(&h, THREAD_ALL_ACCESS, NULL, NULL, NULL, gvm_watchdog_body, NULL); + if (!NT_SUCCESS(s)) { gvm_log("watchdog: thread create failed 0x%x", s); return; } + ObReferenceObjectByHandle(h, THREAD_ALL_ACCESS, *PsThreadType, KernelMode, (PVOID*)&g_thread, NULL); + ZwClose(h); + g_running = TRUE; + gvm_log("watchdog: started (period=%ums, max_hold=%ums)", GVM_WATCHDOG_PERIOD_MS, GVM_WATCHDOG_MAX_MS); +} + +void gvm_watchdog_stop(void) +{ + if (!g_running) return; + KeSetEvent(&g_shutdown, IO_NO_INCREMENT, FALSE); + if (g_thread) { + KeWaitForSingleObject(g_thread, Executive, KernelMode, FALSE, NULL); + ObDereferenceObject(g_thread); + g_thread = NULL; + } + g_running = FALSE; +} + +// IOCTL handler: bypass the normal unload path's mutex wait when a module is +// poisoned. safe only because poisoned modules can no longer make host calls. +NTSTATUS +gvm_ioctl_force_unload(PIRP irp, PIO_STACK_LOCATION sp) +{ + ULONG in_len = sp->Parameters.DeviceIoControl.InputBufferLength; + void* buf = irp->AssociatedIrp.SystemBuffer; + if (in_len < sizeof(gvm_unload_in) || !buf) { + irp->IoStatus.Information = 0; + return STATUS_INVALID_PARAMETER; + } + gvm_unload_in in; RtlCopyMemory(&in, buf, sizeof(in)); + + gvm_module* m = gvm_modtab_get(in.module_id); + if (!m) { irp->IoStatus.Information = 0; return STATUS_NOT_FOUND; } + + InterlockedExchange(&m->poisoned, 1); + gvm_log("force_unload: module %u '%s'", m->id, m->name); + + // drain refcount to 1 then free (which decrements to 0). same pattern + // as unload_all; safe even on a wedged guest because poison stops new calls. + while (InterlockedCompareExchange(&m->refcount, 1, m->refcount) != 1) { + if (!m->used) break; + } + gvm_modtab_free(m); + + irp->IoStatus.Information = 0; + return STATUS_SUCCESS; +} diff --git a/feature_guests/build_all.cmd b/feature_guests/build_all.cmd new file mode 100644 index 0000000..6d839da --- /dev/null +++ b/feature_guests/build_all.cmd @@ -0,0 +1,22 @@ +@echo off +setlocal +cd /d "%~dp0" +set CLANG=%LLVM_HOME%\bin\clang.exe +if not exist "%CLANG%" set CLANG=C:\Program Files\LLVM\bin\clang.exe +if not exist "%CLANG%" ( + echo [X] clang not found. install LLVM or set LLVM_HOME. + exit /b 1 +) + +set CFLAGS=-O2 --target=wasm32 -nostdlib -fno-builtin -I..\guest_sdk +set LFLAGS=-Wl,--no-entry -Wl,--export-dynamic -Wl,--allow-undefined -Wl,--strip-all + +call :one toolkit +call :one process_tracer + +exit /b 0 + +:one +echo [*] %~1 +"%CLANG%" %CFLAGS% %LFLAGS% -o %~1.wasm %~1.c +exit /b 0 diff --git a/feature_guests/feature_guests.vcxproj b/feature_guests/feature_guests.vcxproj new file mode 100644 index 0000000..167d310 --- /dev/null +++ b/feature_guests/feature_guests.vcxproj @@ -0,0 +1,56 @@ + + + + + Debug + x64 + + + Release + x64 + + + + {6D9E2A50-1F1A-4B4E-8D9F-1B0C0D0A0007} + feature_guests + MakeFileProj + 10.0 + + + + Makefile + v143 + + + + + call "$(MSBuildProjectDirectory)\build_all.cmd" + call "$(MSBuildProjectDirectory)\build_all.cmd" + del /Q "$(MSBuildProjectDirectory)\*.wasm" 2>nul + toolkit.wasm;process_tracer.wasm + ..\guest_sdk;$(NMakeIncludeSearchPath) + + + + + + + + + + + + + + $(SolutionDir)deploy + + + + + + + + + + + diff --git a/feature_guests/process_tracer.c b/feature_guests/process_tracer.c new file mode 100644 index 0000000..1fd0107 --- /dev/null +++ b/feature_guests/process_tracer.c @@ -0,0 +1,119 @@ +/* process_tracer.c - live process create/exit + image load stream. + * + * TWO invocation models are demonstrated here: + * + * 1) polled: call start() once, then call poll() repeatedly. poll() drains + * the ring buffer and prints every event. + * + * 2) reactive: call start_reactive(). the driver's dispatch worker will + * invoke on_process_create() / on_process_exit() / on_image_load() below + * directly from the kernel notify callback. no polling. + */ +#include "gvm.h" + +GVM_MANIFEST(GVM_CAP_ALLOC | GVM_CAP_CALLBACKS | GVM_CAP_INTROSPECT); + +static void print_evt(const gvm_event* e) +{ + char line[160]; char t[32]; u32 n = 0; + switch (e->kind) { + case 0: + n += gvm_strcpy(line + n, "PROC_CREATE pid="); + gvm_dec(e->pid, t); n += gvm_strcpy(line + n, t); + n += gvm_strcpy(line + n, " ppid="); + gvm_dec(e->ppid, t); n += gvm_strcpy(line + n, t); + n += gvm_strcpy(line + n, " "); + n += gvm_strcpy(line + n, e->name); + break; + case 1: + n += gvm_strcpy(line + n, "PROC_EXIT pid="); + gvm_dec(e->pid, t); n += gvm_strcpy(line + n, t); + break; + case 2: + n += gvm_strcpy(line + n, "IMAGE_LOAD pid="); + gvm_dec(e->pid, t); n += gvm_strcpy(line + n, t); + n += gvm_strcpy(line + n, " base=0x"); + gvm_hex64(e->image_base, t); n += gvm_strcpy(line + n, t); + n += gvm_strcpy(line + n, " "); + n += gvm_strcpy(line + n, e->name); + break; + default: + n += gvm_strcpy(line + n, "unknown event"); + break; + } + gvm_print(line); +} + +GVM_EXPORT(start) +u64 start(void) +{ + u32 a = gvm_notify_enable(0); + u32 b = gvm_notify_enable(1); + gvm_print(a == 0 ? "process notify: on" : "process notify: FAILED"); + gvm_print(b == 0 ? "image notify: on" : "image notify: FAILED"); + return (u64)a | ((u64)b << 32); +} + +GVM_EXPORT(start_reactive) +u64 start_reactive(void) +{ + u32 a = gvm_notify_enable(0); + u32 b = gvm_notify_enable(1); + u32 d = gvm_dispatch_start(); + gvm_print(d == 0 ? "reactive dispatch: on" : "reactive dispatch: FAILED"); + return ((u64)a) | ((u64)b << 8) | ((u64)d << 16); +} + +GVM_EXPORT(poll) +u64 poll(void) +{ + gvm_event e; + u32 count = 0; + while (gvm_notify_poll((u32)(u64)&e, sizeof(e)) == sizeof(e)) { + print_evt(&e); + count++; + } + return count; +} + +// reactive callbacks: driver-side dispatch worker invokes these directly. +// signature is fixed: (pid, aux1, aux2) where aux1 is ppid or image_base, +// aux2 is 0 or image_size depending on event. + +GVM_EXPORT(on_process_create) +u64 on_process_create(u64 pid, u64 ppid, u64 _unused) +{ + (void)_unused; + char line[80]; char t[32]; u32 n = 0; + n += gvm_strcpy(line + n, "[reactive] PROC_CREATE pid="); + gvm_dec((u32)pid, t); n += gvm_strcpy(line + n, t); + n += gvm_strcpy(line + n, " ppid="); + gvm_dec((u32)ppid, t); n += gvm_strcpy(line + n, t); + gvm_print(line); + return 0; +} + +GVM_EXPORT(on_process_exit) +u64 on_process_exit(u64 pid, u64 _a1, u64 _a2) +{ + (void)_a1; (void)_a2; + char line[80]; char t[32]; u32 n = 0; + n += gvm_strcpy(line + n, "[reactive] PROC_EXIT pid="); + gvm_dec((u32)pid, t); n += gvm_strcpy(line + n, t); + gvm_print(line); + return 0; +} + +GVM_EXPORT(on_image_load) +u64 on_image_load(u64 pid, u64 base, u64 size) +{ + char line[128]; char t[32]; u32 n = 0; + n += gvm_strcpy(line + n, "[reactive] IMAGE_LOAD pid="); + gvm_dec((u32)pid, t); n += gvm_strcpy(line + n, t); + n += gvm_strcpy(line + n, " base=0x"); + gvm_hex64(base, t); n += gvm_strcpy(line + n, t); + n += gvm_strcpy(line + n, " size="); + gvm_dec((u32)size, t); n += gvm_strcpy(line + n, t); + gvm_print(line); + return 0; +} diff --git a/feature_guests/toolkit.c b/feature_guests/toolkit.c new file mode 100644 index 0000000..72eac33 --- /dev/null +++ b/feature_guests/toolkit.c @@ -0,0 +1,138 @@ +/* toolkit.c - frida-style primitives for the GUI to drive + * + * exposes a set of exports that the GUI calls to read/write kernel state. + * results that don't fit in a u64 are written into the guest's own linear + * memory at a caller-provided offset, and the GUI pulls them back with the + * new IOCTL_GVM_READ_GUEST ioctl. + */ + +#include "../guest_sdk/gvm.h" + +// declare all caps we might exercise +GVM_MANIFEST(GVM_CAP_ALLOC | GVM_CAP_READ_KMEM | GVM_CAP_WRITE_KMEM | + GVM_CAP_MSR_READ | GVM_CAP_MSR_WRITE | GVM_CAP_PHYSMEM | + GVM_CAP_CPUID_TSC | GVM_CAP_HOSTCALL | GVM_CAP_INTROSPECT); + +// scratch buffer accessible from linear memory; GUI reads bytes out of here +static u8 g_scratch[16384]; + +// forces the scratch buffer to appear in linear memory so its address is +// stable across calls. return its offset so the GUI knows where to read from. +GVM_EXPORT(toolkit_scratch_off) +u32 toolkit_scratch_off(void) +{ + return (u32)(unsigned long long)&g_scratch[0]; +} + +GVM_EXPORT(toolkit_scratch_size) +u32 toolkit_scratch_size(void) { return sizeof(g_scratch); } + +// KERNEL VIRTUAL MEMORY +GVM_EXPORT(toolkit_read_kmem) +u32 toolkit_read_kmem(u64 va, u32 guest_off, u32 len) +{ + return gvm_read_bytes(va, guest_off, len); +} + +GVM_EXPORT(toolkit_read_u8) +u32 toolkit_read_u8(u64 va) { return gvm_read_u8(va); } + +GVM_EXPORT(toolkit_read_u32) +u32 toolkit_read_u32(u64 va) { return gvm_read_u32(va); } + +GVM_EXPORT(toolkit_read_u64) +u64 toolkit_read_u64(u64 va) { return gvm_read_u64(va); } + +// PHYSICAL MEMORY +GVM_EXPORT(toolkit_read_phys) +u32 toolkit_read_phys(u64 pa, u32 guest_off, u32 len) +{ + return gvm_phys_read(pa, guest_off, len); +} + +// MSR +GVM_EXPORT(toolkit_readmsr) +u64 toolkit_readmsr(u32 idx) { return gvm_readmsr(idx); } + +// CPUID - writes eax,ebx,ecx,edx (4 x u32) at guest_off +GVM_EXPORT(toolkit_cpuid) +void toolkit_cpuid(u32 leaf, u32 sub, u32 guest_off) +{ + gvm_cpuid(leaf, sub, guest_off); +} + +// RDTSC +GVM_EXPORT(toolkit_rdtsc) +u64 toolkit_rdtsc(void) { return gvm_rdtsc(); } + +// EXPORT LOOKUP - returns kernel VA of an ntoskrnl/hal export by name +// name_off in linear memory, name_len bytes (nul terminator NOT required) +GVM_EXPORT(toolkit_resolve) +u64 toolkit_resolve(u32 name_off, u32 name_len) +{ + if (name_len == 0 || name_len > 128) return 0; + // scratch a copy so we can nul-terminate cleanly + static char buf[144]; + for (u32 i = 0; i < name_len && i < sizeof(buf)-1; i++) { + buf[i] = ((char*)0)[name_off + i]; + } + buf[name_len < sizeof(buf) ? name_len : sizeof(buf)-1] = 0; + // use host_call with resolve helper: convention is fn_name = "MmGetSystemRoutineAddress", + // arg0 = pointer to UNICODE_STRING. Building UNICODE_STRING is fragile from wasm. + // Simpler: we do it host-side. Return 0 here for now, GUI can compare with a + // dedicated api if needed. This is a placeholder for future extension. + return 0; +} + +// INTROSPECTION +GVM_EXPORT(toolkit_current_process) +u64 toolkit_current_process(void) { return gvm_current_process(); } + +GVM_EXPORT(toolkit_current_pid) +u32 toolkit_current_pid(void) { return gvm_process_id(); } + +GVM_EXPORT(toolkit_current_tid) +u32 toolkit_current_tid(void) { return gvm_thread_id(); } + +GVM_EXPORT(toolkit_current_irql) +u32 toolkit_current_irql(void) { return gvm_current_irql(); } + +// SIMPLE PROCESS ENUM - walks ActiveProcessLinks from PsGetCurrentProcess() +// writes packed records: [u64 pid][u64 eprocess][char name[16]] each +// returns count of records written. cap = max records the buffer can hold. +// +// offsets are Win10/11 x64. hard-coded here for demo. real toolkit would +// use signatures to derive them at load. +#define EPROC_ACTIVE_LINKS 0x448 // _EPROCESS.ActiveProcessLinks +#define EPROC_UNIQUE_PID 0x440 // _EPROCESS.UniqueProcessId +#define EPROC_IMAGE_NAME 0x5a8 // _EPROCESS.ImageFileName + +GVM_EXPORT(toolkit_enum_procs) +u32 toolkit_enum_procs(u32 guest_off, u32 cap) +{ + if (cap == 0) return 0; + + u64 self = gvm_current_process(); + if (!self) return 0; + + u64 head = self + EPROC_ACTIVE_LINKS; + u64 cur = head; + u32 count = 0; + + for (u32 iter = 0; iter < 4096 && count < cap; iter++) { + cur = gvm_read_u64(cur); // follow Flink + if (!cur || cur == head) break; + u64 eproc = cur - EPROC_ACTIVE_LINKS; + u64 pid = gvm_read_u64(eproc + EPROC_UNIQUE_PID); + + // read record into scratch at guest_off + count*32 + u32 rec_off = guest_off + count * 32; + *(u64*)((char*)0 + rec_off) = pid; + *(u64*)((char*)0 + rec_off + 8) = eproc; + // 16 bytes of image name + gvm_read_bytes(eproc + EPROC_IMAGE_NAME, rec_off + 16, 15); + ((char*)0)[rec_off + 31] = 0; + count++; + } + return count; +} diff --git a/guest_sdk/gvm.h b/guest_sdk/gvm.h new file mode 100644 index 0000000..5027020 --- /dev/null +++ b/guest_sdk/gvm.h @@ -0,0 +1,249 @@ +/* gvm.h - Goodmans guest SDK */ +#pragma once + +typedef unsigned char u8; +typedef unsigned short u16; +typedef unsigned int u32; +typedef unsigned long long u64; +typedef signed char i8; +typedef signed short i16; +typedef signed int i32; +typedef signed long long i64; + +typedef u32 size_t; +typedef u8 BOOLEAN; +typedef u8 UCHAR; +typedef u16 USHORT; +typedef u32 ULONG; +typedef u64 ULONGLONG; +typedef i32 LONG; +typedef i64 LONGLONG; +typedef u64 SIZE_T; +typedef i32 NTSTATUS; +typedef u64 HANDLE; +typedef u64 PVOID; + +#define TRUE 1 +#define FALSE 0 +#define NULL ((PVOID)0) + +#define NonPagedPool 0 +#define PagedPool 1 +#define NonPagedPoolNx 512 + +#define PASSIVE_LEVEL 0 +#define APC_LEVEL 1 +#define DISPATCH_LEVEL 2 + +#define STATUS_SUCCESS ((NTSTATUS)0x00000000L) +#define STATUS_UNSUCCESSFUL ((NTSTATUS)0xC0000001L) +#define STATUS_NOT_IMPLEMENTED ((NTSTATUS)0xC0000002L) +#define STATUS_INVALID_PARAMETER ((NTSTATUS)0xC000000DL) +#define STATUS_INSUFFICIENT_RESOURCES ((NTSTATUS)0xC000009AL) +#define NT_SUCCESS(s) (((NTSTATUS)(s)) >= 0) + +// import from the driver's env namespace: custom host_* helpers, plus any +// nt/hal export the driver auto-resolves via MmGetSystemRoutineAddress. +#define GVM_IMPORT(name) __attribute__((import_module("env"), import_name(#name))) + +// alias for readability when importing a native kernel export directly +#define GVM_IMPORT_KERNEL(name) GVM_IMPORT(name) + +// import from another loaded driver's export table. usage: +// GVM_IMPORT_DRV("vgk", SomeExport) u64 SomeExport(u64 a); +#define GVM_IMPORT_DRV(mod, name) __attribute__((import_module("drv$" mod), import_name(#name))) + +#define GVM_EXPORT(name) __attribute__((export_name(#name))) + +// buffer / memory / debug primitives (need wasm-linear-memory access) + +GVM_IMPORT(host_dbg_print) void gvm_dbg_print_raw(const char* msg, u32 len); +GVM_IMPORT(host_read_bytes) u32 gvm_read_bytes(u64 kaddr, u32 guest_off, u32 len); +GVM_IMPORT(host_write_bytes) u32 gvm_write_bytes(u64 kaddr, u32 guest_off, u32 len); +GVM_IMPORT(host_cpuid) void gvm_cpuid(u32 leaf, u32 subleaf, u32 out_off); +GVM_IMPORT(host_phys_read) u32 gvm_phys_read(u64 pa, u32 guest_off, u32 len); +GVM_IMPORT(host_phys_write) u32 gvm_phys_write(u64 pa, u32 guest_off, u32 len); + +// scalar primitives (thin one-line wrappers exposed via direct FFI) + +GVM_IMPORT(host_current_irql) u32 gvm_current_irql(void); +GVM_IMPORT(host_process_id) u32 gvm_process_id(void); +GVM_IMPORT(host_thread_id) u32 gvm_thread_id(void); +GVM_IMPORT(host_current_process) u64 gvm_current_process(void); +GVM_IMPORT(host_read_u8) u32 gvm_read_u8(u64 kaddr); +GVM_IMPORT(host_read_u32) u32 gvm_read_u32(u64 kaddr); +GVM_IMPORT(host_read_u64) u64 gvm_read_u64(u64 kaddr); +GVM_IMPORT(host_write_u64) void gvm_write_u64(u64 kaddr, u64 val); +GVM_IMPORT(host_readmsr) u64 gvm_readmsr(u32 idx); +GVM_IMPORT(host_writemsr) u32 gvm_writemsr(u32 idx, u64 val); +GVM_IMPORT(host_rdtsc) u64 gvm_rdtsc(void); + +// callback dispatch + +GVM_IMPORT(host_notify_enable) u32 gvm_notify_enable(u32 kind); +GVM_IMPORT(host_notify_poll) u32 gvm_notify_poll(u32 out_off, u32 out_len); +GVM_IMPORT(host_dispatch_start) u32 gvm_dispatch_start(void); +GVM_IMPORT(host_dispatch_stop) u32 gvm_dispatch_stop(void); + +// Native trampoline for InfinityHook. guest resolves the WMI_LOGGER_CONTEXT +// GetCpuClock slot itself (via kernel FFI) and writes this address into it. +// see sample_guest/infinity_hook.c for the full port. +GVM_IMPORT(host_ih_trampoline) u64 gvm_ih_trampoline(void); +GVM_IMPORT(host_ih_configure) void gvm_ih_configure(u32 rate, u64 nt_base, u32 nt_size); +GVM_IMPORT(host_ih_quiesce) u64 gvm_ih_quiesce(void); + +// wasm-linear-memory <-> real-VA bridge for FFI pointer args +// +// host_mem_base returns the kernel VA of the guest's linear memory base. +// build real pointers into your own buffers with gvm_kva(p): +// +// char buf[64]; +// RtlZeroMemory(gvm_kva(buf), sizeof(buf)); + +GVM_IMPORT(host_mem_base) u64 host_mem_base(void); +GVM_IMPORT(host_mem_size) u32 host_mem_size(void); + +static inline u64 gvm_kva(const void* p) +{ + return host_mem_base() + (u64)(u32)(unsigned long)p; +} + +// build a real UNICODE_STRING in non-paged pool from a UTF-16 string in guest +// memory. returns kernel VA usable directly as PUNICODE_STRING. free after. + +GVM_IMPORT(host_make_unistr) u64 host_make_unistr(u32 str_off, u32 byte_len); +GVM_IMPORT(host_free_unistr) void host_free_unistr(u64 kva); + +// dynamic export dispatcher. use only when the target isn't known at compile +// time; direct GVM_IMPORT_KERNEL declarations are faster and traced by name. + +GVM_IMPORT(host_call) u64 __gvm_host_call( + const char* name, u32 name_len, u32 argc, + u64 a0, u64 a1, u64 a2, u64 a3, + u64 a4, u64 a5, u64 a6, u64 a7); + +#define GVM_CALL0(name) __gvm_host_call(#name, sizeof(#name)-1, 0, 0,0,0,0,0,0,0,0) +#define GVM_CALL1(name, a) __gvm_host_call(#name, sizeof(#name)-1, 1, (u64)(a),0,0,0,0,0,0,0) +#define GVM_CALL2(name, a,b) __gvm_host_call(#name, sizeof(#name)-1, 2, (u64)(a),(u64)(b),0,0,0,0,0,0) +#define GVM_CALL3(name, a,b,c) __gvm_host_call(#name, sizeof(#name)-1, 3, (u64)(a),(u64)(b),(u64)(c),0,0,0,0,0) +#define GVM_CALL4(name, a,b,c,d) __gvm_host_call(#name, sizeof(#name)-1, 4, (u64)(a),(u64)(b),(u64)(c),(u64)(d),0,0,0,0) +#define GVM_CALL5(name, a,b,c,d,e) __gvm_host_call(#name, sizeof(#name)-1, 5, (u64)(a),(u64)(b),(u64)(c),(u64)(d),(u64)(e),0,0,0) +#define GVM_CALL6(name, a,b,c,d,e,f) __gvm_host_call(#name, sizeof(#name)-1, 6, (u64)(a),(u64)(b),(u64)(c),(u64)(d),(u64)(e),(u64)(f),0,0) +#define GVM_CALL7(name, a,b,c,d,e,f,g) __gvm_host_call(#name, sizeof(#name)-1, 7, (u64)(a),(u64)(b),(u64)(c),(u64)(d),(u64)(e),(u64)(f),(u64)(g),0) +#define GVM_CALL8(name, a,b,c,d,e,f,g,h) __gvm_host_call(#name, sizeof(#name)-1, 8, (u64)(a),(u64)(b),(u64)(c),(u64)(d),(u64)(e),(u64)(f),(u64)(g),(u64)(h)) + +// direct kernel imports. these go through the FFI trampoline: no name +// lookup at call time, args pass straight into the kernel via x64 ABI. + +GVM_IMPORT_KERNEL(ExAllocatePool2) PVOID ExAllocatePool2(u64 flags, SIZE_T size, u32 tag); +GVM_IMPORT_KERNEL(ExAllocatePoolWithTag) PVOID ExAllocatePoolWithTag(u32 type, SIZE_T size, u32 tag); +GVM_IMPORT_KERNEL(ExFreePool) void ExFreePool(PVOID p); +GVM_IMPORT_KERNEL(ExFreePoolWithTag) void ExFreePoolWithTag(PVOID p, u32 tag); +GVM_IMPORT_KERNEL(PsGetCurrentProcessId) HANDLE PsGetCurrentProcessId(void); +GVM_IMPORT_KERNEL(PsGetCurrentThreadId) HANDLE PsGetCurrentThreadId(void); +GVM_IMPORT_KERNEL(IoGetCurrentProcess) PVOID IoGetCurrentProcess(void); +GVM_IMPORT_KERNEL(KeGetCurrentThread) PVOID KeGetCurrentThread(void); +GVM_IMPORT_KERNEL(KeGetCurrentIrql) u8 KeGetCurrentIrql(void); +GVM_IMPORT_KERNEL(KeGetCurrentProcessorNumber) u32 KeGetCurrentProcessorNumber(void); +GVM_IMPORT_KERNEL(MmIsAddressValid) BOOLEAN MmIsAddressValid(PVOID p); +GVM_IMPORT_KERNEL(MmGetSystemRoutineAddress) PVOID MmGetSystemRoutineAddress(PVOID us); +GVM_IMPORT_KERNEL(PsGetProcessId) HANDLE PsGetProcessId(PVOID eproc); +GVM_IMPORT_KERNEL(PsGetProcessImageFileName) PVOID PsGetProcessImageFileName(PVOID eproc); +GVM_IMPORT_KERNEL(PsGetProcessInheritedFromUniqueProcessId) HANDLE PsGetProcessInheritedFromUniqueProcessId(PVOID eproc); +GVM_IMPORT_KERNEL(PsLookupProcessByProcessId) NTSTATUS PsLookupProcessByProcessId(HANDLE pid, PVOID* out_eproc); +GVM_IMPORT_KERNEL(ObDereferenceObject) void ObDereferenceObject(PVOID obj); +GVM_IMPORT_KERNEL(ZwClose) NTSTATUS ZwClose(HANDLE h); +GVM_IMPORT_KERNEL(ZwQuerySystemInformation) NTSTATUS ZwQuerySystemInformation(u32 cls, PVOID buf, u32 len, u32* got); +GVM_IMPORT_KERNEL(RtlZeroMemory) void RtlZeroMemory(PVOID dst, SIZE_T len); +GVM_IMPORT_KERNEL(RtlCopyMemory) void RtlCopyMemory(PVOID dst, PVOID src, SIZE_T len); +GVM_IMPORT_KERNEL(RtlCompareMemory) SIZE_T RtlCompareMemory(PVOID a, PVOID b, SIZE_T len); +GVM_IMPORT_KERNEL(KeStallExecutionProcessor) void KeStallExecutionProcessor(u32 us); +GVM_IMPORT_KERNEL(KeQueryPerformanceCounter) u64 KeQueryPerformanceCounter(u64* freq_out); + +// gvm_event layout must match driver's gvm_event struct in host_imports.c +typedef struct { + u32 kind; // 0=proc create, 1=proc exit, 2=image load + u32 pid; + u32 ppid; + u32 flags; + u64 eprocess; + u64 image_base; + u32 image_size; + u32 _pad; + char name[80]; +} gvm_event; + +// tiny string helpers + +static inline u32 gvm_strlen(const char* s) +{ + u32 n = 0; + while (s[n]) n++; + return n; +} + +static inline void gvm_print(const char* s) +{ + gvm_dbg_print_raw(s, gvm_strlen(s)); +} + +static inline void gvm_hex64(u64 v, char* out17) +{ + static const char h[] = "0123456789ABCDEF"; + for (int i = 15; i >= 0; i--) { out17[i] = h[v & 0xF]; v >>= 4; } + out17[16] = 0; +} + +static inline void gvm_dec(u32 v, char* out) +{ + if (v == 0) { out[0] = '0'; out[1] = 0; return; } + char buf[16]; int n = 0; + while (v) { buf[n++] = (char)('0' + (v % 10)); v /= 10; } + for (int i = 0; i < n; i++) out[i] = buf[n - 1 - i]; + out[n] = 0; +} + +static inline int gvm_streq(const char* a, const char* b) +{ + while (*a && *a == *b) { a++; b++; } + return *a == *b; +} + +static inline int gvm_stricmp(const char* a, const char* b) +{ + while (*a && *b) { + char ca = *a, cb = *b; + if (ca >= 'A' && ca <= 'Z') ca += 32; + if (cb >= 'A' && cb <= 'Z') cb += 32; + if (ca != cb) return ca - cb; + a++; b++; + } + return *a - *b; +} + +static inline u32 gvm_strcpy(char* dst, const char* src) +{ + u32 n = 0; + while ((dst[n] = src[n]) != 0) n++; + return n; +} + +// capability manifest. embed in the guest to lock down which host imports +// the driver will let you call. see GVM_CAP_* below and shared/goodmans_ioctl.h. +// example: GVM_MANIFEST(GVM_CAP_ALLOC | GVM_CAP_READ_KMEM | GVM_CAP_HOSTCALL); + +#define GVM_CAP_ALLOC (1u << 0) +#define GVM_CAP_READ_KMEM (1u << 1) +#define GVM_CAP_WRITE_KMEM (1u << 2) +#define GVM_CAP_MSR_READ (1u << 3) +#define GVM_CAP_MSR_WRITE (1u << 4) +#define GVM_CAP_PHYSMEM (1u << 5) +#define GVM_CAP_CPUID_TSC (1u << 6) +#define GVM_CAP_CALLBACKS (1u << 7) +#define GVM_CAP_HOSTCALL (1u << 8) +#define GVM_CAP_INTROSPECT (1u << 9) +#define GVM_CAP_ALL 0xFFFFFFFFu + +#define GVM_MANIFEST(caps) \ + GVM_EXPORT(__gvm_caps) \ + u32 __gvm_caps(void) { return (u32)(caps); } diff --git a/gui-qt/CMakeLists.txt b/gui-qt/CMakeLists.txt new file mode 100644 index 0000000..0c08c36 --- /dev/null +++ b/gui-qt/CMakeLists.txt @@ -0,0 +1,47 @@ +cmake_minimum_required(VERSION 3.20) +project(goodmans_gui LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_AUTOUIC ON) + +set(CMAKE_PREFIX_PATH "C:/Qt/6.9.3/msvc2022_64") + +find_package(Qt6 REQUIRED COMPONENTS Widgets Gui Core) + +add_executable(goodmans-gui WIN32 + main.cpp + mainwindow.cpp + mainwindow.h + driver.cpp + driver.h + pages.cpp + pages.h + command_palette.cpp + command_palette.h + resources.qrc +) + +target_include_directories(goodmans-gui PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/../shared" +) + +target_link_libraries(goodmans-gui PRIVATE Qt6::Widgets Qt6::Gui Qt6::Core) + +if (WIN32) + get_target_property(_qmake_exe Qt6::qmake IMPORTED_LOCATION) + get_filename_component(_qt_bin "${_qmake_exe}" DIRECTORY) + set(DEPLOY_GUI "${CMAKE_CURRENT_SOURCE_DIR}/../deploy/gui") + + # windeployqt straight into deploy/gui so we deploy once, not twice + add_custom_command(TARGET goodmans-gui POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory "${DEPLOY_GUI}" + COMMAND ${CMAKE_COMMAND} -E copy_if_different $ "${DEPLOY_GUI}/" + COMMAND "${_qt_bin}/windeployqt.exe" --no-translations --no-opengl-sw --no-system-d3d-compiler "${DEPLOY_GUI}/goodmans-gui.exe" + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${CMAKE_CURRENT_SOURCE_DIR}/../feature_guests/toolkit.wasm" + "${DEPLOY_GUI}/toolkit.wasm" + COMMENT "[deploy] staging gui -> deploy/gui" + ) +endif() diff --git a/gui-qt/command_palette.cpp b/gui-qt/command_palette.cpp new file mode 100644 index 0000000..3ac17fb --- /dev/null +++ b/gui-qt/command_palette.cpp @@ -0,0 +1,144 @@ +#include "command_palette.h" +#include +#include +#include +#include + +// Fuzzy sort proxy: match if all chars in query appear in order in target. +// Scoring: fewer gaps + earlier match wins. Used to power the "type anywhere" flow. +class FuzzyProxy : public QSortFilterProxyModel { +public: + explicit FuzzyProxy(QObject* p = nullptr) : QSortFilterProxyModel(p) { setDynamicSortFilter(true); } + void setQuery(const QString& q) { query = q.toLower(); invalidateFilter(); invalidate(); } +protected: + bool filterAcceptsRow(int row, const QModelIndex&) const override { + if (query.isEmpty()) return true; + QString s = sourceModel()->data(sourceModel()->index(row, 0)).toString().toLower(); + int qi = 0; + for (int i = 0; i < s.size() && qi < query.size(); i++) if (s[i] == query[qi]) qi++; + return qi == query.size(); + } + bool lessThan(const QModelIndex& l, const QModelIndex& r) const override { + if (query.isEmpty()) return l.row() < r.row(); + auto score = [&](const QString& s)->int { + int qi = 0, first = -1, gaps = 0, prev = -100; + QString low = s.toLower(); + for (int i = 0; i < low.size() && qi < query.size(); i++) { + if (low[i] == query[qi]) { + if (first == -1) first = i; + if (prev != i - 1) gaps++; + prev = i; qi++; + } + } + return first * 4 + gaps * 8 + s.size(); + }; + int sl = score(sourceModel()->data(l).toString()); + int sr = score(sourceModel()->data(r).toString()); + return sl < sr; + } +private: + QString query; +}; + +class PaletteDelegate : public QStyledItemDelegate { +public: + using QStyledItemDelegate::QStyledItemDelegate; + QSize sizeHint(const QStyleOptionViewItem&, const QModelIndex&) const override { return {0, 34}; } + void paint(QPainter* p, const QStyleOptionViewItem& o, const QModelIndex& idx) const override { + p->save(); + if (o.state & QStyle::State_Selected) p->fillRect(o.rect, QColor(0x2a, 0x21, 0x17)); + auto full = idx.data().toString(); + auto hint = idx.data(Qt::UserRole + 1).toString(); + p->setPen((o.state & QStyle::State_Selected) ? QColor(0xf0, 0xa3, 0x40) : QColor(0xd4, 0xd7, 0xdd)); + QFont f = p->font(); f.setPointSizeF(11.5); p->setFont(f); + p->drawText(QRect(o.rect.left() + 16, o.rect.top(), o.rect.width() - 200, o.rect.height()), + Qt::AlignVCenter | Qt::AlignLeft, full); + if (!hint.isEmpty()) { + p->setPen(QColor(0x6a, 0x72, 0x80)); + QFont hf = f; hf.setPointSizeF(10.5); hf.setFamily("JetBrainsMono NF"); p->setFont(hf); + p->drawText(QRect(o.rect.right() - 180, o.rect.top(), 164, o.rect.height()), + Qt::AlignVCenter | Qt::AlignRight, hint); + } + p->restore(); + } +}; + +CommandPalette::CommandPalette(QWidget* parent, const QVector& actions) + : QDialog(parent), all_actions(actions) +{ + setWindowFlags(Qt::Popup | Qt::FramelessWindowHint); + setAttribute(Qt::WA_TranslucentBackground, false); + setStyleSheet( + "QDialog { background: #14171c; border: 1px solid #363c47; border-radius: 10px; }" + "QLineEdit { background: transparent; border: none; border-bottom: 1px solid #262a33;" + " padding: 14px 18px; font-size: 15px; color: #f0f2f7; }" + "QListView { background: transparent; border: none; outline: 0; }" + ); + + resize(640, 420); + + auto* l = new QVBoxLayout(this); + l->setContentsMargins(0, 0, 0, 0); + l->setSpacing(0); + + search = new QLineEdit; + search->setPlaceholderText("Type a command…"); + l->addWidget(search); + + model = new QStandardItemModel(this); + for (const auto& a : all_actions) { + auto* it = new QStandardItem(a.label); + it->setData(a.hint, Qt::UserRole + 1); + model->appendRow(it); + } + proxy = new FuzzyProxy(this); + proxy->setSourceModel(model); + + list = new QListView; + list->setModel(proxy); + list->setItemDelegate(new PaletteDelegate(this)); + list->setUniformItemSizes(true); + list->setSelectionMode(QAbstractItemView::SingleSelection); + l->addWidget(list, 1); + + if (proxy->rowCount() > 0) + list->setCurrentIndex(proxy->index(0, 0)); + + connect(search, &QLineEdit::textChanged, this, [this](const QString& t) { + static_cast(proxy)->setQuery(t); + proxy->sort(0); + if (proxy->rowCount() > 0) list->setCurrentIndex(proxy->index(0, 0)); + }); + connect(list, &QListView::activated, this, [this](const QModelIndex&) { runSelected(); }); + + search->setFocus(); +} + +void CommandPalette::runSelected() +{ + auto idx = list->currentIndex(); + if (!idx.isValid()) return; + auto src = proxy->mapToSource(idx); + int i = src.row(); + if (i < 0 || i >= all_actions.size()) return; + auto fn = all_actions[i].run; + accept(); + if (fn) fn(); +} + +void CommandPalette::keyPressEvent(QKeyEvent* e) +{ + if (e->key() == Qt::Key_Escape) { reject(); return; } + if (e->key() == Qt::Key_Return || e->key() == Qt::Key_Enter) { runSelected(); return; } + if (e->key() == Qt::Key_Up) { + auto cur = list->currentIndex().row(); + if (cur > 0) list->setCurrentIndex(proxy->index(cur - 1, 0)); + return; + } + if (e->key() == Qt::Key_Down) { + auto cur = list->currentIndex().row(); + if (cur < proxy->rowCount() - 1) list->setCurrentIndex(proxy->index(cur + 1, 0)); + return; + } + QDialog::keyPressEvent(e); +} diff --git a/gui-qt/command_palette.h b/gui-qt/command_palette.h new file mode 100644 index 0000000..cbbb59b --- /dev/null +++ b/gui-qt/command_palette.h @@ -0,0 +1,31 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct CommandAction { + QString label; + QString hint; // shortcut or context + std::function run; +}; + +class CommandPalette : public QDialog { + Q_OBJECT +public: + CommandPalette(QWidget* parent, const QVector& actions); +protected: + void keyPressEvent(QKeyEvent* e) override; +private: + QLineEdit* search; + QListView* list; + QStandardItemModel* model; + QSortFilterProxyModel* proxy; + QVector all_actions; + void runSelected(); +}; diff --git a/gui-qt/driver.cpp b/gui-qt/driver.cpp new file mode 100644 index 0000000..4e0ea99 --- /dev/null +++ b/gui-qt/driver.cpp @@ -0,0 +1,295 @@ +#include "driver.h" +#include +#include +#include +#include +#include +#include + +#include "goodmans_ioctl.h" + +DriverClient::DriverClient(QObject* p) : QObject(p) {} + +static HANDLE dev_open() +{ + return CreateFileA(GVM_USER_PATH, GENERIC_READ|GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr); +} + +bool DriverClient::device_open() const +{ + HANDLE h = dev_open(); + if (h == INVALID_HANDLE_VALUE) return false; + CloseHandle(h); + return true; +} + +bool DriverClient::refresh_modules(QVector& out) +{ + out.clear(); + HANDLE dev = dev_open(); + if (dev == INVALID_HANDLE_VALUE) return false; + gvm_list_out r{}; + DWORD ret = 0; + BOOL ok = DeviceIoControl(dev, IOCTL_GVM_LIST_MODULES, nullptr, 0, &r, sizeof(r), &ret, nullptr); + CloseHandle(dev); + if (!ok) return false; + for (unsigned int i = 0; i < r.count; i++) { + ModuleRow m; + m.id = r.entries[i].id; + m.wasm_size = r.entries[i].wasm_size; + m.mem_pages = r.entries[i].mem_pages; + m.pool_bytes = r.entries[i].pool_bytes; + m.hash = r.entries[i].hash; + m.name = QString::fromUtf8(r.entries[i].name); + out.push_back(m); + } + return true; +} + +bool DriverClient::module_info(quint32 id, ModuleInfo& info) +{ + info = {}; + HANDLE dev = dev_open(); + if (dev == INVALID_HANDLE_VALUE) return false; + gvm_info_in in{ id }; + auto* out = (gvm_info_out*)calloc(1, sizeof(gvm_info_out)); + if (!out) { CloseHandle(dev); return false; } + DWORD ret = 0; + BOOL ok = DeviceIoControl(dev, IOCTL_GVM_MODULE_INFO, &in, sizeof(in), out, sizeof(*out), &ret, nullptr); + CloseHandle(dev); + if (!ok || out->status != 0) { free(out); return false; } + info.base.id = out->base.id; + info.base.name = QString::fromUtf8(out->base.name); + info.base.wasm_size = out->base.wasm_size; + info.base.mem_pages = out->base.mem_pages; + info.base.pool_bytes = out->base.pool_bytes; + info.base.hash = out->base.hash; + info.caps = 0; + for (unsigned int i = 0; i < out->export_count && i < GVM_MAX_INFO_EXPORTS; i++) + info.exports.push_back(QString::fromUtf8(out->exports[i])); + for (unsigned int i = 0; i < out->import_count && i < GVM_MAX_INFO_IMPORTS; i++) + info.imports.push_back(QString::fromUtf8(out->imports[i])); + info.valid = true; + free(out); + return true; +} + +bool DriverClient::load_module(const QString& path, quint64 budget, quint32& out_id, QString& err) +{ + QFile f(path); + if (!f.open(QIODevice::ReadOnly)) { err = "cannot open file"; return false; } + QByteArray data = f.readAll(); + f.close(); + if (data.isEmpty() || data.size() > (16 << 20)) { err = "bad file size"; return false; } + + QByteArray buf; buf.resize(sizeof(gvm_load_in) + data.size()); + auto* in = (gvm_load_in*)buf.data(); + memset(in, 0, sizeof(*in)); + in->wasm_size = (unsigned int)data.size(); + in->pool_budget = budget; + QByteArray nameU8 = QFileInfo(path).fileName().toUtf8(); + strncpy_s(in->name, GVM_MAX_MODULE_NAME, nameU8.constData(), _TRUNCATE); + memcpy(buf.data() + sizeof(gvm_load_in), data.constData(), data.size()); + + HANDLE dev = dev_open(); + if (dev == INVALID_HANDLE_VALUE) { err = "device closed"; return false; } + gvm_load_out out{}; DWORD ret = 0; + BOOL ok = DeviceIoControl(dev, IOCTL_GVM_LOAD_MODULE, buf.data(), (DWORD)buf.size(), &out, sizeof(out), &ret, nullptr); + CloseHandle(dev); + if (!ok) { err = "ioctl failed"; return false; } + if (out.status) { err = QString::fromUtf8(out.err_msg); return false; } + out_id = out.module_id; + return true; +} + +bool DriverClient::call_export(quint32 id, const QString& exp, quint32 timeout, const QVector& argv, quint64& rv, QString& err) +{ + HANDLE dev = dev_open(); + if (dev == INVALID_HANDLE_VALUE) { err = "device closed"; return false; } + gvm_call_in in{}; in.module_id = id; in.timeout_ms = timeout; + in.argc = (unsigned int)argv.size(); if (in.argc > GVM_MAX_ARGS) in.argc = GVM_MAX_ARGS; + for (unsigned int i = 0; i < in.argc; i++) in.argv[i] = argv[i]; + QByteArray e = exp.toUtf8(); + strncpy_s(in.export_name, e.constData(), _TRUNCATE); + gvm_call_out out{}; DWORD ret = 0; + BOOL ok = DeviceIoControl(dev, IOCTL_GVM_CALL_EXPORT, &in, sizeof(in), &out, sizeof(out), &ret, nullptr); + CloseHandle(dev); + if (!ok) { err = "ioctl failed"; return false; } + if (out.status) { err = QString::fromUtf8(out.err_msg); return false; } + rv = out.rv; + return true; +} + +bool DriverClient::unload(quint32 id) +{ + HANDLE dev = dev_open(); + if (dev == INVALID_HANDLE_VALUE) return false; + gvm_unload_in in{ id }; DWORD ret = 0; + BOOL ok = DeviceIoControl(dev, IOCTL_GVM_UNLOAD_MODULE, &in, sizeof(in), nullptr, 0, &ret, nullptr); + CloseHandle(dev); + return ok; +} + +bool DriverClient::unload_all() +{ + HANDLE dev = dev_open(); + if (dev == INVALID_HANDLE_VALUE) return false; + DWORD ret = 0; + BOOL ok = DeviceIoControl(dev, IOCTL_GVM_UNLOAD_ALL, nullptr, 0, nullptr, 0, &ret, nullptr); + CloseHandle(dev); + return ok; +} + +static int classify_line(const QString& line) +{ + if (line.contains("load module_id")) return EVT_LOAD; + if (line.contains("unload module")) return EVT_UNLOAD; + if (line.contains("call mod=")) return EVT_CALL; + if (line.contains("PROC_CREATE") || line.contains("proc create")) return EVT_PROC; + if (line.contains("PROC_EXIT") || line.contains("proc exit")) return EVT_EXIT; + if (line.contains("IMAGE_LOAD") || line.contains("image load")) return EVT_IMAGE; + if (line.contains("error", Qt::CaseInsensitive) || line.contains("ERR")) return EVT_ERROR; + return EVT_MSG; +} + +bool DriverClient::tail_log(quint64& last_seq, QVector& out_entries, quint32& dropped) +{ + HANDLE dev = dev_open(); + if (dev == INVALID_HANDLE_VALUE) return false; + gvm_tail_in in{ last_seq }; + auto* out = (gvm_tail_out*)malloc(sizeof(gvm_tail_out)); + if (!out) { CloseHandle(dev); return false; } + DWORD ret = 0; + BOOL ok = DeviceIoControl(dev, IOCTL_GVM_TAIL_LOG, &in, sizeof(in), out, sizeof(*out), &ret, nullptr); + CloseHandle(dev); + if (!ok) { free(out); return false; } + last_seq = out->next_seq; + dropped = out->dropped; + for (unsigned int i = 0; i < out->count; i++) { + SYSTEMTIME sys; FILETIME ft, lft; + ft.dwLowDateTime = (DWORD) out->entries[i].timestamp_100ns; + ft.dwHighDateTime = (DWORD)(out->entries[i].timestamp_100ns >> 32); + FileTimeToLocalFileTime(&ft, &lft); + FileTimeToSystemTime(&lft, &sys); + LogEntry e; + e.ts = QString::asprintf("%02u:%02u:%02u.%03u", sys.wHour, sys.wMinute, sys.wSecond, sys.wMilliseconds); + e.line = QString::fromUtf8(out->entries[i].line); + e.kind = classify_line(e.line); + out_entries.push_back(e); + } + free(out); + return true; +} + +bool DriverClient::service_present() const +{ + bool present = false; + SC_HANDLE scm = OpenSCManagerA(nullptr, nullptr, SC_MANAGER_CONNECT); + if (!scm) return false; + SC_HANDLE svc = OpenServiceA(scm, "Goodmans", SERVICE_QUERY_STATUS); + if (svc) { present = true; CloseServiceHandle(svc); } + CloseServiceHandle(scm); + return present; +} + +bool DriverClient::service_running() const +{ + bool running = false; + SC_HANDLE scm = OpenSCManagerA(nullptr, nullptr, SC_MANAGER_CONNECT); + if (!scm) return false; + SC_HANDLE svc = OpenServiceA(scm, "Goodmans", SERVICE_QUERY_STATUS); + if (svc) { + SERVICE_STATUS_PROCESS ss{}; DWORD needed = 0; + if (QueryServiceStatusEx(svc, SC_STATUS_PROCESS_INFO, (LPBYTE)&ss, sizeof(ss), &needed)) + running = (ss.dwCurrentState == SERVICE_RUNNING); + CloseServiceHandle(svc); + } + CloseServiceHandle(scm); + return running; +} + +void DriverClient::run_sc(const QString& args) +{ + QByteArray a = args.toLocal8Bit(); + SHELLEXECUTEINFOA si{ sizeof(si) }; + si.lpVerb = "runas"; si.lpFile = "sc.exe"; si.lpParameters = a.constData(); + si.nShow = SW_HIDE; si.fMask = SEE_MASK_NOCLOSEPROCESS; + if (ShellExecuteExA(&si) && si.hProcess) { + WaitForSingleObject(si.hProcess, 8000); + CloseHandle(si.hProcess); + } +} + +bool DriverClient::read_guest(quint32 module_id, quint32 offset, quint32 len, QByteArray& out, QString& err) +{ + HANDLE dev = dev_open(); + if (dev == INVALID_HANDLE_VALUE) { err = "device closed"; return false; } + gvm_read_guest_in in{ module_id, offset, len }; + auto* r = (gvm_read_guest_out*)calloc(1, sizeof(gvm_read_guest_out)); + if (!r) { CloseHandle(dev); err = "oom"; return false; } + DWORD ret = 0; + BOOL ok = DeviceIoControl(dev, IOCTL_GVM_READ_GUEST, &in, sizeof(in), r, sizeof(*r), &ret, nullptr); + CloseHandle(dev); + if (!ok) { err = "ioctl failed"; free(r); return false; } + if (r->status) { err = QString::fromUtf8(r->err_msg); free(r); return false; } + out = QByteArray((const char*)r->data, (int)r->length); + free(r); + return true; +} + +bool DriverClient::tail_trace(quint64& last_seq, QVector& out, quint32& dropped) +{ + HANDLE dev = dev_open(); + if (dev == INVALID_HANDLE_VALUE) return false; + gvm_trace_in in{ last_seq }; + auto* r = (gvm_trace_out*)malloc(sizeof(gvm_trace_out)); + if (!r) { CloseHandle(dev); return false; } + DWORD ret = 0; + BOOL ok = DeviceIoControl(dev, IOCTL_GVM_TAIL_TRACE, &in, sizeof(in), r, sizeof(*r), &ret, nullptr); + CloseHandle(dev); + if (!ok) { free(r); return false; } + last_seq = r->next_seq; + dropped = r->dropped; + for (unsigned int i = 0; i < r->count; i++) { + SYSTEMTIME sys; FILETIME ft, lft; + ft.dwLowDateTime = (DWORD) r->entries[i].timestamp_100ns; + ft.dwHighDateTime = (DWORD)(r->entries[i].timestamp_100ns >> 32); + FileTimeToLocalFileTime(&ft, &lft); + FileTimeToSystemTime(&lft, &sys); + TraceEvent e; + e.timestamp_100ns = r->entries[i].timestamp_100ns; + e.ts = QString::asprintf("%02u:%02u:%02u.%03u", sys.wHour, sys.wMinute, sys.wSecond, sys.wMilliseconds); + e.module_id = r->entries[i].module_id; + e.kind = r->entries[i].kind; + e.thread_id = r->entries[i].thread_id; + e.irql = r->entries[i].irql; + e.name = QString::fromUtf8(r->entries[i].name); + for (unsigned int a = 0; a < r->entries[i].argc && a < 4; a++) + e.argv.push_back(r->entries[i].argv[a]); + e.rv = r->entries[i].rv; + out.push_back(e); + } + free(r); + return true; +} + +bool DriverClient::trace_ctl(int mode, quint32 module_id) +{ + HANDLE dev = dev_open(); + if (dev == INVALID_HANDLE_VALUE) return false; + gvm_trace_ctl_in in{ (unsigned int)mode, module_id }; + DWORD ret = 0; + BOOL ok = DeviceIoControl(dev, IOCTL_GVM_TRACE_CTL, &in, sizeof(in), nullptr, 0, &ret, nullptr); + CloseHandle(dev); + return ok; +} + +bool DriverClient::notify_stop() +{ + HANDLE dev = dev_open(); + if (dev == INVALID_HANDLE_VALUE) return false; + DWORD ret = 0; + BOOL ok = DeviceIoControl(dev, IOCTL_GVM_NOTIFY_STOP, nullptr, 0, nullptr, 0, &ret, nullptr); + CloseHandle(dev); + return ok; +} diff --git a/gui-qt/driver.h b/gui-qt/driver.h new file mode 100644 index 0000000..ff1e1f1 --- /dev/null +++ b/gui-qt/driver.h @@ -0,0 +1,72 @@ +#pragma once +#include +#include +#include +#include +#include + +struct ModuleRow { + quint32 id{}; + QString name; + quint32 wasm_size{}; + quint32 mem_pages{}; + quint64 pool_bytes{}; + quint64 hash{}; +}; + +struct ModuleInfo { + ModuleRow base; + quint32 caps{0}; + QVector exports; + QVector imports; + bool valid{false}; +}; + +struct LogEntry { + QString ts; + QString line; + int kind{0}; +}; + +enum LogKind { EVT_MSG, EVT_LOAD, EVT_CALL, EVT_UNLOAD, EVT_PROC, EVT_EXIT, EVT_IMAGE, EVT_ERROR }; + +// trace kinds mirror driver-side GVM_TRK_* +enum TraceKind { TRK_IMPORT = 1, TRK_CALL = 2, TRK_RETURN = 3, TRK_TRAP = 4 }; + +struct TraceEvent { + quint64 timestamp_100ns; // raw for delta-time math + QString ts; // human-readable + quint32 module_id; + quint32 kind; + quint32 thread_id; + quint32 irql; + QString name; + QVector argv; + quint64 rv; +}; + +enum TraceMode { TRACE_OFF = 0, TRACE_ON_ALL = 1, TRACE_ON_MODULE = 2 }; + +class DriverClient : public QObject { + Q_OBJECT +public: + explicit DriverClient(QObject* parent = nullptr); + + bool device_open() const; + bool refresh_modules(QVector& out); + bool module_info(quint32 id, ModuleInfo& out); + bool load_module(const QString& path, quint64 budget, quint32& out_id, QString& err); + bool call_export(quint32 id, const QString& exp, quint32 timeout_ms, const QVector& argv, quint64& rv, QString& err); + bool unload(quint32 id); + bool unload_all(); + bool tail_log(quint64& last_seq, QVector& out, quint32& dropped); + + bool service_present() const; + bool service_running() const; + void run_sc(const QString& args); + + bool read_guest(quint32 module_id, quint32 offset, quint32 len, QByteArray& out, QString& err); + bool tail_trace(quint64& last_seq, QVector& out, quint32& dropped); + bool trace_ctl(int mode, quint32 module_id = 0); + bool notify_stop(); +}; diff --git a/gui-qt/main.cpp b/gui-qt/main.cpp new file mode 100644 index 0000000..ee1b10d --- /dev/null +++ b/gui-qt/main.cpp @@ -0,0 +1,25 @@ +#include +#include +#include +#include +#include "mainwindow.h" + +int main(int argc, char** argv) +{ + QApplication::setStyle("Fusion"); + QApplication app(argc, argv); + app.setOrganizationName("goodmans"); + app.setApplicationName("The Goodmans Kernel"); + + QFontDatabase::addApplicationFont(":/fonts/segoeui"); + + QFile qss(":/style.qss"); + if (qss.open(QIODevice::ReadOnly | QIODevice::Text)) { + app.setStyleSheet(QString::fromUtf8(qss.readAll())); + qss.close(); + } + + MainWindow w; + w.show(); + return app.exec(); +} diff --git a/gui-qt/mainwindow.cpp b/gui-qt/mainwindow.cpp new file mode 100644 index 0000000..3ec6eb7 --- /dev/null +++ b/gui-qt/mainwindow.cpp @@ -0,0 +1,522 @@ +#include "mainwindow.h" +#include "command_palette.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +MainWindow::MainWindow() +{ + drv = new DriverClient(this); + + setWindowTitle("The Goodmans Kernel"); + resize(1500, 940); + setMinimumSize(1100, 720); + + // menu bar + auto* mb = menuBar(); + auto* fileMenu = mb->addMenu("&File"); + auto* actLoad = fileMenu->addAction("&Load module…"); + actLoad->setShortcut(QKeySequence("Ctrl+O")); + connect(actLoad, &QAction::triggered, this, &MainWindow::load_wasm_from_dialog); + recentMenu = fileMenu->addMenu("Open &Recent"); + fileMenu->addSeparator(); + auto* actPal = fileMenu->addAction("Command &Palette…"); + actPal->setShortcut(QKeySequence("Ctrl+K")); + connect(actPal, &QAction::triggered, this, &MainWindow::open_command_palette); + fileMenu->addSeparator(); + auto* actQuit = fileMenu->addAction("E&xit"); + actQuit->setShortcut(QKeySequence("Ctrl+Q")); + connect(actQuit, &QAction::triggered, qApp, &QApplication::quit); + + auto* viewMenu = mb->addMenu("&View"); + QStringList page_labels = { "Modules", "Explorer", "Trace", "Profiler", "Events", "Log", "Memory", "Deploy" }; + for (int i = 0; i < page_labels.size(); i++) { + auto* a = viewMenu->addAction(QString("&%1 %2").arg(i + 1).arg(page_labels[i])); + a->setShortcut(QKeySequence(QString("Ctrl+%1").arg(i + 1))); + connect(a, &QAction::triggered, this, [this, i]{ go_to_page(i); }); + } + viewMenu->addSeparator(); + auto* actRefresh = viewMenu->addAction("&Refresh"); + actRefresh->setShortcut(QKeySequence("F5")); + connect(actRefresh, &QAction::triggered, this, &MainWindow::tick_slow); + + auto* driverMenu = mb->addMenu("&Driver"); + auto* actStart = driverMenu->addAction("Start service"); + connect(actStart, &QAction::triggered, this, [this]{ drv->run_sc("start Goodmans"); tick_slow(); }); + auto* actStop = driverMenu->addAction("Stop service"); + connect(actStop, &QAction::triggered, this, [this]{ drv->run_sc("stop Goodmans"); tick_slow(); }); + auto* actRestart = driverMenu->addAction("Restart service"); + connect(actRestart, &QAction::triggered, this, [this]{ + drv->run_sc("stop Goodmans"); drv->run_sc("start Goodmans"); tick_slow(); + }); + + auto* toolsMenu = mb->addMenu("&Tools"); + auto* actPalT = toolsMenu->addAction("Command palette"); + actPalT->setShortcut(QKeySequence("Ctrl+K")); + connect(actPalT, &QAction::triggered, this, &MainWindow::open_command_palette); + + auto* helpMenu = mb->addMenu("&Help"); + auto* actAbout = helpMenu->addAction("&About The Goodmans Kernel"); + connect(actAbout, &QAction::triggered, this, &MainWindow::open_about); + + // toolbar (x64dbg / IDA style row of textual actions) + auto* tbar = addToolBar("Main"); + tbar->setObjectName("mainToolbar"); + tbar->setMovable(false); + tbar->setFloatable(false); + tbar->addAction(actLoad); + auto* actUnloadAll = tbar->addAction("Unload all"); + connect(actUnloadAll, &QAction::triggered, this, [this]{ drv->unload_all(); tick_slow(); }); + tbar->addSeparator(); + tbar->addAction(actStart); + tbar->addAction(actStop); + tbar->addAction(actRestart); + tbar->addSeparator(); + tbar->addAction(actRefresh); + tbar->addSeparator(); + tbar->addAction(actPalT); + + // tab bar as main navigation + tabs = new QTabWidget; + tabs->setObjectName("mainTabs"); + tabs->setDocumentMode(true); + tabs->setUsesScrollButtons(false); + setCentralWidget(tabs); + + workbench = new WorkbenchPage(drv); + explorerp = new ExplorerPage(drv); + tracep = new TracePage(drv); + profilerp = new ProfilerPage; + events = new EventsPage(drv); + connect(events, &EventsPage::toast, this, &MainWindow::show_toast); + logp = new LogPage; + memory = new MemoryPage(drv); + deploy = new DeployPage(drv); + tabs->addTab(workbench, "Modules"); + tabs->addTab(explorerp, "Explorer"); + tabs->addTab(tracep, "Trace"); + tabs->addTab(profilerp, "Profiler"); + tabs->addTab(events, "Events"); + tabs->addTab(logp, "Log"); + tabs->addTab(memory, "Memory"); + tabs->addTab(deploy, "Deploy"); + tabs->setCurrentIndex(0); + + // status bar (permanent widgets on the right, temporary toasts on the left) + statDevice = new QLabel; + statSvc = new QLabel; + statCount = new QLabel; + for (auto* l : { statDevice, statSvc, statCount }) l->setStyleSheet("font-size:11.5px;"); + set_stat(statDevice, "driver ?", "#808080", "#666666"); + set_stat(statSvc, "service ?", "#808080", "#666666"); + set_stat(statCount, "0 modules", "#808080", ""); + auto* sep1 = new QLabel(" | "); sep1->setStyleSheet("color:#444444;"); + auto* sep2 = new QLabel(" | "); sep2->setStyleSheet("color:#444444;"); + statusBar()->addPermanentWidget(statDevice); + statusBar()->addPermanentWidget(sep1); + statusBar()->addPermanentWidget(statSvc); + statusBar()->addPermanentWidget(sep2); + statusBar()->addPermanentWidget(statCount); + + // wiring + connect(tabs, &QTabWidget::currentChanged, this, &MainWindow::on_nav_changed); + connect(workbench, &WorkbenchPage::moduleSelected, this, &MainWindow::on_module_selected); + connect(workbench, &WorkbenchPage::moduleLoaded, this, &MainWindow::on_module_loaded); + connect(workbench, &WorkbenchPage::toast, this, &MainWindow::show_toast); + connect(deploy, &DeployPage::stateChanged, this, &MainWindow::on_state_changed); + connect(deploy, &DeployPage::toast, this, &MainWindow::show_toast); + connect(memory, &MemoryPage::toast, this, &MainWindow::show_toast); + connect(tracep, &TracePage::toast, this, &MainWindow::show_toast); + connect(explorerp, &ExplorerPage::toast, this, &MainWindow::show_toast); + + connect(&fast_timer, &QTimer::timeout, this, &MainWindow::tick_fast); + connect(&slow_timer, &QTimer::timeout, this, &MainWindow::tick_slow); + fast_timer.start(300); + slow_timer.start(2000); + + // hot reload watcher (guest .wasm and driver .sys) + hot_watcher = new QFileSystemWatcher(this); + connect(hot_watcher, &QFileSystemWatcher::fileChanged, this, &MainWindow::on_file_changed); + + // auto-watch the driver binary too; on change: stop/start service + QString here_dir = QCoreApplication::applicationDirPath(); + QStringList driver_candidates = { + here_dir + "/../Goodmans.sys", // same layout as deploy/ + here_dir + "/../../Goodmans.sys", + }; + for (const auto& p : driver_candidates) { + QString abs = QFileInfo(p).absoluteFilePath(); + if (QFile::exists(abs)) { hot_watcher->addPath(abs); break; } + } + + // global shortcuts + auto* palSc = new QShortcut(QKeySequence("Ctrl+K"), this); + connect(palSc, &QShortcut::activated, this, &MainWindow::open_command_palette); + auto* findSc = new QShortcut(QKeySequence("Ctrl+F"), this); + connect(findSc, &QShortcut::activated, this, [this]{ + if (tabs->currentIndex() == 2) tracep->focusFilter(); + }); + + load_settings(); + rebuild_recent_menu(); + tick_slow(); + + // always land on Modules on launch, regardless of saved page + tabs->setCurrentIndex(0); +} + +void MainWindow::closeEvent(QCloseEvent* e) +{ + save_settings(); + QMainWindow::closeEvent(e); +} + +void MainWindow::set_stat(QLabel* lbl, const QString& txt, const QString& color, const QString& dot_color) +{ + if (!dot_color.isEmpty()) { + lbl->setText(QString(" %3") + .arg(dot_color, color, txt.toHtmlEscaped())); + } else { + lbl->setText(QString("%2").arg(color, txt.toHtmlEscaped())); + } +} + +void MainWindow::on_nav_changed(int row) +{ + if (row >= 0) tabs->setCurrentIndex(row); +} + +void MainWindow::go_to_page(int i) +{ + if (i >= 0 && i < tabs->count()) tabs->setCurrentIndex(i); +} + +void MainWindow::on_module_selected(quint32 id) +{ + ModuleInfo info; drv->module_info(id, info); + workbench->setSelected(id, info); +} + +void MainWindow::on_module_loaded(quint32 id) +{ + QVector mods; drv->refresh_modules(mods); + workbench->refreshModules(mods); + ModuleInfo info; drv->module_info(id, info); + workbench->setSelected(id, info); + tabs->setCurrentIndex(0); + + for (const auto& m : mods) { + if (m.id == id) { + add_recent(m.name); + if (QFile::exists(m.name)) { + path_to_module.insert(m.name, id); + hot_watcher->addPath(m.name); + } + break; + } + } +} + +void MainWindow::on_state_changed() { tick_slow(); } + +void MainWindow::show_toast(const QString& msg, int kind) +{ + statusBar()->showMessage(msg, 4000); + statusBar()->setStyleSheet( + QString("QStatusBar { color: %1; font-size:12px; padding-left:8px; }") + .arg(kind == 1 ? "#cd5c5c" : (kind == 2 ? "#8fbf5c" : "#d4d4d4"))); +} + +void MainWindow::tick_fast() +{ + QVector entries; quint32 dropped = 0; + if (drv->tail_log(log_seq, entries, dropped) && !entries.isEmpty()) { + logp->append(entries, log_seq, dropped); + events->append(entries); + } + QVector tr; quint32 tdrop = 0; + if (drv->tail_trace(trace_seq, tr, tdrop) && !tr.isEmpty()) { + tracep->append(tr); + profilerp->observe(tr); + } +} + +void MainWindow::tick_slow() +{ + bool dev = drv->device_open(); + bool pres = drv->service_present(); + bool run = drv->service_running(); + set_stat(statDevice, dev ? "driver loaded" : "driver not loaded", + "#cccccc", dev ? "#8fbf5c" : "#cd5c5c"); + set_stat(statSvc, run ? "service running" : (pres ? "service stopped" : "service not installed"), + "#cccccc", run ? "#8fbf5c" : (pres ? "#e5b055" : "#cd5c5c")); + QVector mods; drv->refresh_modules(mods); + workbench->refreshModules(mods); + int user_count = 0; + for (const auto& m : mods) + if (QFileInfo(m.name).fileName().compare("toolkit.wasm", Qt::CaseInsensitive) != 0) + user_count++; + set_stat(statCount, QString("%1 modules").arg(user_count), "#999999", ""); + deploy->refresh(dev, pres, run); +} + +void MainWindow::load_wasm_from_dialog() +{ + QString path = QFileDialog::getOpenFileName(this, "Load module", QString(), "WebAssembly (*.wasm)"); + if (path.isEmpty()) return; + quint32 id = 0; QString err; + if (drv->load_module(path, 0, id, err)) { + show_toast(QString("loaded module %1").arg(id), 2); + on_module_loaded(id); + } else { + show_toast("load failed: " + err, 1); + } +} + +void MainWindow::reload_current_selection() +{ + // reload every tracked wasm from disk + for (auto it = path_to_module.begin(); it != path_to_module.end(); ++it) { + drv->unload(it.value()); + } + QHash newmap; + for (auto it = path_to_module.begin(); it != path_to_module.end(); ++it) { + quint32 id = 0; QString err; + if (drv->load_module(it.key(), 0, id, err)) { + newmap.insert(it.key(), id); + } + } + path_to_module = newmap; + tick_slow(); + show_toast(QString("reloaded %1 modules").arg(newmap.size()), 2); +} + +void MainWindow::on_file_changed(const QString& path) +{ + // some editors atomically-replace on write; watcher stops after that. re-add. + if (QFile::exists(path)) hot_watcher->addPath(path); + + // driver .sys changed: restart service + if (path.endsWith("Goodmans.sys", Qt::CaseInsensitive)) { + show_toast("driver changed - restarting service", 0); + drv->run_sc("stop Goodmans"); + drv->run_sc("start Goodmans"); + // reload every previously-loaded wasm since the driver forgot them + QHash old = path_to_module; + path_to_module.clear(); + for (auto it = old.begin(); it != old.end(); ++it) { + quint32 id = 0; QString err; + if (drv->load_module(it.key(), 0, id, err)) { + path_to_module.insert(it.key(), id); + if (it.key().endsWith("toolkit.wasm", Qt::CaseInsensitive)) + explorerp->setToolkitId((int)id); + } + } + tick_slow(); + show_toast(QString("driver restarted, %1 modules reloaded").arg(path_to_module.size()), 2); + return; + } + + if (!path_to_module.contains(path)) return; + quint32 old_id = path_to_module.value(path); + drv->unload(old_id); + quint32 new_id = 0; QString err; + if (drv->load_module(path, 0, new_id, err)) { + path_to_module.insert(path, new_id); + if (path.endsWith("toolkit.wasm", Qt::CaseInsensitive)) + explorerp->setToolkitId((int)new_id); + show_toast(QString("hot reload: %1: module %2").arg(QFileInfo(path).fileName()).arg(new_id), 2); + tick_slow(); + } else { + show_toast("hot reload failed: " + err, 1); + } +} + +void MainWindow::add_recent(const QString& path) +{ + recent_files.removeAll(path); + recent_files.prepend(path); + while (recent_files.size() > 10) recent_files.removeLast(); + rebuild_recent_menu(); +} + +void MainWindow::rebuild_recent_menu() +{ + if (!recentMenu) return; + recentMenu->clear(); + if (recent_files.isEmpty()) { + auto* a = recentMenu->addAction("(no recent files)"); + a->setEnabled(false); + return; + } + for (const auto& p : recent_files) { + QString label = QFileInfo(p).fileName(); + auto* a = recentMenu->addAction(label); + a->setToolTip(p); + connect(a, &QAction::triggered, this, [this, p]{ + if (!QFile::exists(p)) { show_toast("file no longer exists", 1); return; } + quint32 id = 0; QString err; + if (drv->load_module(p, 0, id, err)) { show_toast(QString("loaded %1").arg(id), 2); on_module_loaded(id); } + else show_toast(err, 1); + }); + } + recentMenu->addSeparator(); + auto* clr = recentMenu->addAction("Clear recent"); + connect(clr, &QAction::triggered, this, [this]{ recent_files.clear(); rebuild_recent_menu(); }); +} + +void MainWindow::save_settings() +{ + QSettings s("goodmans", "gui"); + s.setValue("geometry", saveGeometry()); + s.setValue("state", saveState()); + s.setValue("page", tabs->currentIndex()); + s.setValue("recent", recent_files); +} + +void MainWindow::load_settings() +{ + QSettings s("goodmans", "gui"); + if (s.contains("geometry")) restoreGeometry(s.value("geometry").toByteArray()); + if (s.contains("state")) restoreState(s.value("state").toByteArray()); + recent_files = s.value("recent").toStringList(); +} + +void MainWindow::open_about() +{ + QDialog dlg(this); + dlg.setWindowTitle("About The Goodmans Kernel"); + dlg.setFixedSize(560, 520); + dlg.setStyleSheet("QDialog { background: #1c1c1c; }"); + + auto* v = new QVBoxLayout(&dlg); + v->setContentsMargins(28, 24, 28, 20); + v->setSpacing(4); + + auto* title = new QLabel("The Goodmans Kernel"); + title->setStyleSheet("color:#ffffff; font-size:22px; font-weight:600;"); + v->addWidget(title); + + auto* tag = new QLabel("Signed WDM driver embedding wasm3."); + tag->setStyleSheet("color:#8b8b8b; font-size:12.5px;"); + v->addWidget(tag); + + auto* sep = new QFrame; sep->setFrameShape(QFrame::HLine); + sep->setStyleSheet("background:#2c2c2c; max-height:1px; border:none; margin-top:14px; margin-bottom:14px;"); + v->addWidget(sep); + + auto* project = new QLabel( + "

" + "Loads unsigned .wasm modules into a signed kernel driver." + "

" + ); + project->setWordWrap(true); + project->setTextFormat(Qt::RichText); + v->addWidget(project); + + v->addSpacing(6); + + auto* authorLbl = new QLabel("Author"); + authorLbl->setStyleSheet("color:#8b8b8b; font-size:11px; font-weight:600;"); + v->addWidget(authorLbl); + auto* author = new QLabel( + "

" + "zer0condition" + "

" + ); + author->setWordWrap(true); + author->setTextFormat(Qt::RichText); + v->addWidget(author); + + v->addSpacing(6); + + auto* stackLbl = new QLabel("Built with"); + stackLbl->setStyleSheet("color:#8b8b8b; font-size:11px; font-weight:600;"); + v->addWidget(stackLbl); + auto* stack = new QLabel( + "

" + "• wasm3 , MIT, Volodymyr Shymanskyy / Steven Massey
" + "• Qt 6 , LGPLv3, The Qt Company
" + "• JetBrainsMono , OFL, JetBrains" + "

" + ); + stack->setTextFormat(Qt::RichText); + v->addWidget(stack); + + v->addStretch(); + + auto* btnRow = new QHBoxLayout; + btnRow->addStretch(); + auto* close = new QPushButton("Close"); + close->setObjectName("btnPrimary"); + close->setDefault(true); + close->setFixedWidth(96); + btnRow->addWidget(close); + v->addLayout(btnRow); + + connect(close, &QPushButton::clicked, &dlg, &QDialog::accept); + dlg.exec(); +} + +void MainWindow::open_command_palette() +{ + QVector actions; + // navigation + QStringList pages = { "Modules", "Explorer", "Trace", "Profiler", "Events", "Log", "Memory", "Deploy" }; + for (int i = 0; i < pages.size(); i++) + actions.push_back({ "Go: " + pages[i], QString("Ctrl+%1").arg(i + 1), [this, i]{ go_to_page(i); }}); + + actions.push_back({ "Load module…", "Ctrl+O", [this]{ load_wasm_from_dialog(); }}); + actions.push_back({ "Reload all modules", "", [this]{ reload_current_selection(); }}); + actions.push_back({ "Unload all modules", "", [this]{ drv->unload_all(); tick_slow(); show_toast("unloaded all", 2); }}); + actions.push_back({ "Refresh state", "F5", [this]{ tick_slow(); }}); + + actions.push_back({ "Trace: Start / Stop", "F5 in trace", [this]{ go_to_page(2) /* trace */; tracep->toggleTracing(); }}); + actions.push_back({ "Trace: Focus filter", "Ctrl+F", [this]{ go_to_page(2) /* trace */; tracep->focusFilter(); }}); + actions.push_back({ "Trace: Save…", "Ctrl+S", [this]{ + go_to_page(2) /* trace */; + QString p = QFileDialog::getSaveFileName(this, "Save trace", "trace.gtrace", + "Goodmans trace (*.gtrace);;CSV (*.csv)"); + if (!p.isEmpty()) tracep->saveTraceFile(p); + }}); + actions.push_back({ "Trace: Open .gtrace…", "", [this]{ + go_to_page(2) /* trace */; + QString p = QFileDialog::getOpenFileName(this, "Open trace", QString(), "Goodmans trace (*.gtrace)"); + if (!p.isEmpty()) tracep->loadTraceFile(p); + }}); + actions.push_back({ "Trace: Toggle bookmark", "Ctrl+B", [this]{ go_to_page(2) /* trace */; tracep->toggleBookmark(); }}); + + actions.push_back({ "Driver: Start service", "", [this]{ drv->run_sc("start Goodmans"); tick_slow(); }}); + actions.push_back({ "Driver: Stop service", "", [this]{ drv->run_sc("stop Goodmans"); tick_slow(); }}); + actions.push_back({ "Help: About", "", [this]{ open_about(); }}); + + for (const auto& p : recent_files) { + QString label = "Open recent: " + QFileInfo(p).fileName(); + actions.push_back({ label, "", [this, p]{ + if (!QFile::exists(p)) { show_toast("file gone", 1); return; } + quint32 id = 0; QString err; + if (drv->load_module(p, 0, id, err)) { on_module_loaded(id); show_toast(QString("loaded %1").arg(id), 2); } + else show_toast(err, 1); + }}); + } + + CommandPalette p(this, actions); + QRect g = geometry(); + p.move(g.center().x() - p.width() / 2, g.top() + 80); + p.exec(); +} diff --git a/gui-qt/mainwindow.h b/gui-qt/mainwindow.h new file mode 100644 index 0000000..7de49bb --- /dev/null +++ b/gui-qt/mainwindow.h @@ -0,0 +1,76 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "driver.h" +#include "pages.h" + +class MainWindow : public QMainWindow { + Q_OBJECT +public: + MainWindow(); +private slots: + void tick_fast(); + void tick_slow(); + void on_module_selected(quint32 id); + void on_module_loaded(quint32 id); + void on_state_changed(); + void on_nav_changed(int row); + void show_toast(const QString& msg, int kind); +private: + DriverClient* drv; + QTabWidget* tabs; + WorkbenchPage* workbench; + MemoryPage* memory; + EventsPage* events; + LogPage* logp; + TracePage* tracep; + ExplorerPage* explorerp; + ProfilerPage* profilerp; + DeployPage* deploy; + QTimer fast_timer; + QTimer slow_timer; + + // status + QLabel* statDevice; + QLabel* statSvc; + QLabel* statCount; + + // log state + quint64 log_seq = 0; + quint64 trace_seq = 0; + + void set_stat(QLabel* lbl, const QString& txt, const QString& color, const QString& dot_color); + + // shortcuts + palette + void open_command_palette(); + void open_about(); + void load_wasm_from_dialog(); + void reload_current_selection(); + void go_to_page(int i); + + // hot reload + QFileSystemWatcher* hot_watcher = nullptr; + QHash path_to_module; + void on_file_changed(const QString& path); + + // recent files + QMenu* recentMenu = nullptr; + QStringList recent_files; + void add_recent(const QString& path); + void rebuild_recent_menu(); + + // session persistence + void save_settings(); + void load_settings(); + +protected: + void closeEvent(QCloseEvent* e) override; +}; diff --git a/gui-qt/pages.cpp b/gui-qt/pages.cpp new file mode 100644 index 0000000..945546a --- /dev/null +++ b/gui-qt/pages.cpp @@ -0,0 +1,1746 @@ +#include "pages.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static QLabel* section_label(const QString& t) +{ + auto* l = new QLabel(t); l->setObjectName("sectionLabel"); return l; +} + +static QLabel* h2(const QString& t) +{ + auto* l = new QLabel(t); l->setObjectName("h2"); return l; +} + +static QFrame* card() +{ + auto* f = new QFrame; f->setObjectName("card"); f->setFrameShape(QFrame::NoFrame); return f; +} + +static QLabel* pill(const QString& t, const QString& kind = "") +{ + auto* l = new QLabel(t); + l->setObjectName("pill"); + if (!kind.isEmpty()) l->setProperty("kind", kind); + return l; +} + +// ModulesPage + +// WorkbenchPage + +static const char* CAP_NAMES[] = { + "ALLOC","READ_KMEM","WRITE_KMEM","MSR_R","MSR_W","PHYSMEM","CPUID_TSC","CALLBACKS","HOSTCALL","INTROSPECT" +}; + +WorkbenchPage::WorkbenchPage(DriverClient* d, QWidget* p) : QWidget(p), drv(d) +{ + auto* root = new QVBoxLayout(this); + root->setContentsMargins(10, 10, 10, 8); + root->setSpacing(14); + + + // load bar - full width, minimal + auto* loadRow = new QHBoxLayout; + loadRow->setSpacing(8); + loadPath = new QLineEdit; + loadPath->setPlaceholderText("path to .wasm (or drag file here / Ctrl+O)"); + auto* browseBtn = new QPushButton("Browse"); + auto* loadBtn = new QPushButton("Load"); loadBtn->setObjectName("btnPrimary"); + loadBudget = new QSpinBox; + loadBudget->setRange(0, 1 << 30); + loadBudget->setToolTip("pool budget in bytes, 0 = 4MB default"); + loadBudget->setFixedWidth(120); + loadBudget->setSpecialValueText("default pool"); + loadRow->addWidget(loadPath, 1); + loadRow->addWidget(loadBudget); + loadRow->addWidget(browseBtn); + loadRow->addWidget(loadBtn); + root->addLayout(loadRow); + + connect(browseBtn, &QPushButton::clicked, this, [this]{ + QString p = QFileDialog::getOpenFileName(this, "Select .wasm", QString(), "WebAssembly (*.wasm);;All (*.*)"); + if (!p.isEmpty()) loadPath->setText(p); + }); + connect(loadBtn, &QPushButton::clicked, this, [this]{ + if (loadPath->text().isEmpty()) { emit toast("pick a .wasm first", 1); return; } + quint32 id = 0; QString err; + if (drv->load_module(loadPath->text(), loadBudget->value(), id, err)) { + pending_select = id; + emit toast(QString("loaded module %1").arg(id), 2); + emit moduleLoaded(id); + } else { + emit toast(QString("load failed: %1").arg(err), 1); + } + }); + connect(loadPath, &QLineEdit::returnPressed, loadBtn, &QPushButton::click); + + // horizontal split: module list left, inspector+invoke right + auto* mainSplit = new QSplitter(Qt::Horizontal); + mainSplit->setChildrenCollapsible(false); + mainSplit->setObjectName("wbSplit"); + + // LEFT: module list + auto* leftCol = new QWidget; + auto* lcv = new QVBoxLayout(leftCol); + lcv->setContentsMargins(0, 0, 0, 0); + lcv->setSpacing(8); + auto* leftHead = new QHBoxLayout; leftHead->setContentsMargins(2, 0, 2, 0); + auto* leftLbl = new QLabel("Loaded"); leftLbl->setObjectName("h2"); + moduleCount = new QLabel("0"); moduleCount->setObjectName("dim"); + auto* unloadAll = new QPushButton("Unload all"); + unloadAll->setToolTip("Unload every module"); + leftHead->addWidget(leftLbl); + leftHead->addWidget(moduleCount); + leftHead->addStretch(); + leftHead->addWidget(unloadAll); + lcv->addLayout(leftHead); + + moduleList = new QListWidget; + moduleList->setObjectName("nav"); // reuse muted nav style + moduleList->setFocusPolicy(Qt::StrongFocus); + moduleList->setFrameShape(QFrame::NoFrame); + lcv->addWidget(moduleList, 1); + mainSplit->addWidget(leftCol); + + connect(unloadAll, &QPushButton::clicked, this, [this]{ + drv->unload_all(); + clearSelection(); + emit toast("unloaded all modules", 2); + }); + connect(moduleList, &QListWidget::currentItemChanged, this, [this](QListWidgetItem* cur, QListWidgetItem*){ + if (!cur) return; + quint32 id = cur->data(Qt::UserRole).toUInt(); + emit moduleSelected(id); + }); + + // RIGHT: inspector + invoke wrapper (empty state OR infoPanel) + auto* rightCol = new QWidget; + auto* rcv = new QVBoxLayout(rightCol); + rcv->setContentsMargins(0, 0, 0, 0); + rcv->setSpacing(0); + + emptyPanel = new QWidget; + auto* el = new QVBoxLayout(emptyPanel); + el->setContentsMargins(0, 60, 0, 0); + auto* emptyLbl = new QLabel("Select a module from the list on the left,\nor load a .wasm above."); + emptyLbl->setAlignment(Qt::AlignCenter); + emptyLbl->setObjectName("emptyState"); + el->addWidget(emptyLbl); + rcv->addWidget(emptyPanel); + + infoPanel = new QWidget; + auto* ipl = new QVBoxLayout(infoPanel); + ipl->setContentsMargins(0, 0, 0, 0); + ipl->setSpacing(14); + + // module header card with unload button + auto* headCard = card(); + auto* hl = new QHBoxLayout(headCard); + hl->setContentsMargins(20, 14, 16, 14); + hl->setSpacing(10); + auto* headText = new QVBoxLayout; headText->setSpacing(2); + modTitle = new QLabel; modTitle->setObjectName("moduleTitle"); + modMeta = new QLabel; modMeta->setObjectName("mono"); modMeta->setStyleSheet("color:#7b818c;"); + headText->addWidget(modTitle); + headText->addWidget(modMeta); + hl->addLayout(headText, 1); + auto* unloadOne = new QPushButton("Unload"); + unloadOne->setObjectName("btnDanger"); + hl->addWidget(unloadOne, 0, Qt::AlignTop); + ipl->addWidget(headCard); + connect(unloadOne, &QPushButton::clicked, this, [this]{ + if (!current.valid) return; + quint32 id = current.base.id; + drv->unload(id); + clearSelection(); + emit toast(QString("unloaded module %1").arg(id), 2); + }); + + // caps card + auto* capCard = card(); + auto* cl = new QVBoxLayout(capCard); + cl->setContentsMargins(20, 14, 20, 14); + cl->setSpacing(8); + cl->addWidget(section_label("Capabilities")); + capsBar = new QWidget; + auto* capsLay = new QHBoxLayout(capsBar); + capsLay->setContentsMargins(0, 0, 0, 0); + capsLay->setSpacing(6); + capsLay->addStretch(); + cl->addWidget(capsBar); + ipl->addWidget(capCard); + + // inner split: exports/imports left, invoke right + auto* split = new QSplitter(Qt::Horizontal); + split->setChildrenCollapsible(false); + split->setObjectName("wbSplit"); + split->setHandleWidth(3); + + // exports/imports tabs card + auto* eiCard = card(); + auto* eil = new QVBoxLayout(eiCard); + eil->setContentsMargins(0, 0, 0, 0); + auto* tabs = new QTabWidget; + tabs->setObjectName("wbTabs"); + expTable = new QTableWidget(0, 2); + expTable->setObjectName("dataTable"); + expTable->setHorizontalHeaderLabels({"#", "Name"}); + expTable->horizontalHeader()->setStretchLastSection(true); + expTable->setColumnWidth(0, 40); + expTable->verticalHeader()->setVisible(false); + expTable->setEditTriggers(QAbstractItemView::NoEditTriggers); + expTable->setSelectionBehavior(QAbstractItemView::SelectRows); + expTable->setShowGrid(false); + expTable->setAlternatingRowColors(true); + tabs->addTab(expTable, "Exports"); + + impTable = new QTableWidget(0, 2); + impTable->setObjectName("dataTable"); + impTable->setHorizontalHeaderLabels({"#", "Name"}); + impTable->horizontalHeader()->setStretchLastSection(true); + impTable->setColumnWidth(0, 40); + impTable->verticalHeader()->setVisible(false); + impTable->setEditTriggers(QAbstractItemView::NoEditTriggers); + impTable->setShowGrid(false); + impTable->setAlternatingRowColors(true); + tabs->addTab(impTable, "Imports"); + eil->addWidget(tabs); + split->addWidget(eiCard); + + connect(expTable, &QTableWidget::itemDoubleClicked, this, [this](QTableWidgetItem* it){ + if (it->column() != 1) return; + callExport->setText(it->text()); + }); + + // invoke card + auto* invCard = card(); + auto* il = new QVBoxLayout(invCard); + il->setContentsMargins(20, 16, 20, 16); + il->setSpacing(10); + il->addWidget(h2("Invoke export")); + + auto* form = new QFormLayout; + form->setLabelAlignment(Qt::AlignRight); + form->setSpacing(8); + callExport = new QLineEdit; + callExport->setPlaceholderText("pick from Exports list, or type name"); + callArgs = new QLineEdit; callArgs->setPlaceholderText("hex/dec, comma-separated"); + callArgs->setFont(QFont("JetBrainsMono NF", 10)); + callTimeout = new QSpinBox; callTimeout->setRange(0, 60000); callTimeout->setSuffix(" ms"); + form->addRow("Export", callExport); + form->addRow("Args", callArgs); + form->addRow("Timeout", callTimeout); + il->addLayout(form); + + auto* callBtn = new QPushButton("Call"); callBtn->setObjectName("btnPrimary"); + il->addWidget(callBtn); + + il->addWidget(section_label("Last result")); + callResult = new QLabel("no calls yet"); + callResult->setObjectName("resultLabel"); + callResult->setTextInteractionFlags(Qt::TextSelectableByMouse); + il->addWidget(callResult); + + il->addWidget(section_label("Call history")); + callHist = new QTableWidget(0, 3); + callHist->setObjectName("dataTable"); + callHist->setHorizontalHeaderLabels({"Time", "Export", "Result"}); + callHist->horizontalHeader()->setStretchLastSection(false); + callHist->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch); + callHist->setColumnWidth(0, 80); + callHist->setColumnWidth(2, 160); + callHist->verticalHeader()->setVisible(false); + callHist->setEditTriggers(QAbstractItemView::NoEditTriggers); + callHist->setShowGrid(false); + callHist->setAlternatingRowColors(true); + il->addWidget(callHist, 1); + + split->addWidget(invCard); + split->setStretchFactor(0, 55); + split->setStretchFactor(1, 45); + split->setSizes({560, 440}); + ipl->addWidget(split, 1); + + rcv->addWidget(infoPanel); + infoPanel->hide(); + + mainSplit->addWidget(rightCol); + mainSplit->setStretchFactor(0, 0); + mainSplit->setStretchFactor(1, 1); + mainSplit->setSizes({260, 900}); + root->addWidget(mainSplit, 1); + + connect(callBtn, &QPushButton::clicked, this, [this]{ + if (!current.valid) return; + QVector args; + for (const QString& tok : callArgs->text().split(',', Qt::SkipEmptyParts)) { + QString s = tok.trimmed(); + if (s.isEmpty()) continue; + bool ok = false; + quint64 v = s.startsWith("0x", Qt::CaseInsensitive) + ? s.mid(2).toULongLong(&ok, 16) + : s.toULongLong(&ok, 0); + if (ok) args.push_back(v); + } + quint64 rv = 0; QString err; + bool ok = drv->call_export(current.base.id, callExport->text(), callTimeout->value(), args, rv, err); + int row = callHist->rowCount(); + callHist->insertRow(0); + callHist->setItem(0, 0, new QTableWidgetItem(QDateTime::currentDateTime().toString("HH:mm:ss"))); + callHist->setItem(0, 1, new QTableWidgetItem(callExport->text())); + QString rs = ok ? QString::asprintf("0x%llx", (unsigned long long)rv) : ("err: " + err); + auto* ri = new QTableWidgetItem(rs); + ri->setForeground(ok ? QColor("#ffffff") : QColor("#e06c6c")); + callHist->setItem(0, 2, ri); + if (callHist->rowCount() > 100) callHist->removeRow(100); + + if (ok) { + callResult->setText(QString::asprintf("0x%016llx", (unsigned long long)rv)); + callResult->setStyleSheet("color:#f3f4f6; font-family:'JetBrainsMono NF'; font-size:15px; font-weight:500;"); + emit toast(QString("call ok: 0x%1").arg(rv, 0, 16), 2); + } else { + callResult->setText(err); + callResult->setStyleSheet("color:#e06c6c; font-family:'JetBrainsMono NF'; font-size:13px;"); + emit toast(QString("call failed: %1").arg(err), 1); + } + (void)row; + }); +} + +void WorkbenchPage::refreshModules(const QVector& mods) +{ + // remember which module was selected, restore selection after rebuild + quint32 prev = current.valid ? current.base.id : (quint32)-1; + if (pending_select) { prev = pending_select; pending_select = 0; } + + // hide the toolkit guest from this list. it's a GUI-internal module the + // Explorer tab drives; if the user sees it here alongside their own wasm + // it just looks like something they didn't load. + QVector visible; + for (const auto& m : mods) { + QString base = QFileInfo(m.name).fileName(); + if (base.compare("toolkit.wasm", Qt::CaseInsensitive) == 0) continue; + visible.push_back(m); + } + + moduleList->blockSignals(true); + moduleList->clear(); + for (const auto& m : visible) { + QString base = QFileInfo(m.name).fileName(); + if (base.isEmpty()) base = m.name; + auto* it = new QListWidgetItem(QString("%1 %2").arg(m.id, 2, 10, QChar('0')).arg(base)); + it->setData(Qt::UserRole, m.id); + it->setToolTip(QString("%1\nhash %2\nwasm %3 B, mem %4 KB, pool %5 B") + .arg(m.name) + .arg(m.hash, 16, 16, QChar('0')) + .arg(m.wasm_size).arg(m.mem_pages * 64).arg(m.pool_bytes)); + moduleList->addItem(it); + if (m.id == prev) moduleList->setCurrentItem(it); + } + moduleList->blockSignals(false); + moduleCount->setText(QString("(%1)").arg(visible.size())); + + if (visible.isEmpty()) clearSelection(); + else if (prev != (quint32)-1) { + // trigger the same signal path so info reloads even if selection didn't change + for (int i = 0; i < moduleList->count(); i++) { + if (moduleList->item(i)->data(Qt::UserRole).toUInt() == prev) { + emit moduleSelected(prev); + break; + } + } + } +} + +void WorkbenchPage::setSelected(quint32 id, const ModuleInfo& info) +{ + (void)id; + current = info; + if (!info.valid) { clearSelection(); return; } + + emptyPanel->hide(); + infoPanel->show(); + + modTitle->setText(QFileInfo(info.base.name).fileName().isEmpty() ? info.base.name : QFileInfo(info.base.name).fileName()); + modMeta->setText(QString::asprintf("id %u %016llx %.1f KB wasm %u KB mem %llu B pool", + info.base.id, (unsigned long long)info.base.hash, info.base.wasm_size/1024.0, + info.base.mem_pages*64, (unsigned long long)info.base.pool_bytes)); + + refreshCaps(info.caps); + + // filter out internal exports (__gvm_caps) so the user sees only what they wrote + QStringList user_exports; + for (const auto& e : info.exports) if (!e.startsWith("__gvm_")) user_exports << e; + + expTable->setRowCount(user_exports.size()); + QFont mono("JetBrainsMono NF", 10); + for (int i = 0; i < user_exports.size(); i++) { + auto* n = new QTableWidgetItem(QString::number(i)); n->setFont(mono); n->setForeground(QColor("#7b818c")); + expTable->setItem(i, 0, n); + auto* e = new QTableWidgetItem(user_exports[i]); e->setFont(mono); + expTable->setItem(i, 1, e); + } + impTable->setRowCount(info.imports.size()); + for (int i = 0; i < info.imports.size(); i++) { + auto* n = new QTableWidgetItem(QString::number(i)); n->setFont(mono); n->setForeground(QColor("#7b818c")); + impTable->setItem(i, 0, n); + auto* e = new QTableWidgetItem(info.imports[i]); e->setFont(mono); e->setForeground(QColor("#a8adb8")); + impTable->setItem(i, 1, e); + } + + // auto-populate export field with first user export, but don't clobber + // whatever the user typed unless it doesn't match anything from this module + if (!user_exports.isEmpty() && !user_exports.contains(callExport->text())) + callExport->setText(user_exports.first()); + + // highlight the corresponding row in the list without re-emitting signals + moduleList->blockSignals(true); + for (int i = 0; i < moduleList->count(); i++) { + if (moduleList->item(i)->data(Qt::UserRole).toUInt() == info.base.id) { + moduleList->setCurrentRow(i); + break; + } + } + moduleList->blockSignals(false); +} + +void WorkbenchPage::clearSelection() +{ + current = {}; + infoPanel->hide(); + emptyPanel->show(); +} + +void WorkbenchPage::refreshCaps(quint32 caps) +{ + auto* lay = static_cast(capsBar->layout()); + while (auto* it = lay->takeAt(0)) { + if (it->widget()) it->widget()->deleteLater(); + delete it; + } + for (int i = 0; i < 10; i++) { + bool on = (caps >> i) & 1; + lay->addWidget(pill(CAP_NAMES[i], on ? "on" : "off")); + } + lay->addStretch(); +} + +// MemoryPage + +MemoryPage::MemoryPage(DriverClient* d, QWidget* p) : QWidget(p), drv(d) +{ + auto* root = new QVBoxLayout(this); + root->setContentsMargins(10, 10, 10, 8); + root->setSpacing(14); + + auto* c = card(); + auto* cl = new QVBoxLayout(c); + cl->setContentsMargins(20, 16, 20, 16); + cl->setSpacing(10); + cl->addWidget(h2("Read")); + + auto* form = new QFormLayout; + form->setSpacing(8); + modId = new QSpinBox; modId->setRange(-1, 999); modId->setValue(-1); modId->setSpecialValueText("(not set)"); + exp = new QLineEdit; exp->setText("toolkit_read_kmem"); + addr = new QLineEdit; addr->setText("0xfffff800`00000000"); addr->setFont(QFont("JetBrainsMono NF", 10)); + length = new QSpinBox; length->setRange(16, 4096); length->setValue(256); length->setSuffix(" bytes"); + form->addRow("Toolkit module id", modId); + form->addRow("Export", exp); + form->addRow("Address", addr); + form->addRow("Length", length); + cl->addLayout(form); + + auto* readBtn = new QPushButton("Read"); readBtn->setObjectName("btnPrimary"); + cl->addWidget(readBtn, 0, Qt::AlignLeft); + status = new QLabel(""); status->setObjectName("dim"); + cl->addWidget(status); + root->addWidget(c); + + hex = new QPlainTextEdit; + hex->setReadOnly(true); + hex->setFont(QFont("JetBrainsMono NF", 10)); + hex->setObjectName("hexView"); + hex->setPlaceholderText("Hex output will appear here after a successful read."); + root->addWidget(hex, 1); + + connect(readBtn, &QPushButton::clicked, this, [this]{ + status->clear(); + if (modId->value() < 0) { status->setText("set toolkit mod id first"); return; } + QString a = addr->text(); a.remove('`').remove(' '); + bool ok = false; + quint64 va = a.startsWith("0x", Qt::CaseInsensitive) ? a.mid(2).toULongLong(&ok, 16) : a.toULongLong(&ok, 0); + if (!ok) { status->setText("bad address"); return; } + QVector argv = { va, 0, (quint64)length->value() }; + quint64 rv = 0; QString err; + if (drv->call_export(modId->value(), exp->text(), 2000, argv, rv, err)) + status->setText(QString::asprintf("toolkit rv=0x%llx bytes read. hex readback needs the new IOCTL.", (unsigned long long)rv)); + else + status->setText("call failed: " + err); + }); +} + +// EventsPage + +static const char* evt_tag(int k) +{ + switch (k) { + case EVT_PROC: return "PROC"; case EVT_EXIT: return "EXIT"; case EVT_IMAGE: return "IMAGE"; + case EVT_CALL: return "CALL"; case EVT_LOAD: return "LOAD"; case EVT_UNLOAD: return "UNLD"; + case EVT_ERROR: return "ERR"; default: return "MSG"; + } +} + +static QColor evt_col(int k) +{ + switch (k) { + case EVT_PROC: case EVT_LOAD: return QColor("#8fbf5c"); + case EVT_EXIT: case EVT_ERROR: return QColor("#cd5c5c"); + case EVT_IMAGE: return QColor("#7cb1f0"); + case EVT_CALL: return QColor("#d4a259"); + case EVT_UNLOAD: return QColor("#b96060"); + default: return QColor("#d4d4d4"); + } +} + +EventsPage::EventsPage(DriverClient* d, QWidget* p) : QWidget(p), drv(d) +{ + auto* root = new QVBoxLayout(this); + root->setContentsMargins(10, 10, 10, 8); + root->setSpacing(10); + + // control card: load + start + stop + status + auto* ctlCard = card(); + auto* cl = new QHBoxLayout(ctlCard); + cl->setContentsMargins(20, 12, 20, 12); + cl->setSpacing(8); + loadBtn = new QPushButton("Load tracer"); + loadBtn->setToolTip("Load samples/process_tracer.wasm"); + startBtn = new QPushButton("Start (poll)"); + startBtn->setToolTip("Enable callbacks. Events log via driver notify handlers."); + reactiveBtn = new QPushButton("Start reactive"); + reactiveBtn->setObjectName("btnPrimary"); + reactiveBtn->setToolTip("Enable callbacks and spawn kernel dispatch worker that invokes guest exports."); + stopBtn = new QPushButton("Stop"); + stopBtn->setObjectName("btnDanger"); + stopBtn->setToolTip("Unload the tracer module (removes registered callbacks)"); + statusLbl = new QLabel; + cl->addWidget(loadBtn); + cl->addWidget(startBtn); + cl->addWidget(reactiveBtn); + cl->addWidget(stopBtn); + cl->addSpacing(8); + cl->addWidget(statusLbl, 1); + root->addWidget(ctlCard); + + set_status("no tracer loaded", "#8b8b8b"); + startBtn->setEnabled(false); + reactiveBtn->setEnabled(false); + stopBtn->setEnabled(false); + + connect(loadBtn, &QPushButton::clicked, this, &EventsPage::on_load_tracer); + connect(startBtn, &QPushButton::clicked, this, [this]{ on_start(false); }); + connect(reactiveBtn, &QPushButton::clicked, this, [this]{ on_start(true); }); + connect(stopBtn, &QPushButton::clicked, this, &EventsPage::on_stop); + + // filter bar + auto* barCard = card(); + auto* bl = new QHBoxLayout(barCard); + bl->setContentsMargins(20, 12, 20, 12); + bl->setSpacing(12); + cbProc = new QCheckBox("Proc"); cbProc->setChecked(true); + cbExit = new QCheckBox("Exit"); cbExit->setChecked(true); + cbImage = new QCheckBox("Image"); cbImage->setChecked(true); + cbCall = new QCheckBox("Call"); cbCall->setChecked(true); + cbError = new QCheckBox("Error"); cbError->setChecked(true); + filter = new QLineEdit; filter->setPlaceholderText("Substring filter"); filter->setFixedWidth(260); + bl->addWidget(cbProc); bl->addWidget(cbExit); bl->addWidget(cbImage); bl->addWidget(cbCall); bl->addWidget(cbError); + bl->addStretch(); + bl->addWidget(filter); + root->addWidget(barCard); + + table = new QTableWidget(0, 3); + table->setObjectName("dataTable"); + table->setHorizontalHeaderLabels({"Time", "Kind", "Line"}); + table->horizontalHeader()->setStretchLastSection(true); + table->setColumnWidth(0, 110); + table->setColumnWidth(1, 80); + table->verticalHeader()->setVisible(false); + table->setEditTriggers(QAbstractItemView::NoEditTriggers); + table->setShowGrid(false); + table->setAlternatingRowColors(true); + root->addWidget(table, 1); +} + +void EventsPage::append(const QVector& entries) +{ + for (const auto& e : entries) { + bool show = + (e.kind == EVT_PROC && cbProc->isChecked()) || + (e.kind == EVT_EXIT && cbExit->isChecked()) || + (e.kind == EVT_IMAGE && cbImage->isChecked()) || + ((e.kind == EVT_CALL || e.kind == EVT_LOAD || e.kind == EVT_UNLOAD) && cbCall->isChecked()) || + (e.kind == EVT_ERROR && cbError->isChecked()); + if (!show) continue; + QString f = filter->text(); + if (!f.isEmpty() && !e.line.contains(f)) continue; + + table->insertRow(0); + auto* ts = new QTableWidgetItem(e.ts); + ts->setFont(QFont("JetBrainsMono NF", 10)); + ts->setForeground(QColor("#8a8a8a")); + table->setItem(0, 0, ts); + auto* kd = new QTableWidgetItem(evt_tag(e.kind)); + kd->setFont(QFont("JetBrainsMono NF", 10)); + kd->setForeground(evt_col(e.kind)); + table->setItem(0, 1, kd); + auto* ln = new QTableWidgetItem(e.line); + ln->setFont(QFont("JetBrainsMono NF", 10)); + table->setItem(0, 2, ln); + if (table->rowCount() > 2000) table->removeRow(2000); + } +} + +void EventsPage::clear() { table->setRowCount(0); } + +void EventsPage::set_status(const QString& s, const QString& color) +{ + statusLbl->setText(s); + statusLbl->setStyleSheet(QString("color:%1; font-size:12px;").arg(color)); +} + +void EventsPage::on_load_tracer() +{ + QString here = QCoreApplication::applicationDirPath(); + QStringList candidates = { + here + "/../features/process_tracer.wasm", + here + "/features/process_tracer.wasm", + here + "/process_tracer.wasm", + here + "/../samples/process_tracer.wasm", + }; + QString path; + for (const auto& p : candidates) { + if (QFile::exists(p)) { path = p; break; } + } + if (path.isEmpty()) { + path = QFileDialog::getOpenFileName(this, "Select process_tracer.wasm", + QString(), "WebAssembly (*.wasm)"); + if (path.isEmpty()) return; + } + quint32 id = 0; QString err; + if (!drv->load_module(path, 0, id, err)) { + set_status(QString("load failed: %1").arg(err), "#e06c6c"); + emit toast("load failed: " + err, 1); + return; + } + tracer_id = (int)id; + set_status(QString("tracer loaded as module %1").arg(id), "#7fb069"); + startBtn->setEnabled(true); + reactiveBtn->setEnabled(true); + stopBtn->setEnabled(true); + loadBtn->setEnabled(false); + emit toast(QString("tracer loaded (module %1)").arg(id), 2); +} + +void EventsPage::on_start(bool reactive) +{ + if (tracer_id < 0) { emit toast("load tracer first", 1); return; } + const char* fn = reactive ? "start_reactive" : "start"; + quint64 rv = 0; QString err; + if (drv->call_export((quint32)tracer_id, fn, 2000, {}, rv, err)) { + running = true; + set_status(QString("%1 running (module %2)").arg(reactive ? "reactive dispatch" : "callbacks").arg(tracer_id), "#7fb069"); + emit toast(QString("%1 on").arg(reactive ? "reactive" : "polling"), 2); + } else { + set_status("start failed: " + err, "#e06c6c"); + emit toast("start failed: " + err, 1); + } +} + +void EventsPage::on_stop() +{ + // stop dispatch worker + remove kernel callbacks first, THEN unload the + // module. otherwise callbacks keep firing after the module is gone. + drv->notify_stop(); + if (tracer_id >= 0) drv->unload((quint32)tracer_id); + tracer_id = -1; + running = false; + set_status("stopped", "#8b8b8b"); + loadBtn->setEnabled(true); + startBtn->setEnabled(false); + reactiveBtn->setEnabled(false); + stopBtn->setEnabled(false); + emit toast("callbacks removed, tracer unloaded", 0); +} + +// LogPage + +LogPage::LogPage(QWidget* p) : QWidget(p) +{ + auto* root = new QVBoxLayout(this); + root->setContentsMargins(10, 10, 10, 8); + root->setSpacing(14); + + auto* bar = new QHBoxLayout; + auto* clearBtn = new QPushButton("Clear"); + autoscroll = new QCheckBox("Auto-scroll"); autoscroll->setChecked(true); + statusLbl = new QLabel(""); statusLbl->setObjectName("dim"); + bar->addWidget(clearBtn); + bar->addWidget(autoscroll); + bar->addStretch(); + bar->addWidget(statusLbl); + root->addLayout(bar); + + view = new QPlainTextEdit; + view->setReadOnly(true); + view->setFont(QFont("JetBrainsMono NF", 10)); + view->setObjectName("logView"); + view->setMaximumBlockCount(4000); + root->addWidget(view, 1); + + connect(clearBtn, &QPushButton::clicked, this, [this]{ view->clear(); }); +} + +void LogPage::append(const QVector& entries, quint64 seq, quint32 dropped) +{ + for (const auto& e : entries) { + QString col = evt_col(e.kind).name(); + QString html = QString("%1 %3") + .arg(e.ts, col, e.line.toHtmlEscaped()); + view->appendHtml(html); + } + statusLbl->setText(QString("seq %1 | %2 dropped").arg(seq).arg(dropped)); + if (autoscroll->isChecked()) { + auto* sb = view->verticalScrollBar(); + sb->setValue(sb->maximum()); + } +} + +void LogPage::clear() { view->clear(); } + +// ProfilerPage + +ProfilerPage::ProfilerPage(QWidget* p) : QWidget(p) +{ + auto* root = new QVBoxLayout(this); + root->setContentsMargins(10, 10, 10, 8); + root->setSpacing(12); + + auto* row = new QHBoxLayout; + auto* clr = new QPushButton("Reset counts"); + total = new QLabel("no data yet"); + total->setStyleSheet("color:#7b828e; font-size:12px;"); + row->addWidget(clr); row->addWidget(total, 1); + root->addLayout(row); + + table = new QTableWidget(0, 5); + table->setObjectName("dataTable"); + table->setHorizontalHeaderLabels({"Function", "Count", "Total", "Max", "Avg"}); + table->horizontalHeader()->setStretchLastSection(false); + table->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch); + table->setColumnWidth(1, 100); + table->setColumnWidth(2, 140); + table->setColumnWidth(3, 140); + table->setColumnWidth(4, 140); + table->verticalHeader()->setVisible(false); + table->setEditTriggers(QAbstractItemView::NoEditTriggers); + table->setShowGrid(false); + table->setAlternatingRowColors(true); + table->setSortingEnabled(true); + root->addWidget(table, 1); + + connect(clr, &QPushButton::clicked, this, &ProfilerPage::clear); +} + +static QString fmt_time_ns(quint64 ns) +{ + if (ns < 1000) return QString::asprintf("%llu ns", (unsigned long long)ns); + if (ns < 1000 * 1000) return QString::asprintf("%.2f us", ns / 1000.0); + if (ns < 1000ULL*1000*1000) return QString::asprintf("%.2f ms", ns / 1e6); + return QString::asprintf("%.2f s", ns / 1e9); +} + +void ProfilerPage::observe(const QVector& events) +{ + for (const auto& e : events) { + auto& s = stats[e.name]; + s.count++; + if (e.kind == TRK_CALL) { + s.last_call_ts = e.timestamp_100ns; + } else if (e.kind == TRK_RETURN && s.last_call_ts && e.timestamp_100ns >= s.last_call_ts) { + quint64 delta_ns = (e.timestamp_100ns - s.last_call_ts) * 100; // 100ns ticks -> ns + s.total_ns += delta_ns; + if (delta_ns > s.max_ns) s.max_ns = delta_ns; + s.last_call_ts = 0; + } + } + refresh(); +} + +void ProfilerPage::refresh() +{ + table->setSortingEnabled(false); + table->setRowCount(stats.size()); + QFont mono("JetBrainsMono NF", 10); + int r = 0; quint64 grand = 0; + for (auto it = stats.constBegin(); it != stats.constEnd(); ++it, r++) { + const auto& s = it.value(); + grand += s.count; + auto* n = new QTableWidgetItem(it.key()); n->setFont(mono); + table->setItem(r, 0, n); + auto* c = new QTableWidgetItem; + c->setData(Qt::DisplayRole, (qulonglong)s.count); + c->setFont(mono); + table->setItem(r, 1, c); + auto* t = new QTableWidgetItem(s.total_ns ? fmt_time_ns(s.total_ns) : ""); + t->setData(Qt::UserRole, (qulonglong)s.total_ns); + t->setFont(mono); t->setForeground(QColor("#ffffff")); + table->setItem(r, 2, t); + auto* mx = new QTableWidgetItem(s.max_ns ? fmt_time_ns(s.max_ns) : ""); + mx->setFont(mono); + table->setItem(r, 3, mx); + quint64 avg = s.count ? s.total_ns / s.count : 0; + auto* av = new QTableWidgetItem(avg ? fmt_time_ns(avg) : ""); + av->setFont(mono); av->setForeground(QColor("#8a919e")); + table->setItem(r, 4, av); + } + table->setSortingEnabled(true); + table->sortItems(1, Qt::DescendingOrder); + total->setText(QString("%1 unique functions | %2 events total").arg(stats.size()).arg(grand)); +} + +void ProfilerPage::clear() { stats.clear(); table->setRowCount(0); total->setText("no data yet"); } + +// DeployPage + +DeployPage::DeployPage(DriverClient* d, QWidget* p) : QWidget(p), drv(d) +{ + auto* root = new QVBoxLayout(this); + root->setContentsMargins(10, 10, 10, 8); + root->setSpacing(14); + + auto* stateCard = card(); + auto* sl = new QVBoxLayout(stateCard); + sl->setContentsMargins(20, 16, 20, 16); + sl->setSpacing(10); + sl->addWidget(section_label("Status")); + + auto* g = new QGridLayout; + g->setHorizontalSpacing(30); + g->setVerticalSpacing(10); + auto mklbl = [](const QString& k){ auto* l = new QLabel(k); l->setStyleSheet("color:#8a8a8a;"); return l; }; + g->addWidget(mklbl("service"), 0, 0); + svcState = new QLabel("-"); g->addWidget(svcState, 0, 1); + g->addWidget(mklbl("state"), 1, 0); + runState = new QLabel("-"); g->addWidget(runState, 1, 1); + g->addWidget(mklbl("driver"), 2, 0); + devState = new QLabel("-"); g->addWidget(devState, 2, 1); + sl->addLayout(g); + root->addWidget(stateCard); + + auto* actCard = card(); + auto* al = new QVBoxLayout(actCard); + al->setContentsMargins(20, 16, 20, 16); + al->setSpacing(10); + al->addWidget(section_label("Actions")); + auto* btns = new QHBoxLayout; + auto* install = new QPushButton("Install"); + auto* start = new QPushButton("Start"); start->setObjectName("btnPrimary"); + auto* stop = new QPushButton("Stop"); + auto* remove = new QPushButton("Remove"); remove->setObjectName("btnDanger"); + auto* refresh = new QPushButton("Refresh"); + btns->addWidget(install); btns->addWidget(start); btns->addWidget(stop); + btns->addWidget(remove); btns->addStretch(); btns->addWidget(refresh); + al->addLayout(btns); + root->addWidget(actCard); + + root->addStretch(); + + connect(install, &QPushButton::clicked, this, [this]{ + wchar_t path[MAX_PATH]; GetModuleFileNameW(nullptr, path, MAX_PATH); + QString exe = QString::fromWCharArray(path); + QString dir = QFileInfo(exe).absolutePath(); + drv->run_sc(QString("create Goodmans type= kernel binPath= \"%1\\Goodmans.sys\"").arg(dir)); + emit stateChanged(); + }); + connect(start, &QPushButton::clicked, this, [this]{ drv->run_sc("start Goodmans"); emit stateChanged(); }); + connect(stop, &QPushButton::clicked, this, [this]{ drv->run_sc("stop Goodmans"); emit stateChanged(); }); + connect(remove, &QPushButton::clicked, this, [this]{ + drv->run_sc("stop Goodmans"); drv->run_sc("delete Goodmans"); emit stateChanged(); + }); + connect(refresh, &QPushButton::clicked, this, [this]{ emit stateChanged(); }); +} + +void DeployPage::refresh(bool devUp, bool present, bool running) +{ + auto set = [](QLabel* l, const QString& t, const QString& color){ + l->setText(t); + l->setStyleSheet(QString("color:%1; font-weight:600;").arg(color)); + }; + set(svcState, present ? "installed" : "not installed", present ? "#8fbf5c" : "#cd5c5c"); + set(runState, running ? "running" : "stopped", running ? "#8fbf5c" : "#e5b055"); + set(devState, devUp ? "loaded" : "not loaded", devUp ? "#8fbf5c" : "#cd5c5c"); +} + +// TracePage + +static const char* trace_kind_str(quint32 k) +{ + switch (k) { + case TRK_IMPORT: return "IMPORT"; + case TRK_CALL: return "CALL"; + case TRK_RETURN: return "RETURN"; + case TRK_TRAP: return "TRAP"; + default: return "?"; + } +} + +static QColor trace_kind_col(quint32 k) +{ + switch (k) { + case TRK_IMPORT: return QColor("#7cb1f0"); + case TRK_CALL: return QColor("#d4a259"); + case TRK_RETURN: return QColor("#8fbf5c"); + case TRK_TRAP: return QColor("#cd5c5c"); + default: return QColor("#8a8a8a"); + } +} + +// argument name hints per host import so details pane can label them properly. +// values are decoded per column type: 'A' = kernel address hex, 'X' = hex u64, +// 'x' = hex u32, 'd' = decimal u32. +struct ArgSpec { const char* fn; const char* names[4]; const char types[4]; }; +static const ArgSpec ARG_SPECS[] = { + { "host_read_u8", {"kaddr", "", "", ""}, {'A','\0','\0','\0'} }, + { "host_read_u32", {"kaddr", "", "", ""}, {'A','\0','\0','\0'} }, + { "host_read_u64", {"kaddr", "", "", ""}, {'A','\0','\0','\0'} }, + { "host_write_u64", {"kaddr", "value", "", ""}, {'A','X','\0','\0'} }, + { "host_read_bytes", {"kaddr", "guest_off", "len", ""}, {'A','x','d','\0'} }, + { "host_write_bytes", {"kaddr", "guest_off", "len", ""}, {'A','x','d','\0'} }, + { "host_alloc", {"size", "", "", ""}, {'d','\0','\0','\0'} }, + { "host_free", {"kernel_va", "", "", ""}, {'A','\0','\0','\0'} }, + { "host_dbg_print", {"guest_off", "len", "", ""}, {'x','d','\0','\0'} }, + { "host_readmsr", {"msr", "", "", ""}, {'x','\0','\0','\0'} }, + { "host_writemsr", {"msr", "value", "", ""}, {'x','X','\0','\0'} }, + { "host_cpuid", {"leaf", "sub", "guest_off",""}, {'x','x','x','\0'} }, + { "host_phys_read", {"pa", "guest_off","len", ""}, {'X','x','d','\0'} }, + { "host_phys_write", {"pa", "guest_off","len", ""}, {'X','x','d','\0'} }, + { "host_rdtsc", {"","","",""}, {'\0','\0','\0','\0'}}, + { "host_current_irql", {"","","",""}, {'\0','\0','\0','\0'}}, + { "host_process_id", {"","","",""}, {'\0','\0','\0','\0'}}, + { "host_thread_id", {"","","",""}, {'\0','\0','\0','\0'}}, + { "host_current_process", {"","","",""}, {'\0','\0','\0','\0'}}, + { nullptr, {}, {} } +}; + +static const ArgSpec* find_argspec(const QString& fn) +{ + for (int i = 0; ARG_SPECS[i].fn; i++) + if (fn == ARG_SPECS[i].fn) return &ARG_SPECS[i]; + return nullptr; +} + +static QString fmt_arg_val(char type, quint64 v) +{ + switch (type) { + case 'A': // kernel address split for readability + return QString::asprintf("0x%08x`%08x", (quint32)(v >> 32), (quint32)v); + case 'X': return QString::asprintf("0x%016llx", (unsigned long long)v); + case 'x': return QString::asprintf("0x%x", (quint32)v); + case 'd': return QString::number((quint32)v); + default: return QString::asprintf("0x%llx", (unsigned long long)v); + } +} + +static QString short_args_display(const TraceEvent& e) +{ + const ArgSpec* spec = find_argspec(e.name); + if (!spec) { + QString s; + for (int i = 0; i < e.argv.size(); i++) { + if (i) s += ", "; + s += QString::asprintf("0x%llx", (unsigned long long)e.argv[i]); + } + return s; + } + QString s; + for (int i = 0; i < e.argv.size() && spec->types[i]; i++) { + if (i) s += ", "; + s += fmt_arg_val(spec->types[i], e.argv[i]); + } + return s; +} + +TracePage::TracePage(DriverClient* d, QWidget* p) : QWidget(p), drv(d) +{ + auto* root = new QVBoxLayout(this); + root->setContentsMargins(10, 10, 10, 8); + root->setSpacing(12); + + // toolbar + auto* toolbar = new QFrame; + toolbar->setObjectName("toolbar"); + auto* tb = new QHBoxLayout(toolbar); + tb->setContentsMargins(14, 8, 14, 8); + tb->setSpacing(8); + + toggleBtn = new QPushButton("Start"); + toggleBtn->setObjectName("btnPrimary"); + toggleBtn->setMinimumWidth(80); + toggleBtn->setShortcut(QKeySequence("F5")); + toggleBtn->setToolTip("Toggle tracing (F5)"); + tb->addWidget(toggleBtn); + + pauseBtn = new QPushButton("Pause"); + pauseBtn->setEnabled(false); + pauseBtn->setShortcut(QKeySequence("F6")); + pauseBtn->setToolTip("Pause UI capture (F6)"); + tb->addWidget(pauseBtn); + + clearBtn = new QPushButton("Clear"); + clearBtn->setShortcut(QKeySequence("Ctrl+L")); + clearBtn->setToolTip("Clear view (Ctrl+L)"); + tb->addWidget(clearBtn); + + openBtn = new QPushButton("Open…"); + openBtn->setToolTip("Load .gtrace file"); + tb->addWidget(openBtn); + + exportBtn = new QPushButton("Save…"); + exportBtn->setShortcut(QKeySequence("Ctrl+S")); + exportBtn->setToolTip("Save trace to .gtrace or .csv (Ctrl+S)"); + tb->addWidget(exportBtn); + + auto* sep1 = new QFrame; sep1->setFrameShape(QFrame::VLine); sep1->setObjectName("tbsep"); + sep1->setFixedHeight(24); + tb->addWidget(sep1); + + onlyModule = new QCheckBox("only"); + onlyModule->setToolTip("Limit tracing to a specific module id"); + moduleFilter = new QSpinBox; + moduleFilter->setRange(0, 999); + moduleFilter->setValue(0); + moduleFilter->setFixedWidth(60); + tb->addWidget(onlyModule); + tb->addWidget(moduleFilter); + + regexMode = new QCheckBox(".*"); + regexMode->setToolTip("Treat filter as regex"); + tb->addWidget(regexMode); + + auto* sep2 = new QFrame; sep2->setFrameShape(QFrame::VLine); sep2->setObjectName("tbsep"); + sep2->setFixedHeight(24); + tb->addWidget(sep2); + + filter = new QLineEdit; + filter->setPlaceholderText("filter (Ctrl+F): substring, module id, kind, or regex"); + filter->setClearButtonEnabled(true); + tb->addWidget(filter, 1); + + root->addWidget(toolbar); + + // splitter: table above, details below + auto* split = new QSplitter(Qt::Vertical); + split->setObjectName("wbSplit"); + split->setChildrenCollapsible(false); + split->setHandleWidth(4); + + table = new QTableWidget(0, 9); + table->setObjectName("dataTable"); + table->setHorizontalHeaderLabels({"", "Time", "Δt", "TID", "IRQL", "Mod", "Kind", "Function", "Arguments"}); + table->horizontalHeader()->setStretchLastSection(true); + table->setColumnWidth(0, 14); + table->setColumnWidth(1, 110); + table->setColumnWidth(2, 80); + table->setColumnWidth(3, 60); + table->setColumnWidth(4, 50); + table->setColumnWidth(5, 46); + table->setColumnWidth(6, 78); + table->setColumnWidth(7, 200); + table->verticalHeader()->setVisible(false); + table->verticalHeader()->setDefaultSectionSize(24); + table->setEditTriggers(QAbstractItemView::NoEditTriggers); + table->setShowGrid(false); + table->setAlternatingRowColors(true); + table->setSelectionBehavior(QAbstractItemView::SelectRows); + table->setSelectionMode(QAbstractItemView::SingleSelection); + split->addWidget(table); + + details = new QPlainTextEdit; + details->setReadOnly(true); + details->setObjectName("detailsPane"); + details->setPlaceholderText("Select an event to see its full details."); + details->setMinimumHeight(120); + split->addWidget(details); + split->setStretchFactor(0, 70); + split->setStretchFactor(1, 30); + split->setSizes({520, 200}); + root->addWidget(split, 1); + + // status bar at bottom + auto* statusRow = new QHBoxLayout; + statusRow->setContentsMargins(4, 0, 4, 0); + modeLbl = new QLabel; modeLbl->setObjectName("dim"); + statsLbl = new QLabel; statsLbl->setObjectName("dim"); + statusRow->addWidget(modeLbl); + statusRow->addStretch(); + statusRow->addWidget(statsLbl); + root->addLayout(statusRow); + + refresh_stats(); + + connect(toggleBtn, &QPushButton::clicked, this, &TracePage::toggleTracing); + connect(pauseBtn, &QPushButton::clicked, this, [this]{ + paused = !paused; pauseBtn->setText(paused ? "Resume" : "Pause"); + }); + connect(clearBtn, &QPushButton::clicked, this, &TracePage::clear); + + connect(openBtn, &QPushButton::clicked, this, [this]{ + QString path = QFileDialog::getOpenFileName(this, "Load trace", QString(), "Goodmans trace (*.gtrace);;All (*.*)"); + if (!path.isEmpty()) loadTraceFile(path); + }); + connect(exportBtn, &QPushButton::clicked, this, [this]{ + QString path = QFileDialog::getSaveFileName(this, "Save trace", "trace.gtrace", + "Goodmans trace (*.gtrace);;CSV (*.csv)"); + if (!path.isEmpty()) saveTraceFile(path); + }); + connect(filter, &QLineEdit::textChanged, this, [this]{ rebuild_from_backing(); }); + connect(regexMode, &QCheckBox::toggled, this, [this]{ rebuild_from_backing(); }); + + connect(table, &QTableWidget::itemSelectionChanged, this, [this]{ + auto sel = table->selectedItems(); + if (sel.isEmpty()) { details->clear(); return; } + int row = sel.first()->row(); + int back_idx = table->item(row, 0)->data(Qt::UserRole).toInt(); + if (back_idx >= 0 && back_idx < backing.size()) render_details(backing[back_idx]); + }); + + auto* bmSc = new QShortcut(QKeySequence("Ctrl+B"), this); + connect(bmSc, &QShortcut::activated, this, &TracePage::toggleBookmark); +} + +void TracePage::toggleTracing() +{ + if (!tracing_on) { + int mode = onlyModule->isChecked() ? TRACE_ON_MODULE : TRACE_ON_ALL; + quint32 mid = onlyModule->isChecked() ? (quint32)moduleFilter->value() : 0; + if (drv->trace_ctl(mode, mid)) { + tracing_on = true; + toggleBtn->setText("Stop"); + pauseBtn->setEnabled(true); + modeLbl->setText(onlyModule->isChecked() + ? QString("tracing module %1").arg(mid) + : "tracing all modules"); + modeLbl->setStyleSheet("color: #8fbf5c;"); + emit toast("tracing on", 2); + } else emit toast("failed to enable trace", 1); + } else { + if (drv->trace_ctl(TRACE_OFF)) { + tracing_on = false; + toggleBtn->setText("Start"); + pauseBtn->setEnabled(false); + paused = false; + pauseBtn->setText("Pause"); + modeLbl->setText("tracing off"); + modeLbl->setStyleSheet("color: #6a7280;"); + emit toast("tracing off", 0); + } + } +} + +void TracePage::toggleBookmark() +{ + auto sel = table->selectedItems(); + if (sel.isEmpty()) return; + int row = sel.first()->row(); + int back_idx = table->item(row, 0)->data(Qt::UserRole).toInt(); + if (bookmarks.contains(back_idx)) { bookmarks.remove(back_idx); table->item(row, 0)->setText(""); } + else { bookmarks.insert(back_idx); table->item(row, 0)->setText("●"); } +} + +bool TracePage::filter_match(const TraceEvent& e) const +{ + QString f = filter->text(); + if (f.isEmpty()) return true; + if (regexMode->isChecked()) { + QRegularExpression re(f, QRegularExpression::CaseInsensitiveOption); + if (!re.isValid()) return true; // don't hide during typing + return re.match(e.name).hasMatch() + || re.match(QString::number(e.module_id)).hasMatch() + || re.match(trace_kind_str(e.kind)).hasMatch(); + } + return e.name.contains(f, Qt::CaseInsensitive) + || QString::number(e.module_id) == f + || QString(trace_kind_str(e.kind)).contains(f, Qt::CaseInsensitive); +} + +void TracePage::insert_row(int row, const TraceEvent& e, int back_idx) +{ + QFont mono("JetBrainsMono NF", 10); + table->insertRow(row); + + auto* bm = new QTableWidgetItem(bookmarks.contains(back_idx) ? "●" : ""); + bm->setForeground(QColor("#ffffff")); + bm->setTextAlignment(Qt::AlignCenter); + bm->setData(Qt::UserRole, back_idx); + table->setItem(row, 0, bm); + + auto* ts = new QTableWidgetItem(e.ts); ts->setFont(mono); ts->setForeground(QColor("#8a919e")); + table->setItem(row, 1, ts); + + // delta-time: microseconds since first event + quint64 delta_100ns = (first_ts && e.timestamp_100ns >= first_ts) ? e.timestamp_100ns - first_ts : 0; + double delta_ms = delta_100ns / 10000.0; + QString dt = delta_ms < 1.0 + ? QString::asprintf("+%.1f us", delta_100ns / 10.0) + : (delta_ms < 1000.0 ? QString::asprintf("+%.2f ms", delta_ms) + : QString::asprintf("+%.3f s", delta_ms / 1000.0)); + auto* dtc = new QTableWidgetItem(dt); dtc->setFont(mono); dtc->setForeground(QColor("#7b828e")); + table->setItem(row, 2, dtc); + + auto* tid = new QTableWidgetItem(QString::number(e.thread_id)); + tid->setFont(mono); tid->setForeground(QColor("#8a919e")); + table->setItem(row, 3, tid); + + QString irqlStr; + switch (e.irql) { + case 0: irqlStr = "PASSIVE"; break; + case 1: irqlStr = "APC"; break; + case 2: irqlStr = "DISPATCH"; break; + default: irqlStr = QString::number(e.irql); + } + auto* iq = new QTableWidgetItem(irqlStr); iq->setFont(mono); + iq->setForeground(e.irql >= 2 ? QColor("#e5b055") : QColor("#8a919e")); + table->setItem(row, 4, iq); + + auto* mid = new QTableWidgetItem(QString::number(e.module_id)); mid->setFont(mono); + table->setItem(row, 5, mid); + + auto* kd = new QTableWidgetItem(trace_kind_str(e.kind)); kd->setFont(mono); + kd->setForeground(trace_kind_col(e.kind)); + table->setItem(row, 6, kd); + + auto* nm = new QTableWidgetItem(e.name); nm->setFont(mono); + table->setItem(row, 7, nm); + + QString argstr = short_args_display(e); + if (e.kind != TRK_TRAP && e.kind != TRK_CALL) argstr += QString::asprintf(" 0x%llx", (unsigned long long)e.rv); + auto* ar = new QTableWidgetItem(argstr); ar->setFont(mono); + ar->setForeground(e.kind == TRK_TRAP ? QColor("#cd5c5c") : QColor("#b0b6c0")); + table->setItem(row, 8, ar); +} + +void TracePage::rebuild_from_backing() +{ + table->setRowCount(0); + shown_events = 0; + // insert newest first (like live append) + for (int i = backing.size() - 1; i >= 0; i--) { + if (!filter_match(backing[i])) continue; + insert_row(table->rowCount(), backing[i], i); + shown_events++; + } + refresh_stats(); +} + +void TracePage::render_details(const TraceEvent& e) +{ + QString out; + QString irqlName = e.irql == 0 ? "PASSIVE_LEVEL" : (e.irql == 1 ? "APC_LEVEL" : (e.irql == 2 ? "DISPATCH_LEVEL" : QString("IRQL %1").arg(e.irql))); + out += QString("
");
+    out += QString("%2  at %3 | module %4 | TID %5 | %6\n")
+        .arg(trace_kind_col(e.kind).name(), trace_kind_str(e.kind), e.ts).arg(e.module_id).arg(e.thread_id).arg(irqlName);
+    out += QString("function   %1\n").arg(e.name.toHtmlEscaped());
+
+    const ArgSpec* spec = find_argspec(e.name);
+    if (!e.argv.isEmpty()) {
+        out += "arguments\n";
+        for (int i = 0; i < e.argv.size(); i++) {
+            QString name = (spec && spec->names[i][0]) ? spec->names[i] : QString("arg%1").arg(i);
+            char type = (spec && spec->types[i]) ? spec->types[i] : 'X';
+            out += QString("  %1  %2\n")
+                .arg(name.leftJustified(12, ' ')).arg(fmt_arg_val(type, e.argv[i]));
+        }
+    }
+    if (e.kind == TRK_TRAP) {
+        out += QString("trap reason  %1\n").arg(e.name.toHtmlEscaped());
+    } else if (e.kind != TRK_CALL) {
+        out += QString("return     0x%1  (%2)\n")
+            .arg((unsigned long long)e.rv, 0, 16).arg((unsigned long long)e.rv);
+    }
+    out += "
"; + details->setPlainText(""); + details->appendHtml(out); +} + +void TracePage::refresh_stats() +{ + QString s; + s = QString("events: %1 total | %2 shown").arg(total_events).arg(shown_events); + if (!kind_counts.isEmpty()) { + s += " | "; + QStringList parts; + for (auto it = kind_counts.constBegin(); it != kind_counts.constEnd(); ++it) + parts << QString("%1=%2").arg(it.key()).arg(it.value()); + s += parts.join(" "); + } + statsLbl->setText(s); +} + +void TracePage::append(const QVector& events) +{ + if (paused) return; + for (const auto& e : events) { + if (first_ts == 0) first_ts = e.timestamp_100ns; + int back_idx = backing.size(); + backing.push_back(e); + total_events++; + kind_counts[trace_kind_str(e.kind)]++; + + if (!filter_match(e)) continue; + insert_row(0, e, back_idx); + shown_events++; + if (table->rowCount() > 5000) table->removeRow(5000); + } + while (backing.size() > 20000) { + backing.pop_front(); + bookmarks.clear(); // indices invalidated + } + refresh_stats(); +} + +void TracePage::saveTraceFile(const QString& path) +{ + QFile f(path); + if (!f.open(QIODevice::WriteOnly)) { emit toast("cannot open file", 1); return; } + if (path.endsWith(".csv", Qt::CaseInsensitive)) { + QTextStream out(&f); + out << "time,delta_us,tid,irql,module,kind,function,args,result\n"; + for (const auto& e : backing) { + quint64 dt = (first_ts && e.timestamp_100ns >= first_ts) ? (e.timestamp_100ns - first_ts) / 10 : 0; + out << e.ts << "," << dt << "," << e.thread_id << "," << e.irql << "," + << e.module_id << "," << trace_kind_str(e.kind) << "," << e.name << ",\"" + << short_args_display(e) << "\"," + << QString::asprintf("0x%llx", (unsigned long long)e.rv) << "\n"; + } + } else { + // .gtrace binary: [magic 'GTR1'][count u32][first_ts u64][entries...] + QDataStream ds(&f); + ds.setByteOrder(QDataStream::LittleEndian); + ds << (quint32)0x31525447u << (quint32)backing.size() << (quint64)first_ts; + for (const auto& e : backing) { + ds << e.timestamp_100ns << e.module_id << e.kind << e.thread_id << e.irql + << (quint32)e.argv.size() << e.rv; + ds << e.name; + for (int i = 0; i < 4; i++) ds << (quint64)(i < e.argv.size() ? e.argv[i] : 0); + } + } + f.close(); + emit toast(QString("saved %1 events -> %2").arg(backing.size()).arg(QFileInfo(path).fileName()), 2); +} + +void TracePage::loadTraceFile(const QString& path) +{ + QFile f(path); + if (!f.open(QIODevice::ReadOnly)) { emit toast("cannot open file", 1); return; } + QDataStream ds(&f); + ds.setByteOrder(QDataStream::LittleEndian); + quint32 magic = 0, count = 0; + quint64 fts = 0; + ds >> magic >> count >> fts; + if (magic != 0x31525447u) { emit toast("not a .gtrace file", 1); return; } + clear(); + first_ts = fts; + QVector loaded; + for (quint32 i = 0; i < count; i++) { + TraceEvent e; + quint32 argc; + ds >> e.timestamp_100ns >> e.module_id >> e.kind >> e.thread_id >> e.irql >> argc >> e.rv; + ds >> e.name; + for (int a = 0; a < 4; a++) { + quint64 v; ds >> v; if ((quint32)a < argc) e.argv.push_back(v); + } + SYSTEMTIME sys; FILETIME ft, lft; + ft.dwLowDateTime = (DWORD) e.timestamp_100ns; + ft.dwHighDateTime = (DWORD)(e.timestamp_100ns >> 32); + FileTimeToLocalFileTime(&ft, &lft); FileTimeToSystemTime(&lft, &sys); + e.ts = QString::asprintf("%02u:%02u:%02u.%03u", sys.wHour, sys.wMinute, sys.wSecond, sys.wMilliseconds); + loaded.push_back(e); + } + f.close(); + append(loaded); + emit toast(QString("loaded %1 events from %2").arg(count).arg(QFileInfo(path).fileName()), 2); +} + +void TracePage::clear() +{ + table->setRowCount(0); backing.clear(); kind_counts.clear(); + total_events = 0; shown_events = 0; details->clear(); refresh_stats(); +} + +// ExplorerPage + +ExplorerPage::ExplorerPage(DriverClient* d, QWidget* p) : QWidget(p), drv(d) +{ + auto* root = new QVBoxLayout(this); + root->setContentsMargins(10, 10, 10, 8); + root->setSpacing(12); + + auto* topbar = new QFrame; + topbar->setObjectName("toolbar"); + auto* tb = new QHBoxLayout(topbar); + tb->setContentsMargins(14, 10, 14, 10); + tb->setSpacing(10); + auto* explain = new QLabel("Explorer uses a helper guest (toolkit.wasm) to read kernel state."); + explain->setStyleSheet("color:#999999; font-size:12px;"); + tb->addWidget(explain); + tb->addStretch(); + toolkitLbl = new QLabel("toolkit: not loaded"); + toolkitLbl->setStyleSheet("color:#cd5c5c; font-weight:600;"); + tb->addWidget(toolkitLbl); + loadToolkitBtn = new QPushButton("Load toolkit"); + loadToolkitBtn->setObjectName("btnPrimary"); + loadToolkitBtn->setToolTip("Loads toolkit.wasm from the app directory (or pick another)."); + tb->addWidget(loadToolkitBtn); + root->addWidget(topbar); + + auto* tabs = new QTabWidget; + tabs->setObjectName("wbTabs"); + + // processes tab + { + auto* w = new QWidget; + auto* l = new QVBoxLayout(w); + l->setContentsMargins(0, 8, 0, 0); + auto* row = new QHBoxLayout; + auto* refreshBtn = new QPushButton("Refresh"); + refreshBtn->setObjectName("btnPrimary"); + row->addWidget(refreshBtn); row->addStretch(); + l->addLayout(row); + procTable = new QTableWidget(0, 4); + procTable->setObjectName("dataTable"); + procTable->setHorizontalHeaderLabels({"PID", "Name", "EPROCESS", "Threads"}); + procTable->horizontalHeader()->setStretchLastSection(false); + procTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch); + procTable->setColumnWidth(0, 80); + procTable->setColumnWidth(2, 200); + procTable->setColumnWidth(3, 80); + procTable->verticalHeader()->setVisible(false); + procTable->setEditTriggers(QAbstractItemView::NoEditTriggers); + procTable->setShowGrid(false); + procTable->setAlternatingRowColors(true); + l->addWidget(procTable, 1); + connect(refreshBtn, &QPushButton::clicked, this, &ExplorerPage::refresh_procs); + tabs->addTab(w, "Processes"); + } + + // memory tab + { + auto* w = new QWidget; + auto* l = new QVBoxLayout(w); + l->setContentsMargins(0, 8, 0, 0); + auto* row = new QHBoxLayout; + row->addWidget(new QLabel("Address:")); + memAddr = new QLineEdit("0xfffff800`00000000"); + memAddr->setFont(QFont("JetBrainsMono NF", 10)); + memAddr->setFixedWidth(280); + row->addWidget(memAddr); + row->addWidget(new QLabel("Bytes:")); + memLen = new QSpinBox; memLen->setRange(16, 4096); memLen->setValue(256); + memLen->setFixedWidth(80); + row->addWidget(memLen); + auto* readBtn = new QPushButton("Read"); + readBtn->setObjectName("btnPrimary"); + row->addWidget(readBtn); + row->addStretch(); + l->addLayout(row); + memHex = new QPlainTextEdit; + memHex->setReadOnly(true); + memHex->setFont(QFont("JetBrainsMono NF", 10)); + memHex->setObjectName("hexView"); + l->addWidget(memHex, 1); + connect(readBtn, &QPushButton::clicked, this, &ExplorerPage::read_memory); + tabs->addTab(w, "Memory"); + } + + // MSR tab + { + auto* w = new QWidget; + auto* l = new QVBoxLayout(w); + l->setContentsMargins(0, 8, 0, 0); + auto* row = new QHBoxLayout; + auto* readBtn = new QPushButton("Read presets"); + readBtn->setObjectName("btnPrimary"); + row->addWidget(readBtn); + msrCustom = new QLineEdit; + msrCustom->setPlaceholderText("custom MSR index (hex or dec)"); + msrCustom->setFont(QFont("JetBrainsMono NF", 10)); + row->addWidget(msrCustom); + auto* customBtn = new QPushButton("Read"); + row->addWidget(customBtn); + row->addStretch(); + l->addLayout(row); + msrTable = new QTableWidget(0, 3); + msrTable->setObjectName("dataTable"); + msrTable->setHorizontalHeaderLabels({"MSR", "Name", "Value"}); + msrTable->horizontalHeader()->setStretchLastSection(false); + msrTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch); + msrTable->setColumnWidth(0, 100); + msrTable->setColumnWidth(2, 220); + msrTable->verticalHeader()->setVisible(false); + msrTable->setEditTriggers(QAbstractItemView::NoEditTriggers); + msrTable->setShowGrid(false); + msrTable->setAlternatingRowColors(true); + l->addWidget(msrTable, 1); + connect(readBtn, &QPushButton::clicked, this, &ExplorerPage::read_msrs); + connect(customBtn, &QPushButton::clicked, this, [this]{ + if (toolkit_id < 0) { emit toast("load toolkit first", 1); return; } + QString s = msrCustom->text().trimmed(); if (s.isEmpty()) return; + bool ok = false; + quint32 idx = s.startsWith("0x", Qt::CaseInsensitive) ? s.mid(2).toUInt(&ok, 16) : s.toUInt(&ok, 0); + if (!ok) { emit toast("bad MSR", 1); return; } + QVector argv = { idx }; + quint64 rv = 0; QString err; + if (drv->call_export(toolkit_id, "toolkit_readmsr", 2000, argv, rv, err)) { + int row = msrTable->rowCount(); + msrTable->insertRow(row); + msrTable->setItem(row, 0, new QTableWidgetItem(QString::asprintf("0x%x", idx))); + msrTable->setItem(row, 1, new QTableWidgetItem("(custom)")); + msrTable->setItem(row, 2, new QTableWidgetItem(QString::asprintf("0x%016llx", (unsigned long long)rv))); + for (int c = 0; c < 3; c++) msrTable->item(row, c)->setFont(QFont("JetBrainsMono NF", 10)); + } else emit toast(err, 1); + }); + tabs->addTab(w, "MSRs"); + } + + // CPUID tab + { + auto* w = new QWidget; + auto* l = new QVBoxLayout(w); + l->setContentsMargins(0, 8, 0, 0); + auto* row = new QHBoxLayout; + row->addWidget(new QLabel("Max leaf:")); + cpuidMax = new QSpinBox; cpuidMax->setRange(0, 0x40); cpuidMax->setValue(0x18); + cpuidMax->setFixedWidth(80); + row->addWidget(cpuidMax); + auto* dumpBtn = new QPushButton("Dump"); + dumpBtn->setObjectName("btnPrimary"); + row->addWidget(dumpBtn); + row->addStretch(); + l->addLayout(row); + cpuidTable = new QTableWidget(0, 6); + cpuidTable->setObjectName("dataTable"); + cpuidTable->setHorizontalHeaderLabels({"Leaf", "Sub", "EAX", "EBX", "ECX", "EDX"}); + cpuidTable->horizontalHeader()->setStretchLastSection(true); + cpuidTable->setColumnWidth(0, 70); + cpuidTable->setColumnWidth(1, 50); + for (int i = 2; i <= 5; i++) cpuidTable->setColumnWidth(i, 110); + cpuidTable->verticalHeader()->setVisible(false); + cpuidTable->setEditTriggers(QAbstractItemView::NoEditTriggers); + cpuidTable->setShowGrid(false); + cpuidTable->setAlternatingRowColors(true); + l->addWidget(cpuidTable, 1); + connect(dumpBtn, &QPushButton::clicked, this, &ExplorerPage::read_cpuid); + tabs->addTab(w, "CPUID"); + } + + root->addWidget(tabs, 1); + + connect(loadToolkitBtn, &QPushButton::clicked, this, [this]{ + QString here = QCoreApplication::applicationDirPath(); + QString def = here + "/toolkit.wasm"; + QString path; + if (QFile::exists(def)) { + // one-click if the shipped toolkit is next to the exe + path = def; + } else { + path = QFileDialog::getOpenFileName(this, "Select toolkit.wasm", + QString(), "WebAssembly (*.wasm)"); + if (path.isEmpty()) return; + } + quint32 id = 0; QString err; + if (drv->load_module(path, 0, id, err)) { + setToolkitId((int)id); + emit toast(QString("toolkit loaded as module %1").arg(id), 2); + } else emit toast("load failed: " + err, 1); + }); +} + +void ExplorerPage::setToolkitId(int id) +{ + toolkit_id = id; + if (id < 0) { + toolkitLbl->setText("toolkit: not loaded"); + toolkitLbl->setStyleSheet("color:#cd5c5c; font-weight:600;"); + } else { + toolkitLbl->setText(QString("toolkit: module %1").arg(id)); + toolkitLbl->setStyleSheet("color:#8fbf5c; font-weight:600;"); + } +} + +void ExplorerPage::refresh_procs() +{ + if (toolkit_id < 0) { emit toast("load toolkit first", 1); return; } + // 1. get scratch offset + QVector a0; quint64 scratch = 0; QString err; + if (!drv->call_export(toolkit_id, "toolkit_scratch_off", 2000, a0, scratch, err)) + { emit toast("toolkit_scratch_off: " + err, 1); return; } + // 2. enumerate procs into scratch + QVector a1 = { scratch, 64 }; + quint64 count = 0; + if (!drv->call_export(toolkit_id, "toolkit_enum_procs", 5000, a1, count, err)) + { emit toast("toolkit_enum_procs: " + err, 1); return; } + // 3. read scratch back + QByteArray data; + if (!drv->read_guest((quint32)toolkit_id, (quint32)scratch, (quint32)(count * 32), data, err)) + { emit toast("read_guest: " + err, 1); return; } + procTable->setRowCount((int)count); + QFont mono("JetBrainsMono NF", 10); + for (int i = 0; i < (int)count; i++) { + const char* p = data.constData() + i * 32; + quint64 pid = *(const quint64*)(p + 0); + quint64 eproc = *(const quint64*)(p + 8); + char name[16] = {0}; + memcpy(name, p + 16, 15); + auto* c0 = new QTableWidgetItem(QString::number(pid)); c0->setFont(mono); + auto* c1 = new QTableWidgetItem(QString::fromLatin1(name)); + auto* c2 = new QTableWidgetItem(QString::asprintf("0x%016llx", (unsigned long long)eproc)); + c2->setFont(mono); c2->setForeground(QColor("#8a8a8a")); + auto* c3 = new QTableWidgetItem("-"); c3->setFont(mono); + procTable->setItem(i, 0, c0); procTable->setItem(i, 1, c1); + procTable->setItem(i, 2, c2); procTable->setItem(i, 3, c3); + } +} + +void ExplorerPage::read_memory() +{ + if (toolkit_id < 0) { emit toast("load toolkit first", 1); return; } + QString a = memAddr->text(); a.remove('`').remove(' '); + bool ok = false; + quint64 va = a.startsWith("0x", Qt::CaseInsensitive) ? a.mid(2).toULongLong(&ok, 16) : a.toULongLong(&ok, 0); + if (!ok) { emit toast("bad address", 1); return; } + QString err; + quint64 scratch = 0; + if (!drv->call_export(toolkit_id, "toolkit_scratch_off", 2000, {}, scratch, err)) + { emit toast(err, 1); return; } + quint32 len = (quint32)memLen->value(); + QVector argv = { va, scratch, len }; + quint64 bytes_read = 0; + if (!drv->call_export(toolkit_id, "toolkit_read_kmem", 2000, argv, bytes_read, err)) + { emit toast(err, 1); return; } + if (bytes_read == 0) { memHex->setPlainText("(zero bytes read - probably invalid address)"); return; } + QByteArray data; + if (!drv->read_guest((quint32)toolkit_id, (quint32)scratch, (quint32)bytes_read, data, err)) + { emit toast("read_guest: " + err, 1); return; } + QString out; + for (int off = 0; off < data.size(); off += 16) { + out += QString::asprintf("%016llx ", (unsigned long long)(va + off)); + QString hex, ascii; + for (int i = 0; i < 16; i++) { + if (off + i < data.size()) { + unsigned char b = (unsigned char)data[off + i]; + hex += QString::asprintf("%02x ", b); + ascii += (b >= 0x20 && b < 0x7f) ? QChar(b) : '.'; + } else { hex += " "; ascii += ' '; } + if (i == 7) hex += " "; + } + out += hex + " " + ascii + "\n"; + } + memHex->setPlainText(out); +} + +static const struct { quint32 idx; const char* name; } MSR_PRESETS[] = { + { 0x001b, "IA32_APIC_BASE" }, + { 0x003a, "IA32_FEATURE_CONTROL" }, + { 0x00c0, "IA32_PMC0" }, + { 0x00c1, "IA32_PMC1" }, + { 0x0174, "IA32_SYSENTER_CS" }, + { 0x0175, "IA32_SYSENTER_ESP" }, + { 0x0176, "IA32_SYSENTER_EIP" }, + { 0x01d9, "IA32_DEBUGCTL" }, + { 0x0277, "IA32_PAT" }, + { 0x02ff, "IA32_MTRR_DEF_TYPE" }, + { 0xc0000080, "IA32_EFER" }, + { 0xc0000081, "IA32_STAR" }, + { 0xc0000082, "IA32_LSTAR" }, + { 0xc0000083, "IA32_CSTAR" }, + { 0xc0000084, "IA32_FMASK" }, + { 0xc0000100, "IA32_FS_BASE" }, + { 0xc0000101, "IA32_GS_BASE" }, + { 0xc0000102, "IA32_KERNEL_GS_BASE" }, + { 0xc0000103, "IA32_TSC_AUX" }, + { 0, nullptr } +}; + +void ExplorerPage::read_msrs() +{ + if (toolkit_id < 0) { emit toast("load toolkit first", 1); return; } + msrTable->setRowCount(0); + QFont mono("JetBrainsMono NF", 10); + for (int i = 0; MSR_PRESETS[i].name; i++) { + QVector argv = { MSR_PRESETS[i].idx }; + quint64 rv = 0; QString err; + bool ok = drv->call_export(toolkit_id, "toolkit_readmsr", 2000, argv, rv, err); + int row = msrTable->rowCount(); + msrTable->insertRow(row); + msrTable->setItem(row, 0, new QTableWidgetItem(QString::asprintf("0x%x", MSR_PRESETS[i].idx))); + msrTable->setItem(row, 1, new QTableWidgetItem(MSR_PRESETS[i].name)); + msrTable->setItem(row, 2, new QTableWidgetItem(ok ? QString::asprintf("0x%016llx", (unsigned long long)rv) : QString("err: " + err))); + for (int c = 0; c < 3; c++) msrTable->item(row, c)->setFont(mono); + if (!ok) msrTable->item(row, 2)->setForeground(QColor("#cd5c5c")); + } +} + +void ExplorerPage::read_cpuid() +{ + if (toolkit_id < 0) { emit toast("load toolkit first", 1); return; } + cpuidTable->setRowCount(0); + QFont mono("JetBrainsMono NF", 10); + QString err; + quint64 scratch = 0; + if (!drv->call_export(toolkit_id, "toolkit_scratch_off", 2000, {}, scratch, err)) + { emit toast(err, 1); return; } + int maxL = cpuidMax->value(); + for (int leaf = 0; leaf <= maxL; leaf++) { + QVector argv = { (quint32)leaf, 0, (quint32)scratch }; + quint64 rv = 0; + if (!drv->call_export(toolkit_id, "toolkit_cpuid", 2000, argv, rv, err)) continue; + QByteArray data; + if (!drv->read_guest((quint32)toolkit_id, (quint32)scratch, 16, data, err)) continue; + quint32 eax = *(const quint32*)(data.constData() + 0); + quint32 ebx = *(const quint32*)(data.constData() + 4); + quint32 ecx = *(const quint32*)(data.constData() + 8); + quint32 edx = *(const quint32*)(data.constData() + 12); + int row = cpuidTable->rowCount(); + cpuidTable->insertRow(row); + cpuidTable->setItem(row, 0, new QTableWidgetItem(QString::asprintf("0x%02x", leaf))); + cpuidTable->setItem(row, 1, new QTableWidgetItem("0")); + cpuidTable->setItem(row, 2, new QTableWidgetItem(QString::asprintf("0x%08x", eax))); + cpuidTable->setItem(row, 3, new QTableWidgetItem(QString::asprintf("0x%08x", ebx))); + cpuidTable->setItem(row, 4, new QTableWidgetItem(QString::asprintf("0x%08x", ecx))); + cpuidTable->setItem(row, 5, new QTableWidgetItem(QString::asprintf("0x%08x", edx))); + for (int c = 0; c < 6; c++) cpuidTable->item(row, c)->setFont(mono); + } +} diff --git a/gui-qt/pages.h b/gui-qt/pages.h new file mode 100644 index 0000000..ff7af95 --- /dev/null +++ b/gui-qt/pages.h @@ -0,0 +1,208 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "driver.h" + +class WorkbenchPage : public QWidget { + Q_OBJECT +public: + WorkbenchPage(DriverClient* d, QWidget* p = nullptr); + void refreshModules(const QVector& mods); + void setSelected(quint32 id, const ModuleInfo& info); + void clearSelection(); +signals: + void moduleLoaded(quint32 id); + void moduleSelected(quint32 id); + void toast(const QString& msg, int kind); +private: + DriverClient* drv; + QLineEdit* loadPath; + QSpinBox* loadBudget; + QListWidget* moduleList; + QLabel* moduleCount; + QLabel* modTitle; + QLabel* modMeta; + QWidget* capsBar; + QTableWidget* expTable; + QTableWidget* impTable; + QLineEdit* callExport; + QLineEdit* callArgs; + QSpinBox* callTimeout; + QLabel* callResult; + QTableWidget* callHist; + ModuleInfo current; + QWidget* infoPanel; + QWidget* emptyPanel; + quint32 pending_select = 0; + void refreshCaps(quint32 caps); +}; + +class MemoryPage : public QWidget { + Q_OBJECT +public: + MemoryPage(DriverClient* d, QWidget* p = nullptr); +signals: + void toast(const QString& msg, int kind); +private: + DriverClient* drv; + QSpinBox* modId; + QLineEdit* exp; + QLineEdit* addr; + QSpinBox* length; + QLabel* status; + QPlainTextEdit* hex; +}; + +class EventsPage : public QWidget { + Q_OBJECT +public: + EventsPage(DriverClient* d, QWidget* p = nullptr); + void append(const QVector& entries); + void clear(); +signals: + void toast(const QString& msg, int kind); +private slots: + void on_load_tracer(); + void on_start(bool reactive); + void on_stop(); +private: + DriverClient* drv; + QTableWidget* table; + QLineEdit* filter; + QCheckBox* cbProc; + QCheckBox* cbExit; + QCheckBox* cbImage; + QCheckBox* cbCall; + QCheckBox* cbError; + QPushButton* loadBtn; + QPushButton* startBtn; + QPushButton* reactiveBtn; + QPushButton* stopBtn; + QLabel* statusLbl; + int tracer_id = -1; + bool running = false; + void set_status(const QString& s, const QString& color); +}; + +class LogPage : public QWidget { + Q_OBJECT +public: + LogPage(QWidget* p = nullptr); + void append(const QVector& entries, quint64 seq, quint32 dropped); + void clear(); +private: + QPlainTextEdit* view; + QLabel* statusLbl; + QCheckBox* autoscroll; +}; + +class TracePage : public QWidget { + Q_OBJECT +public: + TracePage(DriverClient* d, QWidget* p = nullptr); + void append(const QVector& events); + void clear(); + void focusFilter() { filter->setFocus(); filter->selectAll(); } + void saveTraceFile(const QString& path); + void loadTraceFile(const QString& path); + void toggleTracing(); + void toggleBookmark(); +signals: + void toast(const QString& msg, int kind); +private: + DriverClient* drv; + QTableWidget* table; + QPlainTextEdit* details; + QPushButton* toggleBtn; + QPushButton* pauseBtn; + QPushButton* clearBtn; + QPushButton* exportBtn; + QPushButton* openBtn; + QSpinBox* moduleFilter; + QCheckBox* onlyModule; + QCheckBox* regexMode; + QLineEdit* filter; + QLabel* modeLbl; + QLabel* statsLbl; + bool tracing_on = false; + bool paused = false; + quint64 total_events = 0; + quint64 shown_events = 0; + quint64 first_ts = 0; + QHash kind_counts; + QVector backing; + QSet bookmarks; // indices into backing + void render_details(const TraceEvent& e); + void refresh_stats(); + void rebuild_from_backing(); + bool filter_match(const TraceEvent& e) const; + void insert_row(int row, const TraceEvent& e, int back_idx); +}; + +class ExplorerPage : public QWidget { + Q_OBJECT +public: + ExplorerPage(DriverClient* d, QWidget* p = nullptr); + void setToolkitId(int id); +signals: + void toast(const QString& msg, int kind); +private slots: + void refresh_procs(); + void read_memory(); + void read_msrs(); + void read_cpuid(); +private: + DriverClient* drv; + int toolkit_id = -1; + QLabel* toolkitLbl; + QPushButton* loadToolkitBtn; + // procs + QTableWidget* procTable; + // memory + QLineEdit* memAddr; + QSpinBox* memLen; + QPlainTextEdit* memHex; + // msr + QTableWidget* msrTable; + QLineEdit* msrCustom; + // cpuid + QTableWidget* cpuidTable; + QSpinBox* cpuidMax; +}; + +class ProfilerPage : public QWidget { + Q_OBJECT +public: + ProfilerPage(QWidget* p = nullptr); + void observe(const QVector& events); + void clear(); +private: + struct Stat { quint64 count = 0; quint64 total_ns = 0; quint64 max_ns = 0; quint64 last_call_ts = 0; }; + QHash stats; + QTableWidget* table; + QLabel* total; + void refresh(); +}; + +class DeployPage : public QWidget { + Q_OBJECT +public: + DeployPage(DriverClient* d, QWidget* p = nullptr); + void refresh(bool devUp, bool present, bool running); +signals: + void toast(const QString& msg, int kind); + void stateChanged(); +private: + DriverClient* drv; + QLabel* svcState; + QLabel* runState; + QLabel* devState; +}; diff --git a/gui-qt/resources.qrc b/gui-qt/resources.qrc new file mode 100644 index 0000000..84866a8 --- /dev/null +++ b/gui-qt/resources.qrc @@ -0,0 +1,5 @@ + + + style.qss + + diff --git a/gui-qt/style.qss b/gui-qt/style.qss new file mode 100644 index 0000000..22a8cc7 --- /dev/null +++ b/gui-qt/style.qss @@ -0,0 +1,355 @@ +/* professional grayscale. no brand accent, colors only carry meaning. + * sharp rectangular geometry. references: Sublime Merge, IDA Pro, Xcode. + * palette is warm-neutral gray with white for active states, semantic + * colors reserved for ok/warn/err. */ + +* { + font-family: "Segoe UI Variable", "Segoe UI", "Inter", sans-serif; + font-size: 12.5px; +} + +QMainWindow { + background: #1c1c1c; + color: #dcdcdc; +} +QWidget { color: #dcdcdc; } +QLabel { background: transparent; color: #dcdcdc; } + +/* menubar */ + +QMenuBar { + background: #1c1c1c; + color: #dcdcdc; + border-bottom: 1px solid #2c2c2c; + padding: 1px 2px; +} +QMenuBar::item { + padding: 4px 11px; + background: transparent; + border-radius: 2px; +} +QMenuBar::item:selected { background: #333333; color: #ffffff; } +QMenuBar::item:pressed { background: #404040; } + +QMenu { + background: #242424; + color: #dcdcdc; + border: 1px solid #404040; + padding: 3px; + border-radius: 2px; +} +QMenu::item { padding: 5px 24px 5px 22px; border-radius: 2px; } +QMenu::item:selected { background: #3a3a3a; color: #ffffff; } +QMenu::separator { height: 1px; background: #333333; margin: 3px 4px; } + +/* toolbar */ + +QToolBar { + background: #232323; + border: none; + border-bottom: 1px solid #2c2c2c; + padding: 3px 4px; + spacing: 2px; +} +QToolBar::separator { + background: #3a3a3a; + width: 1px; + margin: 4px 5px; +} +QToolButton { + background: transparent; + color: #c8c8c8; + border: 1px solid transparent; + border-radius: 2px; + padding: 4px 11px; + font-size: 12.5px; +} +QToolButton:hover { background: #333333; color: #ffffff; } +QToolButton:pressed { background: #2a2a2a; } +QToolButton:disabled{ color: #5f5f5f; } + +/* tab bar (top-level) */ + +QTabWidget::pane { + border: none; + background: #1c1c1c; + top: -1px; +} +QTabWidget#mainTabs::pane { border-top: 1px solid #2c2c2c; } + +QTabBar { qproperty-drawBase: 0; } +QTabBar::tab { + background: #1c1c1c; + color: #8b8b8b; + padding: 7px 18px; + border: none; + min-width: 70px; + font-size: 12.5px; +} +QTabBar::tab:hover { color: #dcdcdc; } +QTabBar::tab:selected { color: #ffffff; } +QTabBar#mainTabs::tab:selected { border-bottom: 2px solid #dcdcdc; } + +/* status bar */ + +QStatusBar { + background: #1c1c1c; + color: #8b8b8b; + border-top: 1px solid #2c2c2c; + padding: 1px 8px; + font-size: 11.5px; +} +QStatusBar QLabel { color: #8b8b8b; padding: 0 6px; } +QStatusBar::item { border: none; } + +/* typography */ + +QLabel#h2 { font-size: 13.5px; font-weight: 600; color: #ffffff; } +QLabel#subtitle { color: #8b8b8b; font-size: 12px; } +QLabel#sectionLabel { color: #8b8b8b; font-size: 11px; font-weight: 600; } +QLabel#dim { color: #8b8b8b; font-size: 11.5px; } +QLabel#emptyState { color: #5f5f5f; font-size: 12.5px; padding: 30px; } +QLabel#moduleTitle{ font-size: 14.5px; font-weight: 600; color: #ffffff; } +QLabel#mono { font-family: "JetBrainsMono NF", "Consolas", monospace; font-size: 11.5px; color: #8b8b8b; } +QLabel#resultLabel{ font-family: "JetBrainsMono NF", "Consolas", monospace; color: #8b8b8b; font-size: 12px; } + +/* panel */ + +QFrame#card { + background: #242424; + border: 1px solid #2c2c2c; + border-radius: 2px; +} +QFrame#toolbar { + background: #242424; + border: 1px solid #2c2c2c; + border-radius: 2px; +} +QFrame#tbsep { background: #3a3a3a; max-width: 1px; } +QFrame#sep, +QFrame#vsep { background: #2c2c2c; max-height: 1px; max-width: 1px; border: none; } + +/* pill */ + +QLabel#pill { + padding: 2px 9px; + border-radius: 2px; + font-family: "JetBrainsMono NF", monospace; + font-size: 10.5px; + color: #5f5f5f; + background: transparent; + border: 1px solid #333333; +} +QLabel#pill[kind="on"] { + color: #ffffff; + background: #333333; + border: 1px solid #505050; +} +QLabel#pill[kind="off"] { + color: #5f5f5f; + background: transparent; + border: 1px solid #2c2c2c; +} + +/* buttons */ + +QPushButton { + background: #2f2f2f; + color: #dcdcdc; + border: 1px solid #404040; + border-radius: 2px; + padding: 5px 14px; + font-size: 12.5px; + min-height: 22px; +} +QPushButton:hover { background: #3a3a3a; border-color: #505050; color: #ffffff; } +QPushButton:pressed { background: #262626; } +QPushButton:disabled{ color: #5f5f5f; background: #232323; border-color: #2c2c2c; } + +QPushButton#btnPrimary { + background: #e6e6e6; + color: #1c1c1c; + border: 1px solid #e6e6e6; + font-weight: 600; +} +QPushButton#btnPrimary:hover { background: #ffffff; border-color: #ffffff; } +QPushButton#btnPrimary:pressed{ background: #c8c8c8; } +QPushButton#btnPrimary:disabled { background: #333333; color: #5f5f5f; border-color: #333333; } + +QPushButton#btnDanger { + color: #e06c6c; + background: #2f2f2f; + border: 1px solid #4a2626; +} +QPushButton#btnDanger:hover { background: #3a2626; border-color: #6a2f2f; } + +/* inputs */ + +QLineEdit, QSpinBox, QDoubleSpinBox, QComboBox { + background: #1a1a1a; + color: #e6e6e6; + border: 1px solid #333333; + border-radius: 2px; + padding: 5px 8px; + selection-background-color: #4a4a4a; + selection-color: #ffffff; + min-height: 20px; + font-size: 12px; +} +QLineEdit:focus, QSpinBox:focus, QDoubleSpinBox:focus, QComboBox:focus { + border: 1px solid #6a6a6a; + background: #1e1e1e; +} +QSpinBox::up-button, QSpinBox::down-button, +QDoubleSpinBox::up-button, QDoubleSpinBox::down-button { width: 0; border: none; } + +/* tables */ + +QTableWidget#dataTable, QTableView { + background: #1c1c1c; + color: #dcdcdc; + border: 1px solid #2c2c2c; + border-radius: 2px; + gridline-color: transparent; + selection-background-color: #3a3a3a; + selection-color: #ffffff; + outline: 0; + alternate-background-color: #1f1f1f; +} +QTableWidget#dataTable::item, QTableView::item { + padding: 4px 10px; + border: none; +} +QTableWidget#dataTable::item:selected, QTableView::item:selected { + background: #3a3a3a; + color: #ffffff; +} + +QHeaderView::section { + background: #232323; + color: #8b8b8b; + padding: 6px 10px; + border: none; + border-right: 1px solid #2c2c2c; + border-bottom: 1px solid #2c2c2c; + font-size: 11px; + font-weight: 600; +} +QHeaderView::section:hover { color: #dcdcdc; } + +/* inner tabs (Exports/Imports) */ + +QTabWidget#wbTabs::pane { border: none; background: transparent; top: -1px; } +QTabWidget#wbTabs QTabBar::tab { + background: transparent; + color: #8b8b8b; + padding: 6px 16px; + border: none; + border-bottom: 2px solid transparent; + font-weight: 500; + font-size: 12.5px; +} +QTabWidget#wbTabs QTabBar::tab:selected { color: #ffffff; border-bottom: 2px solid #dcdcdc; } +QTabWidget#wbTabs QTabBar::tab:hover { color: #dcdcdc; } + +/* text panels */ + +QPlainTextEdit#logView, QPlainTextEdit#hexView, QPlainTextEdit#detailsPane { + background: #1a1a1a; + color: #dcdcdc; + border: 1px solid #2c2c2c; + border-radius: 2px; + padding: 8px 10px; + selection-background-color: #4a4a4a; + font-family: "JetBrainsMono NF", "Consolas", monospace; + font-size: 11.5px; +} + +/* checkbox */ + +QCheckBox { + color: #dcdcdc; + spacing: 6px; + padding: 2px 0; + font-size: 12px; +} +QCheckBox::indicator { + width: 13px; height: 13px; + border: 1px solid #4a4a4a; + border-radius: 2px; + background: #1a1a1a; +} +QCheckBox::indicator:hover { border-color: #6a6a6a; } +QCheckBox::indicator:checked { background: #dcdcdc; border-color: #dcdcdc; } + +/* list widget */ + +QListWidget { + background: #1c1c1c; + color: #c8c8c8; + border: 1px solid #2c2c2c; + border-radius: 2px; + outline: 0; + padding: 1px; +} +QListWidget::item { + padding: 5px 10px; + border: none; + color: #c8c8c8; +} +QListWidget::item:hover { background: #242424; color: #ffffff; } +QListWidget::item:selected { background: #3a3a3a; color: #ffffff; } + +/* splitter */ + +QSplitter::handle { background: #2c2c2c; } +QSplitter::handle:hover { background: #505050; } +QSplitter::handle:horizontal { width: 1px; } +QSplitter::handle:vertical { height: 1px; } + +/* scrollbars */ + +QScrollBar:vertical { + background: #1c1c1c; + width: 11px; + margin: 0; + border-left: 1px solid #2c2c2c; +} +QScrollBar::handle:vertical { + background: #3a3a3a; + min-height: 24px; + border-radius: 0; + margin: 1px; +} +QScrollBar::handle:vertical:hover { background: #505050; } +QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical, +QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { + background: transparent; height: 0; +} +QScrollBar:horizontal { + background: #1c1c1c; + height: 11px; + margin: 0; + border-top: 1px solid #2c2c2c; +} +QScrollBar::handle:horizontal { + background: #3a3a3a; + min-width: 24px; + margin: 1px; +} +QScrollBar::handle:horizontal:hover { background: #505050; } +QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal, +QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal { + background: transparent; width: 0; +} + +/* tooltip */ + +QToolTip { + background: #242424; + color: #dcdcdc; + border: 1px solid #404040; + padding: 3px 7px; + border-radius: 0; + font-size: 11.5px; +} diff --git a/sample_guest/build_all.cmd b/sample_guest/build_all.cmd new file mode 100644 index 0000000..f95cf4b --- /dev/null +++ b/sample_guest/build_all.cmd @@ -0,0 +1,25 @@ +@echo off +setlocal +cd /d "%~dp0" +set CLANG=%LLVM_HOME%\bin\clang.exe +if not exist "%CLANG%" set CLANG=C:\Program Files\LLVM\bin\clang.exe +if not exist "%CLANG%" ( + echo [X] clang not found. install LLVM or set LLVM_HOME. + exit /b 1 +) + +set CFLAGS=-O2 --target=wasm32 -nostdlib -fno-builtin -I..\guest_sdk +set LFLAGS=-Wl,--no-entry -Wl,--export-dynamic -Wl,--allow-undefined -Wl,--strip-all + +call :one sample_guest +call :one ffi_demo +call :one pslist_dumper +call :one handle_stripper +call :one infinity_hook + +exit /b 0 + +:one +echo [*] %~1 +"%CLANG%" %CFLAGS% %LFLAGS% -o %~1.wasm %~1.c +exit /b 0 diff --git a/sample_guest/ffi_demo.c b/sample_guest/ffi_demo.c new file mode 100644 index 0000000..29a87ec --- /dev/null +++ b/sample_guest/ffi_demo.c @@ -0,0 +1,87 @@ +/* ffi_demo.c - shows the direct FFI patterns exposed by the driver. + * every kernel API call here goes through the trampoline: no name lookup + * at call time, standard x64 ABI, traced with the real symbol name. + */ + +#include "../guest_sdk/gvm.h" + +GVM_MANIFEST(GVM_CAP_ALLOC | GVM_CAP_READ_KMEM | GVM_CAP_INTROSPECT | GVM_CAP_HOSTCALL); + +// direct kernel imports beyond what's in the SDK. any exported nt/hal +// symbol works this way. +GVM_IMPORT_KERNEL(KeQueryTimeIncrement) u32 KeQueryTimeIncrement(void); +GVM_IMPORT_KERNEL(MmGetPhysicalAddress) u64 MmGetPhysicalAddress(PVOID va); +GVM_IMPORT_KERNEL(PsIsSystemThread) BOOLEAN PsIsSystemThread(PVOID thread); + +static char g_scratch[512]; + +GVM_EXPORT(demo_scalar_apis) +u32 demo_scalar_apis(void) +{ + // pure scalar-in / scalar-out FFI. no buffer marshalling needed. + u32 pid = (u32)(u64)PsGetCurrentProcessId(); + u32 tid = (u32)(u64)PsGetCurrentThreadId(); + u8 irql = KeGetCurrentIrql(); + u32 tinc = KeQueryTimeIncrement(); + u8 isys = PsIsSystemThread(KeGetCurrentThread()); + + char buf[128]; + u32 n = 0; + n += gvm_strcpy(buf + n, "pid="); gvm_dec(pid, buf + n); n += gvm_strlen(buf + n); + n += gvm_strcpy(buf + n, " tid="); gvm_dec(tid, buf + n); n += gvm_strlen(buf + n); + n += gvm_strcpy(buf + n, " irql="); gvm_dec(irql, buf + n); n += gvm_strlen(buf + n); + n += gvm_strcpy(buf + n, " tinc="); gvm_dec(tinc, buf + n); n += gvm_strlen(buf + n); + n += gvm_strcpy(buf + n, " sys="); gvm_dec(isys, buf + n); n += gvm_strlen(buf + n); + gvm_print(buf); + return pid; +} + +// showing a POINTER out-param pattern. own-buffer VA computed via gvm_kva. +GVM_EXPORT(demo_pointer_out) +u64 demo_pointer_out(u64 va) +{ + // MmGetPhysicalAddress returns a PHYSICAL_ADDRESS struct (union of u64). + // on x64 with FFI it comes back in RAX as a u64. + u64 pa = MmGetPhysicalAddress((PVOID)va); + return pa; +} + +// buffer read INTO a wasm-side struct via a real kernel VA computed from +// our own linear memory. no host_read_bytes needed. +GVM_EXPORT(demo_buffer) +u64 demo_buffer(u64 kernel_src, u32 len) +{ + if (len > sizeof(g_scratch)) len = sizeof(g_scratch); + // build a real kernel VA of &g_scratch[0] inside our wasm memory + u64 dst = gvm_kva(g_scratch); + RtlCopyMemory((PVOID)dst, (PVOID)kernel_src, len); + // return the first 8 bytes as a sanity check + u64 v = 0; + for (u32 i = 0; i < 8 && i < len; i++) + v |= ((u64)(u8)g_scratch[i]) << (i * 8); + return v; +} + +// UNICODE_STRING roundtrip. build one, resolve a symbol with it, free it. +GVM_EXPORT(demo_unistr) +u64 demo_unistr(void) +{ + // "ExAllocatePool2" as UTF-16LE + static const u16 wname[] = { + 'E','x','A','l','l','o','c','a','t','e','P','o','o','l','2' + }; + u64 us = host_make_unistr((u32)(unsigned long)wname, sizeof(wname)); + if (!us) return 0; + u64 addr = (u64)MmGetSystemRoutineAddress((PVOID)us); + host_free_unistr(us); + return addr; +} + +// canonical entry point exercised by the GUI's Call button +GVM_EXPORT(run_all) +u64 run_all(void) +{ + demo_scalar_apis(); + (void)demo_unistr(); + return 0; +} diff --git a/sample_guest/handle_stripper.c b/sample_guest/handle_stripper.c new file mode 100644 index 0000000..df146eb --- /dev/null +++ b/sample_guest/handle_stripper.c @@ -0,0 +1,82 @@ +/* handle_stripper.c - enumerate handles held by a target process using + * the documented ExEnumHandleTable API. reports each entry's ObjectHeader + * and object type index. useful for auditing what a suspicious process + * is holding open (LSASS handles, process handles with debug rights, etc). + * + * this is a READ-ONLY tool. it doesn't actually strip anything. writing + * to HANDLE_TABLE_ENTRY needs raw kernel writes that are per-build-fragile. + * see the strip_by_object_pattern function below for a scaffold if you + * want to extend. + */ + +#include "gvm.h" + +GVM_MANIFEST(GVM_CAP_ALLOC | GVM_CAP_READ_KMEM | GVM_CAP_INTROSPECT | GVM_CAP_HOSTCALL); + +// callback context passed to ExEnumHandleTable +typedef struct { + u32 count; + u32 target_pid; +} enum_ctx; + +// scan EPROCESS for ObjectTable pointer. On modern Windows this is at +// a stable-ish offset but changes by build. Dynamic search: find the field +// whose value is a valid kernel pointer to something that looks like a +// HANDLE_TABLE (starts with a small integer HandleCount at offset ~0). +static u64 find_object_table(u64 eproc) +{ + for (u32 off = 0x400; off < 0x800; off += 8) { + u64 v = gvm_read_u64(eproc + off); + if ((v >> 48) != 0xFFFF) continue; + u32 first = gvm_read_u32(v); + if (first < 0x100000 && first > 0) return v; // HandleCount-ish + } + return 0; +} + +GVM_EXPORT(list_handles) +u64 list_handles(u64 target_pid) +{ + u64 target_eproc = 0; + u64 status = GVM_CALL2(PsLookupProcessByProcessId, + target_pid, (u64)(u32)(u64)&target_eproc); + if (status != 0 || target_eproc == 0) { + gvm_print("PsLookupProcessByProcessId failed"); + return 0; + } + + u64 table = find_object_table(target_eproc); + if (!table) { + gvm_print("could not locate ObjectTable in EPROCESS"); + GVM_CALL1(ObfDereferenceObject, target_eproc); + return 0; + } + + u32 handle_count = gvm_read_u32(table); + char hex[17]; + gvm_print("target EPROCESS ="); gvm_hex64(target_eproc, hex); gvm_print(hex); + gvm_print("ObjectTable ="); gvm_hex64(table, hex); gvm_print(hex); + gvm_print("HandleCount ="); gvm_hex64(handle_count, hex); gvm_print(hex); + + // full HANDLE_TABLE walk requires TableCode (3-level tree) which is per-build. + // scaffold: use ExEnumHandleTable(HANDLE_TABLE*, callback, ctx, HANDLE*) + //. signature is (HANDLE_TABLE*, ProcedurePtr, void*, HANDLE*) returning BOOLEAN. + // callback signature is BOOLEAN(HANDLE_TABLE_ENTRY* entry, HANDLE h, void* ctx). + // guests can't easily provide a callback (would need JIT'd wasm trampoline). + // for now just report the count and leave detailed walking to future work. + gvm_print("(detailed walk requires ExEnumHandleTable callback. see comments)"); + + GVM_CALL1(ObfDereferenceObject, target_eproc); + return (u64)handle_count; +} + +// scaffold: when we can provide a wasm:native trampoline, this is where +// filtering + close would happen. +GVM_EXPORT(strip_by_object_pattern) +u64 strip_by_object_pattern(u64 target_pid, u64 object_type_index) +{ + (void)target_pid; (void)object_type_index; + gvm_print("strip: not implemented. needs wasm:native callback trampoline"); + gvm_print(" (guest cannot pass a function pointer to ExEnumHandleTable)"); + return 0; +} diff --git a/sample_guest/infinity_hook.c b/sample_guest/infinity_hook.c new file mode 100644 index 0000000..bd66bf7 --- /dev/null +++ b/sample_guest/infinity_hook.c @@ -0,0 +1,168 @@ +/* infinity_hook.c - InfinityHook as a wasm guest. + * original technique: github.com/everdox/InfinityHook (MIT). */ + +#include "../guest_sdk/gvm.h" + +GVM_MANIFEST(GVM_CAP_READ_KMEM | GVM_CAP_WRITE_KMEM | GVM_CAP_INTROSPECT | + GVM_CAP_CALLBACKS | GVM_CAP_HOSTCALL); + +static u64 g_nt_base = 0; +static u32 g_nt_size = 0; +static u64 g_getclock_slot = 0; +static u64 g_orig_getclock = 0; + +// walk pages backwards from any VA inside nt.exe until we find the MZ+PE +// header pair. classic module-base recovery. +static u64 find_nt_base(u64 va_in_nt) +{ + u64 p = va_in_nt & ~0xFFFULL; + for (u32 i = 0; i < 4096 && p >= 0x1000; i++, p -= 0x1000) { + if (!MmIsAddressValid((PVOID)p)) continue; + if ((gvm_read_u32(p) & 0xFFFF) != 0x5A4D) continue; + u32 pe_off = gvm_read_u32(p + 0x3C); + if (!pe_off || pe_off > 0x1000) continue; + if (gvm_read_u32(p + pe_off) != 0x00004550) continue; + return p; + } + return 0; +} + +// scan nt's .data section for the classic InfinityHook signature that +// marks the start of EtwpDebuggerData: 2C 08 04 38 +static u64 find_etwp_debugger_data(u64 nt) +{ + u32 pe_off = gvm_read_u32(nt + 0x3C); + u16 nsec = (u16)(gvm_read_u32(nt + pe_off + 6) & 0xFFFF); + u16 opt_sz = (u16)(gvm_read_u32(nt + pe_off + 20) & 0xFFFF); + u64 sec = nt + pe_off + 24 + opt_sz; + + for (u16 i = 0; i < nsec; i++, sec += 40) { + if (gvm_read_u32(sec) != 0x61746164) continue; // '.dat' + u32 v_sz = gvm_read_u32(sec + 8); + u32 v_rva = gvm_read_u32(sec + 12); + u64 base = nt + v_rva; + u64 end = base + v_sz; + for (u64 q = base; q + 8 < end; q += 4) { + if (!MmIsAddressValid((PVOID)q)) continue; + if (gvm_read_u8(q + 0) == 0x2C && + gvm_read_u8(q + 1) == 0x08 && + gvm_read_u8(q + 2) == 0x04 && + gvm_read_u8(q + 3) == 0x38) + return q; + } + return 0; + } + return 0; +} + +// EtwpDebuggerData -> silo -> kernel WMI_LOGGER_CONTEXT -> GetCpuClock slot. +// GetCpuClock offset inside the context varies by Windows build (0x28 on +// most modern Win10/11, 0x18/0x30 on older). we try each and accept the +// one whose current value points into nt (unhooked baseline). +static u64 locate_getclock_slot(u64 nt, u32 nt_size, u64* out_orig) +{ + u64 etwp = find_etwp_debugger_data(nt); + if (!etwp) return 0; + + u64 silo = gvm_read_u64(etwp + 0x10); + if (!silo || !MmIsAddressValid((PVOID)silo)) return 0; + + u64 ctx = gvm_read_u64(silo + 2 * 8); // silo[2] = kernel logger context + if (!ctx || !MmIsAddressValid((PVOID)ctx)) return 0; + + static const u32 candidates[] = { 0x28, 0x18, 0x30 }; + for (u32 i = 0; i < 3; i++) { + u64 slot = ctx + candidates[i]; + if (!MmIsAddressValid((PVOID)slot)) continue; + u64 v = gvm_read_u64(slot); + if (v >= nt && v < nt + nt_size) { + *out_orig = v; + return slot; + } + } + return 0; +} + +// install: locate everything, atomically swap in the driver's trampoline. +GVM_EXPORT(start) +u64 start(u64 sample_rate) +{ + if (g_getclock_slot) { gvm_print("[ih] already installed"); return 0; } + + PVOID any = MmGetSystemRoutineAddress((PVOID)0); // arg unused for demo + // real resolve: build a UNICODE_STRING for a known nt export, pass it in + u64 ustr = host_make_unistr((u32)(unsigned long)L"KeBugCheckEx", + sizeof(L"KeBugCheckEx") - sizeof(u16)); + any = MmGetSystemRoutineAddress((PVOID)ustr); + host_free_unistr(ustr); + if (!any) { gvm_print("[ih] MmGetSystemRoutineAddress failed"); return 1; } + + g_nt_base = find_nt_base((u64)any); + if (!g_nt_base) { gvm_print("[ih] nt base not found"); return 2; } + u32 pe_off = gvm_read_u32(g_nt_base + 0x3C); + g_nt_size = gvm_read_u32(g_nt_base + pe_off + 24 + 56); // SizeOfImage + + g_getclock_slot = locate_getclock_slot(g_nt_base, g_nt_size, &g_orig_getclock); + if (!g_getclock_slot) { + gvm_print("[ih] GetCpuClock slot not located (build offsets may differ)"); + return 3; + } + + // hand the trampoline our nt bounds so it filters non-syscall callers + gvm_ih_configure((u32)sample_rate, g_nt_base, g_nt_size); + + // atomic pointer swap. aligned 8-byte write on x64 is single-copy atomic + u64 tramp = gvm_ih_trampoline(); + if (!tramp) { gvm_print("[ih] trampoline addr = 0"); return 4; } + gvm_write_u64(g_getclock_slot, tramp); + + char line[128], t[24]; u32 n = 0; + n += gvm_strcpy(line + n, "[ih] hook installed. nt=0x"); + gvm_hex64(g_nt_base, t); n += gvm_strcpy(line + n, t); + n += gvm_strcpy(line + n, " slot=0x"); + gvm_hex64(g_getclock_slot, t); n += gvm_strcpy(line + n, t); + n += gvm_strcpy(line + n, " orig=0x"); + gvm_hex64(g_orig_getclock, t); n += gvm_strcpy(line + n, t); + n += gvm_strcpy(line + n, " tramp=0x"); + gvm_hex64(tramp, t); n += gvm_strcpy(line + n, t); + gvm_print(line); + return 0; +} + +// uninstall: atomically restore original, then wait for in-flight trampoline +// calls to drain before we let the module be unloaded. +GVM_EXPORT(stop) +u64 stop(void) +{ + if (!g_getclock_slot) { gvm_print("[ih] not installed"); return 0; } + gvm_write_u64(g_getclock_slot, g_orig_getclock); + u64 total = gvm_ih_quiesce(); + g_getclock_slot = 0; + g_orig_getclock = 0; + + char line[80], t[24]; u32 n = 0; + n += gvm_strcpy(line + n, "[ih] hook removed. trampoline hits="); + gvm_dec((u32)total, t); n += gvm_strcpy(line + n, t); + gvm_print(line); + return total; +} + +// dispatched by the driver's worker on every sampled syscall event. +static volatile u32 g_seen = 0; + +GVM_EXPORT(on_syscall) +u64 on_syscall(u64 tid, u64 retaddr, u64 counter) +{ + g_seen++; + if ((g_seen & 63) != 0) return 0; // print 1 in 64 to keep log readable + + char line[128], t[24]; u32 n = 0; + n += gvm_strcpy(line + n, "[ih] tid="); + gvm_dec((u32)tid, t); n += gvm_strcpy(line + n, t); + n += gvm_strcpy(line + n, " ret=0x"); + gvm_hex64(retaddr, t); n += gvm_strcpy(line + n, t); + n += gvm_strcpy(line + n, " hit="); + gvm_dec((u32)counter, t); n += gvm_strcpy(line + n, t); + gvm_print(line); + return 0; +} diff --git a/sample_guest/pslist_dumper.c b/sample_guest/pslist_dumper.c new file mode 100644 index 0000000..4a7000d --- /dev/null +++ b/sample_guest/pslist_dumper.c @@ -0,0 +1,111 @@ +/* pslist_dumper.c - walk SYSTEM_PROCESS_INFORMATION via ZwQuerySystemInformation. + * safer than raw EPROCESS.ActiveProcessLinks traversal because we use the + * documented API surface. no offsets, no PatchGuard exposure. */ +#include "gvm.h" + +#define SystemProcessInformation 5 + +// SYSTEM_PROCESS_INFORMATION layout (x64). offsets verified against public winternl. +typedef struct { + u32 next_off; // 0x000 ULONG NextEntryOffset + u32 thread_count; // 0x004 ULONG NumberOfThreads + u64 pad1[3]; // 0x008 reserved + times + u64 create_time; // 0x020 LARGE_INTEGER CreateTime + u64 user_time; + u64 kernel_time; + u16 img_name_len; // 0x038 USHORT Length of ImageName + u16 img_name_max; // 0x03A + u32 pad2; // 0x03C + u64 img_name_ptr; // 0x040 PWSTR Buffer + u32 base_priority; // 0x048 + u32 pad3; // 0x04C + u64 unique_pid; // 0x050 HANDLE UniqueProcessId + u64 inherited_ppid; // 0x058 HANDLE InheritedFromUniqueProcessId + // ... more fields we don't need +} spi_hdr; + +static void wide_to_ascii(u64 wsrc, u16 wlen_bytes, char* dst, u32 dst_max) +{ + u16 chars = wlen_bytes / 2; + if (chars > dst_max - 1) chars = (u16)(dst_max - 1); + for (u16 i = 0; i < chars; i++) + dst[i] = (char)gvm_read_u8(wsrc + i * 2); + dst[chars] = 0; +} + +#define POOL_FLAG_NON_PAGED 0x0000000000000040ull + +GVM_EXPORT(dump) +u64 dump(void) +{ + gvm_print("dump: enter"); + + u32 sz = 64 * 1024; + PVOID buf = 0; + NTSTATUS s = 0; + + for (int tries = 0; tries < 8; tries++) { + if (buf) ExFreePool(buf); + buf = ExAllocatePool2(POOL_FLAG_NON_PAGED, sz, 'psLD'); + if (!buf) { gvm_print("dump: alloc failed"); return 0; } + + gvm_print("dump: alloc ok, calling ZwQSI"); + + u32 got_off = 0; + s = ZwQuerySystemInformation(SystemProcessInformation, buf, sz, (u32*)gvm_kva(&got_off)); + + gvm_print("dump: ZwQSI returned"); + + if (s == 0) break; + if ((u32)s != 0xC0000004) break; + sz *= 2; + } + + if (!NT_SUCCESS(s)) { + char hex[17]; + gvm_print("ZwQuerySystemInformation failed:"); + gvm_hex64((u64)(u32)s, hex); gvm_print(hex); + if (buf) ExFreePool(buf); + return 0; + } + + u32 count = 0; + u64 cur = (u64)buf; + + for (;;) { + // read the header via host_read_bytes into a local, then parse + spi_hdr h; + u32 n = gvm_read_bytes(cur, (u32)(u64)&h, sizeof(h)); + if (n != sizeof(h)) break; + + char name[64] = { 0 }; + if (h.img_name_ptr && h.img_name_len) + wide_to_ascii(h.img_name_ptr, h.img_name_len, name, sizeof(name)); + else + gvm_strcpy(name, ""); + + char line[160]; char t[32]; u32 lp = 0; + lp += gvm_strcpy(line + lp, "pid="); + gvm_dec((u32)h.unique_pid, t); lp += gvm_strcpy(line + lp, t); + lp += gvm_strcpy(line + lp, " ppid="); + gvm_dec((u32)h.inherited_ppid, t); lp += gvm_strcpy(line + lp, t); + lp += gvm_strcpy(line + lp, " thr="); + gvm_dec(h.thread_count, t); lp += gvm_strcpy(line + lp, t); + lp += gvm_strcpy(line + lp, " "); + lp += gvm_strcpy(line + lp, name); + gvm_print(line); + + count++; + if (h.next_off == 0) break; + cur += h.next_off; + } + + ExFreePool(buf); + + char sum[64]; char t[32]; u32 lp = 0; + lp += gvm_strcpy(sum + lp, "total: "); + gvm_dec(count, t); lp += gvm_strcpy(sum + lp, t); + lp += gvm_strcpy(sum + lp, " processes"); + gvm_print(sum); + return count; +} diff --git a/sample_guest/sample_guest.c b/sample_guest/sample_guest.c new file mode 100644 index 0000000..d09e56e --- /dev/null +++ b/sample_guest/sample_guest.c @@ -0,0 +1,66 @@ +/* sample_guest.c - reference guest exercising the Goodmans SDK */ + +#include "gvm.h" + +/* print pid/tid/irql via SDK wrappers over host_call */ +GVM_EXPORT(show_context) +u64 show_context(void) +{ + HANDLE pid = PsGetCurrentProcessId(); + HANDLE tid = PsGetCurrentThreadId(); + u8 irql = KeGetCurrentIrql(); + + char hex[17]; + gvm_print("context:"); + gvm_hex64((u64)pid, hex); gvm_print(hex); + gvm_hex64((u64)tid, hex); gvm_print(hex); + gvm_hex64((u64)irql, hex); gvm_print(hex); + + return ((u64)(u32)(u64)pid << 32) | (u32)(u64)tid; +} + +/* alloc a kernel pool, write a pattern, read it back, free */ +GVM_EXPORT(pool_roundtrip) +u64 pool_roundtrip(void) +{ + const SIZE_T sz = 256; + PVOID p = ExAllocatePoolWithTag(NonPagedPoolNx, sz, 'GsmP'); + if (!p) { gvm_print("alloc failed"); return 0; } + + for (u32 i = 0; i < sz / 8; i++) + gvm_write_u64((u64)p + i * 8, 0xAABBCCDD00000000ULL | i); + + u64 v = gvm_read_u64((u64)p); + char hex[17]; + gvm_print("first slot:"); + gvm_hex64(v, hex); gvm_print(hex); + + ExFreePoolWithTag(p, 'GsmP'); + return (u64)p; +} + +/* resolve arbitrary ntoskrnl exports via host_call */ +GVM_EXPORT(resolve_export) +u64 resolve_export(void) +{ + u64 irql = GVM_CALL0(KeGetCurrentIrql); + u64 tick = GVM_CALL0(KeQueryTimeIncrement); + + char hex[17]; + gvm_print("KeGetCurrentIrql:"); + gvm_hex64(irql, hex); gvm_print(hex); + gvm_print("KeQueryTimeIncrement:"); + gvm_hex64(tick, hex); gvm_print(hex); + + return tick; +} + +GVM_EXPORT(run_all) +u64 run_all(void) +{ + show_context(); + pool_roundtrip(); + resolve_export(); + gvm_print("run_all done"); + return 0; +} diff --git a/sample_guest/sample_guest.vcxproj b/sample_guest/sample_guest.vcxproj new file mode 100644 index 0000000..7db0cb0 --- /dev/null +++ b/sample_guest/sample_guest.vcxproj @@ -0,0 +1,57 @@ + + + + + Debug + x64 + + + Release + x64 + + + + {6D9E2A50-1F1A-4B4E-8D9F-1B0C0D0A0003} + sample_guest + MakeFileProj + 10.0 + + + + Makefile + v143 + + + + + call "$(MSBuildProjectDirectory)\build_all.cmd" + call "$(MSBuildProjectDirectory)\build_all.cmd" + del /Q "$(MSBuildProjectDirectory)\*.wasm" 2>nul + sample_guest.wasm;ffi_demo.wasm;pslist_dumper.wasm;handle_stripper.wasm;infinity_hook.wasm + ..\guest_sdk;$(NMakeIncludeSearchPath) + + + + + + + + + + + + + + + + + $(SolutionDir)deploy + + + + + + + + + diff --git a/shared/goodmans_ioctl.h b/shared/goodmans_ioctl.h new file mode 100644 index 0000000..901b444 --- /dev/null +++ b/shared/goodmans_ioctl.h @@ -0,0 +1,195 @@ +/* goodmans_ioctl.h - shared UM/KM ioctl interface */ +#pragma once + +#ifdef _KERNEL_MODE +#include +#else +#include +#endif + +#define GVM_DEVICE_NAME_U L"\\Device\\Goodmans" +#define GVM_SYMLINK_NAME_U L"\\DosDevices\\Goodmans" +#define GVM_USER_PATH "\\\\.\\Goodmans" + +#define GVM_DEVICE_TYPE 0x8000 +#define GVM_IOCTL(fn) CTL_CODE(GVM_DEVICE_TYPE, (fn), METHOD_BUFFERED, FILE_ANY_ACCESS) + +#define IOCTL_GVM_LOAD_MODULE GVM_IOCTL(0x800) +#define IOCTL_GVM_CALL_EXPORT GVM_IOCTL(0x801) +#define IOCTL_GVM_UNLOAD_MODULE GVM_IOCTL(0x802) +#define IOCTL_GVM_LIST_MODULES GVM_IOCTL(0x803) +#define IOCTL_GVM_MODULE_INFO GVM_IOCTL(0x804) +#define IOCTL_GVM_UNLOAD_ALL GVM_IOCTL(0x805) +#define IOCTL_GVM_TAIL_LOG GVM_IOCTL(0x806) +#define IOCTL_GVM_READ_GUEST GVM_IOCTL(0x807) +#define IOCTL_GVM_TAIL_TRACE GVM_IOCTL(0x808) +#define IOCTL_GVM_TRACE_CTL GVM_IOCTL(0x809) +#define IOCTL_GVM_FORCE_UNLOAD GVM_IOCTL(0x80a) +#define IOCTL_GVM_NOTIFY_STOP GVM_IOCTL(0x80b) + +#define GVM_MAX_MODULE_NAME 64 +#define GVM_MAX_EXPORT_NAME 64 +#define GVM_MAX_ARGS 8 +#define GVM_MAX_MODULES 32 + +// capability bits, packed into gvm_module.caps +#define GVM_CAP_ALLOC (1u << 0) // host_alloc / host_free +#define GVM_CAP_READ_KMEM (1u << 1) // host_read_u8/u32/u64/bytes +#define GVM_CAP_WRITE_KMEM (1u << 2) // host_write_u64/bytes +#define GVM_CAP_MSR_READ (1u << 3) // host_readmsr +#define GVM_CAP_MSR_WRITE (1u << 4) // host_writemsr +#define GVM_CAP_PHYSMEM (1u << 5) // host_phys_read / host_phys_write +#define GVM_CAP_CPUID_TSC (1u << 6) // host_cpuid, host_rdtsc +#define GVM_CAP_CALLBACKS (1u << 7) // host_notify_enable / dispatch +#define GVM_CAP_HOSTCALL (1u << 8) // host_call (arbitrary export resolve) +#define GVM_CAP_INTROSPECT (1u << 9) // pid/tid/irql/current_process +#define GVM_CAP_ALL 0xFFFFFFFFu + +typedef struct _gvm_load_in { + unsigned int wasm_size; + unsigned int stack_bytes; // 0 = 64KB default + unsigned long long pool_budget; // 0 = 4MB default + char name[GVM_MAX_MODULE_NAME]; +} gvm_load_in; + +typedef struct _gvm_load_out { + unsigned int module_id; + int status; + char err_msg[128]; +} gvm_load_out; + +typedef struct _gvm_call_in { + unsigned int module_id; + unsigned int argc; + unsigned int timeout_ms; // 0 = no limit + unsigned int _pad; + char export_name[GVM_MAX_EXPORT_NAME]; + unsigned long long argv[GVM_MAX_ARGS]; +} gvm_call_in; + +typedef struct _gvm_call_out { + unsigned long long rv; + int status; + char err_msg[128]; +} gvm_call_out; + +typedef struct _gvm_unload_in { + unsigned int module_id; +} gvm_unload_in; + +typedef struct _gvm_module_entry { + unsigned int id; + unsigned int wasm_size; + unsigned long long hash; + unsigned int exports; + unsigned int mem_pages; + unsigned long long pool_bytes; + char name[GVM_MAX_MODULE_NAME]; +} gvm_module_entry; + +typedef struct _gvm_list_out { + unsigned int count; + unsigned int pad; + gvm_module_entry entries[GVM_MAX_MODULES]; +} gvm_list_out; + +typedef struct _gvm_info_in { + unsigned int module_id; +} gvm_info_in; + +#define GVM_MAX_INFO_EXPORTS 64 +#define GVM_MAX_INFO_IMPORTS 64 +#define GVM_INFO_NAME_LEN 48 + +typedef struct _gvm_info_out { + gvm_module_entry base; + unsigned int export_count; + unsigned int import_count; + char exports[GVM_MAX_INFO_EXPORTS][GVM_INFO_NAME_LEN]; + char imports[GVM_MAX_INFO_IMPORTS][GVM_INFO_NAME_LEN]; + int status; + char err_msg[128]; +} gvm_info_out; + +// TAIL_LOG: driver keeps a ring of recent DbgPrint lines. GUI polls this to +// show live driver output without needing DebugView/WinDbg. +#define GVM_LOG_ENTRY_LEN 200 +#define GVM_LOG_ENTRIES 256 + +typedef struct _gvm_log_entry { + unsigned long long timestamp_100ns; + char line[GVM_LOG_ENTRY_LEN]; +} gvm_log_entry; + +typedef struct _gvm_tail_in { + unsigned long long last_seq; // returns entries with seq > last_seq +} gvm_tail_in; + +#define GVM_MAX_READ_GUEST_BYTES (16 * 1024) + +typedef struct _gvm_read_guest_in { + unsigned int module_id; + unsigned int offset; + unsigned int length; +} gvm_read_guest_in; + +typedef struct _gvm_read_guest_out { + int status; + unsigned int length; + unsigned char data[GVM_MAX_READ_GUEST_BYTES]; + char err_msg[128]; +} gvm_read_guest_out; + +typedef struct _gvm_tail_out { + unsigned long long next_seq; // pass this back as last_seq next time + unsigned int count; + unsigned int dropped; // events dropped since last poll (ring wrap) + gvm_log_entry entries[GVM_LOG_ENTRIES]; +} gvm_tail_out; + +// trace ring: records every wasm->host import invocation when tracing is +// enabled, plus trap/error captures. lets developers watch their guest +// modules run in real time and see exactly what triggered a failure. +#define GVM_TRACE_ENTRIES 512 +#define GVM_TRACE_NAME_LEN 48 + +// kind field values +#define GVM_TRK_IMPORT 1 // host import invoked +#define GVM_TRK_CALL 2 // export call started +#define GVM_TRK_RETURN 3 // export call returned +#define GVM_TRK_TRAP 4 // guest trapped (OOB, div0, unreachable, timeout) + + +typedef struct _gvm_trace_entry { + unsigned long long timestamp_100ns; + unsigned int module_id; + unsigned int kind; + unsigned int thread_id; // OS TID of the thread that made the call + unsigned int irql; // KIRQL at the time + unsigned int argc; + unsigned int _pad; + unsigned long long argv[4]; // up to 4 args captured + unsigned long long rv; // return value (or 0 for void) + char name[GVM_TRACE_NAME_LEN]; +} gvm_trace_entry; + +typedef struct _gvm_trace_in { + unsigned long long last_seq; +} gvm_trace_in; + +typedef struct _gvm_trace_out { + unsigned long long next_seq; + unsigned int count; + unsigned int dropped; + gvm_trace_entry entries[GVM_TRACE_ENTRIES]; +} gvm_trace_out; + +// trace control +#define GVM_TRACE_OFF 0 +#define GVM_TRACE_ON_ALL 1 // trace every module +#define GVM_TRACE_ON_MODULE 2 // trace only module_id + +typedef struct _gvm_trace_ctl_in { + unsigned int mode; + unsigned int module_id; // used when mode == GVM_TRACE_ON_MODULE +} gvm_trace_ctl_in; diff --git a/tests/fuzz/build.cmd b/tests/fuzz/build.cmd new file mode 100644 index 0000000..9b3b5bc --- /dev/null +++ b/tests/fuzz/build.cmd @@ -0,0 +1,29 @@ +@echo off +setlocal +set CLANG=%LLVM_HOME%\bin\clang.exe +if not exist "%CLANG%" set CLANG=C:\Program Files\LLVM\bin\clang.exe +if not exist "%CLANG%" ( + echo [X] clang not found. install LLVM or set LLVM_HOME. + exit /b 1 +) + +set W3=..\..\driver\wasm3 + +"%CLANG%" -O2 -g -fsanitize=fuzzer,address ^ + -DM3_IMPLEMENT_ERROR_STRINGS ^ + -Dd_m3HasFloat=0 ^ + -I"%W3%" ^ + fuzz_parse.c ^ + "%W3%\m3_bind.c" "%W3%\m3_code.c" "%W3%\m3_compile.c" ^ + "%W3%\m3_core.c" "%W3%\m3_env.c" "%W3%\m3_exec.c" ^ + "%W3%\m3_function.c" "%W3%\m3_info.c" "%W3%\m3_module.c" ^ + "%W3%\m3_parse.c" "%W3%\m3_validate.c" ^ + -o fuzz_parse.exe + +if not exist corpus mkdir corpus +if exist ..\..\sample_guest\sample_guest.wasm copy /Y ..\..\sample_guest\sample_guest.wasm corpus\ >nul + +echo [+] built fuzz_parse.exe. run with: +echo fuzz_parse.exe corpus -max_len=65536 -jobs=4 + +endlocal diff --git a/tests/fuzz/fuzz_parse.c b/tests/fuzz/fuzz_parse.c new file mode 100644 index 0000000..ae5fec4 --- /dev/null +++ b/tests/fuzz/fuzz_parse.c @@ -0,0 +1,46 @@ +/* fuzz_parse.c - libfuzzer target for m3_ParseModule. + * + * exercises the wasm3 parser with attacker-controlled bytes. + * kernel driver embeds the same parser; parser bugs here = potential + * bugcheck when someone loads a crafted .wasm via IOCTL_GVM_LOAD_MODULE. + * + * build (clang): + * clang -O2 -g -fsanitize=fuzzer,address \ + * -DM3_IMPLEMENT_ERROR_STRINGS \ + * -I../../driver/wasm3 \ + * fuzz_parse.c ../../driver/wasm3/m3_*.c \ + * -o fuzz_parse.exe + * + * run: + * fuzz_parse.exe corpus/ -max_len=65536 + * + * seed corpus: + * mkdir corpus && cp ../../sample_guest/sample_guest.wasm corpus/ + */ +#include +#include +#include +#include "wasm3.h" + +int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) +{ + if (size < 8 || size > (16u * 1024u * 1024u)) return 0; + + IM3Environment env = m3_NewEnvironment(); + if (!env) return 0; + + IM3Runtime rt = m3_NewRuntime(env, 64 * 1024, NULL); + if (!rt) { m3_FreeEnvironment(env); return 0; } + + IM3Module mod = NULL; + M3Result r = m3_ParseModule(env, &mod, data, (uint32_t)size); + if (!r && mod) { + m3_LoadModule(rt, mod); + // no m3_Call here. parse+load exercises the surface we care about. + // if wasm3 signals load failure, module is not owned by us to free. + } + + m3_FreeRuntime(rt); + m3_FreeEnvironment(env); + return 0; +} diff --git a/windbg/gvm_ext.cpp b/windbg/gvm_ext.cpp new file mode 100644 index 0000000..9b1d1a0 --- /dev/null +++ b/windbg/gvm_ext.cpp @@ -0,0 +1,233 @@ +/* gvm_ext.cpp - WinDbg extension for inspecting Goodmans state. + * + * bang commands: + * !gvm.modules enumerate loaded wasm modules from the driver + * !gvm.info detailed info on one module (name/size/hash/caps) + * !gvm.mem dump guest linear memory + * !gvm.help + * + * load with: + * .load path\to\gvm_ext.dll + * + * requires: + * - Goodmans.sys loaded with symbols (Goodmans.pdb) in .sympath + * - live kernel debugging OR full/kernel minidump + */ +#include +#include +#include +#include + +extern "C" { + IDebugClient* g_client = nullptr; + IDebugControl* g_control = nullptr; + IDebugSymbols* g_symbols = nullptr; + IDebugDataSpaces* g_data = nullptr; +} + +// mirror of gvm_module layout. must stay in sync with driver/inc/gvm.h +#pragma pack(push, 8) +struct gvm_module_dbg { + ULONG id; + UCHAR used; + UCHAR _pad0[7]; + ULONG64 env; + ULONG64 runtime; + ULONG64 module; + ULONG64 wasm_bytes; + ULONG wasm_size; + ULONG _pad1; + ULONG64 hash; + volatile LONG refcount; + ULONG _pad2; + ULONG64 call_mutex[6]; // KMUTEX opaque + volatile LONG64 pool_used; + LONG64 pool_budget; + ULONG caps; + ULONG _pad3; + volatile ULONG64 exec_deadline_qpc; + CHAR name[64]; +}; +#pragma pack(pop) + +#define GVM_MAX_MODULES 32 + +static HRESULT read_kmem(ULONG64 addr, void* buf, ULONG size) +{ + if (!g_data) return E_FAIL; + ULONG got = 0; + HRESULT hr = g_data->ReadVirtual(addr, buf, size, &got); + if (SUCCEEDED(hr) && got != size) return E_FAIL; + return hr; +} + +static HRESULT resolve_symbol(const char* name, ULONG64* out) +{ + if (!g_symbols) return E_FAIL; + return g_symbols->GetOffsetByName(name, out); +} + +extern "C" __declspec(dllexport) HRESULT CALLBACK +DebugExtensionInitialize(PULONG version, PULONG flags) +{ + *version = DEBUG_EXTENSION_VERSION(1, 0); + *flags = 0; + + if (DebugCreate(__uuidof(IDebugClient), (void**)&g_client) != S_OK) + return E_FAIL; + + g_client->QueryInterface(__uuidof(IDebugControl), (void**)&g_control); + g_client->QueryInterface(__uuidof(IDebugSymbols), (void**)&g_symbols); + g_client->QueryInterface(__uuidof(IDebugDataSpaces), (void**)&g_data); + return S_OK; +} + +extern "C" __declspec(dllexport) void CALLBACK +DebugExtensionUninitialize(void) +{ + if (g_data) g_data->Release(); + if (g_symbols) g_symbols->Release(); + if (g_control) g_control->Release(); + if (g_client) g_client->Release(); + g_data = nullptr; g_symbols = nullptr; g_control = nullptr; g_client = nullptr; +} + +static void printf_ext(const char* fmt, ...) +{ + va_list ap; va_start(ap, fmt); + char buf[1024]; + _vsnprintf_s(buf, sizeof(buf), _TRUNCATE, fmt, ap); + va_end(ap); + if (g_control) g_control->Output(DEBUG_OUTPUT_NORMAL, "%s", buf); +} + +static void print_cap_string(char* out, size_t out_sz, ULONG caps) +{ + static const char* names[] = { + "ALLOC","READ","WRITE","MSR_R","MSR_W","PHYS","CPUID","CB","HCALL","INTRO" + }; + out[0] = 0; + if (caps == 0xFFFFFFFFu) { strncpy_s(out, out_sz, "ALL", _TRUNCATE); return; } + for (int i = 0; i < 10; i++) { + if (caps & (1u << i)) { + if (out[0]) strncat_s(out, out_sz, "|", _TRUNCATE); + strncat_s(out, out_sz, names[i], _TRUNCATE); + } + } +} + +extern "C" __declspec(dllexport) HRESULT CALLBACK +modules(IDebugClient* client, PCSTR args) +{ + UNREFERENCED_PARAMETER(client); + UNREFERENCED_PARAMETER(args); + + ULONG64 base = 0; + if (FAILED(resolve_symbol("Goodmans!g_modules", &base))) { + printf_ext("[!] cannot resolve Goodmans!g_modules. is the .pdb loaded?\n"); + return E_FAIL; + } + + printf_ext("%-4s %-6s %-10s %-10s %-16s %-24s %s\n", + "id","wasm","pool","budget","hash","caps","name"); + printf_ext("---- ------ ---------- ---------- ---------------- ------------------------ ----\n"); + + unsigned int active = 0; + for (unsigned int i = 0; i < GVM_MAX_MODULES; i++) { + gvm_module_dbg m = {}; + if (FAILED(read_kmem(base + i * sizeof(m), &m, sizeof(m)))) break; + if (!m.used) continue; + active++; + char capstr[128] = {}; + print_cap_string(capstr, sizeof(capstr), m.caps); + printf_ext("%-4u %-6u %-10lld %-10lld %016llx %-24s %s\n", + m.id, m.wasm_size, m.pool_used, m.pool_budget, m.hash, capstr, m.name); + } + printf_ext("\n(%u active)\n", active); + return S_OK; +} + +extern "C" __declspec(dllexport) HRESULT CALLBACK +info(IDebugClient* client, PCSTR args) +{ + UNREFERENCED_PARAMETER(client); + if (!args || !*args) { printf_ext("usage: !gvm.info \n"); return E_INVALIDARG; } + + unsigned int id = (unsigned int)strtoul(args, nullptr, 0); + if (id == 0 || id > GVM_MAX_MODULES) { printf_ext("bad id\n"); return E_INVALIDARG; } + + ULONG64 base = 0; + if (FAILED(resolve_symbol("Goodmans!g_modules", &base))) return E_FAIL; + + gvm_module_dbg m = {}; + ULONG64 slot = base + (id - 1) * sizeof(m); + if (FAILED(read_kmem(slot, &m, sizeof(m)))) return E_FAIL; + if (!m.used) { printf_ext("slot %u is free\n", id); return S_OK; } + + char capstr[128] = {}; + print_cap_string(capstr, sizeof(capstr), m.caps); + + printf_ext("id: %u\n", m.id); + printf_ext("name: %s\n", m.name); + printf_ext("wasm size: %u bytes\n", m.wasm_size); + printf_ext("wasm bytes ka: 0x%016llx\n", m.wasm_bytes); + printf_ext("hash: %016llx\n", m.hash); + printf_ext("refcount: %d\n", m.refcount); + printf_ext("pool used: %lld / %lld\n", m.pool_used, m.pool_budget); + printf_ext("caps: %08x (%s)\n", m.caps, capstr); + printf_ext("env ka: 0x%016llx\n", m.env); + printf_ext("runtime ka: 0x%016llx\n", m.runtime); + printf_ext("module ka: 0x%016llx\n", m.module); + return S_OK; +} + +extern "C" __declspec(dllexport) HRESULT CALLBACK +mem(IDebugClient* client, PCSTR args) +{ + UNREFERENCED_PARAMETER(client); + if (!args || !*args) { + printf_ext("usage: !gvm.mem \n"); + return E_INVALIDARG; + } + + unsigned int id, off, len; + if (sscanf_s(args, "%u %x %x", &id, &off, &len) != 3) { + if (sscanf_s(args, "%u %u %u", &id, &off, &len) != 3) { + printf_ext("bad args\n"); return E_INVALIDARG; + } + } + if (id == 0 || id > GVM_MAX_MODULES || len == 0 || len > 4096) { + printf_ext("bad args (len max 4096)\n"); return E_INVALIDARG; + } + + ULONG64 base = 0; + if (FAILED(resolve_symbol("Goodmans!g_modules", &base))) return E_FAIL; + + gvm_module_dbg m = {}; + if (FAILED(read_kmem(base + (id - 1) * sizeof(m), &m, sizeof(m))) || !m.used || !m.runtime) { + printf_ext("slot not loaded\n"); return E_FAIL; + } + + // wasm3 stores linear memory at runtime->memory.mallocated + offset. + // walking the M3Runtime struct requires matching wasm3's layout exactly. + // for portability, emit a KD-side memory command using a symbolic offset + // once the exact struct is resolved via .lookup_field. + printf_ext("[!] guest linear memory walk requires wasm3 pdb. read runtime@0x%016llx manually with dt or !address\n", + m.runtime); + printf_ext(" len=%u off=%u\n", len, off); + return S_OK; +} + +extern "C" __declspec(dllexport) HRESULT CALLBACK +help(IDebugClient* client, PCSTR args) +{ + UNREFERENCED_PARAMETER(client); + UNREFERENCED_PARAMETER(args); + printf_ext("Goodmans WinDbg extension\n"); + printf_ext(" !gvm.modules enumerate loaded modules\n"); + printf_ext(" !gvm.info detailed info for module id\n"); + printf_ext(" !gvm.mem dump guest linear memory (needs wasm3 pdb)\n"); + printf_ext(" !gvm.help this help\n"); + printf_ext("\nrequires Goodmans.pdb in .sympath.\n"); + return S_OK; +} diff --git a/windbg/gvm_ext.def b/windbg/gvm_ext.def new file mode 100644 index 0000000..28a6440 --- /dev/null +++ b/windbg/gvm_ext.def @@ -0,0 +1,8 @@ +LIBRARY gvm_ext +EXPORTS + DebugExtensionInitialize + DebugExtensionUninitialize + modules + info + mem + help diff --git a/windbg/gvm_ext.vcxproj b/windbg/gvm_ext.vcxproj new file mode 100644 index 0000000..d2e5470 --- /dev/null +++ b/windbg/gvm_ext.vcxproj @@ -0,0 +1,47 @@ + + + + + Debug + x64 + + + Release + x64 + + + + {6D9E2A50-1F1A-4B4E-8D9F-1B0C0D0A0004} + gvm_ext + Win32Proj + 10.0 + + + + DynamicLibrary + v143 + MultiByte + true + false + + + + + Level3 + _CRT_SECURE_NO_WARNINGS;WIN32;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + stdcpp17 + true + NotUsing + + + Windows + gvm_ext.def + dbgeng.lib;%(AdditionalDependencies) + true + + + + + + +