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; }