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