Fix startup crash (hopefully) when windows relocates rac1_game.dll; crash log will now name functions

This commit is contained in:
vetusmagnus
2026-10-14 00:06:17 -04:00
parent 28916eaf7d
commit 6805a0f535
11 changed files with 954 additions and 20 deletions
+8 -1
View File
@@ -239,7 +239,14 @@ else:
r = subprocess.run(link, capture_output=True, text=True)
if r.returncode:
print(r.stderr[-6000:]); sys.exit("link failed (dll)")
exe = os.path.join(out, "rac1_wrapper.exe")
# the function names for crash reports (the .pdb is deleted after a launcher build)
try:
sys.path.insert(0, os.path.join(HERE, "tools"))
import pdb_syms
print("symbols:", pdb_syms.write_sym(dll, os.path.join(out, "rac1_game.pdb"), os.path.join(out, "rac1_game.sym")))
except Exception as e:
print("no symbol map (%s): crash reports will show offsets only" % e)
exe =os.path.join(out, "rac1_wrapper.exe")
L = os.path.join(HERE, "launcher")
link = zig + ["cc", "-target", target, "-O2", "-nostdlib", "-o", exe, os.path.join(L, "launcher_win32.c"),
os.path.join(L, "ps2ram_win32.c"), "-lkernel32", "-luser32",
+75
View File
@@ -0,0 +1,75 @@
#!/usr/bin/env python3
"""crash_lookup.py: names the function at a crash offset in rac1_game.dll.
Builds from kit 2026.10.14 on name crashes in rac1_log.txt themselves (rac1_game.sym);
this is for older builds, or offsets from elsewhere.
usage: python crash_lookup.py rac1_game.dll rac1_game.pdb 0x0c4f6807 [more offsets]
The offset is the "Fault offset" of a Windows Application Error event (or a crash
address minus the DLL's base). The .pdb must come from the same build as the .dll:
the launcher deletes it after building, so rebuild by hand to keep it (see below).
A player's crash can be looked up with your own build as long as both were built
from the same kit version: the build is reproducible.
To keep the .pdb, run the build yourself from the game's folder (games\\rac1):
..\\..\\toolchain\\python\\python.exe -u build.py --platform windows --iso "C:\\path\\disc.iso" --repo decomp --levels 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18
with ..\\..\\toolchain\\zig on PATH. It reads only the PDB's public symbols."""
import bisect, struct, sys
def sections(dll):
d = open(dll, "rb").read(4096)
pe = struct.unpack_from("<I", d, 0x3C)[0]
n, opt = struct.unpack_from("<H", d, pe + 6)[0], struct.unpack_from("<H", d, pe + 20)[0]
o = pe + 24 + opt
return [struct.unpack_from("<8sII", d, o + i * 40) for i in range(n)] # name, vsize, rva
def pdb_streams(path):
f = open(path, "rb").read()
if not f.startswith(b"Microsoft C/C++ MSF 7.00\r\n\x1aDS\0\0\0"): sys.exit("not a PDB: " + path)
bs, _, nblocks, dirbytes, _, dirmap = struct.unpack_from("<6I", f, 32)
nb = lambda n: (n + bs - 1) // bs
blk = lambda i: f[i * bs:(i + 1) * bs]
mapblocks = struct.unpack_from("<%dI" % nb(dirbytes), blk(dirmap))
dirdata = b"".join(blk(b) for b in mapblocks)[:dirbytes]
ns = struct.unpack_from("<I", dirdata)[0]
sizes = struct.unpack_from("<%dI" % ns, dirdata, 4)
pos, out = 4 + 4 * ns, []
for s in sizes:
s = 0 if s == 0xFFFFFFFF else s
bl = struct.unpack_from("<%dI" % nb(s), dirdata, pos); pos += 4 * nb(s)
out.append(b"".join(blk(b) for b in bl)[:s])
return out
def publics(pdb):
st = pdb_streams(pdb)
dbi = st[3]
symrec = struct.unpack_from("<H", dbi, 20)[0] # DBI header: SymRecordStream
rec, p, syms = st[symrec], 0, []
while p + 4 <= len(rec):
ln, kind = struct.unpack_from("<HH", rec, p)
if kind == 0x110E: # S_PUB32: flags, offset, segment, name
_, off, seg = struct.unpack_from("<IIH", rec, p + 4)
name = rec[p + 14:rec.index(b"\0", p + 14)].decode("utf-8", "replace")
syms.append((seg, off, name))
p += 2 + ln
return syms
def main():
if len(sys.argv) < 4: sys.exit(__doc__)
secs = sections(sys.argv[1])
table = sorted((secs[seg - 1][2] + off, name) for seg, off, name in publics(sys.argv[2]) if 0 < seg <= len(secs))
addrs = [a for a, _ in table]
for arg in sys.argv[3:]:
rva = int(arg, 16)
i = bisect.bisect_right(addrs, rva) - 1
sec = next((s[0].rstrip(b"\0").decode() for s in secs if s[2] <= rva < s[2] + s[1]), "?")
if i < 0: print("%s: before every symbol (section %s)" % (arg, sec)); continue
print("%s: %s+0x%X (section %s)" % (arg, table[i][1], rva - table[i][0], sec))
main()
+2 -1
View File
@@ -61,6 +61,8 @@ int uya_main(int argc, char **argv)
setvbuf(stdout, 0, _IONBF, 0);
atexit(plat_at_exit);
uya_log_open();
uya_log("Project Solana: starting");
plat_install_trap_handler(); /* first: a crash from here on is logged */
{ /* renderer=gl|soft in the cfg (UYA_RENDERER overrides it) */
extern const char *uya_renderer_cfg;
const char *r = cfg_value("renderer");
@@ -75,7 +77,6 @@ int uya_main(int argc, char **argv)
uya_log("the PS2 address space could not be mapped");
return 1;
}
plat_install_trap_handler();
{
const char *iso = find_iso(argc, argv);
if (!iso || uya_disc_open(iso) != 0) { uya_log("the disc image could not be opened"); return 1; }
+98 -10
View File
@@ -13,19 +13,51 @@
extern int plat_gl_active; /* the GS presents with OpenGL (below) */
/* Something other than the game holds a PS2 address: say what, and stop (taking the memory
* over would break whatever owns it: once it was this DLL's own code, relocated there). */
static int map_conflict(uint32_t addr, uint32_t size, uint32_t at, const MEMORY_BASIC_INFORMATION *mi)
{
char who[MAX_PATH] = "memory of another program loaded into the game (an overlay or similar)", msg[1024];
if (mi && mi->Type == MEM_IMAGE && GetModuleFileNameA((HMODULE)mi->AllocationBase, who, sizeof who)) {}
uya_log("cannot map PS2 memory 0x%08X+0x%X: 0x%08X is in use by %s", addr, size, at, who);
snprintf(msg, sizeof msg, "The game needs the memory at 0x%08X for the PS2, but it is in use by:\n\n%s\n\n"
"If this is a program injected into games (an overlay, recorder or RGB tool), close it or exclude "
"rac1_wrapper.exe from it, then try again.", at, who);
MessageBoxA(0, msg, "Project Solana", MB_ICONERROR);
return -1;
}
int plat_map_fixed(uint32_t addr, uint32_t size, int exec)
{
DWORD prot = exec ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
void *p = VirtualAlloc((void *)(uintptr_t)addr, size, MEM_RESERVE | MEM_COMMIT, prot);
if (p == (void *)(uintptr_t)addr) return 0;
/* already there? (PS2 RAM comes from the launcher's image section) */
MEMORY_BASIC_INFORMATION mi;
uint32_t a = addr;
while (a < addr + size) {
if (!VirtualQuery((void *)(uintptr_t)a, &mi, sizeof mi) || mi.State != MEM_COMMIT) {
uya_log("cannot map 0x%08X+0x%X (0x%08X is in use)", addr, size, a);
static int said;
if (!said++) { /* (normally 0x40000000; elsewhere when Windows relocated it) */
MEMORY_BASIC_INFORMATION me;
if (VirtualQuery((void *)plat_map_fixed, &me, sizeof me)) uya_log("rac1_game.dll loaded at %p", me.AllocationBase);
if ((uintptr_t)me.AllocationBase < 0x40000000u) { /* (rac1_wrapper.exe keeps it above: see launcher_win32.c) */
char msg[512];
snprintf(msg, sizeof msg, "rac1_game.dll was loaded at 0x%08X, but the game needs it at 0x40000000 or above "
"(lower addresses belong to the PS2). Please report this, with rac1_log.txt.", (unsigned)(uintptr_t)me.AllocationBase);
uya_log("%s", msg);
MessageBoxA(0, msg, "Project Solana", MB_ICONERROR);
return -1;
}
}
MEMORY_BASIC_INFORMATION mi;
void *p = 0;
if (VirtualQuery((void *)(uintptr_t)addr, &mi, sizeof mi)) {
if (mi.State == MEM_RESERVE && mi.AllocationBase == (void *)(uintptr_t)addr && mi.RegionSize >= size)
p = VirtualAlloc((void *)(uintptr_t)addr, size, MEM_COMMIT, prot); /* rac1_wrapper.exe reserved it before loading this DLL */
else if (mi.State == MEM_FREE)
p = VirtualAlloc((void *)(uintptr_t)addr, size, MEM_RESERVE | MEM_COMMIT, prot);
}
if (p == (void *)(uintptr_t)addr) return 0;
/* already there: only PS2 RAM, which is the wrapper's own image section (.ps2ram) */
HMODULE self = GetModuleHandleA(0);
uint32_t a = addr;
while (a < addr + size) {
if (!VirtualQuery((void *)(uintptr_t)a, &mi, sizeof mi)) return map_conflict(addr, size, a, 0);
if (mi.State != MEM_COMMIT || mi.AllocationBase != (void *)self) return map_conflict(addr, size, a, &mi);
DWORD old;
VirtualProtect(mi.BaseAddress, mi.RegionSize, prot, &old);
a = (uint32_t)(uintptr_t)mi.BaseAddress + (uint32_t)mi.RegionSize;
@@ -60,6 +92,38 @@ void plat_at_exit(void)
}
}
/* the function at rva in rac1_game.sym (written by build.py: "RVA name" per line, sorted),
* read with Win32 calls only: the C library may be what crashed */
static int sym_lookup(uint32_t rva, char *name, int cap, uint32_t *start)
{
char p[MAX_PATH + 32];
DWORD n = GetModuleFileNameA(0, p, MAX_PATH);
while (n && p[n - 1] != '\\') n--;
lstrcpyA(p + n, "rac1_game.sym");
HANDLE f = CreateFileA(p, GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, 0, 0);
if (f == INVALID_HANDLE_VALUE) return 0;
DWORD sz = GetFileSize(f, 0), got = 0;
char *b = sz && sz < (64u << 20) ? VirtualAlloc(0, sz + 1, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE) : 0;
int ok = b && ReadFile(f, b, sz, &got, 0) && got == sz;
CloseHandle(f);
if (!ok) { if (b) VirtualFree(b, 0, MEM_RELEASE); return 0; }
b[sz] = '\n';
const char *best = 0; uint32_t ba = 0;
for (char *q = b; q < b + sz;) {
uint32_t a = 0; char *s = q;
for (; s < q + 8; s++) a = a * 16 + (uint32_t)(*s <= '9' ? *s - '0' : (*s | 32) - 'a' + 10);
if (a > rva) break;
best = s + 1; ba = a;
while (*q != '\n') q++;
q++;
}
int k = 0;
if (best) for (; best[k] != '\n' && best[k] != '\r' && k < cap - 1; k++) name[k] = best[k];
name[k] = 0; *start = ba;
VirtualFree(b, 0, MEM_RELEASE);
return k > 0;
}
static LONG CALLBACK on_exception(EXCEPTION_POINTERS *ep)
{
EXCEPTION_RECORD *er = ep->ExceptionRecord;
@@ -72,8 +136,32 @@ static LONG CALLBACK on_exception(EXCEPTION_POINTERS *ep)
c->Eip = ret; c->Esp += 4;
return EXCEPTION_CONTINUE_EXECUTION;
}
uya_log("crash: exception 0x%08lX at 0x%08X, fault address 0x%08lX", er->ExceptionCode, at,
er->NumberParameters > 1 ? (unsigned long)er->ExceptionInformation[1] : 0ul);
static volatile LONG in_crash;
if (InterlockedExchange(&in_crash, 1)) ExitProcess(1); /* (a crash while reporting one) */
{ /* where: module+offset, the form crash_lookup.py and Windows' event log use */
char mod[MAX_PATH] = "?"; MEMORY_BASIC_INFORMATION mi;
uintptr_t base = 0;
if (VirtualQuery(er->ExceptionAddress, &mi, sizeof mi) && mi.Type == MEM_IMAGE) {
base = (uintptr_t)mi.AllocationBase;
if (GetModuleFileNameA((HMODULE)base, mod, sizeof mod)) { char *s = strrchr(mod, '\\'); if (s) memmove(mod, s + 1, strlen(s)); }
}
uint32_t off = (uint32_t)((uintptr_t)er->ExceptionAddress - base);
uya_log("crash: exception 0x%08lX at 0x%08X (%s+0x%X), fault address 0x%08lX", er->ExceptionCode, at, base ? mod : "no module",
off, er->NumberParameters > 1 ? (unsigned long)er->ExceptionInformation[1] : 0ul);
char fn[256]; uint32_t fs;
MEMORY_BASIC_INFORMATION me; /* (this DLL: is the crash in it?) */
if (base && VirtualQuery((void *)sym_lookup, &me, sizeof me) && me.AllocationBase == (void *)base && sym_lookup(off, fn, sizeof fn, &fs))
uya_log("crash: in %s+0x%X", fn, off - fs);
/* the return addresses on the stack, named: the call chain */
const uint32_t *sp = (const uint32_t *)(uintptr_t)c->Esp;
for (int i = 0, shown = 0; i < 256 && shown < 12 && !IsBadReadPtr(sp + i, 4); i++) {
uintptr_t v = sp[i];
if (base && v > base && v < base + 0x10000000 && VirtualQuery((void *)v, &me, sizeof me) && me.AllocationBase == (void *)base &&
(me.Protect & 0xF0) && sym_lookup((uint32_t)(v - base), fn, sizeof fn, &fs)) {
uya_log("crash: called from %s+0x%X", fn, (uint32_t)(v - base) - fs); shown++;
}
}
}
uya_log("crash: thread %lu, eax %08lX ecx %08lX edx %08lX ebx %08lX esi %08lX edi %08lX esp %08lX ebp %08lX",
GetCurrentThreadId(), c->Eax, c->Ecx, c->Edx, c->Ebx, c->Esi, c->Edi, c->Esp, c->Ebp);
{ /* the stack: return addresses near its top, then the frame chain */
+30 -2
View File
@@ -25,6 +25,8 @@ __declspec(dllimport) int __stdcall DeleteFileA(const char *);
__declspec(dllimport) int __stdcall MoveFileExA(const char *, const char *, unsigned);
__declspec(dllimport) void *__stdcall VirtualAlloc(void *, unsigned long, unsigned, unsigned);
__declspec(dllimport) int __stdcall VirtualFree(void *, unsigned long, unsigned);
typedef struct { void *base, *alloc_base; unsigned alloc_prot; unsigned long size; unsigned state, prot, type; } mbi_t;
__declspec(dllimport) unsigned long __stdcall VirtualQuery(const void *, mbi_t *, unsigned long);
__declspec(dllimport) unsigned __stdcall GetModuleFileNameA(HMODULE, char *, unsigned);
#define INVALID ((HANDLE)(long)-1)
@@ -48,7 +50,7 @@ static void path(char *out, const char *dir, const char *name, int i)
static void fail(const char *m) { MessageBoxA(0, m, "RAC1", 0x10); ExitProcess(1); }
/* rac1_game.dll from its parts, when a manifest says they are there */
static void join_parts(void)
static __attribute__((noinline)) void join_parts(void)
{
char dir[600], man[700], tmp[700], dll[700], nm[700];
unsigned dl = GetModuleFileNameA(0, dir, 520);
@@ -93,10 +95,36 @@ static void join_parts(void)
void __stdcall launcher_start(void)
{
/* The PS2's fixed addresses beyond RAM (EE registers, VU memory, GS registers, scratchpad),
* reserved before rac1_game.dll loads: if Windows relocates the DLL (Mandatory ASLR, or its
* address taken), it must not land on them. The runtime commits them (plat_map_fixed). */
VirtualAlloc((void *)0x10000000, 0x10000, 0x2000 /* MEM_RESERVE */, 1 /* PAGE_NOACCESS */);
VirtualAlloc((void *)0x11000000, 0x10000, 0x2000, 1);
VirtualAlloc((void *)0x12000000, 0x2000, 0x2000, 1);
VirtualAlloc((void *)0x70000000, 0x4000, 0x2000, 1);
join_parts();
/* The runtime takes addresses below 0x40000000 for PS2 ones (RAM, its uncached mirrors at
* 0x20000000-0x3FFFFFFF, the registers), so the DLL must load at or above 0x40000000 (its own
* base, unless something holds it or Windows forces relocation). While it loads, every free
* range below that is reserved, then released: Windows can only place it higher. */
void *held[64]; unsigned nheld = 0;
for (unsigned a = 0x00010000; a < 0x40000000 && nheld < 64;) {
mbi_t m;
if (!VirtualQuery((void *)a, &m, sizeof m)) break;
unsigned end = (unsigned)m.base + (unsigned)m.size;
if (end <= a) break;
if (m.state == 0x10000 /* MEM_FREE */) {
unsigned lo = (a + 0xFFFF) & ~0xFFFFu, hi = end > 0x40000000 ? 0x40000000 : end;
void *r = lo < hi ? VirtualAlloc((void *)lo, hi - lo, 0x2000, 1) : 0;
if (r) held[nheld++] = r;
}
a = end;
}
HMODULE h = LoadLibraryA("rac1_game.dll");
while (nheld) VirtualFree(held[--nheld], 0, 0x8000 /* MEM_RELEASE */);
if (!h) {
MessageBoxA(0, "rac1_game.dll was not found next to rac1_wrapper.exe.", "UYA Wrapper", 0x10);
MessageBoxA(0, "rac1_game.dll could not be loaded. It should be next to rac1_wrapper.exe; if it is, "
"Windows found no room for it above address 0x40000000.", "Project Solana", 0x10);
ExitProcess(1);
}
uya_main_fn f = (uya_main_fn)GetProcAddress(h, "uya_main_win32");
+60
View File
@@ -0,0 +1,60 @@
"""pdb_syms.py: the public symbols of a Windows DLL, from its .pdb (no other tools needed).
build.py writes rac1_game.sym with it after linking: one line per function, "RVA name"
in hex, sorted. The launcher deletes the .pdb (hundreds of MB) but keeps the .sym, and
the crash handler names the function a crash is in from it (in rac1_log.txt)."""
import struct
def sections(dll):
"""(name, virtual size, RVA) of each section of a PE file"""
with open(dll, "rb") as fh: d = fh.read(4096)
pe = struct.unpack_from("<I", d, 0x3C)[0]
n, opt = struct.unpack_from("<H", d, pe + 6)[0], struct.unpack_from("<H", d, pe + 20)[0]
o = pe + 24 + opt
return [struct.unpack_from("<8sII", d, o + i * 40) for i in range(n)]
def _streams(path):
with open(path, "rb") as fh: f = fh.read()
if not f.startswith(b"Microsoft C/C++ MSF 7.00\r\n\x1aDS\0\0\0"): raise ValueError("not a PDB: " + path)
bs, _, _, dirbytes, _, dirmap = struct.unpack_from("<6I", f, 32)
nb = lambda n: (n + bs - 1) // bs
blk = lambda i: f[i * bs:(i + 1) * bs]
mapblocks = struct.unpack_from("<%dI" % nb(dirbytes), blk(dirmap))
dirdata = b"".join(blk(b) for b in mapblocks)[:dirbytes]
ns = struct.unpack_from("<I", dirdata)[0]
sizes = struct.unpack_from("<%dI" % ns, dirdata, 4)
pos, out = 4 + 4 * ns, []
for s in sizes:
s = 0 if s == 0xFFFFFFFF else s
bl = struct.unpack_from("<%dI" % nb(s), dirdata, pos); pos += 4 * nb(s)
out.append(b"".join(blk(b) for b in bl)[:s])
return out
def symbols(dll, pdb):
"""sorted (RVA, name) of the public symbols of dll"""
st = _streams(pdb)
rec = st[struct.unpack_from("<H", st[3], 20)[0]] # DBI header: the symbol record stream
secs = sections(dll)
p, out = 0, []
while p + 4 <= len(rec):
ln, kind = struct.unpack_from("<HH", rec, p)
if kind == 0x110E: # S_PUB32: flags, offset, segment, name
_, off, seg = struct.unpack_from("<IIH", rec, p + 4)
if 0 < seg <= len(secs):
name = rec[p + 14:rec.index(b"\0", p + 14)].decode("utf-8", "replace")
out.append((secs[seg - 1][2] + off, name[1:] if name.startswith("_") else name))
p += 2 + ln
return sorted(out)
def write_sym(dll, pdb, path):
"""rac1_game.sym: the code symbols, "RVA name" per line"""
text = [s for s in sections(dll) if s[0].rstrip(b"\0") == b".text"]
lo, hi = (text[0][2], text[0][2] + text[0][1]) if text else (0, 1 << 32)
syms = [(a, n) for a, n in symbols(dll, pdb) if lo <= a < hi]
with open(path, "w", newline="\n") as f:
for a, n in syms: f.write("%08x %s\n" % (a, n))
return len(syms)