From 7a72cdabbdcd2c7c0aafe9d6028c1b04c1a3801e Mon Sep 17 00:00:00 2001 From: AdvDebug <90452585+AdvDebug@users.noreply.github.com> Date: Sat, 8 Aug 2026 21:48:08 +0300 Subject: [PATCH] Persistent Unicorn JIT cache, TCG + Android fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a persistent TCG code cache to Unicorn via a version-portable build-time patch system, plus the TCG and Android work that came out of making it releasable. Patch infrastructure. Brovan/native/unicorn/ holds Brovan's additions to the Unicorn tree; Brovan.Unicorn.targets applies them after extraction through a RoslynCodeTaskFactory inline task — no git/patch/python dependency, works on Windows, Linux and Android build hosts. Edits are single-line anchors and whole-file appends rather than hunks, so they survive upstream drift; every rule is idempotent, and a missing anchor fails the build naming all misses. The patch-set hash is part of the source and build directory names, so editing a patch forces a clean re-extract. Validated against the previous two Unicorn tags. Code cache. Saves the TCG buffer and reloads it on the next launch. Helper addresses route through an indirect slot table; uc/tcg_ctx are pinned in a replayed address reservation chosen from a probed candidate list. Every restored block is re-verified against the guest bytes it was translated from, and page_addr/hash are recomputed from the live mapping so self-modifying-code invalidation still finds them. A save-time audit scans emitted code for host pointers a reload could not repoint and refuses rather than writing a poisoned blob — it caught a real miss (the i386 backend tail-jumps into qemu_st_helpers rather than calling it). Blocks in pages the loader hasn't reached yet are retried later instead of re-translated, and the blob is dropped once mostly dead code, bounding growth. Inline hooks stay enabled. Slots carry a kind — image-relative for helpers, hook identity for callbacks re-resolved from uc->hook[] on load — so the cache no longer has to disable Unicorn's inline-hook path. That was costing ~13% throughput. Per-block exit poll inlined. Unicorn calls a helper on every block to test icount_decr; this emits a load and a not-taken branch, with the helper kept for the slow path, following QEMU's shape of branching out of the block to a trailing label. Direct register access. brov_reg_ptr hands out host pointers into the guest CPU state, used by ReadRegister/WriteRegister through unsafe loads and stores. Only registers Unicorn stores verbatim are exposed, and only those it would write with a plain store are writable — the program counter is read-only because writing it also raises quit_request and flushes blocks, and nothing is exposed in 16/32-bit mode where the same storage is reached under different truncation rules. Android. JIT caching switch in Settings, default on. The Vulkan shim is a guest PE, so it now ships as an APK asset and is deployed into the guest's System32/SysWOW64 on launch, refreshed on app update. The cross-build no longer shares a CMake cache with the host build. API set map from the installed image. Read from the .apiset section of the imported apisetschema.dll instead of being dumped from the host or synthesised, so the contract names match the DLLs actually installed. Regenerated when the schema is newer than the map. --- .gitignore | 1 + Brovan/Android/BrovanAndroidApi.cs | 3 + .../main/java/dev/brovan/app/GuestAssets.java | 93 ++ .../java/dev/brovan/app/MainActivity.java | 8 + .../java/dev/brovan/app/PlayerActivity.java | 5 +- .../main/java/dev/brovan/app/Settings.java | 9 + .../src/main/res/layout/screen_settings.xml | 15 + .../brovan/src/main/res/values/strings.xml | 2 + Brovan/Android/build-apk.sh | 38 +- .../Android/java/dev/brovan/BrovanNative.java | 7 + Brovan/Android/jni/brovan_jni.c | 7 + Brovan/Brovan.Unicorn.targets | 231 +++- .../Core/Emulation/Backends/BackendFactory.cs | 4 +- .../Emulation/Backends/IEmulationBackend.cs | 18 + .../Core/Emulation/Backends/Kvm/KvmBackend.cs | 7 + .../Backends/Unicorn/UnicornBackend.cs | 12 +- .../Core/Emulation/Backends/Whp/WhpBackend.cs | 7 + Brovan/Core/Emulation/BinaryEmulator.cs | 16 +- .../Windows/Process/GuestProcessLauncher.cs | 5 + .../Core/Emulation/UnicornBinding/Native.cs | 93 ++ .../Core/Emulation/UnicornBinding/Unicorn.cs | 240 +++- .../UnicornBinding/UnicornCodeCache.cs | 461 ++++++ .../WindowsImage/WindowsImageImporter.cs | 54 + .../Core/Helpers/WindowsImage/WindowsSetup.cs | 3 + Brovan/Program.cs | 38 +- .../native/unicorn/brovan_tcg_exitcheck.inc.h | 58 + Brovan/native/unicorn/brovan_tcg_slots.inc.c | 94 ++ Brovan/native/unicorn/brovan_uc.h | 266 ++++ Brovan/native/unicorn/brovan_uc_api.inc.c | 533 +++++++ Brovan/native/unicorn/brovan_uc_tcg.inc.c | 1232 +++++++++++++++++ .../native/unicorn/brovan_uc_tcg_decls.inc.h | 14 + Brovan/native/unicorn/patches.manifest | 62 + 32 files changed, 3585 insertions(+), 51 deletions(-) create mode 100644 Brovan/Android/app/brovan/src/main/java/dev/brovan/app/GuestAssets.java create mode 100644 Brovan/Core/Emulation/UnicornBinding/UnicornCodeCache.cs create mode 100644 Brovan/native/unicorn/brovan_tcg_exitcheck.inc.h create mode 100644 Brovan/native/unicorn/brovan_tcg_slots.inc.c create mode 100644 Brovan/native/unicorn/brovan_uc.h create mode 100644 Brovan/native/unicorn/brovan_uc_api.inc.c create mode 100644 Brovan/native/unicorn/brovan_uc_tcg.inc.c create mode 100644 Brovan/native/unicorn/brovan_uc_tcg_decls.inc.h create mode 100644 Brovan/native/unicorn/patches.manifest diff --git a/.gitignore b/.gitignore index 77832ac..cdf29bf 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,4 @@ nunit-*.xml /Brovan/Android/app/local.properties /Brovan/Android/app/brovan/src/main/jniLibs/ /*.apk +/Brovan/Android/app/brovan/src/main/assets diff --git a/Brovan/Android/BrovanAndroidApi.cs b/Brovan/Android/BrovanAndroidApi.cs index 037df4c..442d6ef 100644 --- a/Brovan/Android/BrovanAndroidApi.cs +++ b/Brovan/Android/BrovanAndroidApi.cs @@ -92,6 +92,9 @@ public static int Init(byte* baseDirectory) [UnmanagedCallersOnly(EntryPoint = "brovan_set_verbose")] public static void SetVerbose(int enabled) => _verbose = enabled != 0; + [UnmanagedCallersOnly(EntryPoint = "brovan_set_jit_cache")] + public static void SetJitCache(int enabled) => UnicornCodeCache.Enabled = enabled != 0; + [UnmanagedCallersOnly(EntryPoint = "brovan_set_surface")] public static void SetSurface(IntPtr nativeWindow, int width, int height, int densityDpi) { diff --git a/Brovan/Android/app/brovan/src/main/java/dev/brovan/app/GuestAssets.java b/Brovan/Android/app/brovan/src/main/java/dev/brovan/app/GuestAssets.java new file mode 100644 index 0000000..8808760 --- /dev/null +++ b/Brovan/Android/app/brovan/src/main/java/dev/brovan/app/GuestAssets.java @@ -0,0 +1,93 @@ +package dev.brovan.app; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.util.Log; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * Copies the guest-side files bundled in the APK into the emulated Windows filesystem. + * + *

The Vulkan shim is a Windows PE, so it cannot live in jniLibs like the host libraries. It is + * generated from vk.xml together with the managed marshalling layer, which means it only matches + * the build it shipped with and has to be refreshed whenever the app is updated. + */ +final class GuestAssets { + + private static final String TAG = "Brovan"; + private static final String SOURCE = "virtualfs"; + private static final String STAMP = "virtualfs-assets.stamp"; + + private GuestAssets() { + } + + static void deploy(Context context) { + File files = context.getFilesDir(); + File stamp = new File(files, STAMP); + long updated = updateTime(context); + + if (stamp.exists() && stamp.lastModified() >= updated) { + return; + } + + File system32 = new File(files, "VirtualFS/C/Windows/System32"); + File sysWow64 = new File(files, "VirtualFS/C/Windows/SysWOW64"); + + try { + copyDirectory(context, SOURCE + "/System32", system32); + copyDirectory(context, SOURCE + "/SysWOW64", sysWow64); + + if (!stamp.exists() && !stamp.createNewFile()) { + return; + } + stamp.setLastModified(updated); + } catch (IOException error) { + Log.e(TAG, "Could not deploy the bundled guest files: " + error.getMessage()); + } + } + + private static long updateTime(Context context) { + try { + return context.getPackageManager() + .getPackageInfo(context.getPackageName(), 0) + .lastUpdateTime; + } catch (PackageManager.NameNotFoundException error) { + return System.currentTimeMillis(); + } + } + + private static void copyDirectory(Context context, String source, File target) throws IOException { + String[] names = context.getAssets().list(source); + if (names == null || names.length == 0) { + return; + } + + if (!target.isDirectory() && !target.mkdirs()) { + throw new IOException("cannot create " + target); + } + + for (String name : names) { + String child = source + "/" + name; + String[] nested = context.getAssets().list(child); + + if (nested != null && nested.length != 0) { + copyDirectory(context, child, new File(target, name)); + continue; + } + + try (InputStream in = context.getAssets().open(child); + OutputStream out = new FileOutputStream(new File(target, name))) { + byte[] buffer = new byte[64 * 1024]; + int read; + while ((read = in.read(buffer)) > 0) { + out.write(buffer, 0, read); + } + } + } + } +} diff --git a/Brovan/Android/app/brovan/src/main/java/dev/brovan/app/MainActivity.java b/Brovan/Android/app/brovan/src/main/java/dev/brovan/app/MainActivity.java index 7ea4ff7..17f8fd1 100644 --- a/Brovan/Android/app/brovan/src/main/java/dev/brovan/app/MainActivity.java +++ b/Brovan/Android/app/brovan/src/main/java/dev/brovan/app/MainActivity.java @@ -66,6 +66,10 @@ protected void onCreate(Bundle savedInstanceState) { library = new Library(this); settings = new Settings(this); + // Refreshes the guest-side files bundled in the APK, so an app update cannot leave + // a stale Vulkan shim behind for the new emulator to load. + worker.execute(() -> GuestAssets.deploy(this)); + drawer = findViewById(R.id.drawer); content = findViewById(R.id.content); toolbar = findViewById(R.id.toolbar); @@ -467,6 +471,10 @@ private View createSettings() { controls.setText(schemeLabels[settings.controlScheme()], false); controls.setOnItemClickListener((parent, item, position, id) -> settings.setControlScheme(position)); + MaterialSwitch jitCache = view.findViewById(R.id.jit_cache); + jitCache.setChecked(settings.jitCache()); + jitCache.setOnCheckedChangeListener((button, checked) -> settings.setJitCache(checked)); + MaterialSwitch developer = view.findViewById(R.id.developer); developer.setChecked(settings.developerMode()); developer.setOnCheckedChangeListener((button, checked) -> settings.setDeveloperMode(checked)); diff --git a/Brovan/Android/app/brovan/src/main/java/dev/brovan/app/PlayerActivity.java b/Brovan/Android/app/brovan/src/main/java/dev/brovan/app/PlayerActivity.java index 0671d7d..98ff5d3 100644 --- a/Brovan/Android/app/brovan/src/main/java/dev/brovan/app/PlayerActivity.java +++ b/Brovan/Android/app/brovan/src/main/java/dev/brovan/app/PlayerActivity.java @@ -41,6 +41,7 @@ public class PlayerActivity extends AppCompatActivity implements BrovanNative.Li private static final String EXTRA_NETWORK = "network"; private static final String EXTRA_DEVELOPER = "developer"; private static final String EXTRA_CONTROLS = "controls"; + private static final String EXTRA_JIT_CACHE = "jit_cache"; private static final int MAX_LINES = 1200; private static final int TRIM_CHUNK = 200; @@ -63,7 +64,8 @@ static Intent intentFor(Context context, Program program, Settings settings) { .putExtra(EXTRA_NAME, program.name()) .putExtra(EXTRA_NETWORK, settings.network()) .putExtra(EXTRA_DEVELOPER, settings.developerMode()) - .putExtra(EXTRA_CONTROLS, settings.controlScheme()); + .putExtra(EXTRA_CONTROLS, settings.controlScheme()) + .putExtra(EXTRA_JIT_CACHE, settings.jitCache()); } @Override @@ -110,6 +112,7 @@ private void start() { } BrovanNative.setVerbose(developerMode); + BrovanNative.setJitCache(getIntent().getBooleanExtra(EXTRA_JIT_CACHE, true)); setStatus(getIntent().getStringExtra(EXTRA_NAME)); // Developer mode leaves the guest at the debugger prompt instead of running it, so the console has to diff --git a/Brovan/Android/app/brovan/src/main/java/dev/brovan/app/Settings.java b/Brovan/Android/app/brovan/src/main/java/dev/brovan/app/Settings.java index ae06c2d..7daa36b 100644 --- a/Brovan/Android/app/brovan/src/main/java/dev/brovan/app/Settings.java +++ b/Brovan/Android/app/brovan/src/main/java/dev/brovan/app/Settings.java @@ -13,6 +13,7 @@ final class Settings { private static final String KEY_DEVELOPER = "developer"; private static final String KEY_FIT_WINDOW = "fit_window"; private static final String KEY_CONTROLS = "controls"; + private static final String KEY_JIT_CACHE = "jit_cache"; private final SharedPreferences preferences; @@ -51,4 +52,12 @@ boolean fitWindow() { void setFitWindow(boolean value) { preferences.edit().putBoolean(KEY_FIT_WINDOW, value).apply(); } + + boolean jitCache() { + return preferences.getBoolean(KEY_JIT_CACHE, true); + } + + void setJitCache(boolean value) { + preferences.edit().putBoolean(KEY_JIT_CACHE, value).apply(); + } } diff --git a/Brovan/Android/app/brovan/src/main/res/layout/screen_settings.xml b/Brovan/Android/app/brovan/src/main/res/layout/screen_settings.xml index c3e26be..1963107 100644 --- a/Brovan/Android/app/brovan/src/main/res/layout/screen_settings.xml +++ b/Brovan/Android/app/brovan/src/main/res/layout/screen_settings.xml @@ -52,6 +52,21 @@ android:textColor="@color/text_primary" /> + + + + Advanced On-screen controls Network access + JIT code caching + Experimental JIT code caching that can help with performance Developer mode Show the console and emulator trace while a program runs diff --git a/Brovan/Android/build-apk.sh b/Brovan/Android/build-apk.sh index 8ddc26f..bf57928 100644 --- a/Brovan/Android/build-apk.sh +++ b/Brovan/Android/build-apk.sh @@ -55,9 +55,10 @@ CONFIG="${CONFIG:-Release}" API_LEVEL="${API_LEVEL:-26}" PUBLISH_DIR="${BROVAN_PUBLISH_DIR:-/tmp/brovan-android-publish}" APK_OUTPUT="${BROVAN_APK_OUTPUT:-$REPO_ROOT/artifacts/android/brovan-arm64-v8a.apk}" -UNICORN_SRC="$REPO_ROOT/Brovan/.cache/unicorn/unicorn-2.1.4" -UNICORN_BUILD="$REPO_ROOT/Brovan/.cache/unicorn/build-android-arm64" -UNICORN_ARTIFACT="$UNICORN_BUILD/libunicorn.so" +# Resolved from Brovan.Unicorn.targets rather than hardcoded: the source tree is named +# after a hash of Brovan/native/unicorn, so editing a patch moves it. +UNICORN_SRC="" +UNICORN_PATCH_KEY="" # Exit code 3 means "this host has no Android toolchain", which Brovan.Android/Brovan.Android.csproj treats # as a skip rather than a build failure. Anything else is a real failure. @@ -75,11 +76,16 @@ esac mkdir -p "$JNI_LIBS" -if [ ! -f "$UNICORN_SRC/CMakeLists.txt" ]; then - echo "==> Fetching the Unicorn source through Brovan.Unicorn.targets" - "$DOTNET" msbuild "$PROJECT" -t:ExtractUnicornSource -nologo -v:minimal || true - [ -f "$UNICORN_SRC/CMakeLists.txt" ] || { echo "Unicorn source missing at $UNICORN_SRC" >&2; exit 1; } -fi +echo "==> Fetching and patching the Unicorn source through Brovan.Unicorn.targets" +"$DOTNET" msbuild "$PROJECT" -t:PatchUnicornSource -nologo -v:minimal +eval "$("$DOTNET" msbuild "$PROJECT" -t:PrintUnicornPaths -nologo -v:minimal \ + | sed -n 's/^ *\(UNICORN_SRC=\|UNICORN_PATCH_KEY=\)/\1/p')" +[ -f "$UNICORN_SRC/CMakeLists.txt" ] || { echo "Unicorn source missing at '$UNICORN_SRC'" >&2; exit 1; } + +# Keyed on the patch set like the host build, and kept separate from it: sharing one +# CMake cache between the Windows and WSL views of the same path breaks the configure. +UNICORN_BUILD="$REPO_ROOT/Brovan/.cache/unicorn/build-android-arm64-$UNICORN_PATCH_KEY" +UNICORN_ARTIFACT="$UNICORN_BUILD/libunicorn.so" # Unicorn has to be cross-built before the publish: Brovan.Unicorn.targets copies whatever sits at # UnicornArtifact into the publish output, and if that path is empty it configures a host-arch build there @@ -102,6 +108,22 @@ if [ ! -f "$UNICORN_ARTIFACT" ]; then fi cp "$UNICORN_ARTIFACT" "$JNI_LIBS/libunicorn.so" +# The Vulkan shim is a guest PE, not a host library, so it ships as an asset and the +# app drops it into the guest's System32 on launch. It is generated from vk.xml +# alongside the managed marshaller, so it has to travel with the APK that built it. +echo "==> Building the BrovVulk Vulkan shim" +SHIM_DIR="$REPO_ROOT/Brovan.Graphics/brovvulk-icd" +GUEST_ASSETS="$GRADLE_PROJECT/brovan/src/main/assets/virtualfs" +if [ -f "$SHIM_DIR/obj/generated/brovvulk_gen.c" ]; then + sh "$SHIM_DIR/build.sh" + mkdir -p "$GUEST_ASSETS/System32" "$GUEST_ASSETS/SysWOW64" + cp -f "$SHIM_DIR/bin/vulkan-1.dll" "$GUEST_ASSETS/System32/vulkan-1.dll" + [ -f "$SHIM_DIR/bin/x86/vulkan-1.dll" ] && cp -f "$SHIM_DIR/bin/x86/vulkan-1.dll" "$GUEST_ASSETS/SysWOW64/vulkan-1.dll" + echo " packaged vulkan-1.dll into the APK assets" +else + echo "warning: BrovVulk generated sources missing; the APK will ship without the Vulkan shim." >&2 +fi + # .NET's crypto shim aborts the process the moment any OpenSSL-backed primitive is touched # ("No usable version of libssl was found"), and Android exposes no libssl to apps. Its probe list includes # the unversioned names, which is what OpenSSL's android targets emit and what Android will package. diff --git a/Brovan/Android/java/dev/brovan/BrovanNative.java b/Brovan/Android/java/dev/brovan/BrovanNative.java index 6a1d040..10191b8 100644 --- a/Brovan/Android/java/dev/brovan/BrovanNative.java +++ b/Brovan/Android/java/dev/brovan/BrovanNative.java @@ -120,6 +120,11 @@ public static void setVerbose(boolean enabled) { nativeSetVerbose(enabled ? 1 : 0); } + /** Reuses translated guest code between runs. Must be called before {@link #start}. */ + public static void setJitCache(boolean enabled) { + nativeSetJitCache(enabled ? 1 : 0); + } + /** Feeds one line to the emulator's debugger prompt. Verbose mode only. */ public static void sendCommand(String command) { nativeSendCommand(command); @@ -237,6 +242,8 @@ private static native int nativeStart(String binaryPath, String guestCommandLine private static native void nativeSetVerbose(int enabled); + private static native void nativeSetJitCache(int enabled); + private static native void nativeSendCommand(String command); private static native int nativeIsRunning(); diff --git a/Brovan/Android/jni/brovan_jni.c b/Brovan/Android/jni/brovan_jni.c index ac92340..421a2a5 100644 --- a/Brovan/Android/jni/brovan_jni.c +++ b/Brovan/Android/jni/brovan_jni.c @@ -17,6 +17,7 @@ extern void brovan_set_log_sink(void *sink); extern void brovan_set_exit_sink(void *sink); extern void brovan_set_install_progress_sink(void *sink); extern void brovan_set_verbose(int enabled); +extern void brovan_set_jit_cache(int enabled); extern void brovan_set_surface(void *nativeWindow, int width, int height, int densityDpi); extern void brovan_clear_surface(void); extern int brovan_start(const char *binaryPath, const char *guestCommandLine, const char *workingDirectory, @@ -293,6 +294,12 @@ JNIEXPORT void JNICALL METHOD(SetVerbose)(JNIEnv *env, jclass clazz, jint enable brovan_set_verbose(enabled); } +JNIEXPORT void JNICALL METHOD(SetJitCache)(JNIEnv *env, jclass clazz, jint enabled) { + (void)env; + (void)clazz; + brovan_set_jit_cache(enabled); +} + JNIEXPORT void JNICALL METHOD(SendCommand)(JNIEnv *env, jclass clazz, jstring command) { (void)clazz; diff --git a/Brovan/Brovan.Unicorn.targets b/Brovan/Brovan.Unicorn.targets index 475c340..edb0fd6 100644 --- a/Brovan/Brovan.Unicorn.targets +++ b/Brovan/Brovan.Unicorn.targets @@ -2,37 +2,236 @@ 2.1.4 $([MSBuild]::NormalizeDirectory($(MSBuildProjectDirectory), '.cache', 'unicorn')) - $(UnicornCacheDir)unicorn-$(UnicornTag) - $(UnicornCacheDir)build + $([MSBuild]::NormalizeDirectory($(MSBuildProjectDirectory), 'native', 'unicorn')) $(UnicornCacheDir)unicorn-$(UnicornTag).tar.gz + https://github.com/unicorn-engine/unicorn/archive/refs/tags/$(UnicornTag).tar.gz <_UnicornCacheNorm>$([MSBuild]::NormalizePath($(UnicornCacheDir)).TrimEnd('\').TrimEnd('/')) - <_UnicornSrcNorm>$([MSBuild]::NormalizePath($(UnicornSrcDir)).TrimEnd('\').TrimEnd('/')) - <_UnicornBuildNorm>$([MSBuild]::NormalizePath($(UnicornBuildDir)).TrimEnd('\').TrimEnd('/')) <_UnicornTarballNorm>$([MSBuild]::NormalizePath($(UnicornTarball))) unicorn.dll libunicorn.so + - $(UnicornBuildDir)\Release\$(UnicornLibName) - $(UnicornBuildDir)\$(UnicornLibName) + + + + + + + + - https://github.com/unicorn-engine/unicorn/archive/refs/tags/$(UnicornTag).tar.gz - + + + + + + + (); +var files = new Dictionary>(StringComparer.OrdinalIgnoreCase); +var dirty = new HashSet(StringComparer.OrdinalIgnoreCase); +var manifest = Path.Combine(PatchDir, "patches.manifest"); + +if (!File.Exists(manifest)) +{ + Log.LogError("Unicorn patch manifest not found: " + manifest); + return false; +} + +Func> load = rel => +{ + List lines; + if (files.TryGetValue(rel, out lines)) return lines; + var full = Path.Combine(SourceDir, rel.Replace('/', Path.DirectorySeparatorChar)); + if (!File.Exists(full)) + { + errors.Add("target file missing: " + rel); + lines = null; + } + else + { + lines = new List(File.ReadAllLines(full)); + } + files[rel] = lines; + return lines; +}; + +Func, string, bool> present = (lines, text) => +{ + var needle = text.Trim(); + for (int i = 0; i < lines.Count; i++) + { + if (lines[i].Trim() == needle) return true; + } + return false; +}; + +Func, string, int> indexOfAnchor = (lines, anchor) => +{ + for (int i = 0; i < lines.Count; i++) + { + if (lines[i].Trim() == anchor) return i; + } + return -1; +}; + +int applied = 0, skipped = 0, copied = 0; + +foreach (var raw in File.ReadAllLines(manifest)) +{ + var line = raw.Trim(); + if (line.Length == 0 || line.StartsWith("#")) continue; + + var f = line.Split('|'); + var kind = f[0]; + + if (kind == "schema" || kind == "validated") continue; + + if (kind == "copy") + { + var src = Path.Combine(PatchDir, f[1]); + var dst = Path.Combine(SourceDir, f[2].Replace('/', Path.DirectorySeparatorChar)); + if (!File.Exists(src)) { errors.Add("patch source missing: " + f[1]); continue; } + Directory.CreateDirectory(Path.GetDirectoryName(dst)); + if (!File.Exists(dst) || File.ReadAllText(dst) != File.ReadAllText(src)) + { + File.Copy(src, dst, true); + copied++; + } + continue; + } + + var rel = f[1]; + var lines = load(rel); + if (lines == null) continue; + + if (kind == "replace-all") + { + var from = f[2]; + var to = f[3]; + bool hit = false, already = false; + for (int i = 0; i < lines.Count; i++) + { + if (lines[i].Contains(to)) { already = true; continue; } + if (lines[i].Contains(from)) { lines[i] = lines[i].Replace(from, to); hit = true; } + } + if (hit) { dirty.Add(rel); applied++; } + else if (already) skipped++; + else errors.Add("anchor not found in " + rel + ": " + from); + continue; + } + + var text = f[kind == "append" ? 2 : 3]; + if (present(lines, text)) { skipped++; continue; } + + if (kind == "append") + { + lines.Add(text); + dirty.Add(rel); + applied++; + continue; + } - + var anchor = f[2]; + int at = indexOfAnchor(lines, anchor); + if (at < 0) { errors.Add("anchor not found in " + rel + ": " + anchor); continue; } + + if (kind == "insert-before") lines.Insert(at, text); + else if (kind == "insert-after") lines.Insert(at + 1, text); + else if (kind == "insert-in-body") + { + int brace = -1; + for (int i = at; i < lines.Count && i < at + 8; i++) + { + if (lines[i].Trim() == "{") { brace = i; break; } + } + if (brace < 0) { errors.Add("no opening brace after anchor in " + rel + ": " + anchor); continue; } + lines.Insert(brace + 1, text); + } + else { errors.Add("unknown rule kind: " + kind); continue; } + + dirty.Add(rel); + applied++; +} + +if (errors.Count > 0) +{ + foreach (var e in errors) Log.LogError("[Brovan.Unicorn] " + e); + Log.LogError("[Brovan.Unicorn] " + errors.Count + " patch anchor(s) failed against unicorn " + + Path.GetFileName(SourceDir.TrimEnd(Path.DirectorySeparatorChar)) + + ". Update Brovan/native/unicorn/patches.manifest."); + return false; +} + +foreach (var rel in dirty) +{ + var full = Path.Combine(SourceDir, rel.Replace('/', Path.DirectorySeparatorChar)); + File.WriteAllText(full, string.Join("\n", files[rel].ToArray()) + "\n"); +} + +Log.LogMessage(MessageImportance.High, + "[Brovan.Unicorn] Patched: " + applied + " edit(s), " + copied + " file(s) copied, " + + skipped + " already applied."); +]]> + + + + + + + + + + + <_UnicornPatchHashes>@(_UnicornPatchHashed->'%(FileHash)') + <_UnicornPatchKeyRaw>$([MSBuild]::StableStringHash($(_UnicornPatchHashes))) + $(_UnicornPatchKeyRaw.Replace('-', 'n')) + + + $(UnicornCacheDir)unicorn-$(UnicornTag)-$(UnicornPatchKey) + $(UnicornCacheDir)build-$(UnicornTag)-$(UnicornPatchKey) + + <_UnicornSrcNorm>$([MSBuild]::NormalizePath($(UnicornSrcDir)).TrimEnd('\').TrimEnd('/')) + <_UnicornBuildNorm>$([MSBuild]::NormalizePath($(UnicornBuildDir)).TrimEnd('\').TrimEnd('/')) + <_UnicornPatchNorm>$([MSBuild]::NormalizePath($(UnicornPatchDir)).TrimEnd('\').TrimEnd('/')) + $(_UnicornSrcNorm)/.brovan-patched + + $(UnicornBuildDir)\Release\$(UnicornLibName) + $(UnicornBuildDir)\$(UnicornLibName) + + + + + + + + + + - + - + + + + + + + + - + @@ -41,15 +240,15 @@ - + - + - + - \ No newline at end of file + diff --git a/Brovan/Core/Emulation/Backends/BackendFactory.cs b/Brovan/Core/Emulation/Backends/BackendFactory.cs index 2cc7e43..93faeed 100644 --- a/Brovan/Core/Emulation/Backends/BackendFactory.cs +++ b/Brovan/Core/Emulation/Backends/BackendFactory.cs @@ -2,11 +2,11 @@ namespace Brovan.Core.Emulation { public static class BackendFactory { - public static IEmulationBackend Create(EmulationBackendKind kind, Arch arch, Mode mode, bool noHooks) + public static IEmulationBackend Create(EmulationBackendKind kind, Arch arch, Mode mode, bool noHooks, string guestImagePath = null, string hostImagePath = null) { IEmulationBackend backend = kind switch { - EmulationBackendKind.Unicorn => new UnicornBackend(arch, mode), + EmulationBackendKind.Unicorn => new UnicornBackend(arch, mode, guestImagePath, hostImagePath), EmulationBackendKind.Kvm => new KvmBackend(arch, mode), EmulationBackendKind.Whp => new WhpBackend(arch, mode), _ => throw new System.ArgumentOutOfRangeException(nameof(kind), kind, "Unknown emulation backend."), diff --git a/Brovan/Core/Emulation/Backends/IEmulationBackend.cs b/Brovan/Core/Emulation/Backends/IEmulationBackend.cs index a828765..bd40da1 100644 --- a/Brovan/Core/Emulation/Backends/IEmulationBackend.cs +++ b/Brovan/Core/Emulation/Backends/IEmulationBackend.cs @@ -171,5 +171,23 @@ bool WriteRegisterBatch(int[] registers, ulong[] values, int count) bool RemoveHooks(); bool IsRangeMapped(ulong address, ulong size); + + ///

+ /// Reuse translated guest code from a previous run. Called once execution is about + /// to begin, because a backend that verifies restored code against guest memory + /// needs the image mapped first. Backends that do not translate do nothing. + /// + void RestoreCodeCache(); + + /// + /// Cheap periodic follow-up to , called from the + /// scheduler while the guest runs. Returns once there is nothing left to do. + /// + void ResolveCodeCache(); + + /// + /// Persist translated guest code. Called with the guest stopped. + /// + void PersistCodeCache(); } } diff --git a/Brovan/Core/Emulation/Backends/Kvm/KvmBackend.cs b/Brovan/Core/Emulation/Backends/Kvm/KvmBackend.cs index 138b1ea..239bac8 100644 --- a/Brovan/Core/Emulation/Backends/Kvm/KvmBackend.cs +++ b/Brovan/Core/Emulation/Backends/Kvm/KvmBackend.cs @@ -166,5 +166,12 @@ public void Dispose() KvmErrors.Exception => BackendError.Exception, _ => BackendError.InternalError, }; + + // KVM runs guest code on the CPU directly, so there is no translated code to keep. + public void RestoreCodeCache() { } + + public void ResolveCodeCache() { } + + public void PersistCodeCache() { } } } diff --git a/Brovan/Core/Emulation/Backends/Unicorn/UnicornBackend.cs b/Brovan/Core/Emulation/Backends/Unicorn/UnicornBackend.cs index 9fda343..c145d7d 100644 --- a/Brovan/Core/Emulation/Backends/Unicorn/UnicornBackend.cs +++ b/Brovan/Core/Emulation/Backends/Unicorn/UnicornBackend.cs @@ -9,8 +9,12 @@ public sealed class UnicornBackend : IEmulationBackend { public Unicorn Inner { get; } - public UnicornBackend(Arch arch, Mode mode) + public UnicornBackend(Arch arch, Mode mode, string guestImagePath = null, string hostImagePath = null) { + // Has to precede uc_open. the code cache reserves the address range that the + // translation buffer and the uc struct are then carved out of. + UnicornCodeCache.Configure(guestImagePath, hostImagePath); + Inner = new Unicorn(arch, mode); Arch = arch; Mode = mode; @@ -364,5 +368,11 @@ public InstructionBoolThunk(InstructionBoolHookCallback user) } private readonly Dictionary _liveThunks = new(); + + public void RestoreCodeCache() => UnicornCodeCache.TryLoad(Inner); + + public void ResolveCodeCache() => UnicornCodeCache.ResolvePending(Inner); + + public void PersistCodeCache() => UnicornCodeCache.TrySave(Inner); } } diff --git a/Brovan/Core/Emulation/Backends/Whp/WhpBackend.cs b/Brovan/Core/Emulation/Backends/Whp/WhpBackend.cs index fa35eab..803ff5c 100644 --- a/Brovan/Core/Emulation/Backends/Whp/WhpBackend.cs +++ b/Brovan/Core/Emulation/Backends/Whp/WhpBackend.cs @@ -166,5 +166,12 @@ public void Dispose() WhpErrors.Exception => BackendError.Exception, _ => BackendError.InternalError, }; + + // WHP runs guest code on the CPU directly, so there is no translated code to keep. + public void RestoreCodeCache() { } + + public void ResolveCodeCache() { } + + public void PersistCodeCache() { } } } diff --git a/Brovan/Core/Emulation/BinaryEmulator.cs b/Brovan/Core/Emulation/BinaryEmulator.cs index a91016b..6e31984 100644 --- a/Brovan/Core/Emulation/BinaryEmulator.cs +++ b/Brovan/Core/Emulation/BinaryEmulator.cs @@ -533,7 +533,7 @@ public BinaryEmulator(BinaryFile Binary, BinaryEmulatorSettings Settings) BackendMode = Binary.Architecture == BinaryArchitecture.x64 ? Mode.MODE_64 : Mode.MODE_32; GeneralHelper.IO.Wow64FileRedirect = Binary.FileFormat == BinaryFormat.PE && Binary.Architecture == BinaryArchitecture.x86; GuestImagePath = ResolveGuestImagePath(Binary); - _emulator = BackendFactory.Create(Settings.BackendKind, BackendArch, BackendMode, Settings.NoHooks); + _emulator = BackendFactory.Create(Settings.BackendKind, BackendArch, BackendMode, Settings.NoHooks, GuestImagePath, Binary.Location); _emulator.NoHooks = Settings.NoHooks; this.Settings = Settings; Debug = Settings.Debug; @@ -575,7 +575,7 @@ public BinaryEmulator(IGuestEnvironment Guest, BinaryEmulatorSettings Settings, BackendMode = mode; GeneralHelper.IO.Wow64FileRedirect = Binary?.FileFormat == BinaryFormat.PE && Binary?.Architecture == BinaryArchitecture.x86; GuestImagePath = ResolveGuestImagePath(_binary, Guest); - _emulator = BackendFactory.Create(Settings.BackendKind, arch, mode, Settings.NoHooks); + _emulator = BackendFactory.Create(Settings.BackendKind, arch, mode, Settings.NoHooks, GuestImagePath, _binary?.Location); this.Settings = Settings; Debug = Settings.Debug; RawProgramArguments = Settings.RawProgramArguments ?? string.Empty; @@ -1640,7 +1640,7 @@ private static void ReadProcessorBrandLeaf(uint Leaf, out uint Eax, out uint Ebx } /// - /// CPUD Handler. + /// CPUID Handler. /// private bool CPUID_Handler() { @@ -2426,6 +2426,7 @@ private void RebuildMlfqReadyQueues(Queue[] ReadyQueues, HashSet InQue public bool RunMlfqScheduler(uint BaseQuantumInstructions = 200000, int Levels = 4, ulong MaxTotalInstructions = 0, uint MaxSlices = 0, long AgingThresholdSlices = 50) { + _emulator.RestoreCodeCache(); TrimDeadThreadsFromOrder(); if (ThreadOrder.Count == 0) { @@ -2473,6 +2474,9 @@ public bool RunMlfqScheduler(uint BaseQuantumInstructions = 200000, int Levels = { SchedulerTick++; + if ((SchedulerTick & 0x7) == 0) + _emulator.ResolveCodeCache(); + if (WinHelper != null) OS.Windows.RemoteProcessRequests.Drain(this); @@ -2828,6 +2832,10 @@ private void SyscallInstructionHandler() public void Start() { Guest.Start(this); + + // The guest has stopped here. Dispose() is not a reliable hook: the menu's + // "exit" command calls Environment.Exit. + _emulator.PersistCodeCache(); } /// @@ -3019,6 +3027,7 @@ public bool StartEmulation(ulong StartAddress, ulong EndAddress, uint Timeout = if (Disposed) return false; + _emulator.RestoreCodeCache(); TriggerDebugMessage(() => $"emu: start 0x{StartAddress:X}->0x{EndAddress:X} timeout={Timeout} count={Count}"); bool Result = _emulator.Emulate(StartAddress, EndAddress, Timeout, Count); if (!Result && LogErrors) @@ -3498,6 +3507,7 @@ public void Dispose() if (_emulator != null) { _emulator.StopEmulation(); + _emulator.PersistCodeCache(); _emulator.Dispose(); } diff --git a/Brovan/Core/Emulation/OS/Windows/Process/GuestProcessLauncher.cs b/Brovan/Core/Emulation/OS/Windows/Process/GuestProcessLauncher.cs index 15bed6c..021017b 100644 --- a/Brovan/Core/Emulation/OS/Windows/Process/GuestProcessLauncher.cs +++ b/Brovan/Core/Emulation/OS/Windows/Process/GuestProcessLauncher.cs @@ -239,6 +239,11 @@ private static void AppendEmulatorOptions(BinaryEmulator Instance, System.Collec if (Instance.Settings.NoHooks) Arguments.Add("--no-hooks"); + if (!UnicornCodeCache.Enabled) + Arguments.Add("--no-jit-cache"); + else if (!string.IsNullOrEmpty(UnicornCodeCache.CacheDirectory)) + Arguments.Add($"--jit-cache={UnicornCodeCache.CacheDirectory}"); + ForwardHostOptions(Arguments); Arguments.Add("-c"); diff --git a/Brovan/Core/Emulation/UnicornBinding/Native.cs b/Brovan/Core/Emulation/UnicornBinding/Native.cs index d0bd326..1c79348 100644 --- a/Brovan/Core/Emulation/UnicornBinding/Native.cs +++ b/Brovan/Core/Emulation/UnicornBinding/Native.cs @@ -135,6 +135,99 @@ public struct uc_x86_mmr [DllImport("unicorn", CallingConvention = CallingConvention.Cdecl, EntryPoint = "uc_ctl")] public static extern UCErrors uc_ctl1_uint(IntPtr uc, int control, uint arg1); + + // Brovan extensions, added to the unicorn tree by Brovan/native/unicorn. + // See Brovan/native/unicorn/brovan_uc.h for the layout contract. + + public const uint BROV_CFG_ENABLE_CACHE = 0x1; + public const uint BROV_CFG_STRICT_AUDIT = 0x2; + + [StructLayout(LayoutKind.Sequential)] + public struct BrovConfig + { + public uint StructSize; + public uint Flags; + public ulong ReserveBase; + public ulong ReserveSize; + public uint SlotCount; + public uint Reserved; + } + + [StructLayout(LayoutKind.Sequential)] + public struct BrovCacheInfo + { + public uint StructSize; + public uint LastReason; + + public ulong ReservationBase; + public ulong ReservationSize; + public ulong CodeGenBuffer; + public ulong CodeGenBufferSize; + public ulong CodeGenUsed; + + public ulong TbCount; + public ulong FlushCount; + + public uint SlotCount; + public uint SlotsUsed; + public uint SlotsOverflowed; + public uint InlineHooksDisabled; + + public ulong LoadCount; + public ulong LoadedTbs; + public ulong StaleTbs; + public ulong SaveCount; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] + public struct BrovAuditResult + { + public uint StructSize; + public uint HitCount; + public ulong FirstOffset; + public ulong FirstValue; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] + public string FirstObject; + } + + [DllImport("unicorn", CallingConvention = CallingConvention.Cdecl)] + public static extern UCErrors brov_abi_version(out uint abi); + + [DllImport("unicorn", CallingConvention = CallingConvention.Cdecl)] + public static extern UCErrors brov_configure(ref BrovConfig cfg); + + [DllImport("unicorn", CallingConvention = CallingConvention.Cdecl)] + public static extern UCErrors brov_reservation_info(out ulong reservationBase, out ulong size); + + [DllImport("unicorn", CallingConvention = CallingConvention.Cdecl)] + public static extern UCErrors brov_blob_reservation(byte[] blob, UIntPtr length, out ulong reservationBase, out ulong size); + + [DllImport("unicorn", CallingConvention = CallingConvention.Cdecl)] + public static extern UCErrors brov_last_reason(IntPtr uc, out uint reason); + + [DllImport("unicorn", CallingConvention = CallingConvention.Cdecl)] + public static extern UCErrors brov_cc_info(IntPtr uc, ref BrovCacheInfo info); + + [DllImport("unicorn", CallingConvention = CallingConvention.Cdecl)] + public static extern UCErrors brov_cc_validate(IntPtr uc, ref BrovAuditResult result); + + [DllImport("unicorn", CallingConvention = CallingConvention.Cdecl)] + public static extern UCErrors brov_cc_save(IntPtr uc, out IntPtr blob, out UIntPtr length); + + [DllImport("unicorn", CallingConvention = CallingConvention.Cdecl)] + public static extern UCErrors brov_cc_load(IntPtr uc, byte[] blob, UIntPtr length); + + [DllImport("unicorn", CallingConvention = CallingConvention.Cdecl)] + public static extern UCErrors brov_cc_resolve(IntPtr uc, out uint resolved, out uint remaining); + + [DllImport("unicorn", CallingConvention = CallingConvention.Cdecl)] + public static extern UCErrors brov_cc_free(IntPtr blob); + + public const uint BROV_REG_READABLE = 0x1; + public const uint BROV_REG_WRITABLE = 0x2; + + [DllImport("unicorn", CallingConvention = CallingConvention.Cdecl)] + public static extern UCErrors brov_reg_ptr(IntPtr uc, int regid, out IntPtr ptr, out UIntPtr size, out uint flags); } internal static class NativeLibraryResolver diff --git a/Brovan/Core/Emulation/UnicornBinding/Unicorn.cs b/Brovan/Core/Emulation/UnicornBinding/Unicorn.cs index a55b34f..2b26506 100644 --- a/Brovan/Core/Emulation/UnicornBinding/Unicorn.cs +++ b/Brovan/Core/Emulation/UnicornBinding/Unicorn.cs @@ -793,23 +793,24 @@ public unsafe string ReadMemoryString(ulong address, int length, Encoding encodi /// returns true if successful, otherwise false. public bool WriteRegister(Registers register, ulong value) { - if (DisposedCheck()) - return false; - - lock (_registerLock) - { - _error = uc_reg_write(_uc, register, ref value); - return _error == UCErrors.UC_ERR_OK; - } + return WriteRegister((int)register, value); } - public bool WriteRegister(int Register, ulong Value) + public unsafe bool WriteRegister(int Register, ulong Value) { if (DisposedCheck()) return false; lock (_registerLock) { + ulong* Slot = DirectRegister(Register, DirectWritable); + if (Slot != null) + { + *Slot = Value; + _error = UCErrors.UC_ERR_OK; + return true; + } + _error = uc_reg_write_raw(_uc, Register, ref Value); return _error == UCErrors.UC_ERR_OK; } @@ -893,15 +894,7 @@ public bool WriteRegisterByte(Registers register, byte[] value) /// returns the value of the register. public ulong ReadRegister(Registers register) { - if (DisposedCheck()) - return 0; - - if (_uc == IntPtr.Zero) - throw new InvalidOperationException("Unicorn engine is not initialized."); - - ulong value = 0; - _error = uc_reg_read(_uc, register, out value); - return value; + return ReadRegister((int)register); } /// @@ -909,7 +902,7 @@ public ulong ReadRegister(Registers register) /// /// Register to read. /// returns the value of the register. - public ulong ReadRegister(int Register) + public unsafe ulong ReadRegister(int Register) { if (DisposedCheck()) return 0; @@ -917,6 +910,13 @@ public ulong ReadRegister(int Register) if (_uc == IntPtr.Zero) throw new InvalidOperationException("Unicorn engine is not initialized."); + ulong* Slot = DirectRegister(Register, DirectReadable); + if (Slot != null) + { + _error = UCErrors.UC_ERR_OK; + return *Slot; + } + ulong Value = 0; _error = uc_reg_read_raw(_uc, Register, out Value); return Value; @@ -1363,6 +1363,208 @@ public bool SetTcgBufferSize(uint Size) return _error == UCErrors.UC_ERR_OK; } + /// + /// Reserve the address range that the TCG code buffer, the slot table and the + /// uc struct live in. Must be called before any instance + /// is created: the reservation is what makes a saved code cache reloadable. + /// + /// Base recorded by a previous run, or 0 to let the OS choose. + /// Total bytes to reserve, including the header region. + /// Whether saving and loading are wanted this run. + /// Also flag pointers into the interior of tracked objects. + public static bool ConfigureCodeCache(ulong ReserveBase, ulong ReserveSize, bool EnableCache, bool StrictAudit = false) + { + BrovConfig Config = new BrovConfig + { + StructSize = (uint)Marshal.SizeOf(), + Flags = (EnableCache ? BROV_CFG_ENABLE_CACHE : 0u) | (StrictAudit ? BROV_CFG_STRICT_AUDIT : 0u), + ReserveBase = ReserveBase, + ReserveSize = ReserveSize, + SlotCount = 0, + Reserved = 0, + }; + + return brov_configure(ref Config) == UCErrors.UC_ERR_OK; + } + + /// + /// Get the base and size of the address reservation actually obtained. + /// + public static bool GetCodeCacheReservation(out ulong ReservationBase, out ulong ReservationSize) + { + return brov_reservation_info(out ReservationBase, out ReservationSize) == UCErrors.UC_ERR_OK; + } + + /// + /// Read the reservation a saved blob needs, so it can be requested before uc_open. + /// + public static bool GetBlobReservation(byte[] Blob, out ulong ReservationBase, out ulong ReservationSize) + { + ReservationBase = 0; + ReservationSize = 0; + + if (Blob == null || Blob.Length == 0) + return false; + + return brov_blob_reservation(Blob, (UIntPtr)Blob.Length, out ReservationBase, out ReservationSize) == UCErrors.UC_ERR_OK; + } + + internal bool GetCodeCacheInfo(out BrovCacheInfo Info) + { + Info = new BrovCacheInfo { StructSize = (uint)Marshal.SizeOf() }; + + if (DisposedCheck()) + return false; + + _error = brov_cc_info(_uc, ref Info); + return _error == UCErrors.UC_ERR_OK; + } + + /// + /// Run the relocation audit without saving. Reports any host pointer baked into + /// generated code that a reload could not repoint. + /// + internal bool ValidateCodeCache(out BrovAuditResult Result) + { + Result = new BrovAuditResult { StructSize = (uint)Marshal.SizeOf() }; + + if (DisposedCheck()) + return false; + + _error = brov_cc_validate(_uc, ref Result); + return _error == UCErrors.UC_ERR_OK && Result.HitCount == 0; + } + + /// + /// Serialize the TCG code cache. Returns null when the cache cannot be saved; + /// says why. + /// + public byte[] SaveCodeCache() + { + if (DisposedCheck()) + return null; + + _error = brov_cc_save(_uc, out IntPtr Blob, out UIntPtr Length); + if (_error != UCErrors.UC_ERR_OK || Blob == IntPtr.Zero) + return null; + + try + { + byte[] Managed = new byte[(int)Length]; + Marshal.Copy(Blob, Managed, 0, Managed.Length); + return Managed; + } + finally + { + brov_cc_free(Blob); + } + } + + /// + /// Restore a previously saved TCG code cache. The guest image must already be + /// mapped: every restored block is verified against the guest bytes it was + /// translated from. + /// + public bool LoadCodeCache(byte[] Blob) + { + if (DisposedCheck() || Blob == null || Blob.Length == 0) + return false; + + _error = brov_cc_load(_uc, Blob, (UIntPtr)Blob.Length); + return _error == UCErrors.UC_ERR_OK; + } + + /// + /// Register restored blocks whose guest pages were not mapped yet when the cache + /// was loaded. Returns false once nothing is left to resolve. + /// + public bool ResolveCodeCache(out uint Resolved, out uint Remaining) + { + Resolved = 0; + Remaining = 0; + + if (DisposedCheck()) + return false; + + _error = brov_cc_resolve(_uc, out Resolved, out Remaining); + return _error == UCErrors.UC_ERR_OK && Remaining != 0; + } + + public uint GetCodeCacheReason() + { + if (DisposedCheck()) + return 0; + + return brov_last_reason(_uc, out uint Reason) == UCErrors.UC_ERR_OK ? Reason : 0; + } + + /// + /// Host pointers to register storage inside the guest CPU state, so the common + /// 64-bit reads and writes become a load or a store instead of a native call. + /// + /// + /// Only registers Unicorn stores verbatim get an entry, and only those it would + /// have written with a plain store get . EFLAGS is + /// absent because its condition codes are computed lazily, the program counter is + /// read-only here because writing it also raises quit_request and flushes + /// translated blocks, and nothing is exposed in 16- or 32-bit mode where the same + /// storage is reached under different truncation rules. + /// + private const int DirectRegisterCount = 512; + private const byte DirectProbed = 0x1; + private const byte DirectReadable = 0x2; + private const byte DirectWritable = 0x4; + + private IntPtr[] _directRegisters; + private byte[] _directRegisterState; + + private byte ProbeDirectRegister(int Register) + { + IntPtr[] Pointers = _directRegisters; + byte[] State = _directRegisterState; + + if (Pointers == null || State == null) + { + Interlocked.CompareExchange(ref _directRegisters, new IntPtr[DirectRegisterCount], null); + Interlocked.CompareExchange(ref _directRegisterState, new byte[DirectRegisterCount], null); + Pointers = _directRegisters; + State = _directRegisterState; + } + + byte Known = Volatile.Read(ref State[Register]); + if (Known != 0) + return Known; + + byte Result = DirectProbed; + if (brov_reg_ptr(_uc, Register, out IntPtr Pointer, out UIntPtr Bytes, out uint Flags) == UCErrors.UC_ERR_OK + && Pointer != IntPtr.Zero && (ulong)Bytes == sizeof(ulong)) + { + Pointers[Register] = Pointer; + + if ((Flags & BROV_REG_READABLE) != 0) + Result |= DirectReadable; + if ((Flags & BROV_REG_WRITABLE) != 0) + Result |= DirectWritable; + } + + Volatile.Write(ref State[Register], Result); + return Result; + } + + private unsafe ulong* DirectRegister(int Register, byte Access) + { + if ((uint)Register >= DirectRegisterCount) + return null; + + byte[] State = _directRegisterState; + byte Known = State != null ? Volatile.Read(ref State[Register]) : (byte)0; + + if (Known == 0) + Known = ProbeDirectRegister(Register); + + return (Known & Access) != 0 ? (ulong*)_directRegisters[Register] : null; + } + /// /// Get the current emulator context. /// diff --git a/Brovan/Core/Emulation/UnicornBinding/UnicornCodeCache.cs b/Brovan/Core/Emulation/UnicornBinding/UnicornCodeCache.cs new file mode 100644 index 0000000..b651704 --- /dev/null +++ b/Brovan/Core/Emulation/UnicornBinding/UnicornCodeCache.cs @@ -0,0 +1,461 @@ +using System; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using Brovan.Core.Helpers; +using static Brovan.Core.Emulation.Native; + +namespace Brovan.Core.Emulation +{ + /// + /// Persists Unicorn's TCG code cache between runs so a guest does not have to be + /// re-translated from scratch on every launch. + /// + public static class UnicornCodeCache + { + // Must match BROV_RESERVE_HEADER_SIZE and BROV_BLOB_MAGIC in + // Brovan/native/unicorn/brovan_uc.h. + private const ulong ReserveHeaderSize = 128 * 1024; + private const uint BlobMagic = 0x4356524B; + private const ulong DefaultCodeBufferSize = 2UL * 1024 * 1024 * 1024; + private const long CacheDirectoryBudget = 512L * 1024 * 1024; + private const int TrimGraceSeconds = 60; + + private static readonly object Gate = new object(); + + private static readonly string[] ReasonNames = + { + // Order must match brov_reason in Brovan/native/unicorn/brovan_uc.h. + "ok", "no-reservation", "unsupported-target", "truncated", "bad-magic", + "abi-mismatch", "layout-mismatch", "host-mismatch", "target-mismatch", + "reservation-mismatch", "prologue-mismatch", "arena-mismatch", + "code-hash-mismatch", "slot-table-full", "audit-failed", + "too-many-stale-blocks", "empty", "mostly-dead-code", "slot-unresolved", + }; + + private static byte[] PendingBlob; + private static string BlobPath; + private static string MarkerPath; + private static bool Configured; + private static bool Loaded; + private static bool PendingResolves; + private static int IdleResolves; + private const int MaxIdleResolves = 8; + private static ulong SavedUsedBytes = ulong.MaxValue; + + public static bool Enabled { get; set; } = true; + + public static bool PrintStats { get; set; } + + public static string CacheDirectory { get; set; } + + public static string ReasonName(uint Reason) + { + return Reason < ReasonNames.Length ? ReasonNames[Reason] : "reason-" + Reason.ToString(CultureInfo.InvariantCulture); + } + + /// + /// Reserve the address range the code cache needs. Must run before the first + /// instance is created, because the reservation is what + /// pins the uc struct and the code buffer at reproducible addresses. + /// + public static void Configure(string GuestImagePath, string HostImagePath) + { + lock (Gate) + { + if (Configured) + return; + + Configured = true; + + if (!Enabled) + return; + + try + { + string Directory = ResolveCacheDirectory(); + string Key = ComputeKey(GuestImagePath, HostImagePath); + + BlobPath = Path.Combine(Directory, Key + ".bjc"); + MarkerPath = BlobPath + ".inflight"; + + DiscardIfPreviousRunDied(); + + ulong ReserveBase = 0; + ulong ReserveSize = ReserveHeaderSize + DefaultCodeBufferSize; + + if (File.Exists(BlobPath)) + { + PendingBlob = File.ReadAllBytes(BlobPath); + + if (Unicorn.GetBlobReservation(PendingBlob, out ulong BlobBase, out ulong BlobSize)) + { + ReserveBase = BlobBase; + ReserveSize = BlobSize; + } + else + { + PendingBlob = null; + } + } + + if (!Unicorn.ConfigureCodeCache(ReserveBase, ReserveSize, true)) + { + Utils.LogError("[jit-cache] address reservation failed; running without a code cache."); + PendingBlob = null; + return; + } + + if (PendingBlob != null && Unicorn.GetCodeCacheReservation(out ulong GotBase, out _) && GotBase != ReserveBase) + { + // The recorded range was taken by something else. This run still + // records a fresh base for next time, it just cannot load today. + Utils.LogError($"[jit-cache] wanted reservation 0x{ReserveBase:X} but got 0x{GotBase:X}; running cold."); + PendingBlob = null; + } + } + catch (Exception Error) + { + Utils.LogError("[jit-cache] configure failed: " + Error.Message); + PendingBlob = null; + } + } + } + + /// + /// Restore the saved cache. Call once the guest image is mapped: every restored + /// block is verified against the guest bytes it was translated from. + /// + public static void TryLoad(Unicorn Engine) + { + lock (Gate) + { + if (!Enabled || Loaded || PendingBlob == null || Engine == null) + return; + + Loaded = true; + + byte[] Blob = PendingBlob; + PendingBlob = null; + + if (!Engine.LoadCodeCache(Blob)) + { + Utils.PrintHighlight($"[!] JIT cache not reused ({ReasonName(Engine.GetCodeCacheReason())}).", true); + return; + } + + WriteMarker(); + PendingResolves = true; + + if (Engine.GetCodeCacheInfo(out BrovCacheInfo Info)) + { + Utils.PrintHighlight($"[+] JIT cache restored: {Info.LoadedTbs} blocks, {Info.StaleTbs} stale, {Info.CodeGenUsed / 1024} KB.", true); + } + } + } + + /// + /// Register restored blocks whose pages had not been mapped yet at load time. + /// Cheap: their code is already in the buffer, this only verifies and files them. + /// + public static void ResolvePending(Unicorn Engine) + { + if (!PendingResolves || Engine == null) + return; + + PendingResolves = Engine.ResolveCodeCache(out uint Resolved, out _); + + // Some blocks never become verifiable. a page the guest maps once and + // drops, say. Retrying them for the rest of the run costs a native call + // and a guest read per pass and recovers nothing. + IdleResolves = Resolved == 0 ? IdleResolves + 1 : 0; + if (IdleResolves >= MaxIdleResolves) + PendingResolves = false; + } + + /// + /// Persist the cache. Only reached on a clean shutdown, which is deliberate: the + /// marker left behind by a crashed run is what invalidates a suspect blob. + /// + public static void TrySave(Unicorn Engine) + { + lock (Gate) + { + if (!Enabled || Engine == null || BlobPath == null) + return; + + try + { + if (Engine.GetCodeCacheInfo(out BrovCacheInfo Before)) + { + if (PrintStats) + Utils.PrintHighlight(Describe(Before), true, false, true); + + // Start() and Dispose() can both land here; the audit is a full + // scan of the buffer, so do not repeat it for nothing. + if (Before.CodeGenUsed == SavedUsedBytes) + return; + + SavedUsedBytes = Before.CodeGenUsed; + } + + byte[] Blob = Engine.SaveCodeCache(); + if (Blob == null) + { + Utils.LogError($"[jit-cache] not saved ({ReasonName(Engine.GetCodeCacheReason())})."); + + if (!Engine.ValidateCodeCache(out BrovAuditResult Audit) && Audit.HitCount != 0) + Utils.LogError($"[jit-cache] audit hit {Audit.HitCount} site(s); first at +0x{Audit.FirstOffset:X} = 0x{Audit.FirstValue:X} ({Audit.FirstObject})."); + + return; + } + + string Temporary = BlobPath + "." + Environment.ProcessId.ToString(CultureInfo.InvariantCulture) + ".tmp"; + File.WriteAllBytes(Temporary, Blob); + File.Move(Temporary, BlobPath, true); + TrimCacheDirectory(Path.GetDirectoryName(BlobPath)); + } + catch (Exception Error) + { + Utils.LogError("[jit-cache] save failed: " + Error.Message); + } + finally + { + ClearMarker(); + } + } + } + + internal static string Describe(BrovCacheInfo Info) + { + return $"[#] JIT cache: {Info.TbCount} blocks, {Info.CodeGenUsed / 1024} KB of {Info.CodeGenBufferSize / (1024 * 1024)} MB, " + + $"{Info.SlotsUsed}/{Info.SlotCount} helper slots, {Info.FlushCount} flush(es), last: {ReasonName(Info.LastReason)}."; + } + + private static string ResolveCacheDirectory() + { + string Directory = CacheDirectory; + + if (string.IsNullOrWhiteSpace(Directory)) + Directory = Path.Combine(AppContext.BaseDirectory, ".jitcache"); + + System.IO.Directory.CreateDirectory(Directory); + DiscardForeignBlobs(Directory); + return Directory; + } + + /// + /// Drop blobs an older Brovan wrote in a format this one can no longer read. + /// They would otherwise sit there until the size trim evicted them: their key + /// covers the unicorn build, so a newer Brovan never looks at them again. + /// + private static void DiscardForeignBlobs(string Directory) + { + Span Header = stackalloc byte[8]; + + if (brov_abi_version(out uint CurrentAbi) != UCErrors.UC_ERR_OK) + return; + + foreach (string Path in System.IO.Directory.EnumerateFiles(Directory, "*.bjc")) + { + try + { + uint Magic; + uint Abi; + + using (FileStream Stream = File.OpenRead(Path)) + { + if (Stream.Read(Header) != Header.Length) + continue; + + Magic = BitConverter.ToUInt32(Header.Slice(0, 4)); + Abi = BitConverter.ToUInt32(Header.Slice(4, 4)); + } + + if (Magic == BlobMagic && Abi == CurrentAbi) + continue; + + File.Delete(Path); + File.Delete(Path + ".inflight"); + } + catch (IOException) + { + } + catch (UnauthorizedAccessException) + { + } + } + } + + /// + /// A blob is only trusted when the run that last used it exited cleanly. A bad + /// block can be reached minutes in, so clearing the marker on a timer would turn + /// one crash into a boot loop. + /// + private static void DiscardIfPreviousRunDied() + { + if (!File.Exists(MarkerPath)) + return; + + if (IsMarkerOwnerAlive()) + return; + + try + { + File.Delete(MarkerPath); + File.Delete(BlobPath); + } + catch (IOException) + { + } + + Utils.LogError("[jit-cache] previous run did not exit cleanly; discarded the blob."); + } + + private static bool IsMarkerOwnerAlive() + { + try + { + string[] Parts = File.ReadAllText(MarkerPath).Split('|'); + if (Parts.Length != 2) + return false; + + int Pid = int.Parse(Parts[0], CultureInfo.InvariantCulture); + long StartTicks = long.Parse(Parts[1], CultureInfo.InvariantCulture); + + using Process Owner = Process.GetProcessById(Pid); + return Owner.StartTime.Ticks == StartTicks; + } + catch (Exception) + { + // No such process, or it is a different one that reused the pid. + return false; + } + } + + private static void WriteMarker() + { + try + { + using Process Self = Process.GetCurrentProcess(); + File.WriteAllText(MarkerPath, Self.Id.ToString(CultureInfo.InvariantCulture) + "|" + Self.StartTime.Ticks.ToString(CultureInfo.InvariantCulture)); + } + catch (Exception Error) + { + Utils.LogError("[jit-cache] could not write the in-flight marker: " + Error.Message); + } + } + + private static void ClearMarker() + { + try + { + if (MarkerPath != null && File.Exists(MarkerPath)) + File.Delete(MarkerPath); + } + catch (IOException) + { + } + } + + private static void TrimCacheDirectory(string Directory) + { + DirectoryInfo Info = new DirectoryInfo(Directory); + FileInfo[] Blobs = Info.GetFiles("*.bjc"); + long Total = 0; + + foreach (FileInfo Blob in Blobs) + Total += Blob.Length; + + if (Total <= CacheDirectoryBudget) + return; + + Array.Sort(Blobs, (a, b) => a.LastWriteTimeUtc.CompareTo(b.LastWriteTimeUtc)); + DateTime Grace = DateTime.UtcNow.AddSeconds(-TrimGraceSeconds); + + foreach (FileInfo Blob in Blobs) + { + if (Total <= CacheDirectoryBudget) + break; + + // Another Brovan may be mid-load on a blob it just wrote. + if (Blob.LastWriteTimeUtc > Grace) + continue; + + try + { + long Size = Blob.Length; + Blob.Delete(); + Total -= Size; + } + catch (IOException) + { + } + } + } + + private static string ComputeKey(string GuestImagePath, string HostImagePath) + { + ulong Hash = 0xcbf29ce484222325; + + // GuestImagePath is in the guest namespace and usually does not exist on the + // host, so the content has to be read through the path Brovan actually opened. + MixText(ref Hash, GuestImagePath ?? string.Empty); + MixFile(ref Hash, HostImagePath); + MixFile(ref Hash, Path.Combine(AppContext.BaseDirectory, GeneralHelper.IsWindows ? "unicorn.dll" : "libunicorn.so")); + MixNumber(ref Hash, (ulong)IntPtr.Size); + + return Hash.ToString("x16", CultureInfo.InvariantCulture); + } + + private static void MixFile(ref ulong Hash, string Path) + { + if (string.IsNullOrEmpty(Path) || !File.Exists(Path)) + { + MixNumber(ref Hash, 0); + return; + } + + FileInfo Info = new FileInfo(Path); + MixNumber(ref Hash, (ulong)Info.Length); + MixNumber(ref Hash, (ulong)Info.LastWriteTimeUtc.Ticks); + + // mtime alone has one-second resolution on some filesystems, so take a slice + // of the content too. + try + { + byte[] Head = new byte[4096]; + using FileStream Stream = File.OpenRead(Path); + int Read = Stream.Read(Head, 0, Head.Length); + for (int i = 0; i < Read; i++) + { + Hash ^= Head[i]; + Hash *= 0x100000001b3; + } + } + catch (IOException) + { + } + } + + private static void MixText(ref ulong Hash, string Text) + { + string Normalized = GeneralHelper.IsWindows ? Text.ToLowerInvariant() : Text; + + foreach (char Character in Normalized) + { + Hash ^= Character; + Hash *= 0x100000001b3; + } + } + + private static void MixNumber(ref ulong Hash, ulong Value) + { + for (int i = 0; i < 8; i++) + { + Hash ^= (Value >> (i * 8)) & 0xFF; + Hash *= 0x100000001b3; + } + } + } +} diff --git a/Brovan/Core/Helpers/WindowsImage/WindowsImageImporter.cs b/Brovan/Core/Helpers/WindowsImage/WindowsImageImporter.cs index af607f5..ad248e4 100644 --- a/Brovan/Core/Helpers/WindowsImage/WindowsImageImporter.cs +++ b/Brovan/Core/Helpers/WindowsImage/WindowsImageImporter.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.IO; +using Brovan.Core; +using static Brovan.Core.Helpers.BinaryHelpers; namespace Brovan.Core.Helpers.WindowsImage { @@ -20,6 +22,58 @@ internal static class WindowsImageImporter private static readonly string[] RegistryHives = { "SOFTWARE", "SYSTEM", "DEFAULT", "SAM", "SECURITY" }; + private const string ApiSetSchemaName = "apisetschema.dll"; + private const string ApiSetSectionName = ".apiset"; + + public static bool TryReadApiSetMap(string LibrariesDirectory, out byte[] Map) + { + Map = Array.Empty(); + + string SchemaPath = Path.Combine(LibrariesDirectory, ApiSetSchemaName); + if (!File.Exists(SchemaPath)) + return false; + + using BinaryFile Schema = new BinaryFile(SchemaPath, true); + if (Schema.FileFormat != BinaryFormat.PE || Schema.PE.Sections == null) + return false; + + foreach (PortableBinarySection Section in Schema.PE.Sections) + { + if (!string.Equals(Section.SectionName, ApiSetSectionName, StringComparison.Ordinal)) + continue; + + byte[] Data = Schema.GetBinaryData().ToArray(); + long End = (long)Section.RawOffset + Section.RawSize; + if (Section.RawSize == 0 || End > Data.Length) + return false; + + // VirtualSize is the meaningful length; RawSize is padded to file alignment. + int Length = Section.VirtualSize != 0 && Section.VirtualSize < Section.RawSize + ? (int)Section.VirtualSize + : (int)Section.RawSize; + + Map = new byte[Length]; + Array.Copy(Data, (int)Section.RawOffset, Map, 0, Length); + return true; + } + + return false; + } + + /// + /// Writes apisetmap.bin from the imported apisetschema.dll. Returns false when the + /// image had no schema to read, leaving the caller's existing map alone. + /// + public static bool TryWriteApiSetMap(string BaseDirectory, Action Report) + { + if (!TryReadApiSetMap(Path.Combine(BaseDirectory, "WindowsLibs"), out byte[] Map)) + return false; + + File.WriteAllBytes(Path.Combine(BaseDirectory, "apisetmap.bin"), Map); + Report?.Invoke($"[+] Wrote apisetmap.bin from the image's apisetschema.dll ({Map.Length} bytes)."); + return true; + } + private readonly struct PendingFile { public readonly string Name; diff --git a/Brovan/Core/Helpers/WindowsImage/WindowsSetup.cs b/Brovan/Core/Helpers/WindowsImage/WindowsSetup.cs index 9c77747..908a973 100644 --- a/Brovan/Core/Helpers/WindowsImage/WindowsSetup.cs +++ b/Brovan/Core/Helpers/WindowsImage/WindowsSetup.cs @@ -91,6 +91,9 @@ public static bool Install(string BaseDirectory, WindowsSetupOptions Options, Ac WindowsImageImporter.Import(Media, BaseDirectory, Options.ImageIndex, Report, Progress); + if (!WindowsImageImporter.TryWriteApiSetMap(BaseDirectory, Report)) + Report("[!] The image had no apisetschema.dll; keeping the existing API set map."); + if (Media is HttpImageDataSource Remote) Report($"[*] Transferred {Remote.TransferredBytes / (1024 * 1024)} MB over the network."); diff --git a/Brovan/Program.cs b/Brovan/Program.cs index 6353e38..3977869 100644 --- a/Brovan/Program.cs +++ b/Brovan/Program.cs @@ -130,6 +130,12 @@ static void ShowHelp() Console.WriteLine(" --no-hooks Run the emulator with no hooks. useful when you want maximum performance and want to see some program output."); Console.WriteLine(" --backend= Choose the emulation backend: unicorn (default), kvm (Linux), or whp (Windows Hypervisor Platform)."); Console.WriteLine(" --cwd Directory the emulated program starts in. Defaults to the directory of the binary."); + Console.WriteLine(" --jit-cache[=]"); + Console.WriteLine(" Directory for the persisted Unicorn JIT code cache, which lets a"); + Console.WriteLine(" program skip re-translating itself on every launch. Defaults to"); + Console.WriteLine(" .jitcache next to Brovan, and is enabled by default."); + Console.WriteLine(" --no-jit-cache Do not reuse or write a persisted JIT code cache."); + Console.WriteLine(" --jit-cache-stats Print code cache statistics when the emulated program exits."); Console.WriteLine(" --install-windows Download Windows installation media from Microsoft and extract the system"); Console.WriteLine(" libraries, NLS tables and registry hives Brovan needs, then the Visual C++"); Console.WriteLine(" runtimes, then exit."); @@ -406,7 +412,21 @@ static void Main(string[] args) return; } - if (!File.Exists(BinaryEmulator.ApiSetMapPath)) + // Prefer the map from the imported apisetschema.dll over anything else + bool ApiSetMapReady = false; + if (!File.Exists(BinaryEmulator.ApiSetMapPath) || File.GetLastWriteTimeUtc(Path.Combine(WindowsLibsPath, "apisetschema.dll")) > File.GetLastWriteTimeUtc(BinaryEmulator.ApiSetMapPath)) + { + try + { + ApiSetMapReady = WindowsImageImporter.TryWriteApiSetMap(AppContext.BaseDirectory, Message => PrintHighlight(Message, true)); + } + catch (Exception Error) + { + PrintHighlight($"[-] Could not read the API set map from apisetschema.dll: \"{Error.Message}\".", true); + } + } + + if (!ApiSetMapReady && !File.Exists(BinaryEmulator.ApiSetMapPath)) { if (IsWindows) { @@ -542,6 +562,15 @@ static void Main(string[] args) case "--no-hooks": NoHooks = true; continue; + case "--no-jit-cache": + UnicornCodeCache.Enabled = false; + continue; + case "--jit-cache-stats": + UnicornCodeCache.PrintStats = true; + continue; + case "--jit-cache": + UnicornCodeCache.Enabled = true; + continue; case "--cwd": if (i + 1 >= args.Length) continue; @@ -626,6 +655,13 @@ static void Main(string[] args) continue; } + if (Arg.StartsWith("--jit-cache=", StringComparison.OrdinalIgnoreCase)) + { + UnicornCodeCache.Enabled = true; + UnicornCodeCache.CacheDirectory = DecodeArgumentValue(Arg.Substring("--jit-cache=".Length)); + continue; + } + if (Arg.StartsWith("-", StringComparison.Ordinal)) continue; diff --git a/Brovan/native/unicorn/brovan_tcg_exitcheck.inc.h b/Brovan/native/unicorn/brovan_tcg_exitcheck.inc.h new file mode 100644 index 0000000..9fdeeb4 --- /dev/null +++ b/Brovan/native/unicorn/brovan_tcg_exitcheck.inc.h @@ -0,0 +1,58 @@ +/* Included into qemu/include/exec/gen-icount.h, just above gen_tb_start. + * + * Unicorn polls the exit request with a helper call at the top of every block, + * where the condition is just a 32-bit load at a constant offset from cpu_env. + * QEMU emits a load and a not-taken branch instead; see gen_tb_start/gen_tb_end + * in accel/tcg/translator.c. + * + * The shape is load-bearing. Branching over the helper and rejoining inside the + * block prologue splits the TCG basic block and fails liveness analysis - that is + * what Unicorn's comment about brcondi warns about. Upstream branches *out* of + * the block to a label emitted after its final exit, so there is no join. + */ +#ifndef BROVAN_TCG_EXITCHECK_H +#define BROVAN_TCG_EXITCHECK_H + +static inline void brov_gen_exit_check_start(TCGContext *tcg_ctx, TCGv_ptr puc, + TCGv_i32 delay_slot) +{ + TCGv_i32 count; + + /* Targets with delay slots pass a runtime flag the trailing block cannot see + * once its temp is freed; leave those on the unconditional helper call. */ + if (tcg_ctx->delay_slot_flag != NULL) { + gen_helper_check_exit_request(tcg_ctx, puc, delay_slot); + tcg_ctx->brov_exitreq_label = NULL; + return; + } + + count = tcg_temp_new_i32(tcg_ctx); + tcg_gen_ld_i32(tcg_ctx, count, tcg_ctx->cpu_env, + offsetof(ArchCPU, neg.icount_decr.u32) - offsetof(ArchCPU, env)); + tcg_ctx->brov_exitreq_label = gen_new_label(tcg_ctx); + tcg_gen_brcondi_i32(tcg_ctx, TCG_COND_LT, count, 0, tcg_ctx->brov_exitreq_label); + tcg_temp_free_i32(tcg_ctx, count); +} + +/* Emitted after the block's own exit_tb, so it is only ever reached by the + * branch above and never falls through into or out of the block body. */ +static inline void brov_gen_exit_check_end(TCGContext *tcg_ctx) +{ + TCGv_ptr puc; + TCGv_i32 zero; + + if (tcg_ctx->brov_exitreq_label == NULL) { + return; + } + + gen_set_label(tcg_ctx, tcg_ctx->brov_exitreq_label); + tcg_ctx->brov_exitreq_label = NULL; + + puc = tcg_const_ptr(tcg_ctx, tcg_ctx->uc); + zero = tcg_const_i32(tcg_ctx, 0); + gen_helper_check_exit_request(tcg_ctx, puc, zero); + tcg_temp_free_i32(tcg_ctx, zero); + tcg_temp_free_ptr(tcg_ctx, puc); +} + +#endif diff --git a/Brovan/native/unicorn/brovan_tcg_slots.inc.c b/Brovan/native/unicorn/brovan_tcg_slots.inc.c new file mode 100644 index 0000000..0393d07 --- /dev/null +++ b/Brovan/native/unicorn/brovan_tcg_slots.inc.c @@ -0,0 +1,94 @@ +/* Included into qemu/tcg/{i386,aarch64}/tcg-target.inc.c immediately above + * tcg_out_call, which early-returns through it. + * + * Helper addresses are the one class of baked-in host pointer that cannot be + * pinned: they move with the library's load address. Routing every reference + * through an indirect slot lets a reloaded code cache be repointed by rewriting + * the slot table, which is stored as offsets from an anchor inside our own + * image. Targets that already live inside the reservation are left alone: their + * encodings are relative and stay self-consistent. */ + +static bool brov_in_reservation(TCGContext *s, const void *p) +{ + const uint8_t *low = (const uint8_t *)s->brov_slots; + const uint8_t *high = (const uint8_t *)s->initial_buffer + s->initial_buffer_size; + + return (const uint8_t *)p >= low && (const uint8_t *)p < high; +} + +static bool brov_slot_emit(TCGContext *s, const void *dest, bool is_call) +{ + uint32_t idx; + uintptr_t slot; + + if (!s->brov_slots || brov_in_reservation(s, dest)) { + return false; + } + + idx = brov_slot_intern(s->brov_slots, s->brov_slot_map, s->brov_slot_map_mask, + &s->brov_slots_used, s->brov_slot_count, dest); + if (idx == (uint32_t)-1) { + /* Falling back to a direct branch bakes an unrelocatable address, so the + * session can still run but must not be saved. */ + s->brov_slots_overflowed = 1; + return false; + } + + slot = (uintptr_t)&s->brov_slots[idx]; + +#if defined(__aarch64__) + { + intptr_t pc = (intptr_t)s->code_ptr; + intptr_t page_delta = ((intptr_t)slot & ~(intptr_t)0xfff) - (pc & ~(intptr_t)0xfff); + intptr_t imm = page_delta >> 12; + uint32_t immlo, immhi; + + if (!is_call || imm != sextract64(imm, 0, 21)) { + s->brov_slots_overflowed = 1; + return false; + } + immlo = (uint32_t)(imm & 3); + immhi = (uint32_t)((imm >> 2) & 0x7ffff); + + /* ADRP TMP, page(slot) ; LDR TMP, [TMP, #off] ; BLR TMP */ + tcg_out32(s, 0x90000000u | (immlo << 29) | (immhi << 5) | (uint32_t)TCG_REG_TMP); + tcg_out32(s, 0xf9400000u | ((uint32_t)((slot & 0xfff) >> 3) << 10) | + ((uint32_t)TCG_REG_TMP << 5) | (uint32_t)TCG_REG_TMP); + tcg_out32(s, 0xd63f0000u | ((uint32_t)TCG_REG_TMP << 5)); + } +#elif TCG_TARGET_REG_BITS == 64 + { + intptr_t disp = (intptr_t)slot - ((intptr_t)s->code_ptr + 6); + + if (disp != (int32_t)disp) { + s->brov_slots_overflowed = 1; + return false; + } + /* call/jmp qword ptr [rip + disp32] */ + tcg_out8(s, 0xff); + tcg_out8(s, is_call ? 0x15 : 0x25); + tcg_out32(s, (int32_t)disp); + } +#else + /* call/jmp dword ptr [slot] */ + tcg_out8(s, 0xff); + tcg_out8(s, is_call ? 0x15 : 0x25); + tcg_out32(s, (int32_t)(intptr_t)slot); +#endif + + return true; +} + +static bool brov_tcg_out_call_slot(TCGContext *s, const void *dest) +{ + return brov_slot_emit(s, dest, true); +} + +#if !defined(__aarch64__) +/* The i386 backend tail-jumps into qemu_st_helpers rather than calling it, so + * the jump needs the same treatment as a call. */ +static bool brov_tcg_out_jmp_slot(TCGContext *s, const void *dest) +{ + return brov_slot_emit(s, dest, false); +} +#endif diff --git a/Brovan/native/unicorn/brovan_uc.h b/Brovan/native/unicorn/brovan_uc.h new file mode 100644 index 0000000..0bb18bd --- /dev/null +++ b/Brovan/native/unicorn/brovan_uc.h @@ -0,0 +1,266 @@ +/* Brovan extensions to Unicorn. Applied to the upstream tree at build time by + * Brovan.Unicorn.targets; see patches.manifest for the anchor list. + * + * This header is pulled into uc_priv.h and tcg.h, so it must not depend on any + * Unicorn or QEMU type. + */ +#ifndef BROVAN_UC_H +#define BROVAN_UC_H + +#include +#include +#include + +#define BROV_ABI_VERSION 2u +#define BROV_BLOB_MAGIC 0x4356524Bu /* "KRVC" */ + +/* The reservation is laid out [slot table][arena][code gen buffer]. Replaying a + * single base address therefore pins every object whose address gets baked into + * generated code. */ +#define BROV_SLOT_AREA_SIZE (64u * 1024u) +#define BROV_ARENA_SIZE (64u * 1024u) +#define BROV_RESERVE_HEADER_SIZE (BROV_SLOT_AREA_SIZE + BROV_ARENA_SIZE) +#define BROV_MAX_SLOTS (BROV_SLOT_AREA_SIZE / sizeof(void *)) +#define BROV_DEFAULT_SLOTS 4096u + +/* Fraction of restored blocks that may fail guest-source verification before the + * whole blob is discarded: a mostly-invalidated cache is slower than none. */ +#define BROV_MAX_STALE_PERCENT 25u + +typedef enum brov_reason { + BROV_OK = 0, + BROV_REASON_NO_RESERVATION, + BROV_REASON_UNSUPPORTED_TARGET, + BROV_REASON_TRUNCATED, + BROV_REASON_MAGIC, + BROV_REASON_ABI, + BROV_REASON_LAYOUT, + BROV_REASON_HOST, + BROV_REASON_TARGET, + BROV_REASON_BASE_MISMATCH, + BROV_REASON_PROLOGUE, + BROV_REASON_ARENA_MISMATCH, + BROV_REASON_CODE_HASH, + BROV_REASON_SLOT_OVERFLOW, + BROV_REASON_AUDIT, + BROV_REASON_TOO_MANY_STALE, + BROV_REASON_EMPTY, + BROV_REASON_BLOATED, + BROV_REASON_SLOT_UNRESOLVED, + BROV_REASON_MAX +} brov_reason; + +/* Slot contents are not all the same kind of thing. Helpers live in unicorn's + * image and are stored relative to it; a hook callback is a host callback the + * embedder registered - for Brovan a .NET thunk, which lands somewhere new every + * run - so it is stored as the identity of the hook that owns it and looked up + * again on load. */ +#define BROV_SLOT_IMAGE 0u +#define BROV_SLOT_HOOK 1u + +typedef struct brov_slot_record_t { + uint32_t kind; + uint32_t hook_idx; /* BROV_SLOT_HOOK: which uc->hook[] list */ + uint64_t detail; /* BROV_SLOT_IMAGE: offset from the image anchor */ + uint64_t tag; /* BROV_SLOT_HOOK: identity of the owning hook */ +} brov_slot_record_t; + +/* Restored blocks that later turn out to be unusable leave their code behind: + * nothing can move it, because other blocks branch to it by absolute address. + * Once the buffer is mostly dead weight the blob is dropped so the next save + * starts compact again. */ +#define BROV_MIN_LIVE_PERCENT 50u + +/* brov_configure() flags. */ +#define BROV_CFG_ENABLE_CACHE 0x1u +#define BROV_CFG_STRICT_AUDIT 0x2u /* also flag pointers into the interior of a tracked object */ + +typedef struct brov_config_t { + uint32_t struct_size; + uint32_t flags; + uint64_t reserve_base; /* 0: let the OS choose and report it back */ + uint64_t reserve_size; /* 0: default */ + uint32_t slot_count; /* 0: BROV_DEFAULT_SLOTS */ + uint32_t reserved; +} brov_config_t; + +typedef struct brov_cc_info_t { + uint32_t struct_size; + uint32_t last_reason; + + uint64_t reservation_base; + uint64_t reservation_size; + uint64_t code_gen_buffer; + uint64_t code_gen_buffer_size; + uint64_t code_gen_used; + + uint64_t tb_count; + uint64_t flush_count; + + uint32_t slot_count; + uint32_t slots_used; + uint32_t slots_overflowed; + + uint64_t load_count; + uint64_t loaded_tbs; + uint64_t stale_tbs; + uint64_t save_count; +} brov_cc_info_t; + +/* Populated by a failed audit so the offending site can be reported rather than + * silently dropped. */ +typedef struct brov_audit_result_t { + uint32_t struct_size; + uint32_t hit_count; + uint64_t first_offset; + uint64_t first_value; + char first_object[32]; +} brov_audit_result_t; + +typedef struct brov_blob_header_t { + uint32_t magic; + uint32_t abi; + uint32_t header_bytes; + uint32_t flags; + + uint64_t layout_fingerprint; + uint64_t host_fingerprint; + uint32_t target_arch; + uint32_t target_mode; + + uint64_t reservation_base; + uint64_t reservation_size; + uint64_t code_gen_buffer_off; + uint64_t code_gen_buffer_size; + uint64_t code_gen_used; + + uint64_t prologue_hash; + uint64_t prologue_bytes; + + uint64_t uc_off; + uint64_t tcg_ctx_off; + uint64_t arena_used; + + uint64_t region_current; + uint64_t region_agg_size_full; + + uint32_t slot_count; + uint32_t slots_used; + uint64_t tb_count; + + uint64_t code_hash; +} brov_blob_header_t; + +typedef struct brov_tb_record_t { + uint64_t offset; /* from code_gen_buffer */ + uint64_t src_hash; +} brov_tb_record_t; + +struct uc_struct; + +/* brov_reg_ptr flags. A register is only writable through its pointer when + * uc_reg_write() would have done nothing but store to it: the program counter is + * excluded because writing it also raises quit_request and flushes translated + * blocks. */ +#define BROV_REG_READABLE 0x1u +#define BROV_REG_WRITABLE 0x2u + +/* Installed from inside the per-target translation unit, which is the only place + * TCGContext and TranslationBlock are complete types. Mirrors how Unicorn wires + * up uc->tb_flush / uc->uc_gen_tb. */ +struct brov_ops { + int (*info)(struct uc_struct *uc, brov_cc_info_t *out); + int (*audit)(struct uc_struct *uc, brov_audit_result_t *out); + int (*save)(struct uc_struct *uc, void **blob, size_t *len); + int (*load)(struct uc_struct *uc, const void *blob, size_t len); + int (*resolve)(struct uc_struct *uc, uint32_t *resolved, uint32_t *remaining); + int (*reg_ptr)(struct uc_struct *uc, int regid, void **ptr, size_t *size, uint32_t *flags); +}; + +#define BROVAN_UC_FIELDS \ + uint32_t brov_last_reason; \ + struct brov_ops brov; + +#define BROVAN_TCG_FIELDS \ + void **brov_slots; \ + uint32_t *brov_slot_map; \ + uint32_t brov_slot_count; \ + uint32_t brov_slot_map_mask; \ + uint32_t brov_slots_used; \ + uint32_t brov_slots_overflowed; \ + uint64_t brov_prologue_hash; \ + uint64_t brov_prologue_bytes; \ + uint64_t brov_load_count; \ + uint64_t brov_loaded_tbs; \ + uint64_t brov_stale_tbs; \ + uint64_t brov_save_count; \ + void *brov_pending; \ + uint64_t brov_pending_count; \ + uint64_t brov_pending_flush; \ + struct TCGLabel *brov_exitreq_label; + +/* The slot table is interned during code generation (inside the tcg.c + * translation unit) and rebuilt after a load (inside translate-all.c). Both + * need the identical probe sequence, so it lives here. */ +static inline uint32_t brov_slot_hash(const void *fn) +{ + uint64_t v = (uint64_t)(uintptr_t)fn >> 4; + v *= 0x9e3779b97f4a7c15ULL; + return (uint32_t)(v >> 32); +} + +/* Returns the index of an existing slot, or the map bucket to fill (via + * *bucket) when the pointer is not interned yet. */ +static inline uint32_t brov_slot_find(void *const *slots, const uint32_t *map, + uint32_t mask, const void *fn, + uint32_t *bucket) +{ + uint32_t i = brov_slot_hash(fn) & mask; + + for (;;) { + uint32_t entry = map[i]; + if (entry == 0) { + *bucket = i; + return (uint32_t)-1; + } + if (slots[entry - 1] == fn) { + return entry - 1; + } + i = (i + 1) & mask; + } +} + +static inline uint32_t brov_slot_intern(void **slots, uint32_t *map, uint32_t mask, + uint32_t *used, uint32_t count, const void *fn) +{ + uint32_t bucket = 0; + uint32_t idx = brov_slot_find(slots, map, mask, fn, &bucket); + + if (idx != (uint32_t)-1) { + return idx; + } + if (*used >= count) { + return (uint32_t)-1; + } + idx = (*used)++; + slots[idx] = (void *)(uintptr_t)fn; + map[bucket] = idx + 1; + return idx; +} + +/* Defined in brovan_uc_api.inc.c (appended to uc.c) and therefore global to the + * whole library; the per-target implementations are all static. */ +void *brov_alloc_uc(size_t size); +void brov_free_uc(void *p); +void *brov_alloc_arena(size_t size); +bool brov_reservation(uint64_t *base, uint64_t *size, uint32_t *slot_count); +uint64_t brov_arena_offset(const void *p); +uint64_t brov_arena_used(void); +uintptr_t brov_image_base(void); +bool brov_image_range(uint64_t *lo, uint64_t *hi); +uint64_t brov_hash_bytes(const void *data, size_t len, uint64_t seed); +bool brov_cache_requested(void); +bool brov_strict_audit(void); +bool brov_commit_rwx(void *addr, uint64_t size); + +#endif /* BROVAN_UC_H */ diff --git a/Brovan/native/unicorn/brovan_uc_api.inc.c b/Brovan/native/unicorn/brovan_uc_api.inc.c new file mode 100644 index 0000000..9b54f44 --- /dev/null +++ b/Brovan/native/unicorn/brovan_uc_api.inc.c @@ -0,0 +1,533 @@ +/* Appended to uc.c. Arch-neutral half of the Brovan extensions: the address + * reservation, the arena that pins uc/tcg_ctx, and the exported entry points. + * Everything that needs TCGContext or TranslationBlock lives in + * brovan_uc_tcg.inc.c and is reached through uc->brov. */ + +#ifndef _WIN32 +#include +#ifndef MAP_FIXED_NOREPLACE +#define MAP_FIXED_NOREPLACE 0x100000 +#endif +#endif + +/* Address of an object in our own image. Slot contents are stored relative to + * this rather than to the module base, so no OS module-lookup is needed and the + * delta stays constant however the loader places the library. */ +static const char brov_image_anchor_obj = 0; + +static struct { + uint8_t *base; + uint64_t size; + uint32_t slot_count; + uint8_t *arena; + size_t arena_used; + bool active; + bool cache_requested; + bool strict_audit; +} g_brov; + +static bool brov_os_reserve(void *want, uint64_t size, void **got) +{ +#ifdef _WIN32 + void *p = VirtualAlloc(want, (SIZE_T)size, MEM_RESERVE, PAGE_EXECUTE_READWRITE); + if (!p || (want && p != want)) { + if (p) { + VirtualFree(p, 0, MEM_RELEASE); + } + return false; + } + *got = p; + return true; +#else + int flags = MAP_PRIVATE | MAP_ANONYMOUS; + void *p; + + /* MAP_FIXED would silently unmap whatever is already there, and + * MAP_FIXED_NOREPLACE needs Linux 4.17 which Android kernels predate, so + * fall back to a hint and check what we actually got. */ + if (want) { + p = mmap(want, (size_t)size, PROT_NONE, flags | MAP_FIXED_NOREPLACE, -1, 0); + if (p == MAP_FAILED) { + p = mmap(want, (size_t)size, PROT_NONE, flags, -1, 0); + } + } else { + p = mmap(NULL, (size_t)size, PROT_NONE, flags, -1, 0); + } + + if (p == MAP_FAILED) { + return false; + } + if (want && p != want) { + munmap(p, (size_t)size); + return false; + } + *got = p; + return true; +#endif +} + +static void brov_os_release(void *base, uint64_t size) +{ +#ifdef _WIN32 + (void)size; + VirtualFree(base, 0, MEM_RELEASE); +#else + munmap(base, (size_t)size); +#endif +} + +static bool brov_os_commit_rw(void *addr, uint64_t size) +{ +#ifdef _WIN32 + return VirtualAlloc(addr, (SIZE_T)size, MEM_COMMIT, PAGE_READWRITE) != NULL; +#else + return mprotect(addr, (size_t)size, PROT_READ | PROT_WRITE) == 0; +#endif +} + +bool brov_commit_rwx(void *addr, uint64_t size) +{ +#ifdef _WIN32 + /* Left reserved on Windows: the vectored handler installed by + * alloc_code_gen_buffer() commits code pages on first touch. */ + (void)addr; + (void)size; + return true; +#else + return mprotect(addr, (size_t)size, PROT_READ | PROT_WRITE | PROT_EXEC) == 0; +#endif +} + +/* A saved cache can only be reloaded at the address it was generated for, so the + * base has to be reproducible across runs. Letting the OS pick is not: the CLR + * has usually taken part of that range by the next launch. These are tried in + * order and each is verified, never assumed - the last entry is small enough to + * sit under a 39-bit user VA, which older Android kernels still use. */ +static const uint64_t brov_preferred_bases[] = { + 0x0000100000000000ull, + 0x0000004000000000ull, + 0x0000000200000000ull, +}; + +static bool brov_reserve_somewhere(uint64_t wanted, uint64_t size, void **got) +{ + size_t i; + + if (wanted && brov_os_reserve((void *)(uintptr_t)wanted, size, got)) { + return true; + } + + for (i = 0; i < sizeof(brov_preferred_bases) / sizeof(brov_preferred_bases[0]); i++) { + if (brov_preferred_bases[i] == wanted) { + continue; + } + if (brov_os_reserve((void *)(uintptr_t)brov_preferred_bases[i], size, got)) { + return true; + } + } + + return brov_os_reserve(NULL, size, got); +} + +uintptr_t brov_image_base(void) +{ + return (uintptr_t)&brov_image_anchor_obj; +} + +/* Where this library is actually mapped. The audit needs real bounds: guessing a + * window around the anchor flags ordinary constants as image pointers whenever the + * code buffer happens to land near the library, which is the common case on + * Android. */ +bool brov_image_range(uint64_t *lo, uint64_t *hi) +{ + uintptr_t anchor = (uintptr_t)&brov_image_anchor_obj; + +#ifdef _WIN32 + MEMORY_BASIC_INFORMATION info; + + if (VirtualQuery((LPCVOID)anchor, &info, sizeof(info)) == sizeof(info) && info.AllocationBase) { + const uint8_t *base = (const uint8_t *)info.AllocationBase; + const IMAGE_DOS_HEADER *dos = (const IMAGE_DOS_HEADER *)base; + + if (dos->e_magic == IMAGE_DOS_SIGNATURE) { + const IMAGE_NT_HEADERS *nt = (const IMAGE_NT_HEADERS *)(base + dos->e_lfanew); + if (nt->Signature == IMAGE_NT_SIGNATURE && nt->OptionalHeader.SizeOfImage) { + *lo = (uint64_t)(uintptr_t)base; + *hi = *lo + nt->OptionalHeader.SizeOfImage; + return true; + } + } + } + return false; +#else + FILE *maps = fopen("/proc/self/maps", "r"); + char line[512]; + char owner[256]; + bool found = false; + + if (!maps) { + return false; + } + + owner[0] = 0; + while (fgets(line, sizeof(line), maps)) { + unsigned long long start, end; + char path[256]; + int fields; + + path[0] = 0; + fields = sscanf(line, "%llx-%llx %*s %*s %*s %*s %255s", &start, &end, path); + if (fields < 2) { + continue; + } + + if (!found) { + if (anchor < start || anchor >= end) { + continue; + } + /* Anonymous mapping: nothing to extend over, take this range alone. */ + if (!path[0]) { + *lo = start; + *hi = end; + fclose(maps); + return true; + } + snprintf(owner, sizeof(owner), "%s", path); + *lo = start; + *hi = end; + found = true; + continue; + } + + /* The library spans several segments; keep extending while they belong to it. */ + if (path[0] && strcmp(path, owner) == 0 && start <= *hi) { + *hi = end; + } else if (start > *hi) { + break; + } + } + + fclose(maps); + + if (found) { + /* The first segment may not be the lowest one, so sweep again for the start. */ + maps = fopen("/proc/self/maps", "r"); + if (maps) { + while (fgets(line, sizeof(line), maps)) { + unsigned long long start, end; + char path[256]; + + path[0] = 0; + if (sscanf(line, "%llx-%llx %*s %*s %*s %*s %255s", &start, &end, path) >= 3 && + strcmp(path, owner) == 0 && start < *lo) { + *lo = start; + } + } + fclose(maps); + } + } + + return found; +#endif +} + +uint64_t brov_hash_bytes(const void *data, size_t len, uint64_t seed) +{ + const uint8_t *p = (const uint8_t *)data; + uint64_t h = seed ^ ((uint64_t)len * 0x9e3779b97f4a7c15ULL); + uint64_t k; + + while (len >= 8) { + memcpy(&k, p, 8); + k *= 0xff51afd7ed558ccdULL; + k ^= k >> 33; + h ^= k; + h *= 0xc4ceb9fe1a85ec53ULL; + h = (h << 31) | (h >> 33); + p += 8; + len -= 8; + } + if (len) { + k = 0; + memcpy(&k, p, len); + k *= 0xff51afd7ed558ccdULL; + h ^= k; + h *= 0xc4ceb9fe1a85ec53ULL; + } + + h ^= h >> 33; + h *= 0xff51afd7ed558ccdULL; + h ^= h >> 33; + return h; +} + +bool brov_reservation(uint64_t *base, uint64_t *size, uint32_t *slot_count) +{ + if (!g_brov.active) { + return false; + } + if (base) { + *base = (uint64_t)(uintptr_t)g_brov.base; + } + if (size) { + *size = g_brov.size; + } + if (slot_count) { + *slot_count = g_brov.slot_count; + } + return true; +} + +bool brov_cache_requested(void) +{ + return g_brov.active && g_brov.cache_requested; +} + +bool brov_strict_audit(void) +{ + return g_brov.strict_audit; +} + +uint64_t brov_arena_offset(const void *p) +{ + if (!g_brov.active || (const uint8_t *)p < g_brov.arena || + (const uint8_t *)p >= g_brov.arena + BROV_ARENA_SIZE) { + return (uint64_t)-1; + } + return (uint64_t)((const uint8_t *)p - g_brov.base); +} + +uint64_t brov_arena_used(void) +{ + return (uint64_t)g_brov.arena_used; +} + +static void *brov_arena_alloc(size_t size) +{ + size_t aligned = (size + 63u) & ~(size_t)63u; + void *p; + + if (!g_brov.active || g_brov.arena_used + aligned > BROV_ARENA_SIZE) { + return NULL; + } + p = g_brov.arena + g_brov.arena_used; + g_brov.arena_used += aligned; + memset(p, 0, size); + return p; +} + +/* The uc struct is baked into generated code by tcg_const_ptr(uc), so it has to + * land at the same address on every run for a restored cache to be valid. */ +void *brov_alloc_uc(size_t size) +{ + void *p = brov_arena_alloc(size); + return p ? p : calloc(1, size); +} + +void brov_free_uc(void *p) +{ + if (g_brov.active && (uint8_t *)p >= g_brov.arena && + (uint8_t *)p < g_brov.arena + BROV_ARENA_SIZE) { + return; + } + free(p); +} + +void *brov_alloc_arena(size_t size) +{ + void *p = brov_arena_alloc(size); + return p ? p : g_malloc0(size); +} + +UNICORN_EXPORT +uc_err brov_abi_version(uint32_t *abi) +{ + if (!abi) { + return UC_ERR_ARG; + } + *abi = BROV_ABI_VERSION; + return UC_ERR_OK; +} + +UNICORN_EXPORT +uc_err brov_configure(const brov_config_t *cfg) +{ + uint64_t size; + uint32_t slots; + void *got = NULL; + + if (!cfg || cfg->struct_size != sizeof(brov_config_t)) { + return UC_ERR_ARG; + } + if (g_brov.active) { + return UC_ERR_OK; + } + + slots = cfg->slot_count ? cfg->slot_count : BROV_DEFAULT_SLOTS; + if (slots > BROV_MAX_SLOTS) { + return UC_ERR_ARG; + } + + size = cfg->reserve_size ? cfg->reserve_size + : (BROV_RESERVE_HEADER_SIZE + (1024ull * 1024ull * 1024ull)); + size = (size + 0xffffull) & ~0xffffull; + if (size <= BROV_RESERVE_HEADER_SIZE) { + return UC_ERR_ARG; + } + + if (!brov_reserve_somewhere(cfg->reserve_base, size, &got)) { + return UC_ERR_NOMEM; + } + + if (!brov_os_commit_rw(got, BROV_RESERVE_HEADER_SIZE)) { + brov_os_release(got, size); + return UC_ERR_NOMEM; + } + memset(got, 0, BROV_RESERVE_HEADER_SIZE); + + g_brov.base = (uint8_t *)got; + g_brov.size = size; + g_brov.slot_count = slots; + g_brov.arena = g_brov.base + BROV_SLOT_AREA_SIZE; + g_brov.arena_used = 0; + g_brov.cache_requested = (cfg->flags & BROV_CFG_ENABLE_CACHE) != 0; + g_brov.strict_audit = (cfg->flags & BROV_CFG_STRICT_AUDIT) != 0; + g_brov.active = true; + return UC_ERR_OK; +} + +UNICORN_EXPORT +uc_err brov_reservation_info(uint64_t *base, uint64_t *size) +{ + if (!brov_reservation(base, size, NULL)) { + return UC_ERR_RESOURCE; + } + return UC_ERR_OK; +} + +/* Lets the caller learn which base a blob needs without duplicating the header + * layout outside this file. */ +UNICORN_EXPORT +uc_err brov_blob_reservation(const void *blob, size_t len, uint64_t *base, uint64_t *size) +{ + const brov_blob_header_t *h = (const brov_blob_header_t *)blob; + + if (!blob || len < sizeof(brov_blob_header_t) || !base || !size) { + return UC_ERR_ARG; + } + if (h->magic != BROV_BLOB_MAGIC || h->abi != BROV_ABI_VERSION) { + return UC_ERR_ARG; + } + *base = h->reservation_base; + *size = h->reservation_size; + return UC_ERR_OK; +} + +UNICORN_EXPORT +uc_err brov_last_reason(uc_engine *uc, uint32_t *reason) +{ + if (!uc || !reason) { + return UC_ERR_ARG; + } + *reason = uc->brov_last_reason; + return UC_ERR_OK; +} + +UNICORN_EXPORT +uc_err brov_cc_info(uc_engine *uc, brov_cc_info_t *out) +{ + uc_err err; + + if (!uc || !out || out->struct_size != sizeof(brov_cc_info_t)) { + return UC_ERR_ARG; + } + + UC_INIT(uc); + err = uc->brov.info ? (uc_err)uc->brov.info(uc, out) : UC_ERR_RESOURCE; + restore_jit_state(uc); + return err; +} + +UNICORN_EXPORT +uc_err brov_cc_validate(uc_engine *uc, brov_audit_result_t *out) +{ + uc_err err; + + if (!uc || !out || out->struct_size != sizeof(brov_audit_result_t)) { + return UC_ERR_ARG; + } + + UC_INIT(uc); + err = uc->brov.audit ? (uc_err)uc->brov.audit(uc, out) : UC_ERR_RESOURCE; + restore_jit_state(uc); + return err; +} + +UNICORN_EXPORT +uc_err brov_cc_save(uc_engine *uc, void **blob, size_t *len) +{ + uc_err err; + + if (!uc || !blob || !len) { + return UC_ERR_ARG; + } + + UC_INIT(uc); + err = uc->brov.save ? (uc_err)uc->brov.save(uc, blob, len) : UC_ERR_RESOURCE; + restore_jit_state(uc); + return err; +} + +UNICORN_EXPORT +uc_err brov_cc_load(uc_engine *uc, const void *blob, size_t len) +{ + uc_err err; + + if (!uc || !blob) { + return UC_ERR_ARG; + } + + UC_INIT(uc); + err = uc->brov.load ? (uc_err)uc->brov.load(uc, blob, len) : UC_ERR_RESOURCE; + restore_jit_state(uc); + return err; +} + +/* Retries blocks the load could not verify because their pages were not mapped + * yet. Cheap and idempotent; call it periodically while remaining is non-zero. */ +UNICORN_EXPORT +uc_err brov_cc_resolve(uc_engine *uc, uint32_t *resolved, uint32_t *remaining) +{ + uc_err err; + + if (!uc) { + return UC_ERR_HANDLE; + } + + UC_INIT(uc); + err = uc->brov.resolve ? (uc_err)uc->brov.resolve(uc, resolved, remaining) : UC_ERR_RESOURCE; + restore_jit_state(uc); + return err; +} + +UNICORN_EXPORT +uc_err brov_cc_free(void *blob) +{ + free(blob); + return UC_ERR_OK; +} + +UNICORN_EXPORT +uc_err brov_reg_ptr(uc_engine *uc, int regid, void **ptr, size_t *size, uint32_t *flags) +{ + uc_err err; + + if (!uc || !ptr || !size || !flags) { + return UC_ERR_ARG; + } + + UC_INIT(uc); + err = uc->brov.reg_ptr ? (uc_err)uc->brov.reg_ptr(uc, regid, ptr, size, flags) : UC_ERR_RESOURCE; + restore_jit_state(uc); + return err; +} diff --git a/Brovan/native/unicorn/brovan_uc_tcg.inc.c b/Brovan/native/unicorn/brovan_uc_tcg.inc.c new file mode 100644 index 0000000..4b15ae0 --- /dev/null +++ b/Brovan/native/unicorn/brovan_uc_tcg.inc.c @@ -0,0 +1,1232 @@ +/* Appended to qemu/accel/tcg/translate-all.c, which is compiled once per target + * and is the only place TranslationBlock, TCGContext and this file's page-list + * statics are all in scope. Everything here is static; uc->brov is the only way + * in from outside. */ + +#if defined(TARGET_I386) +#include "unicorn/x86.h" +#define BROV_TARGET_SUPPORTED 1 +#elif defined(TARGET_AARCH64) +#include "unicorn/arm64.h" +/* translate-a64.c bakes ARMCPRegInfo heap pointers into generated code, which + * nothing here pins; the audit would reject every save anyway. */ +#define BROV_TARGET_SUPPORTED 0 +#else +#define BROV_TARGET_SUPPORTED 0 +#endif + +/* TARGET_PAGE_SIZE is a runtime value on some targets, so this cannot be sized + * from it. A TB never covers more than one page. */ +#define BROV_MAX_TB_SRC 16384 + +#define BROV_TB_DROPPED 0 +#define BROV_TB_READY 1 +#define BROV_TB_PENDING 2 + +static bool brov_owns_buffer; + +/* Opt-in tracing of why blocks were kept, dropped or deferred. Resolved once: + * some of the callers below are per-block. */ +static bool brov_dump(void) +{ + static int enabled = -1; + + if (enabled < 0) { + enabled = getenv("BROVAN_JIT_AUDIT_DUMP") != NULL; + } + return enabled != 0; +} + +static bool brov_owns_code_gen_buffer(struct uc_struct *uc) +{ + (void)uc; + return brov_owns_buffer; +} + +static bool brov_try_alloc_code_gen_buffer(struct uc_struct *uc, size_t tb_size) +{ + TCGContext *tcg_ctx = uc->tcg_ctx; + uint64_t base = 0, size = 0; + uint32_t slots = 0, map_entries; + uint8_t *code; + size_t code_size, want; + + if (!brov_reservation(&base, &size, &slots)) { + return false; + } + + code = (uint8_t *)(uintptr_t)base + BROV_RESERVE_HEADER_SIZE; + code_size = (size_t)(size - BROV_RESERVE_HEADER_SIZE); + want = size_code_gen_buffer(tb_size); + if (want < code_size) { + code_size = want; + } + +#ifdef _WIN32 + /* alloc_code_gen_buffer() is also what installs the vectored handler that + * commits code pages on first touch; the buffer it returns is discarded and + * the handler then bounds-checks the initial_buffer set below. */ + { + void *scratch; + tcg_ctx->code_gen_buffer_size = code_size; + scratch = alloc_code_gen_buffer(uc); + if (!scratch) { + return false; + } + VirtualFree(scratch, 0, MEM_RELEASE); + } +#endif + + if (!brov_commit_rwx(code, code_size)) { + return false; + } + + map_entries = 16; + while (map_entries < slots * 2u) { + map_entries <<= 1; + } + + tcg_ctx->brov_slots = (void **)(uintptr_t)base; + tcg_ctx->brov_slot_map = g_malloc0(map_entries * sizeof(uint32_t)); + tcg_ctx->brov_slot_map_mask = map_entries - 1; + tcg_ctx->brov_slot_count = slots; + tcg_ctx->brov_slots_used = 0; + tcg_ctx->brov_slots_overflowed = 0; + + tcg_ctx->code_gen_buffer_size = code_size; + tcg_ctx->code_gen_buffer = code; + tcg_ctx->initial_buffer = code; + tcg_ctx->initial_buffer_size = code_size; + uc->tcg_buffer_size = (uint32_t)code_size; + brov_owns_buffer = true; + return true; +} + +static uint64_t brov_layout_fingerprint(void) +{ + uint64_t v[24]; + int n = 0; + + v[n++] = sizeof(TranslationBlock); + v[n++] = offsetof(TranslationBlock, pc); + v[n++] = offsetof(TranslationBlock, cs_base); + v[n++] = offsetof(TranslationBlock, flags); + v[n++] = offsetof(TranslationBlock, size); + v[n++] = offsetof(TranslationBlock, cflags); + v[n++] = offsetof(TranslationBlock, tc); + v[n++] = offsetof(TranslationBlock, page_next); + v[n++] = offsetof(TranslationBlock, page_addr); + v[n++] = offsetof(TranslationBlock, jmp_reset_offset); + v[n++] = offsetof(TranslationBlock, jmp_target_arg); + v[n++] = offsetof(TranslationBlock, jmp_list_head); + v[n++] = offsetof(TranslationBlock, jmp_dest); + v[n++] = offsetof(TranslationBlock, hash); + v[n++] = sizeof(TCGContext); + v[n++] = offsetof(TCGContext, code_gen_buffer); + v[n++] = offsetof(TCGContext, code_gen_ptr); + v[n++] = offsetof(TCGContext, brov_slots); + v[n++] = sizeof(struct uc_struct); + v[n++] = offsetof(struct uc_struct, brov); + v[n++] = sizeof(CPUArchState); + v[n++] = TARGET_LONG_BITS; + v[n++] = BROV_ABI_VERSION; + + return brov_hash_bytes(v, (size_t)n * sizeof(uint64_t), 0x62726f76616eULL); +} + +static uint64_t brov_host_fingerprint(struct uc_struct *uc) +{ + uint64_t v[6]; + int n = 0; + + v[n++] = sizeof(void *); + v[n++] = uc->qemu_real_host_page_size; + v[n++] = (uint64_t)TARGET_PAGE_SIZE; + v[n++] = (uint64_t)uc->qemu_icache_linesize; + v[n++] = TCG_TARGET_REG_BITS; + v[n++] = TCG_TARGET_NB_REGS; + + return brov_hash_bytes(v, (size_t)n * sizeof(uint64_t), 0x686f7374ULL); +} + +static uint64_t brov_prologue_bytes(struct uc_struct *uc) +{ + TCGContext *s = uc->tcg_ctx; + return (uint64_t)((uint8_t *)s->code_gen_buffer - (uint8_t *)s->initial_buffer); +} + +/* Reads guest memory without faulting: a page the guest has since unmapped has + * to read as a miss rather than crash the host mid-load. */ +static bool brov_read_guest(struct uc_struct *uc, uint64_t addr, void *dst, size_t len) +{ + uint8_t *out = (uint8_t *)dst; + + while (len) { + uint64_t page_end = (addr | (uint64_t)(TARGET_PAGE_SIZE - 1)) + 1; + size_t chunk = (size_t)(page_end - addr); + + if (chunk > len) { + chunk = len; + } + if (!uc->memory_mapping(uc, addr)) { + return false; + } + if (!uc->read_mem(&uc->address_space_memory, addr, out, (int)chunk)) { + return false; + } + addr += chunk; + out += chunk; + len -= chunk; + } + return true; +} + +/* Hashes the guest instructions the block was translated from, so a reload can + * drop blocks whose source has changed. Addressed through tb->pc rather than + * page_addr[], which holds ram-block offsets and not guest addresses. */ +static bool brov_tb_src_hash(struct uc_struct *uc, TranslationBlock *tb, uint64_t *out) +{ + uint8_t buf[BROV_MAX_TB_SRC]; + size_t total = tb->size; + + if (total == 0 || total > sizeof(buf)) { + return false; + } + if (!brov_read_guest(uc, tb->pc, buf, total)) { + return false; + } + + *out = brov_hash_bytes(buf, total, tb->pc ^ ((uint64_t)tb->flags << 32)); + return true; +} + +typedef struct { + TranslationBlock **tbs; + size_t count; + size_t cap; + bool oom; +} brov_tb_list; + +static gboolean brov_collect_tb(gpointer key, gpointer value, gpointer data) +{ + brov_tb_list *list = (brov_tb_list *)data; + TranslationBlock *tb = (TranslationBlock *)value; + + (void)key; + + if (tb_cflags(tb) & (CF_NOCACHE | CF_INVALID)) { + return FALSE; + } + if (tb->page_addr[0] == (tb_page_addr_t)-1) { + return FALSE; + } + + if (list->count == list->cap) { + size_t cap = list->cap ? list->cap * 2 : 1024; + TranslationBlock **grown = + (TranslationBlock **)realloc(list->tbs, cap * sizeof(*grown)); + if (!grown) { + list->oom = true; + return TRUE; + } + list->tbs = grown; + list->cap = cap; + } + list->tbs[list->count++] = tb; + return FALSE; +} + +/* ---- relocation audit -------------------------------------------------- */ + +/* Any host pointer baked into generated code that is neither pinned in the + * reservation nor routed through the slot table would point at the wrong object + * after a reload. Rather than trust the static enumeration, a save scans the + * emitted bytes for such values and refuses instead of writing a poisoned blob. */ + +typedef struct { + uint64_t addr; + const char *name; +} brov_tracked; + +typedef struct { + brov_tracked *items; + size_t count; + uint64_t lo; + uint64_t hi; + uint64_t image_lo; + uint64_t image_hi; + uint64_t env_lo; + uint64_t env_hi; +} brov_track_set; + +#define BROV_IMAGE_WINDOW (64ull * 1024ull * 1024ull) + +static int brov_tracked_cmp(const void *a, const void *b) +{ + uint64_t x = ((const brov_tracked *)a)->addr; + uint64_t y = ((const brov_tracked *)b)->addr; + return x < y ? -1 : (x > y ? 1 : 0); +} + +static void brov_track_add(brov_track_set *set, size_t cap, const void *p, const char *name) +{ + uint64_t v = (uint64_t)(uintptr_t)p; + + if (!v || set->count >= cap) { + return; + } + set->items[set->count].addr = v; + set->items[set->count].name = name; + set->count++; +} + +static bool brov_build_track_set(struct uc_struct *uc, brov_track_set *set) +{ + uint64_t anchor = (uint64_t)brov_image_base(); + size_t cap = 64; + int i; + + for (i = 0; i < UC_HOOK_MAX; i++) { + struct list_item *cur; + for (cur = uc->hook[i].head; cur; cur = cur->next) { + cap += 3; + } + } + + memset(set, 0, sizeof(*set)); + set->items = (brov_tracked *)calloc(cap, sizeof(brov_tracked)); + if (!set->items) { + return false; + } + + brov_track_add(set, cap, uc->cpu, "CPUState"); + brov_track_add(set, cap, uc->cpu ? uc->cpu->env_ptr : NULL, "CPUArchState"); + brov_track_add(set, cap, uc->l1_map, "l1_map"); + brov_track_add(set, cap, uc->tcg_ctx->brov_slot_map, "slot_map"); + + for (i = 0; i < UC_HOOK_MAX; i++) { + struct list_item *cur; + for (cur = uc->hook[i].head; cur; cur = cur->next) { + struct hook *hk = (struct hook *)cur->data; + brov_track_add(set, cap, hk, "hook"); + brov_track_add(set, cap, hk->callback, "hook.callback"); + brov_track_add(set, cap, hk->user_data, "hook.user_data"); + } + } + + qsort(set->items, set->count, sizeof(brov_tracked), brov_tracked_cmp); + + if (!brov_image_range(&set->image_lo, &set->image_hi)) { + set->image_lo = anchor > BROV_IMAGE_WINDOW ? anchor - BROV_IMAGE_WINDOW : 0; + set->image_hi = anchor + BROV_IMAGE_WINDOW; + } + + set->env_lo = 0; + set->env_hi = 0; + if (brov_strict_audit() && uc->cpu && uc->cpu->env_ptr) { + set->env_lo = (uint64_t)(uintptr_t)uc->cpu->env_ptr; + set->env_hi = set->env_lo + sizeof(CPUArchState); + } + + set->lo = set->image_lo; + set->hi = set->image_hi; + if (set->count) { + if (set->items[0].addr < set->lo) { + set->lo = set->items[0].addr; + } + if (set->items[set->count - 1].addr > set->hi) { + set->hi = set->items[set->count - 1].addr; + } + } + if (set->env_hi) { + if (set->env_lo < set->lo) { + set->lo = set->env_lo; + } + if (set->env_hi > set->hi) { + set->hi = set->env_hi; + } + } + return true; +} + +static uint64_t brov_hook_tag(const struct hook *hk) +{ + uint64_t v[4]; + + v[0] = (uint64_t)(uint32_t)hk->type; + v[1] = (uint64_t)(uint32_t)hk->insn; + v[2] = hk->begin; + v[3] = hk->end; + return brov_hash_bytes(v, sizeof(v), 0x686f6f6bULL); +} + +/* Describes a slot in terms that survive a restart. */ +static bool brov_classify_slot(struct uc_struct *uc, uint64_t value, brov_slot_record_t *out) +{ + uint64_t anchor = (uint64_t)brov_image_base(); + uint64_t delta = value > anchor ? value - anchor : anchor - value; + int i; + + memset(out, 0, sizeof(*out)); + + if (delta < BROV_IMAGE_WINDOW) { + out->kind = BROV_SLOT_IMAGE; + out->detail = value - anchor; + return true; + } + + for (i = 0; i < UC_HOOK_MAX; i++) { + struct list_item *cur; + for (cur = uc->hook[i].head; cur; cur = cur->next) { + struct hook *hk = (struct hook *)cur->data; + if ((uint64_t)(uintptr_t)hk->callback == value) { + out->kind = BROV_SLOT_HOOK; + out->hook_idx = (uint32_t)i; + out->tag = brov_hook_tag(hk); + return true; + } + } + } + + return false; +} + +static bool brov_resolve_slot(struct uc_struct *uc, const brov_slot_record_t *rec, void **out) +{ + struct list_item *cur; + void *found = NULL; + unsigned matches = 0; + + if (rec->kind == BROV_SLOT_IMAGE) { + *out = (void *)(uintptr_t)(brov_image_base() + rec->detail); + return true; + } + if (rec->kind != BROV_SLOT_HOOK || rec->hook_idx >= UC_HOOK_MAX) { + return false; + } + + /* Matched on identity rather than position: hooks can be registered in a + * different order, but two hooks identical in type, instruction and range + * would be ambiguous and are refused. */ + for (cur = uc->hook[rec->hook_idx].head; cur; cur = cur->next) { + struct hook *hk = (struct hook *)cur->data; + if (brov_hook_tag(hk) == rec->tag) { + found = hk->callback; + matches++; + } + } + + if (matches != 1 || !found) { + return false; + } + *out = found; + return true; +} + +static bool brov_is_slot_value(struct uc_struct *uc, uint64_t v) +{ + uint32_t i; + for (i = 0; i < uc->tcg_ctx->brov_slots_used; i++) { + if ((uint64_t)(uintptr_t)uc->tcg_ctx->brov_slots[i] == v) { + return true; + } + } + return false; +} + +static int brov_audit_impl(struct uc_struct *uc, brov_audit_result_t *out) +{ + TCGContext *s = uc->tcg_ctx; + const uint8_t *code = (const uint8_t *)s->code_gen_buffer; + size_t used = (size_t)((uint8_t *)s->code_gen_ptr - (uint8_t *)s->code_gen_buffer); + brov_track_set set; + size_t i; + bool dump = brov_dump(); + + out->hit_count = 0; + out->first_offset = 0; + out->first_value = 0; + memset(out->first_object, 0, sizeof(out->first_object)); + + if (used < 8) { + return UC_ERR_OK; + } + if (!brov_build_track_set(uc, &set)) { + return UC_ERR_NOMEM; + } + + for (i = 0; i + 8 <= used; i++) { + const char *name = NULL; + size_t lo, hi; + uint64_t v; + + memcpy(&v, code + i, 8); + if (v < set.lo || v > set.hi) { + continue; + } + + lo = 0; + hi = set.count; + while (lo < hi) { + size_t mid = lo + (hi - lo) / 2; + if (set.items[mid].addr < v) { + lo = mid + 1; + } else { + hi = mid; + } + } + + if (lo < set.count && set.items[lo].addr == v) { + name = set.items[lo].name; + } else if (v >= set.image_lo && v <= set.image_hi && !brov_is_slot_value(uc, v)) { + name = "unicorn image"; + } else if (set.env_hi && v > set.env_lo && v < set.env_hi) { + name = "CPUArchState interior"; + } + + if (name) { + if (out->hit_count == 0) { + size_t n = strlen(name); + if (n >= sizeof(out->first_object)) { + n = sizeof(out->first_object) - 1; + } + memcpy(out->first_object, name, n); + out->first_offset = (uint64_t)i; + out->first_value = v; + } + if (dump && out->hit_count < 32) { + size_t ctx = i > 8 ? i - 8 : 0; + int b; + fprintf(stderr, "[brov-audit] +0x%08zx %016llx (%s) image%+lld ctx:", + i, (unsigned long long)v, name, + (long long)(v - (uint64_t)brov_image_base())); + for (b = 0; b < 24 && ctx + b < used; b++) { + fprintf(stderr, "%s%02x", ctx + b == i ? " |" : " ", code[ctx + b]); + } + fprintf(stderr, "\n"); + } + out->hit_count++; + } + } + + free(set.items); + return UC_ERR_OK; +} + +/* ---- save -------------------------------------------------------------- */ + +static int brov_save_impl(struct uc_struct *uc, void **blob_out, size_t *len_out) +{ + TCGContext *s = uc->tcg_ctx; + brov_blob_header_t hdr; + brov_audit_result_t audit; + brov_tb_list list; + brov_tb_record_t *records = NULL; + uint64_t base = 0, size = 0; + uint32_t slots = 0; + size_t used, slot_bytes, tb_bytes, total, i, kept = 0; + uint8_t *blob, *p; + brov_slot_record_t *slot_section; + bool dump = brov_dump(); + int err; + + *blob_out = NULL; + *len_out = 0; + + if (!BROV_TARGET_SUPPORTED) { + uc->brov_last_reason = BROV_REASON_UNSUPPORTED_TARGET; + return UC_ERR_ARG; + } + if (!brov_reservation(&base, &size, &slots) || !brov_owns_buffer || + brov_arena_offset(uc) == (uint64_t)-1 || brov_arena_offset(s) == (uint64_t)-1) { + uc->brov_last_reason = BROV_REASON_NO_RESERVATION; + return UC_ERR_RESOURCE; + } + if (s->brov_slots_overflowed) { + uc->brov_last_reason = BROV_REASON_SLOT_OVERFLOW; + return UC_ERR_RESOURCE; + } + + used = (size_t)((uint8_t *)s->code_gen_ptr - (uint8_t *)s->code_gen_buffer); + if (used == 0) { + uc->brov_last_reason = BROV_REASON_EMPTY; + return UC_ERR_ARG; + } + + audit.struct_size = sizeof(audit); + err = brov_audit_impl(uc, &audit); + if (err != UC_ERR_OK) { + return err; + } + if (audit.hit_count) { + uc->brov_last_reason = BROV_REASON_AUDIT; + return UC_ERR_RESOURCE; + } + + memset(&list, 0, sizeof(list)); + tcg_tb_foreach(s, brov_collect_tb, &list); + if (list.oom) { + free(list.tbs); + return UC_ERR_NOMEM; + } + if (list.count == 0) { + free(list.tbs); + uc->brov_last_reason = BROV_REASON_EMPTY; + return UC_ERR_ARG; + } + + records = (brov_tb_record_t *)malloc(list.count * sizeof(*records)); + if (!records) { + free(list.tbs); + return UC_ERR_NOMEM; + } + + for (i = 0; i < list.count; i++) { + TranslationBlock *tb = list.tbs[i]; + uint64_t src; + + if (!brov_tb_src_hash(uc, tb, &src)) { + if (dump && i < 8) { + fprintf(stderr, + "[brov-save] unreadable tb pc=%llx page0=%llx page1=%llx size=%u mapped=%d\n", + (unsigned long long)tb->pc, (unsigned long long)tb->page_addr[0], + (unsigned long long)tb->page_addr[1], (unsigned)tb->size, + uc->memory_mapping(uc, tb->page_addr[0]) != NULL); + } + continue; + } + records[kept].offset = (uint64_t)((uint8_t *)tb - (uint8_t *)s->code_gen_buffer); + records[kept].src_hash = src; + kept++; + } + if (dump) { + fprintf(stderr, "[brov-save] used=%zu tbs=%zu kept=%zu\n", used, list.count, kept); + } + free(list.tbs); + + if (kept == 0) { + free(records); + uc->brov_last_reason = BROV_REASON_EMPTY; + return UC_ERR_ARG; + } + + slot_bytes = (size_t)s->brov_slots_used * sizeof(brov_slot_record_t); + tb_bytes = kept * sizeof(brov_tb_record_t); + total = sizeof(hdr) + slot_bytes + tb_bytes + used; + + blob = (uint8_t *)malloc(total); + if (!blob) { + free(records); + return UC_ERR_NOMEM; + } + + p = blob + sizeof(hdr); + slot_section = (brov_slot_record_t *)p; + for (i = 0; i < s->brov_slots_used; i++) { + if (!brov_classify_slot(uc, (uint64_t)(uintptr_t)s->brov_slots[i], &slot_section[i])) { + free(blob); + free(records); + uc->brov_last_reason = BROV_REASON_SLOT_UNRESOLVED; + return UC_ERR_RESOURCE; + } + } + p += slot_bytes; + memcpy(p, records, tb_bytes); + p += tb_bytes; + memcpy(p, s->code_gen_buffer, used); + free(records); + + memset(&hdr, 0, sizeof(hdr)); + hdr.magic = BROV_BLOB_MAGIC; + hdr.abi = BROV_ABI_VERSION; + hdr.header_bytes = (uint32_t)sizeof(hdr); + hdr.layout_fingerprint = brov_layout_fingerprint(); + hdr.host_fingerprint = brov_host_fingerprint(uc); + hdr.target_arch = (uint32_t)uc->arch; + hdr.target_mode = (uint32_t)uc->mode; + hdr.reservation_base = base; + hdr.reservation_size = size; + hdr.code_gen_buffer_off = + (uint64_t)((uint8_t *)s->code_gen_buffer - (uint8_t *)(uintptr_t)base); + hdr.code_gen_buffer_size = s->code_gen_buffer_size; + hdr.code_gen_used = used; + hdr.prologue_bytes = brov_prologue_bytes(uc); + hdr.prologue_hash = brov_hash_bytes(s->initial_buffer, (size_t)hdr.prologue_bytes, 0); + hdr.uc_off = brov_arena_offset(uc); + hdr.tcg_ctx_off = brov_arena_offset(s); + hdr.arena_used = brov_arena_used(); + hdr.region_current = s->region.current; + hdr.region_agg_size_full = s->region.agg_size_full; + hdr.slot_count = s->brov_slot_count; + hdr.slots_used = s->brov_slots_used; + hdr.tb_count = kept; + hdr.code_hash = brov_hash_bytes(s->code_gen_buffer, used, 0); + memcpy(blob, &hdr, sizeof(hdr)); + + s->brov_save_count++; + uc->brov_last_reason = BROV_OK; + *blob_out = blob; + *len_out = total; + return UC_ERR_OK; +} + +/* ---- load -------------------------------------------------------------- */ + +/* page_addr[] and hash hold ram-block offsets from the run that generated the + * block. Nothing guarantees the guest lands in the same ram offsets this time, + * and a block filed under a stale page would be missed by the invalidation that + * self-modifying code relies on, so both are recomputed from the live mapping. */ +static bool brov_retarget_tb(struct uc_struct *uc, TranslationBlock *tb) +{ + CPUArchState *env = (CPUArchState *)uc->cpu->env_ptr; + tb_page_addr_t phys_pc, phys_page2 = -1; + target_ulong virt_page2; + bool ok = false; + + uc->nested_level++; + if (sigsetjmp(uc->jmp_bufs[uc->nested_level - 1], 0) != 0) { + uc->nested_level--; + return false; + } + + phys_pc = get_page_addr_code(env, tb->pc); + if (phys_pc != (tb_page_addr_t)-1) { + virt_page2 = (tb->pc + tb->size - 1) & TARGET_PAGE_MASK; + if ((tb->pc & TARGET_PAGE_MASK) != virt_page2) { + phys_page2 = get_page_addr_code(env, virt_page2); + } + ok = phys_page2 != (tb_page_addr_t)-1 || (tb->pc & TARGET_PAGE_MASK) == virt_page2; + } + uc->nested_level--; + + if (!ok) { + return false; + } + + tb->page_addr[0] = phys_pc & TARGET_PAGE_MASK; + tb->page_addr[1] = phys_page2; + tb->hash = tb_hash_func(phys_pc, tb->pc, tb->flags, tb->cflags & CF_HASH_MASK, + tb->trace_vcpu_dstate); + return true; +} + +static bool brov_relink_tb(struct uc_struct *uc, TranslationBlock *tb) +{ + PageDesc *p = NULL, *p2 = NULL; + void *existing = NULL; + tb_page_addr_t phys1, phys2; + + if (!brov_retarget_tb(uc, tb)) { + return false; + } + + phys1 = tb->page_addr[0]; + phys2 = tb->page_addr[1]; + + tb->jmp_list_head = (uintptr_t)NULL; + tb->jmp_list_next[0] = (uintptr_t)NULL; + tb->jmp_list_next[1] = (uintptr_t)NULL; + tb->jmp_dest[0] = (uintptr_t)NULL; + tb->jmp_dest[1] = (uintptr_t)NULL; + tb->orig_tb = NULL; + tb->cflags &= ~CF_INVALID; + + page_lock_pair(uc, &p, phys1, &p2, phys2, 1); + if (!p) { + return false; + } + tb_page_add(uc, p, tb, 0, phys1); + if (p2) { + tb_page_add(uc, p2, tb, 1, phys2); + } else { + tb->page_addr[1] = -1; + } + + qht_insert(uc, &uc->tcg_ctx->tb_ctx.htable, tb, tb->hash, &existing); + if (existing) { + tb_page_remove(p, tb); + invalidate_page_bitmap(p); + if (p2) { + tb_page_remove(p2, tb); + invalidate_page_bitmap(p2); + } + if (p2 && p2 != p) { + page_unlock(p2); + } + page_unlock(p); + return false; + } + + if (p2 && p2 != p) { + page_unlock(p2); + } + page_unlock(p); + + tcg_tb_insert(uc->tcg_ctx, tb); + + /* Restored blocks start unchained. Anything still sitting in the buffer that + * did not get registered - a block that failed verification, or one the save + * skipped - must not be reachable through a stale direct jump. */ + if (tb->jmp_reset_offset[0] != TB_JMP_RESET_OFFSET_INVALID) { + tb_reset_jump(tb, 0); + } + if (tb->jmp_reset_offset[1] != TB_JMP_RESET_OFFSET_INVALID) { + tb_reset_jump(tb, 1); + } + return true; +} + +static int brov_load_impl(struct uc_struct *uc, const void *blob, size_t len) +{ + TCGContext *s = uc->tcg_ctx; + const brov_blob_header_t *hdr = (const brov_blob_header_t *)blob; + const brov_slot_record_t *slot_section; + const brov_tb_record_t *tb_section; + const uint8_t *code_section; + uint64_t base = 0, size = 0; + uint32_t slots = 0, reason = BROV_REASON_MAGIC; + uint8_t *usable = NULL; + size_t expect, i, stale = 0, live = 0, unreadable = 0, changed = 0, live_bytes = 0; + + if (!BROV_TARGET_SUPPORTED) { + uc->brov_last_reason = BROV_REASON_UNSUPPORTED_TARGET; + return UC_ERR_ARG; + } + if (len < sizeof(*hdr)) { + uc->brov_last_reason = BROV_REASON_TRUNCATED; + return UC_ERR_ARG; + } + if (hdr->magic != BROV_BLOB_MAGIC) { + uc->brov_last_reason = BROV_REASON_MAGIC; + return UC_ERR_ARG; + } + if (hdr->abi != BROV_ABI_VERSION || hdr->header_bytes != sizeof(*hdr)) { + uc->brov_last_reason = BROV_REASON_ABI; + return UC_ERR_ARG; + } + + expect = sizeof(*hdr) + (size_t)hdr->slots_used * sizeof(brov_slot_record_t) + + (size_t)hdr->tb_count * sizeof(brov_tb_record_t) + (size_t)hdr->code_gen_used; + if (len < expect || hdr->tb_count == 0) { + uc->brov_last_reason = hdr->tb_count ? BROV_REASON_TRUNCATED : BROV_REASON_EMPTY; + return UC_ERR_ARG; + } + + if (!brov_reservation(&base, &size, &slots) || !brov_owns_buffer || + brov_arena_offset(uc) == (uint64_t)-1) { + reason = BROV_REASON_NO_RESERVATION; + goto reject; + } + if (hdr->layout_fingerprint != brov_layout_fingerprint()) { + reason = BROV_REASON_LAYOUT; + goto reject; + } + if (hdr->host_fingerprint != brov_host_fingerprint(uc)) { + reason = BROV_REASON_HOST; + goto reject; + } + if (hdr->target_arch != (uint32_t)uc->arch || hdr->target_mode != (uint32_t)uc->mode) { + reason = BROV_REASON_TARGET; + goto reject; + } + if (hdr->reservation_base != base || hdr->reservation_size != size || + hdr->slot_count != s->brov_slot_count || hdr->slots_used > s->brov_slot_count) { + reason = BROV_REASON_BASE_MISMATCH; + goto reject; + } + if (hdr->code_gen_buffer_off != + (uint64_t)((uint8_t *)s->code_gen_buffer - (uint8_t *)(uintptr_t)base) || + hdr->code_gen_buffer_size != s->code_gen_buffer_size || + hdr->code_gen_used > s->code_gen_buffer_size) { + reason = BROV_REASON_BASE_MISMATCH; + goto reject; + } + if (hdr->prologue_bytes != brov_prologue_bytes(uc) || + hdr->prologue_hash != + brov_hash_bytes(s->initial_buffer, (size_t)hdr->prologue_bytes, 0)) { + /* A different host CPU generates a different prologue, which shifts every + * block address in the buffer. */ + reason = BROV_REASON_PROLOGUE; + goto reject; + } + if (hdr->uc_off != brov_arena_offset(uc) || hdr->tcg_ctx_off != brov_arena_offset(s)) { + reason = BROV_REASON_ARENA_MISMATCH; + goto reject; + } + + slot_section = (const brov_slot_record_t *)((const uint8_t *)blob + sizeof(*hdr)); + tb_section = (const brov_tb_record_t *)(slot_section + hdr->slots_used); + code_section = (const uint8_t *)(tb_section + hdr->tb_count); + + if (hdr->code_hash != brov_hash_bytes(code_section, (size_t)hdr->code_gen_used, 0)) { + reason = BROV_REASON_CODE_HASH; + goto reject; + } + + usable = (uint8_t *)calloc((size_t)hdr->tb_count, 1); + if (!usable) { + return UC_ERR_NOMEM; + } + + uc_tb_flush(uc); + + memcpy(s->code_gen_buffer, code_section, (size_t)hdr->code_gen_used); + s->code_gen_ptr = (uint8_t *)s->code_gen_buffer + hdr->code_gen_used; + + memset(s->brov_slot_map, 0, ((size_t)s->brov_slot_map_mask + 1) * sizeof(uint32_t)); + s->brov_slots_used = 0; + s->brov_slots_overflowed = 0; + for (i = 0; i < hdr->slots_used; i++) { + void *fn = NULL; + + if (!brov_resolve_slot(uc, &slot_section[i], &fn)) { + reason = BROV_REASON_SLOT_UNRESOLVED; + goto reject_flush; + } + if (brov_slot_intern(s->brov_slots, s->brov_slot_map, s->brov_slot_map_mask, + &s->brov_slots_used, s->brov_slot_count, fn) != (uint32_t)i) { + reason = BROV_REASON_SLOT_OVERFLOW; + goto reject_flush; + } + } + + { + bool dump = brov_dump(); + + for (i = 0; i < hdr->tb_count; i++) { + TranslationBlock *tb; + uint64_t src; + + if (tb_section[i].offset + sizeof(TranslationBlock) > hdr->code_gen_used) { + stale++; + continue; + } + tb = (TranslationBlock *)((uint8_t *)s->code_gen_buffer + tb_section[i].offset); + if (!brov_tb_src_hash(uc, tb, &src)) { + if (dump && unreadable < 4) { + fprintf(stderr, "[brov-load] unreadable pc=%llx size=%u\n", + (unsigned long long)tb->pc, (unsigned)tb->size); + } + unreadable++; + stale++; + usable[i] = BROV_TB_PENDING; + continue; + } + if (src != tb_section[i].src_hash) { + if (dump && changed < 4) { + fprintf(stderr, "[brov-load] changed pc=%llx size=%u\n", + (unsigned long long)tb->pc, (unsigned)tb->size); + } + changed++; + stale++; + continue; + } + usable[i] = BROV_TB_READY; + } + + for (i = 0; i < hdr->tb_count; i++) { + if (usable[i] != BROV_TB_DROPPED && + tb_section[i].offset + sizeof(TranslationBlock) <= hdr->code_gen_used) { + TranslationBlock *tb = + (TranslationBlock *)((uint8_t *)s->code_gen_buffer + tb_section[i].offset); + live_bytes += sizeof(TranslationBlock) + tb->tc.size; + } + } + + if (dump) { + fprintf(stderr, + "[brov-load] tbs=%llu stale=%zu unreadable=%zu changed=%zu live=%zu/%llu\n", + (unsigned long long)hdr->tb_count, stale, unreadable, changed, live_bytes, + (unsigned long long)hdr->code_gen_used); + } + } + + if (live_bytes * 100u < (size_t)hdr->code_gen_used * BROV_MIN_LIVE_PERCENT) { + reason = BROV_REASON_BLOATED; + goto reject_flush; + } + + /* Only blocks whose guest bytes actually differ say the blob is wrong. A page + * the loader has not mapped yet is expected: much of the program is still + * being brought in when execution starts. Those blocks are dropped either + * way, they just do not count as evidence against the whole cache. */ + if (changed * 100u > (uint64_t)hdr->tb_count * BROV_MAX_STALE_PERCENT) { + s->brov_stale_tbs = stale; + s->brov_loaded_tbs = 0; + reason = BROV_REASON_TOO_MANY_STALE; + goto reject_flush; + } + + for (i = 0; i < hdr->tb_count; i++) { + TranslationBlock *tb; + + if (usable[i] != BROV_TB_READY) { + continue; + } + tb = (TranslationBlock *)((uint8_t *)s->code_gen_buffer + tb_section[i].offset); + if (brov_relink_tb(uc, tb)) { + live++; + } + } + + /* Blocks in pages the loader has not reached yet keep their code in the + * buffer; registering them later is what stops the next save from carrying + * both the restored copy and a freshly translated duplicate. */ + free(s->brov_pending); + s->brov_pending = NULL; + s->brov_pending_count = 0; + if (unreadable) { + brov_tb_record_t *pend = (brov_tb_record_t *)malloc(unreadable * sizeof(*pend)); + if (pend) { + size_t n = 0; + for (i = 0; i < hdr->tb_count && n < unreadable; i++) { + if (usable[i] == BROV_TB_PENDING) { + pend[n++] = tb_section[i]; + } + } + s->brov_pending = pend; + s->brov_pending_count = n; + s->brov_pending_flush = s->tb_ctx.tb_flush_count; + } + } + + cpu_tb_jmp_cache_clear(uc->cpu); + free(usable); + + s->brov_load_count++; + s->brov_loaded_tbs = live; + s->brov_stale_tbs = stale; + uc->brov_last_reason = BROV_OK; + return UC_ERR_OK; + +reject_flush: + uc_tb_flush(uc); +reject: + free(usable); + uc->brov_last_reason = reason; + return UC_ERR_ARG; +} + +/* Retries the blocks whose pages were not mapped when the blob was loaded. Their + * code is already sitting in the buffer, so this only has to verify and file + * them; nothing is translated and nothing grows. */ +static int brov_resolve_impl(struct uc_struct *uc, uint32_t *resolved, uint32_t *remaining) +{ + TCGContext *s = uc->tcg_ctx; + brov_tb_record_t *pend = (brov_tb_record_t *)s->brov_pending; + size_t kept = 0, done = 0, i; + bool dump = brov_dump(); + + if (resolved) { + *resolved = 0; + } + if (remaining) { + *remaining = 0; + } + if (!pend || !s->brov_pending_count) { + return UC_ERR_OK; + } + + /* A flush reuses the buffer, so the pending offsets no longer name the + * blocks they were recorded for. */ + if (s->tb_ctx.tb_flush_count != s->brov_pending_flush) { + free(pend); + s->brov_pending = NULL; + s->brov_pending_count = 0; + return UC_ERR_OK; + } + + for (i = 0; i < s->brov_pending_count; i++) { + TranslationBlock *tb = + (TranslationBlock *)((uint8_t *)s->code_gen_buffer + pend[i].offset); + uint64_t src; + + if (!brov_tb_src_hash(uc, tb, &src)) { + pend[kept++] = pend[i]; + continue; + } + if (src != pend[i].src_hash) { + continue; + } + if (brov_relink_tb(uc, tb)) { + done++; + } + } + + if (dump && done) { + fprintf(stderr, "[brov-resolve] resolved=%zu still-pending=%zu\n", done, kept); + } + + s->brov_pending_count = kept; + s->brov_loaded_tbs += done; + if (!kept) { + free(pend); + s->brov_pending = NULL; + } + + if (done) { + cpu_tb_jmp_cache_clear(uc->cpu); + } + if (resolved) { + *resolved = (uint32_t)done; + } + if (remaining) { + *remaining = (uint32_t)kept; + } + return UC_ERR_OK; +} + +/* ---- info / flush / registers ------------------------------------------ */ + +static int brov_info_impl(struct uc_struct *uc, brov_cc_info_t *out) +{ + TCGContext *s = uc->tcg_ctx; + uint64_t base = 0, size = 0; + uint32_t slots = 0; + + brov_reservation(&base, &size, &slots); + + out->last_reason = uc->brov_last_reason; + out->reservation_base = base; + out->reservation_size = size; + out->code_gen_buffer = (uint64_t)(uintptr_t)s->code_gen_buffer; + out->code_gen_buffer_size = s->code_gen_buffer_size; + out->code_gen_used = + (uint64_t)((uint8_t *)s->code_gen_ptr - (uint8_t *)s->code_gen_buffer); + out->tb_count = tcg_nb_tbs(s); + out->flush_count = s->tb_ctx.tb_flush_count; + out->slot_count = s->brov_slot_count; + out->slots_used = s->brov_slots_used; + out->slots_overflowed = s->brov_slots_overflowed; + out->load_count = s->brov_load_count; + out->loaded_tbs = s->brov_loaded_tbs; + out->stale_tbs = s->brov_stale_tbs; + out->save_count = s->brov_save_count; + return UC_ERR_OK; +} + +/* The pointers below stay valid for the lifetime of uc: Unicorn allocates + * CPUState once and never moves it. */ +#if defined(TARGET_I386) +static int brov_reg_ptr_impl(struct uc_struct *uc, int regid, void **ptr, size_t *size, + uint32_t *flags) +{ + CPUX86State *env; + + if (!uc->cpu || !uc->cpu->env_ptr) { + return UC_ERR_HANDLE; + } + /* 16- and 32-bit modes reach the same storage through different rules - + * reg_write() zero-extends EAX there but preserves the upper half in 64-bit + * mode - so only the unambiguous 64-bit ids are handed out. */ + if (!(uc->mode & UC_MODE_64)) { + return UC_ERR_ARG; + } + env = (CPUX86State *)uc->cpu->env_ptr; + *flags = BROV_REG_READABLE | BROV_REG_WRITABLE; + + if (regid >= UC_X86_REG_XMM0 && regid <= UC_X86_REG_XMM31) { + int n = regid - UC_X86_REG_XMM0; + if (n >= (int)ARRAY_SIZE(env->xmm_regs)) { + return UC_ERR_ARG; + } + *ptr = &env->xmm_regs[n]; + if (size) { + *size = 16; + } + return UC_ERR_OK; + } + + switch (regid) { + case UC_X86_REG_RAX: *ptr = &env->regs[R_EAX]; break; + case UC_X86_REG_RCX: *ptr = &env->regs[R_ECX]; break; + case UC_X86_REG_RDX: *ptr = &env->regs[R_EDX]; break; + case UC_X86_REG_RBX: *ptr = &env->regs[R_EBX]; break; + case UC_X86_REG_RSP: *ptr = &env->regs[R_ESP]; break; + case UC_X86_REG_RBP: *ptr = &env->regs[R_EBP]; break; + case UC_X86_REG_RSI: *ptr = &env->regs[R_ESI]; break; + case UC_X86_REG_RDI: *ptr = &env->regs[R_EDI]; break; +#ifdef TARGET_X86_64 + case UC_X86_REG_R8: *ptr = &env->regs[8]; break; + case UC_X86_REG_R9: *ptr = &env->regs[9]; break; + case UC_X86_REG_R10: *ptr = &env->regs[10]; break; + case UC_X86_REG_R11: *ptr = &env->regs[11]; break; + case UC_X86_REG_R12: *ptr = &env->regs[12]; break; + case UC_X86_REG_R13: *ptr = &env->regs[13]; break; + case UC_X86_REG_R14: *ptr = &env->regs[14]; break; + case UC_X86_REG_R15: *ptr = &env->regs[15]; break; +#endif + case UC_X86_REG_RIP: + /* Readable only: uc_reg_write() also sets quit_request and flushes the + * translated blocks, which a bare store would skip. */ + *ptr = &env->eip; + *flags = BROV_REG_READABLE; + break; + case UC_X86_REG_FS_BASE: *ptr = &env->segs[R_FS].base; break; + case UC_X86_REG_GS_BASE: *ptr = &env->segs[R_GS].base; break; + default: + /* EFLAGS is deliberately absent: env->eflags omits the lazily evaluated + * condition codes, so a raw pointer would not hold the architectural + * value that uc_reg_read() computes. */ + return UC_ERR_ARG; + } + + if (size) { + *size = sizeof(target_ulong); + } + return UC_ERR_OK; +} +#elif defined(TARGET_AARCH64) +static int brov_reg_ptr_impl(struct uc_struct *uc, int regid, void **ptr, size_t *size, + uint32_t *flags) +{ + CPUARMState *env; + + if (!uc->cpu || !uc->cpu->env_ptr) { + return UC_ERR_HANDLE; + } + env = (CPUARMState *)uc->cpu->env_ptr; + *flags = BROV_REG_READABLE | BROV_REG_WRITABLE; + + if (regid >= UC_ARM64_REG_X0 && regid <= UC_ARM64_REG_X28) { + *ptr = &env->xregs[regid - UC_ARM64_REG_X0]; + } else if (regid == UC_ARM64_REG_X29) { + *ptr = &env->xregs[29]; + } else if (regid == UC_ARM64_REG_X30) { + *ptr = &env->xregs[30]; + } else if (regid == UC_ARM64_REG_SP) { + *ptr = &env->xregs[31]; + } else if (regid == UC_ARM64_REG_PC) { + *ptr = &env->pc; + *flags = BROV_REG_READABLE; + } else { + return UC_ERR_ARG; + } + + if (size) { + *size = sizeof(uint64_t); + } + return UC_ERR_OK; +} +#else +static int brov_reg_ptr_impl(struct uc_struct *uc, int regid, void **ptr, size_t *size, + uint32_t *flags) +{ + (void)uc; + (void)regid; + (void)ptr; + (void)size; + (void)flags; + return UC_ERR_ARG; +} +#endif + +static void brov_install(struct uc_struct *uc) +{ + uc->brov.info = brov_info_impl; + uc->brov.audit = brov_audit_impl; + uc->brov.save = brov_save_impl; + uc->brov.load = brov_load_impl; + uc->brov.resolve = brov_resolve_impl; + uc->brov.reg_ptr = brov_reg_ptr_impl; +} diff --git a/Brovan/native/unicorn/brovan_uc_tcg_decls.inc.h b/Brovan/native/unicorn/brovan_uc_tcg_decls.inc.h new file mode 100644 index 0000000..b32fde6 --- /dev/null +++ b/Brovan/native/unicorn/brovan_uc_tcg_decls.inc.h @@ -0,0 +1,14 @@ +/* Included near the top of qemu/accel/tcg/translate-all.c. + * + * The definitions live in brovan_uc_tcg.inc.c, appended to the end of the same + * file, because they need that file's statics (tb_page_add, tb_phys_invalidate, + * page_lock_pair). translate-all.c is compiled once per target, so everything + * here is static and reached from outside only through uc->brov. */ +#ifndef BROVAN_UC_TCG_DECLS_H +#define BROVAN_UC_TCG_DECLS_H + +static bool brov_try_alloc_code_gen_buffer(struct uc_struct *uc, size_t tb_size); +static bool brov_owns_code_gen_buffer(struct uc_struct *uc); +static void brov_install(struct uc_struct *uc); + +#endif diff --git a/Brovan/native/unicorn/patches.manifest b/Brovan/native/unicorn/patches.manifest new file mode 100644 index 0000000..c92c094 --- /dev/null +++ b/Brovan/native/unicorn/patches.manifest @@ -0,0 +1,62 @@ +# Brovan patch manifest for the Unicorn source tree. +# +# Applied by the PatchUnicornSource target in Brovan.Unicorn.targets after the +# tarball is extracted. Every rule is idempotent and every missing anchor is a +# hard build error, so a silently unpatched unicorn cannot be produced. +# +# Rule syntax, '|' separated: +# schema| +# validated| +# copy|| +# insert-after||| +# insert-before||| +# insert-in-body||| +# replace-all||| +# append|| +# +# Anchors are deliberately single lines or whole-file appends: those survive +# upstream edits to the surrounding code, hunk-based diffs do not. + +schema|1 +validated|2.1.4 + +copy|brovan_uc.h|include/brovan_uc.h +copy|brovan_uc_api.inc.c|include/brovan_uc_api.inc.c +copy|brovan_uc_tcg_decls.inc.h|include/brovan_uc_tcg_decls.inc.h +copy|brovan_uc_tcg.inc.c|include/brovan_uc_tcg.inc.c +copy|brovan_tcg_slots.inc.c|include/brovan_tcg_slots.inc.c +copy|brovan_tcg_exitcheck.inc.h|include/brovan_tcg_exitcheck.inc.h + +# --- uc_struct gains the brov ops table and the inline-hook switch ----------- +insert-after|include/uc_priv.h|#include "list.h"|#include "brovan_uc.h" +insert-after|include/uc_priv.h|uc_del_inline_hook_t del_inline_hook;| BROVAN_UC_FIELDS + +# --- TCGContext gains the slot table and the cache counters ----------------- +insert-before|qemu/include/tcg/tcg.h|struct TCGContext {|#include "brovan_uc.h" +insert-after|qemu/include/tcg/tcg.h|struct uc_struct *uc;| BROVAN_TCG_FIELDS + +# --- uc is pinned in the reservation: it is baked in by tcg_const_ptr(uc) ---- +replace-all|uc.c|uc = calloc(1, sizeof(*uc));|uc = brov_alloc_uc(sizeof(*uc)); +replace-all|uc.c|free(uc);|brov_free_uc(uc); +append|uc.c|#include "brovan_uc_api.inc.c" + +# --- code buffer, slot table, ops install ----------------------------------- +insert-after|qemu/accel/tcg/translate-all.c|#include "uc_priv.h"|#include "brovan_uc_tcg_decls.inc.h" +replace-all|qemu/accel/tcg/translate-all.c|uc->tcg_ctx = g_malloc(sizeof(TCGContext));|uc->tcg_ctx = brov_alloc_arena(sizeof(TCGContext)); +insert-before|qemu/accel/tcg/translate-all.c|tcg_ctx->code_gen_buffer_size = size_code_gen_buffer(tb_size);| if (brov_try_alloc_code_gen_buffer(uc, tb_size)) { return; } +insert-after|qemu/accel/tcg/translate-all.c|uc->tb_flush = uc_tb_flush;| brov_install(uc); +replace-all|qemu/accel/tcg/translate-all.c|if (tcg_ctx->initial_buffer) {|if (tcg_ctx->initial_buffer && !brov_owns_code_gen_buffer(uc)) { +append|qemu/accel/tcg/translate-all.c|#include "brovan_uc_tcg.inc.c" + + +# --- per-block exit poll: load+branch on the fast path, helper on the slow one +insert-before|qemu/include/exec/gen-icount.h|static inline void gen_tb_start(TCGContext *tcg_ctx, TranslationBlock *tb)|#include "brovan_tcg_exitcheck.inc.h" +replace-all|qemu/include/exec/gen-icount.h|gen_helper_check_exit_request(tcg_ctx, puc, tmp);|brov_gen_exit_check_start(tcg_ctx, puc, tmp); +replace-all|qemu/include/exec/gen-icount.h|tcg_gen_exit_tb(tcg_ctx, tb, TB_EXIT_REQUESTED);|tcg_gen_exit_tb(tcg_ctx, tb, TB_EXIT_REQUESTED); brov_gen_exit_check_end(tcg_ctx); + +# --- helper calls route through the slot table ------------------------------ +insert-before|qemu/tcg/i386/tcg-target.inc.c|static inline void tcg_out_call(TCGContext *s, tcg_insn_unit *dest)|#include "brovan_tcg_slots.inc.c" +insert-in-body|qemu/tcg/i386/tcg-target.inc.c|static inline void tcg_out_call(TCGContext *s, tcg_insn_unit *dest)| if (brov_tcg_out_call_slot(s, dest)) { return; } +insert-in-body|qemu/tcg/i386/tcg-target.inc.c|static void tcg_out_jmp(TCGContext *s, tcg_insn_unit *dest)| if (brov_tcg_out_jmp_slot(s, dest)) { return; } +insert-before|qemu/tcg/aarch64/tcg-target.inc.c|static inline void tcg_out_call(TCGContext *s, tcg_insn_unit *target)|#include "brovan_tcg_slots.inc.c" +insert-in-body|qemu/tcg/aarch64/tcg-target.inc.c|static inline void tcg_out_call(TCGContext *s, tcg_insn_unit *target)| if (brov_tcg_out_call_slot(s, target)) { return; }