mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-01 14:58:14 +00:00
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_<address>. 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 <[email protected]> Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
This commit is contained in:
1 parent
d4e2382e1a
commit
d72623c4a0
4 files changed
+137
-3
No files matched your search
@@ -349,9 +349,13 @@ A working invocation, and the traps around it:
|
|||||||
|
|
||||||
## Debugging a game that works on hardware but not in PPSSPP
|
## 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`
|
First, turn on `bAutoSaveLoadSymbols` (`--auto-save-load-symbols` in headless): when homebrew ships its
|
||||||
alongside `app.prx`, common for Zig/Rust/SDK homebrew), **the pre-link ELF is a ground-truth oracle for anything the
|
unstripped ELF next to the EBOOT (`app.elf` alongside `app.prx`, common for Zig/Rust/SDK homebrew), PPSSPP loads
|
||||||
loader computes**. It still has the symbol table (so an address can be turned into a
|
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
|
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
|
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
|
this pairing produces land inside the section its symbol belongs to" turned a guess into a measurement over 8589
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
|
|
||||||
#include "Common/StringUtils.h"
|
#include "Common/StringUtils.h"
|
||||||
#include "Common/Thread/ParallelLoop.h"
|
#include "Common/Thread/ParallelLoop.h"
|
||||||
|
#include "Common/File/DirListing.h"
|
||||||
|
#include "Common/File/FileUtil.h"
|
||||||
|
|
||||||
#include "Core/MemMap.h"
|
#include "Core/MemMap.h"
|
||||||
#include "Core/Reporting.h"
|
#include "Core/Reporting.h"
|
||||||
@@ -824,3 +826,117 @@ bool ElfReader::LoadSymbols()
|
|||||||
}
|
}
|
||||||
return hasSymbols;
|
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_<address> 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<File::FileInfo> 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;
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include "Common/CommonTypes.h"
|
#include "Common/CommonTypes.h"
|
||||||
|
#include "Common/File/Path.h"
|
||||||
#include "Core/ELF/PSPElfTypes.h"
|
#include "Core/ELF/PSPElfTypes.h"
|
||||||
|
|
||||||
enum {
|
enum {
|
||||||
@@ -166,3 +167,12 @@ private:
|
|||||||
size_t size_ = 0;
|
size_t size_ = 0;
|
||||||
u32 firstSegAlign = 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_<address>.
|
||||||
|
// 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);
|
||||||
@@ -1373,6 +1373,10 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load
|
|||||||
if (idx > 0) {
|
if (idx > 0) {
|
||||||
g_symbolMap->LoadModuleSymbols(idx, SymbolMap::GetModuleSymbolsPath(moduleName, module->crc));
|
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_<address>. See LoadCompanionElfSymbols.
|
||||||
|
LoadCompanionElfSymbols(PSP_CoreParameter().fileToStart, module->memoryBlockAddr, module->memoryBlockSize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user