From a6dd949df44371a85d82ea5873ae5c929cf0de6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 18 Aug 2026 13:58:03 +0200 Subject: [PATCH] Load ELF debug info regardless of the symbol auto-save setting bAutoSaveLoadSymbols is about writing .ppsym files back out and reading them in again. It had also come to gate reading debug info that's simply sitting next to the game, which is a different thing and shouldn't need asking for: the main ELF's own symbols were already loaded unconditionally, but the companion ELF's symbols and all line info were not. Now the ELF is always the baseline - main or companion, symbols and line info - and the setting only adds the .ppsym half on top of it. Line info also loads from the module being loaded, not just from a companion, so an ELF launched directly brings its own. A PRX has no .debug section for it to find (prxgen strips them), so that's a cheap no-op for the usual EBOOT case, which the companion path still covers. That second source needs the two shapes distinguished, so AddModule takes an explicit address delta rather than assuming a base: a companion links at zero and wants the module's base added, while an ELF loaded at the addresses it asked for already has final ones (bRelocate is just e_type != ET_EXEC). Rows that don't land inside the module after that are dropped either way, which is a better check than the old "offset smaller than the module" one. Splitting the companion's identity check out of the symbol loader lets line info reuse it, and drops an accidental requirement along the way: it used to reject any companion without a symbol table, so an ELF built with -g but stripped of its symbols would have contributed no line numbers either. Verified with --auto-save-load-symbols off: CrossCraft's companion app.elf loads 3734 symbols and 98383 line rows where it previously loaded neither. The direct-ELF path is not verified at runtime - it needs a bootable ELF that carries DWARF, and there isn't one to hand. Both candidates here (pspautotests' .elf builds and CrossCraft's own app.elf) are linked at address 0 and fail to boot on that alone, which is pre-existing loader behaviour and nothing to do with this. pspautotests 314/314, UnitTest 55/55. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9 --- Core/Debugger/LineInfo.cpp | 15 ++++---- Core/Debugger/LineInfo.h | 12 ++++--- Core/ELF/ElfReader.cpp | 68 ++++++++++++++++++++++++------------ Core/ELF/ElfReader.h | 8 +++-- Core/HLE/sceKernelModule.cpp | 21 ++++++++--- 5 files changed, 84 insertions(+), 40 deletions(-) diff --git a/Core/Debugger/LineInfo.cpp b/Core/Debugger/LineInfo.cpp index 16526b4b2a..412ff99bec 100644 --- a/Core/Debugger/LineInfo.cpp +++ b/Core/Debugger/LineInfo.cpp @@ -148,7 +148,7 @@ enum { // One .debug_line unit: header, then a bytecode program that walks a virtual machine whose output // rows are (address, file, line). See the DWARF spec, "Line Number Information". static bool ParseUnit(Reader &r, size_t unitEnd, std::vector *entries, - std::vector *files, std::map *fileIndex, u32 moduleStart, u32 moduleSize) { + std::vector *files, std::map *fileIndex, u32 moduleStart, u32 moduleSize, u32 addressDelta) { const uint16_t version = r.U16(); if (version < 2 || version > 4) { // v5 rewrote the file table to use form-coded entries, which is a different parser. Nothing @@ -208,12 +208,13 @@ static bool ParseUnit(Reader &r, size_t unitEnd, std::vector *entries bool sawAddress = false; auto emit = [&](u32 lineNo) { - // Rows before a DW_LNE_set_address belong to no real code; and anything outside the module - // we're relocating into would only produce bogus lookups. - if (!sawAddress || address > moduleSize) + // Rows before a DW_LNE_set_address belong to no real code, and anything that doesn't land + // inside the module after relocation would only produce bogus lookups. + const u32 finalAddress = addressDelta + address; + if (!sawAddress || finalAddress < moduleStart || finalAddress - moduleStart >= moduleSize) return; LineEntry e; - e.address = moduleStart + address; + e.address = finalAddress; e.line = lineNo; e.fileIndex = file < unitFiles.size() ? unitFiles[file] : 0; entries->push_back(e); @@ -282,7 +283,7 @@ static bool ParseUnit(Reader &r, size_t unitEnd, std::vector *entries return r.ok(); } -int LineInfoMap::AddModule(std::string_view elfData, u32 moduleStart, u32 moduleSize) { +int LineInfoMap::AddModule(std::string_view elfData, u32 moduleStart, u32 moduleSize, u32 addressDelta) { RemoveModule(moduleStart, moduleSize); const uint8_t *data = (const uint8_t *)elfData.data(); @@ -345,7 +346,7 @@ int LineInfoMap::AddModule(std::string_view elfData, u32 moduleStart, u32 module if (unitEnd > debugLine->sh_size) break; - if (!ParseUnit(r, unitEnd, &mod.entries, &mod.files, &fileIndex, moduleStart, moduleSize)) + if (!ParseUnit(r, unitEnd, &mod.entries, &mod.files, &fileIndex, moduleStart, moduleSize, addressDelta)) skippedUnits++; r.Seek(unitEnd); } diff --git a/Core/Debugger/LineInfo.h b/Core/Debugger/LineInfo.h index 61f013fe18..e42bebad4f 100644 --- a/Core/Debugger/LineInfo.h +++ b/Core/Debugger/LineInfo.h @@ -45,10 +45,14 @@ struct LineEntry { class LineInfoMap { public: - // Parses .debug_line out of an unstripped ELF image and relocates it to moduleStart. - // Returns the number of rows kept, or 0 if the ELF has nothing usable. Replaces whatever was - // held for the same module. - int AddModule(std::string_view elfData, u32 moduleStart, u32 moduleSize); + // Parses .debug_line out of an unstripped ELF image. Returns the number of rows kept, or 0 if + // the ELF has nothing usable. Replaces whatever was held for the same module. + // + // addressDelta is added to every address in the table, and rows that don't then land inside + // the module are dropped. That covers both shapes this arrives in: a companion ELF links at + // zero, so the delta is the module's base; an ELF launched directly and loaded where it asked + // to be already has final addresses, so the delta is zero. + int AddModule(std::string_view elfData, u32 moduleStart, u32 moduleSize, u32 addressDelta); // Keyed the same way SymbolMap::UnloadModule is, so that unloading one module drops only its // own lines. Each module owns its rows and its file names outright - there's no shared table diff --git a/Core/ELF/ElfReader.cpp b/Core/ELF/ElfReader.cpp index 961a83b9bd..068e06c3cb 100644 --- a/Core/ELF/ElfReader.cpp +++ b/Core/ELF/ElfReader.cpp @@ -830,47 +830,62 @@ bool ElfReader::LoadSymbols() // 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) { +// Does this ELF describe the module we just loaded? Split out from the symbol loader so line info +// can reuse it: the two want the same identity check but different sections, and an ELF built with +// -g but stripped of its symbol table still has usable line numbers. +static bool CompanionElfMatchesModule(const std::string &data, u32 moduleSize, const char **why) { *why = "too small"; if (data.size() < sizeof(Elf32_Ehdr)) - return 0; + return false; 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; + return false; if (header->e_ident[EI_CLASS] != ELFCLASS32) - return 0; + return false; *why = "no section headers"; if (!header->e_shoff || header->e_shentsize < sizeof(Elf32_Shdr)) - return 0; + return false; 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); - }; + return false; // 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); + const Elf32_Shdr *s = (const Elf32_Shdr *)(data.data() + header->e_shoff + (size_t)i * header->e_shentsize); if (s->sh_addr) top = std::max(top, s->sh_addr + s->sh_size); - if (s->sh_type == SHT_SYMTAB) + } + *why = "image size doesn't match the loaded module"; + if (top > moduleSize || top + 0x1000 < moduleSize) + return false; + + *why = "ok"; + return true; +} + +static int LoadSymbolsFromCompanion(const std::string &data, u32 moduleBase, u32 moduleSize, const char **why) { + if (!CompanionElfMatchesModule(data, moduleSize, why)) + return 0; + + const Elf32_Ehdr *header = (const Elf32_Ehdr *)data.data(); + auto section = [&](int i) { + return (const Elf32_Shdr *)(data.data() + header->e_shoff + (size_t)i * header->e_shentsize); + }; + + int symtabIndex = -1; + for (int i = 0; i < header->e_shnum; i++) { + if (section(i)->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)) @@ -915,7 +930,7 @@ static int LoadSymbolsFromCompanion(const std::string &data, u32 moduleBase, u32 return added; } -int LoadCompanionElfSymbols(const Path &gameFile, u32 moduleBase, u32 moduleSize) { +int LoadCompanionElfDebugInfo(const Path &gameFile, u32 moduleBase, u32 moduleSize) { if (gameFile.empty() || gameFile.Type() != PathType::NATIVE) return 0; @@ -931,16 +946,23 @@ int LoadCompanionElfSymbols(const Path &gameFile, u32 moduleBase, u32 moduleSize if (!File::ReadBinaryFileToString(file.fullName, &data)) continue; const char *why = ""; + if (!CompanionElfMatchesModule(data, moduleSize, &why)) { + DEBUG_LOG(Log::Loader, "Companion ELF '%s' skipped: %s", file.name.c_str(), why); + continue; + } + + // A companion links at base 0, so its line table needs the module's base added. + const int lines = g_lineInfo.AddModule(data, moduleBase, moduleSize, moduleBase); + 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()); + if (added > 0) g_symbolMap->SortSymbols(); - // Same file, already validated as belonging to this module, so its DWARF line table - // relocates the same way. Absent from anything built without -g, which is fine. - g_lineInfo.AddModule(data, moduleBase, moduleSize); + + if (lines > 0 || added > 0) { + INFO_LOG(Log::Loader, "Companion ELF '%s': %d symbols, %d line rows", file.name.c_str(), added, lines); return added; } - DEBUG_LOG(Log::Loader, "Companion ELF '%s' skipped: %s", file.name.c_str(), why); + DEBUG_LOG(Log::Loader, "Companion ELF '%s' matched but had nothing usable: %s", file.name.c_str(), why); } return 0; } diff --git a/Core/ELF/ElfReader.h b/Core/ELF/ElfReader.h index 2ded92d77f..132a7f8669 100644 --- a/Core/ELF/ElfReader.h +++ b/Core/ELF/ElfReader.h @@ -174,5 +174,9 @@ private: // 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); +// Symbols and DWARF line info both come out of that file, and both load unconditionally - +// bAutoSaveLoadSymbols governs writing .ppsym files back out, not reading debug info that's +// already sitting next to the game. Same reason the main ELF's own symbols aren't gated either. +// +// Returns the number of symbols added, 0 if none were or no matching ELF was found. +int LoadCompanionElfDebugInfo(const Path &gameFile, u32 moduleBase, u32 moduleSize); diff --git a/Core/HLE/sceKernelModule.cpp b/Core/HLE/sceKernelModule.cpp index 1483864e16..9adbc9f92f 100644 --- a/Core/HLE/sceKernelModule.cpp +++ b/Core/HLE/sceKernelModule.cpp @@ -1371,15 +1371,28 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load if (module->memoryBlockAddr != 0) { g_symbolMap->AddModule(moduleName, module->memoryBlockAddr, module->memoryBlockSize, module->crc); + + // Line info out of the module we just loaded. That covers an ELF launched directly - + // pspautotests' .elf builds, or homebrew you built yourself - where the debug sections are + // right here in the file. A PRX has none (prxgen strips every .debug section), so it's a + // cheap no-op for the usual EBOOT case, which the companion below handles instead. + // A relocated module's ELF addresses are relative to where it ended up; one loaded at the + // addresses it asked for already has final ones. + const u32 lineDelta = reader.DidRelocate() ? reader.GetVaddr() : 0; + g_lineInfo.AddModule(std::string_view((const char *)ptr, elfSize), module->memoryBlockAddr, module->memoryBlockSize, lineDelta); + + // 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 LoadCompanionElfDebugInfo. + LoadCompanionElfDebugInfo(PSP_CoreParameter().fileToStart, module->memoryBlockAddr, module->memoryBlockSize); + + // Only the .ppsym files follow the setting - it's about writing symbols back out, not about + // reading debug info that's already sitting next to the game. if (g_Config.bAutoSaveLoadSymbols) { int idx = g_symbolMap->GetModuleIndexByName(moduleName); 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); } }