mirror of
https://github.com/ayozetr/bloodborne_linux.git
synced 2026-11-07 09:46:04 -10:00
Runtime: run the game's movbe on processors without MOVBE
The game's code was built for the PS4's CPU and has 987 movbe. AMD's FX processors of the Piledriver generation have every other instruction the game uses but not this one, so the game stopped with SIGILL (exit code 132) at the first one, while reading a file header (#39). The SIGILL handler now decodes the movbe at the faulting address, does the load or store with its bytes swapped and resumes after it. On a processor with MOVBE this never runs. The CPU check no longer refuses to start without MOVBE and says in the log that the port runs them; the log also counts them every 10 s (a few thousand a second while loading, about ten a second while playing). BB_MOVBE_TRAP=file turns the movbe listed by tools/movbe_sites.py into ud2, so the emulation can be tested on any processor. tests/test_movbe.c checks the forms the game uses.
This commit is contained in:
1 parent
e8c7eaaf9c
commit
498a37d24b
8 files changed
+402
-7
No files matched your search
@@ -118,6 +118,8 @@ if [[ ${1:-} == --test ]]; then
|
||||
out/pad-atlas-test
|
||||
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -pthread -I. -Isrc tests/test_runtime.c "${runtime[@]}" out/libatrac9.a -lm "${gpu[@]}" "${libraries[@]}" -o out/runtime-test
|
||||
out/runtime-test
|
||||
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -Isrc tests/test_movbe.c src/runtime_movbe.c -o out/movbe-test
|
||||
out/movbe-test
|
||||
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -pthread -Isrc tests/test_file_mods.c src/runtime_remote.c -o out/file-mods-test
|
||||
out/file-mods-test
|
||||
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -pthread -I. -Isrc tests/test_sema.c "${runtime[@]}" out/libatrac9.a -lm "${gpu[@]}" "${libraries[@]}" -o out/sema-test
|
||||
|
||||
+5
-3
@@ -55,7 +55,7 @@ at its start (`Environment:` in the log) and warns about `BB_*` names it does no
|
||||
`=1: ...` / `=0: ...` / `=N: ...` say what that value does; `set: ...` means any value
|
||||
turns it on (even `0`).
|
||||
|
||||
## Settings (106)
|
||||
## Settings (107)
|
||||
|
||||
What players and the launchers set. The menu, bbport.ini and the launcher write most of them; set one by hand to override them.
|
||||
|
||||
@@ -98,6 +98,7 @@ What players and the launchers set. The menu, bbport.ini and the launcher write
|
||||
| `BB_GAMEPAD` | =SDL GUID or part of the name: the controller to use (the launcher's) |
|
||||
| `BB_GAME_DIR` | the game's folder (with eboot.bin) |
|
||||
| `BB_GAME_MENU` | =0: no port pages in the game's System menu |
|
||||
| `BB_GESTURES_NO_MOTION` | =1: the gestures menu without the motion-control icons (Dommo's mod; the launcher's switch) |
|
||||
| `BB_GPU_ID` | =N: the GPU to use, by Vulkan index (the launcher's) |
|
||||
| `BB_GPU_PROFILE` | =1: GPU profiler: time per labelled pass in the log (the launcher's) |
|
||||
| `BB_HDR` | =1: HDR output allowed |
|
||||
@@ -155,7 +156,7 @@ What players and the launchers set. The menu, bbport.ini and the launcher write
|
||||
| `BB_SHADNET_SERVER` | the shadNet server |
|
||||
| `BB_SHADNET_TOKEN` | the shadNet account token (the launcher's) |
|
||||
| `BB_SHADNET_WEBAPI` | the shadNet web API server |
|
||||
| `BB_SKIP_CPU_CHECK` | =1: skip the check of the CPU's bsr/bsf behaviour before the game starts |
|
||||
| `BB_SKIP_CPU_CHECK` | =1: skip the check of the processor's instructions (AVX, BMI1, LZCNT...) before the game starts |
|
||||
| `BB_SKIP_GAME_CHECK` | =1: start with game files that did not pass the check |
|
||||
| `BB_STEAM_INPUT_X11` | =0: keep the native Wayland window when Steam made a virtual controller for the game (default: Xwayland, so Steam Input reaches it) |
|
||||
| `BB_TEXTURE_QUALITY` | texture quality over bbport.ini's texture_quality |
|
||||
@@ -410,7 +411,7 @@ Defaults to change on a machine where they misbehave, workarounds, and switches
|
||||
| `BB_VULKANINFO` | path of vulkaninfo for the launcher |
|
||||
| `BB_WORK_RETIRED` | =0: the recording thread and fences do not wait for retired work |
|
||||
|
||||
## Diagnostics (281)
|
||||
## Diagnostics (282)
|
||||
|
||||
Logs, dumps, traces, probes and experiments, for finding bugs. Most cost frame time; none is meant for playing.
|
||||
|
||||
@@ -572,6 +573,7 @@ Logs, dumps, traces, probes and experiments, for finding bugs. Most cost frame t
|
||||
| `BB_MOD_TRACE` | =1: every file a mod replaces |
|
||||
| `BB_MOTION_SELECT_LOG` | =1: each G-buffer vertex shader once with the motion vector selection |
|
||||
| `BB_MOTION_TRACE` | =file: the frame after the file appears prints its motion draws |
|
||||
| `BB_MOVBE_TRAP` | =file: the movbe at the image offsets listed there (tools/movbe_sites.py) made ud2 and run by the port, as on a processor without MOVBE |
|
||||
| `BB_MSG_TRACE` | =1: the game's text lookups per 2 s by category (#255) |
|
||||
| `BB_NGX_LOG` | =1 / 2: NVIDIA NGX log into the port's (2: verbose) |
|
||||
| `BB_NULL_GPU` | =1: no GPU work at all (tests) |
|
||||
|
||||
@@ -106,7 +106,7 @@ BB_ENV(BB_SHADNET_PASSWORD, SETTING, "the shadNet account password (the launcher
|
||||
BB_ENV(BB_SHADNET_SERVER, SETTING, "the shadNet server")
|
||||
BB_ENV(BB_SHADNET_TOKEN, SETTING, "the shadNet account token (the launcher's)")
|
||||
BB_ENV(BB_SHADNET_WEBAPI, SETTING, "the shadNet web API server")
|
||||
BB_ENV(BB_SKIP_CPU_CHECK, SETTING, "=1: skip the check of the CPU's bsr/bsf behaviour before the game starts")
|
||||
BB_ENV(BB_SKIP_CPU_CHECK, SETTING, "=1: skip the check of the processor's instructions (AVX, BMI1, LZCNT...) before the game starts")
|
||||
BB_ENV(BB_SKIP_GAME_CHECK, SETTING, "=1: start with game files that did not pass the check")
|
||||
BB_ENV(BB_STEAM_INPUT_X11, SETTING, "=0: keep the native Wayland window when Steam made a virtual controller for the game (default: Xwayland, so Steam Input reaches it)")
|
||||
BB_ENV(BB_TEXTURE_QUALITY, SETTING, "texture quality over bbport.ini's texture_quality")
|
||||
@@ -410,6 +410,7 @@ BB_ENV(BB_EXP_COPY_SRC_SNAPSHOT, DEBUG, "set (experiment): copy shader sources s
|
||||
BB_ENV(BB_EXP_CPU_READS_GPU_WAIT, DEBUG, "set (experiment): CPU-side reads also wait for the GPU, as before")
|
||||
BB_ENV(BB_EXP_DMA_UNTRACKED, DEBUG, "set (experiment): stages reading memory through pointers count as unlisted reads")
|
||||
BB_ENV(BB_FAULT_WAIT, DEBUG, "=1: a fatal fault or a GPU library assertion leaves the process alive for a debugger (tools/debug_attach.sh)")
|
||||
BB_ENV(BB_MOVBE_TRAP, DEBUG, "=file: the movbe at the image offsets listed there (tools/movbe_sites.py) made ud2 and run by the port, as on a processor without MOVBE")
|
||||
BB_ENV(BB_WRITE_SHADOWS, DEBUG, "=1: ranges the game's copy shader writes in place copied into VRAM after it, small read-only bindings read there (no gain measured)")
|
||||
BB_ENV(BB_WRITE_SHADOW_TRACE, DEBUG, "=1: why BB_WRITE_SHADOWS lookups miss and who drops the copies (log lines)")
|
||||
BB_ENV(BB_HOT_VERTEX_COPY, TUNING, "=1: vertex data bound in place copied into VRAM once per submission (no reads over the bus per pass)")
|
||||
|
||||
+29
-3
@@ -14,6 +14,7 @@
|
||||
#include "runtime_trophy.h"
|
||||
#include "runtime_map.h"
|
||||
#include "runtime_atlas_state.h"
|
||||
#include "runtime_movbe.h"
|
||||
#include "gpu/bbgpu.h"
|
||||
#if !defined(__x86_64__) || !defined(__GNUC__)
|
||||
#error This prototype requires x86-64 GCC or Clang (including MinGW).
|
||||
@@ -100,8 +101,12 @@ static void print_environment(void) {
|
||||
/* The game's code was compiled for the PS4's CPU (AMD Jaguar) and runs as it is: AVX, BMI1
|
||||
* (andn/bextr/blsr/tzcnt), MOVBE, LZCNT and POPCNT, thousands of each in eboot.bin. A CPU without
|
||||
* them stops at the first one with SIGILL (exit code 132, issue #26), and lzcnt/tzcnt even run as
|
||||
* bsr/bsf there, with other results. Said before the game starts; BB_SKIP_CPU_CHECK=1 skips it. */
|
||||
* bsr/bsf there, with other results. Said before the game starts; BB_SKIP_CPU_CHECK=1 skips it.
|
||||
* MOVBE alone is no obstacle: the port runs those itself (runtime_movbe.c), so AMD's FX
|
||||
* processors, which lack only that one, play (issue #39). */
|
||||
static void check_cpu(void) {
|
||||
if (!runtime_movbe_supported())
|
||||
printf("CPU: no MOVBE; the port runs the game's movbe itself (runtime_movbe.c)\n");
|
||||
const char *skip = bb_env("BB_SKIP_CPU_CHECK");
|
||||
if (skip && !strcmp(skip, "1")) return;
|
||||
unsigned a, b, c, d, leaf1_c = 0, leaf7_b = 0, ext1_c = 0;
|
||||
@@ -115,7 +120,7 @@ static void check_cpu(void) {
|
||||
avx = (lo & 6) == 6; /* the OS saves SSE and AVX state */
|
||||
}
|
||||
const struct { int present; const char *name; } features[] = {
|
||||
{avx, "AVX"}, {(leaf7_b & bit_BMI) != 0, "BMI1"}, {(leaf1_c & bit_MOVBE) != 0, "MOVBE"},
|
||||
{avx, "AVX"}, {(leaf7_b & bit_BMI) != 0, "BMI1"},
|
||||
{(ext1_c & bit_LZCNT) != 0, "LZCNT"}, {(leaf1_c & bit_POPCNT) != 0, "POPCNT"},
|
||||
{(leaf1_c & bit_SSE4_2) != 0, "SSE4.2"},
|
||||
};
|
||||
@@ -129,7 +134,8 @@ static void check_cpu(void) {
|
||||
fprintf(stderr,
|
||||
"ERROR: this CPU lacks %s. Bloodborne's code was compiled for the PS4's CPU and uses "
|
||||
"these instructions directly: it needs an Intel Haswell (4th generation Core, 2013) or "
|
||||
"newer, or an AMD Ryzen. BB_SKIP_CPU_CHECK=1 starts anyway.\n", missing);
|
||||
"newer, or an AMD FX-4300, 6300 or 8300 series (Piledriver) or newer. "
|
||||
"BB_SKIP_CPU_CHECK=1 starts anyway.\n", missing);
|
||||
exit(1);
|
||||
}
|
||||
static uint64_t read64(FILE *f) {
|
||||
@@ -244,6 +250,23 @@ void bb_fault_wait(const char *what) {
|
||||
#endif
|
||||
}
|
||||
static void fault(int sig, siginfo_t *info, void *context) {
|
||||
#ifndef _WIN32
|
||||
/* A movbe on a processor without MOVBE (AMD FX, issue #39): run by the port, the game goes on. */
|
||||
if (sig == SIGILL) {
|
||||
greg_t *g = ((ucontext_t *)context)->uc_mcontext.gregs;
|
||||
static const int order[16] = {REG_RAX, REG_RCX, REG_RDX, REG_RBX, REG_RSP, REG_RBP, REG_RSI, REG_RDI,
|
||||
REG_R8, REG_R9, REG_R10, REG_R11, REG_R12, REG_R13, REG_R14, REG_R15};
|
||||
uint64_t regs[16], rip = (uint64_t)g[REG_RIP];
|
||||
if (rip - (uintptr_t)image < 0x10000000) {
|
||||
for (int i = 0; i < 16; ++i) regs[i] = (uint64_t)g[order[i]];
|
||||
if (runtime_movbe_fault(regs, &rip)) {
|
||||
for (int i = 0; i < 16; ++i) g[order[i]] = (greg_t)regs[i];
|
||||
g[REG_RIP] = (greg_t)rip;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
/* GPU page tracking (write-protected guest pages) is resolved first. */
|
||||
if (gpu_enabled && sig == SIGSEGV && bbgpu_handle_fault(context, info->si_addr)) return;
|
||||
/* A speculative guest memory read (runtime_memory.c) failed: resume its recovery point. */
|
||||
@@ -810,6 +833,9 @@ int main(int argc, char **argv) {
|
||||
runtime_atlas_state_bind(image, size);
|
||||
}
|
||||
}
|
||||
#ifndef _WIN32
|
||||
runtime_movbe_bind(image, size); /* BB_MOVBE_TRAP (testing): movbe made ud2 */
|
||||
#endif
|
||||
#ifdef _WIN32
|
||||
patch_guest_tls(segments, ns);
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 bbport contributors
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
/* MOVBE on processors without it. The game's code was built for the PS4's CPU (AMD Jaguar) and
|
||||
* has 987 movbe (a load or store with its bytes swapped: the big-endian headers of the game's
|
||||
* files, network data). AMD's FX processors (Bulldozer to Piledriver) have every other instruction
|
||||
* the game uses, but not this one: the first movbe stopped the game with SIGILL (issue #39). The
|
||||
* SIGILL handler (probe.c) does it here instead and resumes after it; on a processor with MOVBE
|
||||
* this code never runs.
|
||||
*
|
||||
* BB_MOVBE_TRAP=file (testing on any processor): the image offsets of movbe in the file
|
||||
* (one hex number per line, tools/movbe_sites.py) become ud2, which this code runs as the movbe
|
||||
* that was there, as a processor without MOVBE would. */
|
||||
#define _GNU_SOURCE
|
||||
#include "runtime_movbe.h"
|
||||
#include "../gpu/shim/bb_env.h"
|
||||
#include <inttypes.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/mman.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* x86 register numbers (rax 0 ... r15 15). */
|
||||
enum { RSP = 4, RBP = 5 };
|
||||
|
||||
int runtime_movbe_run(const uint8_t *code, uint64_t regs[16], uint64_t rip) {
|
||||
const uint8_t *p = code;
|
||||
int rex = 0, operand16 = 0;
|
||||
for (;; ++p) {
|
||||
if (p - code >= 4) return 0;
|
||||
if (*p == 0x66) operand16 = 1;
|
||||
else break;
|
||||
}
|
||||
if ((*p & 0xf0) == 0x40) rex = *p++;
|
||||
if (p[0] != 0x0f || p[1] != 0x38 || (p[2] != 0xf0 && p[2] != 0xf1)) return 0;
|
||||
const int store = p[2] == 0xf1;
|
||||
p += 3;
|
||||
const uint8_t modrm = *p++;
|
||||
const int mod = modrm >> 6, rm = modrm & 7;
|
||||
const int reg = ((modrm >> 3) & 7) | (rex & 4 ? 8 : 0);
|
||||
if (mod == 3) return 0; /* the register form does not exist */
|
||||
uint64_t address = 0;
|
||||
int rip_relative = 0;
|
||||
if (rm == 4) {
|
||||
const uint8_t sib = *p++;
|
||||
const int scale = sib >> 6, index = ((sib >> 3) & 7) | (rex & 2 ? 8 : 0);
|
||||
const int base = (sib & 7) | (rex & 1 ? 8 : 0);
|
||||
if (index != RSP) address += regs[index] << scale;
|
||||
if ((base & 7) == RBP && mod == 0) {
|
||||
int32_t disp;
|
||||
memcpy(&disp, p, 4);
|
||||
p += 4;
|
||||
address += (uint64_t)(int64_t)disp;
|
||||
} else {
|
||||
address += regs[base];
|
||||
}
|
||||
} else if (rm == 5 && mod == 0) {
|
||||
rip_relative = 1;
|
||||
} else {
|
||||
address = regs[rm | (rex & 1 ? 8 : 0)];
|
||||
}
|
||||
if (mod == 1) {
|
||||
address += (uint64_t)(int64_t)(int8_t)*p++;
|
||||
} else if (mod == 2 || rip_relative) {
|
||||
int32_t disp;
|
||||
memcpy(&disp, p, 4);
|
||||
p += 4;
|
||||
address += (uint64_t)(int64_t)disp;
|
||||
}
|
||||
const int length = (int)(p - code);
|
||||
if (rip_relative) address += rip + (uint64_t)length;
|
||||
uint8_t *memory = (uint8_t *)(uintptr_t)address;
|
||||
if (rex & 8) {
|
||||
uint64_t value;
|
||||
if (store) {
|
||||
value = __builtin_bswap64(regs[reg]);
|
||||
memcpy(memory, &value, 8);
|
||||
} else {
|
||||
memcpy(&value, memory, 8);
|
||||
regs[reg] = __builtin_bswap64(value);
|
||||
}
|
||||
} else if (operand16) {
|
||||
uint16_t value;
|
||||
if (store) {
|
||||
value = __builtin_bswap16((uint16_t)regs[reg]);
|
||||
memcpy(memory, &value, 2);
|
||||
} else {
|
||||
memcpy(&value, memory, 2);
|
||||
regs[reg] = (regs[reg] & ~(uint64_t)0xffff) | __builtin_bswap16(value);
|
||||
}
|
||||
} else {
|
||||
uint32_t value;
|
||||
if (store) {
|
||||
value = __builtin_bswap32((uint32_t)regs[reg]);
|
||||
memcpy(memory, &value, 4);
|
||||
} else {
|
||||
memcpy(&value, memory, 4);
|
||||
regs[reg] = __builtin_bswap32(value); /* a 32-bit result clears the upper half */
|
||||
}
|
||||
}
|
||||
return length;
|
||||
}
|
||||
|
||||
/* BB_MOVBE_TRAP: the replaced instructions, by image offset. */
|
||||
typedef struct {
|
||||
uint64_t offset;
|
||||
uint8_t code[16];
|
||||
} Trapped;
|
||||
static Trapped *trapped;
|
||||
static size_t trapped_count;
|
||||
static uintptr_t trapped_image;
|
||||
|
||||
static int by_offset(const void *a, const void *b) {
|
||||
const uint64_t x = ((const Trapped *)a)->offset, y = ((const Trapped *)b)->offset;
|
||||
return x < y ? -1 : x > y;
|
||||
}
|
||||
|
||||
void runtime_movbe_bind(unsigned char *image, uint64_t size) {
|
||||
const char *path = bb_env("BB_MOVBE_TRAP");
|
||||
if (!path || !path[0]) return;
|
||||
FILE *f = fopen(path, "r");
|
||||
if (!f) {
|
||||
printf("MOVBE: BB_MOVBE_TRAP=%s cannot be read\n", path);
|
||||
return;
|
||||
}
|
||||
size_t capacity = 0, skipped = 0;
|
||||
char line[64];
|
||||
const uint64_t page = (uint64_t)sysconf(_SC_PAGESIZE);
|
||||
while (fgets(line, sizeof(line), f)) {
|
||||
char *end;
|
||||
const uint64_t offset = strtoull(line, &end, 16);
|
||||
if (end == line) continue;
|
||||
if (offset + 16 > size) {
|
||||
++skipped;
|
||||
continue;
|
||||
}
|
||||
Trapped t = {.offset = offset};
|
||||
memcpy(t.code, image + offset, sizeof(t.code));
|
||||
const uint8_t *op = t.code + ((t.code[0] & 0xf0) == 0x40); /* past a REX prefix */
|
||||
if (op[0] != 0x0f || op[1] != 0x38 || (op[2] != 0xf0 && op[2] != 0xf1)) {
|
||||
++skipped;
|
||||
continue;
|
||||
}
|
||||
if (trapped_count == capacity) {
|
||||
capacity = capacity ? capacity * 2 : 1024;
|
||||
trapped = realloc(trapped, capacity * sizeof(*trapped));
|
||||
if (!trapped) {
|
||||
fclose(f);
|
||||
trapped_count = 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
trapped[trapped_count++] = t;
|
||||
const uint64_t first = offset & ~(page - 1);
|
||||
mprotect(image + first, (size_t)((offset + 2 + page - 1) / page * page - first), PROT_READ | PROT_WRITE | PROT_EXEC);
|
||||
image[offset] = 0x0f;
|
||||
image[offset + 1] = 0x0b; /* ud2 */
|
||||
}
|
||||
fclose(f);
|
||||
qsort(trapped, trapped_count, sizeof(*trapped), by_offset);
|
||||
trapped_image = (uintptr_t)image;
|
||||
printf("MOVBE: BB_MOVBE_TRAP: %zu movbe made ud2 and run by the port (%zu lines skipped)\n", trapped_count,
|
||||
skipped);
|
||||
}
|
||||
|
||||
int runtime_movbe_supported(void) {
|
||||
unsigned a, b, c, d;
|
||||
__asm__("cpuid" : "=a"(a), "=b"(b), "=c"(c), "=d"(d) : "a"(1), "c"(0));
|
||||
return (c >> 22) & 1;
|
||||
}
|
||||
|
||||
/* How many the port ran (the log says it every 10 s). */
|
||||
static volatile uint64_t emulated;
|
||||
|
||||
int runtime_movbe_fault(uint64_t regs[16], uint64_t *rip) {
|
||||
const uint8_t *code = (const uint8_t *)(uintptr_t)*rip;
|
||||
if (trapped_count && code[0] == 0x0f && code[1] == 0x0b) {
|
||||
const Trapped key = {.offset = *rip - trapped_image};
|
||||
const Trapped *t = bsearch(&key, trapped, trapped_count, sizeof(*trapped), by_offset);
|
||||
if (t) code = t->code;
|
||||
}
|
||||
const int length = runtime_movbe_run(code, regs, *rip);
|
||||
if (!length) return 0;
|
||||
*rip += (uint64_t)length;
|
||||
const uint64_t total = __atomic_add_fetch(&emulated, 1, __ATOMIC_RELAXED);
|
||||
/* In the log: the first one, then how many every 10 s (a signal each: a cost if many). */
|
||||
struct timespec now;
|
||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
static int64_t next_report;
|
||||
static uint64_t reported;
|
||||
int64_t due = __atomic_load_n(&next_report, __ATOMIC_RELAXED);
|
||||
if (total == 1 || (now.tv_sec >= due &&
|
||||
__atomic_compare_exchange_n(&next_report, &due, (int64_t)now.tv_sec + 10, 0,
|
||||
__ATOMIC_RELAXED, __ATOMIC_RELAXED))) {
|
||||
if (total == 1) __atomic_store_n(&next_report, (int64_t)now.tv_sec + 10, __ATOMIC_RELAXED);
|
||||
char line[96] = "MOVBE: run by the port: ";
|
||||
size_t at = strlen(line);
|
||||
char digits[24];
|
||||
size_t n = 0;
|
||||
uint64_t value = total - __atomic_exchange_n(&reported, total, __ATOMIC_RELAXED);
|
||||
do digits[n++] = (char)('0' + value % 10); while ((value /= 10) && n < sizeof(digits));
|
||||
while (n) line[at++] = digits[--n];
|
||||
static const char tail[] = " (this processor has no MOVBE)\n";
|
||||
memcpy(line + at, tail, sizeof(tail) - 1);
|
||||
ssize_t written = write(1, line, at + sizeof(tail) - 1);
|
||||
(void)written;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 bbport contributors
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
/* MOVBE on processors without it (runtime_movbe.c). */
|
||||
#ifndef BBPORT_RUNTIME_MOVBE_H
|
||||
#define BBPORT_RUNTIME_MOVBE_H
|
||||
#include <stdint.h>
|
||||
|
||||
/* Runs the movbe in `code` (at address `rip`) on `regs` (rax 0 ... r15 15) and its memory:
|
||||
* its length, or 0 when `code` is no movbe (nothing is changed then). */
|
||||
int runtime_movbe_run(const uint8_t *code, uint64_t regs[16], uint64_t rip);
|
||||
/* The SIGILL handler's part: the movbe at *rip run and *rip moved past it (1), or 0. */
|
||||
int runtime_movbe_fault(uint64_t regs[16], uint64_t *rip);
|
||||
/* BB_MOVBE_TRAP=file: the listed movbe of the image made ud2 (while it is still writable). */
|
||||
void runtime_movbe_bind(unsigned char *image, uint64_t size);
|
||||
/* Whether this processor has MOVBE (CPUID 1, ECX bit 22). */
|
||||
int runtime_movbe_supported(void);
|
||||
#endif
|
||||
@@ -0,0 +1,96 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 bbport contributors
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
/* runtime_movbe.c: the movbe forms the game uses, run on registers and a buffer. */
|
||||
#include "runtime_movbe.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
static int failures;
|
||||
#define CHECK(cond) \
|
||||
do { \
|
||||
if (!(cond)) { \
|
||||
printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \
|
||||
++failures; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
enum { RAX, RCX, RDX, RBX, RSP, RBP, RSI, RDI, R8, R9, R10, R11, R12, R13, R14, R15 };
|
||||
|
||||
int main(void) {
|
||||
uint8_t memory[64];
|
||||
uint64_t regs[16];
|
||||
const uint64_t base = (uint64_t)(uintptr_t)memory;
|
||||
|
||||
/* movbe eax, [rcx+4] (0f 38 f0 41 04): the game's DCX header read (issue #39). */
|
||||
{
|
||||
static const uint8_t code[] = {0x0f, 0x38, 0xf0, 0x41, 0x04};
|
||||
memset(regs, 0, sizeof(regs));
|
||||
memcpy(memory + 4, "\x00\x01\x10\x00", 4);
|
||||
regs[RCX] = base;
|
||||
regs[RAX] = 0xffffffffffffffffull;
|
||||
CHECK(runtime_movbe_run(code, regs, 0) == 5);
|
||||
CHECK(regs[RAX] == 0x00011000); /* upper half cleared */
|
||||
}
|
||||
/* movbe [rax+r12*4-0x10], r9d (46 0f 38 f1 4c a0 f0): a store, REX.R and REX.X, disp8. */
|
||||
{
|
||||
static const uint8_t code[] = {0x46, 0x0f, 0x38, 0xf1, 0x4c, 0xa0, 0xf0};
|
||||
memset(regs, 0, sizeof(regs));
|
||||
memset(memory, 0, sizeof(memory));
|
||||
regs[RAX] = base + 0x10;
|
||||
regs[R12] = 2;
|
||||
regs[R9] = 0xaabbccdd11223344ull;
|
||||
CHECK(runtime_movbe_run(code, regs, 0) == 7);
|
||||
CHECK(memcmp(memory + 8, "\x11\x22\x33\x44", 4) == 0); /* big-endian */
|
||||
CHECK(regs[R9] == 0xaabbccdd11223344ull); /* the source is left as it was */
|
||||
}
|
||||
/* movbe rdx, [r13+0x100] (49 0f 38 f0 95 00 01 00 00): 64-bit, REX.B, disp32. */
|
||||
{
|
||||
static const uint8_t code[] = {0x49, 0x0f, 0x38, 0xf0, 0x95, 0x00, 0x01, 0x00, 0x00};
|
||||
memset(regs, 0, sizeof(regs));
|
||||
memcpy(memory + 8, "\x01\x02\x03\x04\x05\x06\x07\x08", 8);
|
||||
regs[R13] = base + 8 - 0x100;
|
||||
CHECK(runtime_movbe_run(code, regs, 0) == 9);
|
||||
CHECK(regs[RDX] == 0x0102030405060708ull);
|
||||
}
|
||||
/* movbe [rsp+8], ecx (0f 38 f1 4c 24 08): rsp base through a SIB with no index. */
|
||||
{
|
||||
static const uint8_t code[] = {0x0f, 0x38, 0xf1, 0x4c, 0x24, 0x08};
|
||||
memset(regs, 0, sizeof(regs));
|
||||
memset(memory, 0, sizeof(memory));
|
||||
regs[RSP] = base;
|
||||
regs[RCX] = 0x12345678;
|
||||
CHECK(runtime_movbe_run(code, regs, 0) == 6);
|
||||
CHECK(memcmp(memory + 8, "\x12\x34\x56\x78", 4) == 0);
|
||||
}
|
||||
/* movbe esi, [rbp+0] (0f 38 f0 75 00): rbp needs a displacement byte. */
|
||||
{
|
||||
static const uint8_t code[] = {0x0f, 0x38, 0xf0, 0x75, 0x00};
|
||||
memset(regs, 0, sizeof(regs));
|
||||
memcpy(memory, "\xde\xad\xbe\xef", 4);
|
||||
regs[RBP] = base;
|
||||
CHECK(runtime_movbe_run(code, regs, 0) == 5);
|
||||
CHECK(regs[RSI] == 0xdeadbeef);
|
||||
}
|
||||
/* movbe ax, [rip+0x10] (66 0f 38 f0 05 10 00 00 00): 16 bits keep the rest, rip-relative. */
|
||||
{
|
||||
static const uint8_t code[] = {0x66, 0x0f, 0x38, 0xf0, 0x05, 0x10, 0x00, 0x00, 0x00};
|
||||
memset(regs, 0, sizeof(regs));
|
||||
memcpy(memory + 0x20, "\xab\xcd", 2);
|
||||
regs[RAX] = 0x1111111111111111ull;
|
||||
const uint64_t rip = base + 0x20 - 0x10 - sizeof(code);
|
||||
CHECK(runtime_movbe_run(code, regs, rip) == 9);
|
||||
CHECK(regs[RAX] == 0x111111111111abcdull);
|
||||
}
|
||||
/* Not a movbe: nothing is run. */
|
||||
{
|
||||
static const uint8_t crc32[] = {0xf2, 0x0f, 0x38, 0xf1, 0xc1};
|
||||
static const uint8_t ud2[] = {0x0f, 0x0b};
|
||||
static const uint8_t reg_form[] = {0x0f, 0x38, 0xf0, 0xc1};
|
||||
memset(regs, 0, sizeof(regs));
|
||||
CHECK(runtime_movbe_run(crc32, regs, 0) == 0);
|
||||
CHECK(runtime_movbe_run(ud2, regs, 0) == 0);
|
||||
CHECK(runtime_movbe_run(reg_form, regs, 0) == 0);
|
||||
}
|
||||
printf("movbe: %s\n", failures ? "FAILED" : "all passed");
|
||||
return failures != 0;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
"""The image offsets of the movbe in the game's code, one per line, for BB_MOVBE_TRAP
|
||||
(src/runtime_movbe.c): a game run with them made ud2 tests the port's movbe on any processor.
|
||||
|
||||
python3 tools/movbe_sites.py out/eboot.elf > out/movbe-sites.txt
|
||||
BB_MOVBE_TRAP=out/movbe-sites.txt bash run.sh
|
||||
|
||||
Needs objdump (binutils)."""
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
def code_segments(elf):
|
||||
phoff, = struct.unpack_from('<Q', elf, 0x20)
|
||||
phentsize, phnum = struct.unpack_from('<HH', elf, 0x36)
|
||||
for i in range(phnum):
|
||||
kind, flags, offset, vaddr, _, filesz = struct.unpack_from('<IIQQQQ', elf, phoff + i * phentsize)
|
||||
if kind == 1 and flags & 1:
|
||||
yield vaddr, elf[offset:offset + filesz]
|
||||
|
||||
|
||||
def main():
|
||||
elf = open(sys.argv[1], 'rb').read()
|
||||
for vaddr, code in code_segments(elf):
|
||||
with tempfile.NamedTemporaryFile() as f:
|
||||
f.write(code)
|
||||
f.flush()
|
||||
listing = subprocess.run(
|
||||
['objdump', '-D', '--insn-width=16', '-b', 'binary', '-m', 'i386:x86-64',
|
||||
f'--adjust-vma={vaddr:#x}', f.name], check=True, capture_output=True, text=True).stdout
|
||||
for line in listing.splitlines():
|
||||
fields = line.split('\t')
|
||||
if len(fields) >= 3 and fields[2].startswith('movbe'):
|
||||
print(fields[0].strip().rstrip(':'))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in new issue
Block a user