From d72623c4a0477fbd8ac3403856d6d7893a2b139c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 17 Aug 2026 20:10:24 +0200 Subject: [PATCH] Load symbols from the unstripped ELF homebrew ships next to its EBOOT Homebrew almost always ships the ELF it was built from alongside the EBOOT - app.elf next to app.prx - but prxgen strips the symbol table on the way to the PRX, so the module PPSSPP loads has no names at all and MIPSAnalyst calls every function it finds z_un_
. Working out what any of them are meant hand- parsing that ELF with a throwaway script, which is how the CrossCraft relocation bug got identified. So read it directly. On module load, scan the game's own directory for an ELF with a symbol table and add its STT_FUNC/STT_OBJECT entries at the module's base. CrossCraft picks up 3734 symbols, and the disassembly turns from z_un_088c00f0 into world.init_empty, with static_allocator.alloc at the vtable entry it calls - the two functions that took the longest to identify by hand. Matching is the part worth getting right, since a wrong match puts confident nonsense at real addresses, which beats having no names only in the sense that it's worse. A candidate has to be a 32-bit ELF with a symbol table whose highest section ends within a page of the loaded module's size - the companion links at base 0 and covers the same image, so that's a tight check, and unrelated ELFs sitting in the same folder fail it. Symbols outside the module are skipped individually too. Names go in with updateName, so they win over the analyzer's placeholders rather than losing to whichever got there first. Gated on the existing bAutoSaveLoadSymbols setting (off by default), which already means "keep symbol names around for me" and avoids a directory scan per module load otherwise. pspautotests 314/314. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9 --- AGENTS.md | 10 ++- Core/ELF/ElfReader.cpp | 116 +++++++++++++++++++++++++++++++++++ Core/ELF/ElfReader.h | 10 +++ Core/HLE/sceKernelModule.cpp | 4 ++ 4 files changed, 137 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 96429b84b7..0de18cd3e1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -349,9 +349,13 @@ A working invocation, and the traps around it: ## Debugging a game that works on hardware but not in PPSSPP -One technique paid for itself twice over here. When a homebrew ELF is shipped next to the EBOOT (`app.elf` -alongside `app.prx`, common for Zig/Rust/SDK homebrew), **the pre-link ELF is a ground-truth oracle for anything the -loader computes**. It still has the symbol table (so an address can be turned into a +First, turn on `bAutoSaveLoadSymbols` (`--auto-save-load-symbols` in headless): when homebrew ships its +unstripped ELF next to the EBOOT (`app.elf` alongside `app.prx`, common for Zig/Rust/SDK homebrew), PPSSPP loads +the function and data names out of it, so the disassembly reads `world.init_empty` instead of `z_un_088c00f0`. +prxgen strips the symbol table on the way to the PRX, which is why the loaded module has none of its own. + +That same file is also a ground-truth oracle for anything the loader computes. It still has the symbol table (so +an address can be turned into a function name) and the full `.rel.*` sections *with symbol indices*, which the PRX format throws away. That makes it possible to check the emulator's work exhaustively offline - for the HI16/LO16 relocation bug, "does the address this pairing produces land inside the section its symbol belongs to" turned a guess into a measurement over 8589 diff --git a/Core/ELF/ElfReader.cpp b/Core/ELF/ElfReader.cpp index 37e9830df1..b95940b781 100644 --- a/Core/ELF/ElfReader.cpp +++ b/Core/ELF/ElfReader.cpp @@ -19,6 +19,8 @@ #include "Common/StringUtils.h" #include "Common/Thread/ParallelLoop.h" +#include "Common/File/DirListing.h" +#include "Common/File/FileUtil.h" #include "Core/MemMap.h" #include "Core/Reporting.h" @@ -824,3 +826,117 @@ bool ElfReader::LoadSymbols() } return hasSymbols; } + +// Adds the STT_FUNC/STT_OBJECT symbols from one candidate ELF, if it looks like it belongs to a +// module of this size. Returns the number added, 0 if it doesn't match or has nothing to offer. +static int LoadSymbolsFromCompanion(const std::string &data, u32 moduleBase, u32 moduleSize, const char **why) { + *why = "too small"; + if (data.size() < sizeof(Elf32_Ehdr)) + return 0; + const Elf32_Ehdr *header = (const Elf32_Ehdr *)data.data(); + *why = "not an ELF"; + if (header->e_ident[EI_MAG0] != ELFMAG0 || header->e_ident[EI_MAG1] != ELFMAG1 + || header->e_ident[EI_MAG2] != ELFMAG2 || header->e_ident[EI_MAG3] != ELFMAG3) + return 0; + if (header->e_ident[EI_CLASS] != ELFCLASS32) + return 0; + + *why = "no section headers"; + if (!header->e_shoff || header->e_shentsize < sizeof(Elf32_Shdr)) + return 0; + if ((size_t)header->e_shoff + (size_t)header->e_shnum * header->e_shentsize > data.size()) + return 0; + + auto section = [&](int i) { + return (const Elf32_Shdr *)(data.data() + header->e_shoff + (size_t)i * header->e_shentsize); + }; + + // Identity check. The companion links at base 0 and covers the same image the module was + // built into, so the top of its highest section should land within a page of the module's + // size. Without this an unrelated ELF sitting in the same folder would happily contribute + // nonsense names at real addresses, which is worse than having none. + u32 top = 0; + int symtabIndex = -1; + for (int i = 0; i < header->e_shnum; i++) { + const Elf32_Shdr *s = section(i); + if (s->sh_addr) + top = std::max(top, s->sh_addr + s->sh_size); + if (s->sh_type == SHT_SYMTAB) + symtabIndex = i; + } + *why = "no symbol table"; + if (symtabIndex < 0) + return 0; + *why = "image size doesn't match the loaded module"; + if (top > moduleSize || top + 0x1000 < moduleSize) + return 0; + + const Elf32_Shdr *symtab = section(symtabIndex); + if (symtab->sh_link >= header->e_shnum || symtab->sh_entsize < sizeof(Elf32_Sym)) + return 0; + const Elf32_Shdr *strtab = section(symtab->sh_link); + if ((size_t)symtab->sh_offset + symtab->sh_size > data.size()) + return 0; + if ((size_t)strtab->sh_offset + strtab->sh_size > data.size()) + return 0; + + const char *strings = data.data() + strtab->sh_offset; + const int numSymbols = symtab->sh_size / symtab->sh_entsize; + int added = 0; + for (int i = 0; i < numSymbols; i++) { + const Elf32_Sym *sym = (const Elf32_Sym *)(data.data() + symtab->sh_offset + (size_t)i * symtab->sh_entsize); + if (!sym->st_size || sym->st_name >= strtab->sh_size) + continue; + if (sym->st_value > moduleSize) + continue; + const char *name = strings + sym->st_name; + if (!name[0]) + continue; + + const u32 addr = moduleBase + sym->st_value; + switch (sym->st_info & 0xF) { + case STT_FUNC: + // updateName: these are the names a human wrote, so they beat the analyzer's + // z_un_
placeholders rather than losing to whichever got there first. + g_symbolMap->AddFunction(name, addr, sym->st_size, -1, true); + added++; + break; + case STT_OBJECT: + g_symbolMap->AddData(addr, sym->st_size, DATATYPE_BYTE); + g_symbolMap->AddLabel(name, addr, -1, true); + added++; + break; + default: + break; + } + } + *why = added ? "ok" : "symbol table had nothing usable"; + return added; +} + +int LoadCompanionElfSymbols(const Path &gameFile, u32 moduleBase, u32 moduleSize) { + if (gameFile.empty() || gameFile.Type() != PathType::NATIVE) + return 0; + + const Path dir = gameFile.NavigateUp(); + std::vector files; + if (!File::GetFilesInDir(dir, &files, "elf:")) + return 0; + + for (const File::FileInfo &file : files) { + if (file.isDirectory || file.size < sizeof(Elf32_Ehdr) || file.size > 256 * 1024 * 1024) + continue; + std::string data; + if (!File::ReadBinaryFileToString(file.fullName, &data)) + continue; + const char *why = ""; + const int added = LoadSymbolsFromCompanion(data, moduleBase, moduleSize, &why); + if (added > 0) { + INFO_LOG(Log::Loader, "Loaded %d symbols from companion ELF '%s'", added, file.name.c_str()); + g_symbolMap->SortSymbols(); + return added; + } + DEBUG_LOG(Log::Loader, "Companion ELF '%s' skipped: %s", file.name.c_str(), why); + } + return 0; +} diff --git a/Core/ELF/ElfReader.h b/Core/ELF/ElfReader.h index ea8bc1a5da..2ded92d77f 100644 --- a/Core/ELF/ElfReader.h +++ b/Core/ELF/ElfReader.h @@ -19,6 +19,7 @@ #include #include "Common/CommonTypes.h" +#include "Common/File/Path.h" #include "Core/ELF/PSPElfTypes.h" enum { @@ -166,3 +167,12 @@ private: size_t size_ = 0; u32 firstSegAlign = 0; }; + +// Homebrew usually ships the unstripped ELF it was built from next to the EBOOT (app.elf beside +// app.prx, and similar) - prxgen strips the symbol table on the way to the PRX, so the module +// PPSSPP actually loads has no names in it and every function ends up called z_un_
. +// This looks for such a companion in the game's own directory and, if one plausibly belongs to +// this module, adds its function and data symbols at the module's load address. +// +// Returns the number of symbols added, 0 if no matching ELF was found. +int LoadCompanionElfSymbols(const Path &gameFile, u32 moduleBase, u32 moduleSize); diff --git a/Core/HLE/sceKernelModule.cpp b/Core/HLE/sceKernelModule.cpp index bdb7086491..fefd8bb69b 100644 --- a/Core/HLE/sceKernelModule.cpp +++ b/Core/HLE/sceKernelModule.cpp @@ -1373,6 +1373,10 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load if (idx > 0) { g_symbolMap->LoadModuleSymbols(idx, SymbolMap::GetModuleSymbolsPath(moduleName, module->crc)); } + // Homebrew commonly ships the unstripped ELF next to the EBOOT; prxgen strips the + // symbols out of the PRX we actually load, so without this every function in it is + // just z_un_
. See LoadCompanionElfSymbols. + LoadCompanionElfSymbols(PSP_CoreParameter().fileToStart, module->memoryBlockAddr, module->memoryBlockSize); } }