From c5e7c4890ab61ddf2370dcef404823ff9d3707cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 18 Aug 2026 12:31:42 +0200 Subject: [PATCH] Debugger: decode DWARF line info, and show source locations Homebrew commonly ships its unstripped ELF next to the EBOOT, which is already how the symbol loader turns z_un_08841f98 into a function name. That same ELF carries a DWARF .debug_line section, so the addresses can be mapped to source files and lines too - and a backtrace stops being four hex numbers: 08841f98 move sp,fp mesh.zig:163 0883afa4 li v0,0x0 MenuState.zig:821 088260d8 andi at,v0,0xFFFF State.zig:40 0882a27c andi at,v0,0xFFFF engine.zig:468 Surfaced in three places: per frame in hle.backtrace, in the "hit" object that cpu.breakpoint.hit and cpu.stepping share, and appended to the disassembly window's status bar. The breakpoint case keys on the pc rather than the address, since for a memory breakpoint the useful source location is the instruction that did the access, not the data it touched. Storage is a plain sorted table of absolute addresses per module. SymbolMap keeps module-relative addresses because its .ppsym files are meant to be reloaded by a different game that pulls in the same module; none of this is ever written anywhere - it's regenerated from the ELF each boot - so there'd be nothing for relative addresses to buy. Each module owns its own rows and file names outright and is keyed the way SymbolMap::UnloadModule is, so unloading one module drops its lines and nobody else's. The subtle part is end-of-sequence markers. Without them a lookup for an address in a gap - a compilation unit built without debug info - confidently reports the last line of an unrelated file. A prototype run over one test binary mis-attributed 70 of its 349 functions that way, so sequence ends are recorded as rows with line 0 and a lookup landing on one reports nothing instead. DWARF 2 through 4 are decoded (psp-gcc emits 2, Zig 4). Version 5 re-encoded the file table, so those units are skipped with a warning rather than mis-parsed - nothing targeting the PSP produces it today. Scope, since it's narrower than it sounds: PRX conversion strips every .debug section. I checked all 437 pspautotests .prx and CrossCraft's own app.prx - none have any. Of 24 installed homebrew EBOOTs, zero carry debug info; CrossCraft only does because it ships app.elf separately. So this helps someone developing homebrew, and does nothing at all for a commercial game. Costs about 1.2 MB for a large Zig binary (98383 rows, 438 files) and nothing for anything without debug info. Follows bAutoSaveLoadSymbols like the symbols do. pspautotests 314/314, UnitTest 55/55, CoreUWP builds. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9 --- Core/CMakeLists.txt | 2 + Core/Core.vcxproj | 2 + Core/Core.vcxproj.filters | 6 + Core/Debugger/LineInfo.cpp | 421 ++++++++++++++++++ Core/Debugger/LineInfo.h | 81 ++++ .../WebSocket/BreakpointSubscriber.cpp | 14 + Core/Debugger/WebSocket/HLESubscriber.cpp | 17 + Core/ELF/ElfReader.cpp | 4 + Core/HLE/sceKernelModule.cpp | 3 + UI/ImDebugger/ImDisasmView.cpp | 8 + UWP/CoreUWP/CoreUWP.vcxproj | 2 + UWP/CoreUWP/CoreUWP.vcxproj.filters | 2 + android/jni/Android.mk | 1 + docs/WebSocketDebugger.md | 26 ++ libretro/Makefile.common | 1 + 15 files changed, 590 insertions(+) create mode 100644 Core/Debugger/LineInfo.cpp create mode 100644 Core/Debugger/LineInfo.h diff --git a/Core/CMakeLists.txt b/Core/CMakeLists.txt index f2106e43a5..59eb9c83f1 100644 --- a/Core/CMakeLists.txt +++ b/Core/CMakeLists.txt @@ -284,6 +284,8 @@ add_library(Core STATIC Debugger/Breakpoints.cpp Debugger/Breakpoints.h Debugger/DebugInterface.h + Debugger/LineInfo.cpp + Debugger/LineInfo.h Debugger/MemBlockInfo.cpp Debugger/MemBlockInfo.h Debugger/SymbolMap.cpp diff --git a/Core/Core.vcxproj b/Core/Core.vcxproj index 2b4c3acb22..c9694ce3fb 100644 --- a/Core/Core.vcxproj +++ b/Core/Core.vcxproj @@ -441,6 +441,7 @@ + @@ -982,6 +983,7 @@ + diff --git a/Core/Core.vcxproj.filters b/Core/Core.vcxproj.filters index 83ea1b310b..94946d503c 100644 --- a/Core/Core.vcxproj.filters +++ b/Core/Core.vcxproj.filters @@ -967,6 +967,9 @@ MIPS\fake + + Debugger + Debugger @@ -2112,6 +2115,9 @@ MIPS\fake + + Debugger + Debugger diff --git a/Core/Debugger/LineInfo.cpp b/Core/Debugger/LineInfo.cpp new file mode 100644 index 0000000000..16526b4b2a --- /dev/null +++ b/Core/Debugger/LineInfo.cpp @@ -0,0 +1,421 @@ +// Copyright (c) 2026- PPSSPP Project. + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 2.0 or later versions. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License 2.0 for more details. + +// A copy of the GPL 2.0 should have been included with the program. +// If not, see http://www.gnu.org/licenses/ + +// Official git repository and contact information can be found at +// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. + +#include +#include +#include + +#include "Common/Log.h" +#include "Common/StringUtils.h" +#include "Core/Debugger/LineInfo.h" +#include "Core/ELF/ElfReader.h" + +LineInfoMap g_lineInfo; + +namespace { + +// Bounds-checked cursor over the section. Everything here is parsing a file we didn't produce, so +// a truncated or malformed one has to fail rather than read past the end - Fail() latches, and +// every read after it returns zero, which lets the parse loops check once at the end instead of +// after every field. +class Reader { +public: + Reader(const uint8_t *data, size_t size) : data_(data), size_(size) {} + + bool ok() const { return ok_; } + size_t pos() const { return pos_; } + void Seek(size_t pos) { + if (pos > size_) + Fail(); + else + pos_ = pos; + } + bool AtEnd(size_t limit) const { return pos_ >= limit; } + + uint8_t U8() { + if (pos_ + 1 > size_) + return Fail(); + return data_[pos_++]; + } + uint16_t U16() { + if (pos_ + 2 > size_) + return Fail(); + uint16_t v; + memcpy(&v, data_ + pos_, 2); + pos_ += 2; + return v; + } + uint32_t U32() { + if (pos_ + 4 > size_) + return Fail(); + uint32_t v; + memcpy(&v, data_ + pos_, 4); + pos_ += 4; + return v; + } + uint64_t ULEB() { + uint64_t result = 0; + int shift = 0; + for (int i = 0; i < 10; i++) { + const uint8_t b = U8(); + if (!ok_) + return 0; + if (shift < 64) + result |= (uint64_t)(b & 0x7f) << shift; + shift += 7; + if (!(b & 0x80)) + return result; + } + return Fail(); + } + int64_t SLEB() { + int64_t result = 0; + int shift = 0; + for (int i = 0; i < 10; i++) { + const uint8_t b = U8(); + if (!ok_) + return 0; + if (shift < 64) + result |= (int64_t)(b & 0x7f) << shift; + shift += 7; + if (!(b & 0x80)) { + if (shift < 64 && (b & 0x40)) + result -= (int64_t)1 << shift; + return result; + } + } + return Fail(); + } + std::string Str() { + const size_t start = pos_; + while (pos_ < size_ && data_[pos_]) + pos_++; + if (pos_ >= size_) { + Fail(); + return std::string(); + } + std::string s((const char *)data_ + start, pos_ - start); + pos_++; + return s; + } + +private: + uint32_t Fail() { + ok_ = false; + pos_ = size_; + return 0; + } + + const uint8_t *data_; + size_t size_; + size_t pos_ = 0; + bool ok_ = true; +}; + +// Standard opcode numbers we act on. The rest are skipped generically using the header's operand +// counts, which is what makes an unknown-but-well-formed producer harmless. +enum { + DW_LNS_copy = 1, + DW_LNS_advance_pc = 2, + DW_LNS_advance_line = 3, + DW_LNS_set_file = 4, + DW_LNS_set_column = 5, + DW_LNS_negate_stmt = 6, + DW_LNS_set_basic_block = 7, + DW_LNS_const_add_pc = 8, + DW_LNS_fixed_advance_pc = 9, + DW_LNE_end_sequence = 1, + DW_LNE_set_address = 2, + DW_LNE_define_file = 3, +}; + +} // namespace + +// 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) { + 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 + // that targets the PSP emits it today (psp-gcc is on 2, Zig on 4), so skip rather than + // risk decoding it wrong. + return false; + } + + const uint32_t headerLength = r.U32(); + const size_t programStart = r.pos() + headerLength; + + const uint8_t minInstLength = r.U8(); + if (version >= 4) + r.U8(); // maximum_operations_per_instruction + const uint8_t defaultIsStmt = r.U8(); + (void)defaultIsStmt; + const int8_t lineBase = (int8_t)r.U8(); + const uint8_t lineRange = r.U8(); + const uint8_t opcodeBase = r.U8(); + if (!r.ok() || lineRange == 0 || minInstLength == 0 || opcodeBase == 0) + return false; + + std::vector stdOpcodeLengths(opcodeBase > 0 ? opcodeBase - 1 : 0); + for (size_t i = 0; i < stdOpcodeLengths.size(); i++) + stdOpcodeLengths[i] = r.U8(); + + // include_directories, then file_names - each a list terminated by an empty string. We only + // keep the names; the directory index is ignored, since a bare file name is what's actually + // readable in a status bar or a backtrace. + while (r.ok() && !r.Str().empty()) { + } + // Index 0 is unused in DWARF < 5; entries start at 1. + std::vector unitFiles{ 0 }; + while (r.ok()) { + const std::string name = r.Str(); + if (name.empty()) + break; + r.ULEB(); // directory index + r.ULEB(); // modification time + r.ULEB(); // length + // Deduped across units so a header included by 500 files is stored once. + auto it = fileIndex->find(name); + if (it == fileIndex->end()) { + it = fileIndex->insert({ name, (u32)files->size() }).first; + files->push_back(name); + } + unitFiles.push_back(it->second); + } + if (!r.ok()) + return false; + + r.Seek(programStart); + + u32 address = 0; + u32 file = 1; + int line = 1; + 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) + return; + LineEntry e; + e.address = moduleStart + address; + e.line = lineNo; + e.fileIndex = file < unitFiles.size() ? unitFiles[file] : 0; + entries->push_back(e); + }; + + while (r.ok() && !r.AtEnd(unitEnd)) { + const uint8_t op = r.U8(); + if (op >= opcodeBase) { + // Special opcode: one byte encoding both an address advance and a line delta. + const uint8_t adjusted = op - opcodeBase; + address += (adjusted / lineRange) * minInstLength; + line += lineBase + (adjusted % lineRange); + emit(line > 0 ? (u32)line : 1); + } else if (op == 0) { + // Extended opcode, length-prefixed so unknown ones can be skipped. + const uint64_t length = r.ULEB(); + const size_t next = r.pos() + (size_t)length; + const uint8_t sub = r.U8(); + if (sub == DW_LNE_end_sequence) { + // line 0 terminates the sequence - see the comment on LineEntry::line. + emit(0); + address = 0; + file = 1; + line = 1; + sawAddress = false; + } else if (sub == DW_LNE_set_address) { + address = r.U32(); + sawAddress = true; + } + r.Seek(next); + } else { + switch (op) { + case DW_LNS_copy: + emit(line > 0 ? (u32)line : 1); + break; + case DW_LNS_advance_pc: + address += (u32)r.ULEB() * minInstLength; + break; + case DW_LNS_advance_line: + line += (int)r.SLEB(); + break; + case DW_LNS_set_file: + file = (u32)r.ULEB(); + break; + case DW_LNS_set_column: + r.ULEB(); + break; + case DW_LNS_negate_stmt: + case DW_LNS_set_basic_block: + break; + case DW_LNS_const_add_pc: + address += ((255 - opcodeBase) / lineRange) * minInstLength; + break; + case DW_LNS_fixed_advance_pc: + address += r.U16(); + break; + default: + // Known length, unknown meaning - skip its operands and carry on. + for (uint8_t i = 0; op - 1 < (int)stdOpcodeLengths.size() && i < stdOpcodeLengths[op - 1]; i++) + r.ULEB(); + break; + } + } + } + + return r.ok(); +} + +int LineInfoMap::AddModule(std::string_view elfData, u32 moduleStart, u32 moduleSize) { + RemoveModule(moduleStart, moduleSize); + + const uint8_t *data = (const uint8_t *)elfData.data(); + const size_t size = elfData.size(); + if (size < sizeof(Elf32_Ehdr)) + return 0; + if (data[0] != ELFMAG0 || data[1] != ELFMAG1 || data[2] != ELFMAG2 || data[3] != ELFMAG3) + return 0; + + const Elf32_Ehdr *header = (const Elf32_Ehdr *)data; + if (header->e_shoff == 0 || header->e_shnum == 0 || header->e_shentsize < sizeof(Elf32_Shdr)) + return 0; + if ((size_t)header->e_shoff + (size_t)header->e_shnum * header->e_shentsize > size) + return 0; + if (header->e_shstrndx >= header->e_shnum) + return 0; + + auto section = [&](int i) { + return (const Elf32_Shdr *)(data + header->e_shoff + (size_t)i * header->e_shentsize); + }; + + const Elf32_Shdr *shstr = section(header->e_shstrndx); + if ((size_t)shstr->sh_offset + shstr->sh_size > size) + return 0; + + const Elf32_Shdr *debugLine = nullptr; + for (int i = 0; i < header->e_shnum; i++) { + const Elf32_Shdr *s = section(i); + if (s->sh_name >= shstr->sh_size) + continue; + const char *name = (const char *)data + shstr->sh_offset + s->sh_name; + if (!strcmp(name, ".debug_line")) { + debugLine = s; + break; + } + } + if (!debugLine || debugLine->sh_size == 0) + return 0; + if ((size_t)debugLine->sh_offset + debugLine->sh_size > size) + return 0; + + ModuleLines mod; + mod.start = moduleStart; + mod.size = moduleSize; + mod.files.push_back(""); + std::map fileIndex; + + Reader r(data + debugLine->sh_offset, debugLine->sh_size); + int skippedUnits = 0; + while (r.ok() && !r.AtEnd(debugLine->sh_size)) { + const size_t unitStart = r.pos(); + const uint32_t unitLength = r.U32(); + if (!r.ok() || unitLength == 0) + break; + // 0xfffffff0 and up are reserved; 0xffffffff introduces 64-bit DWARF, which nothing here + // produces, and misreading it would walk off into nonsense. + if (unitLength >= 0xfffffff0) + break; + const size_t unitEnd = unitStart + 4 + unitLength; + if (unitEnd > debugLine->sh_size) + break; + + if (!ParseUnit(r, unitEnd, &mod.entries, &mod.files, &fileIndex, moduleStart, moduleSize)) + skippedUnits++; + r.Seek(unitEnd); + } + + if (skippedUnits > 0) { + WARN_LOG(Log::Loader, "Line info: skipped %d compilation unit(s) - unsupported DWARF version?", skippedUnits); + } + if (mod.entries.empty()) + return 0; + + // Sorted for binary search. Rows at the same address are collapsed to the last one, which is + // what a debugger wants: the innermost/most recent statement wins, and an end-of-sequence + // marker sharing an address with the next sequence's first row loses to it. + std::stable_sort(mod.entries.begin(), mod.entries.end(), [](const LineEntry &a, const LineEntry &b) { + return a.address < b.address; + }); + mod.entries.erase(std::unique(mod.entries.begin(), mod.entries.end(), [](const LineEntry &a, const LineEntry &b) { + return a.address == b.address; + }), mod.entries.end()); + + const int count = (int)mod.entries.size(); + INFO_LOG(Log::Loader, "Line info: %d rows across %d files for module at %08x", + count, (int)mod.files.size() - 1, moduleStart); + modules_.push_back(std::move(mod)); + return count; +} + +void LineInfoMap::RemoveModule(u32 moduleStart, u32 moduleSize) { + modules_.erase(std::remove_if(modules_.begin(), modules_.end(), [&](const ModuleLines &m) { + return m.start == moduleStart && m.size == moduleSize; + }), modules_.end()); +} + +void LineInfoMap::Clear() { + modules_.clear(); +} + +const LineInfoMap::ModuleLines *LineInfoMap::FindModule(u32 address) const { + for (const ModuleLines &m : modules_) { + if (address >= m.start && address < m.start + m.size) + return &m; + } + return nullptr; +} + +bool LineInfoMap::Lookup(u32 address, std::string *file, int *line) const { + const ModuleLines *mod = FindModule(address); + if (!mod) + return false; + + // The row describing an address is the last one at or before it. + auto it = std::upper_bound(mod->entries.begin(), mod->entries.end(), address, + [](u32 addr, const LineEntry &e) { return addr < e.address; }); + if (it == mod->entries.begin()) + return false; + --it; + if (it->line == 0) + return false; // Past the end of that sequence - see LineEntry::line. + + if (file) + *file = it->fileIndex < mod->files.size() ? mod->files[it->fileIndex] : mod->files[0]; + if (line) + *line = (int)it->line; + return true; +} + +std::string LineInfoMap::LookupString(u32 address) const { + std::string file; + int line = 0; + if (!Lookup(address, &file, &line)) + return std::string(); + return StringFromFormat("%s:%d", file.c_str(), line); +} diff --git a/Core/Debugger/LineInfo.h b/Core/Debugger/LineInfo.h new file mode 100644 index 0000000000..61f013fe18 --- /dev/null +++ b/Core/Debugger/LineInfo.h @@ -0,0 +1,81 @@ +// Copyright (c) 2026- PPSSPP Project. + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 2.0 or later versions. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License 2.0 for more details. + +// A copy of the GPL 2.0 should have been included with the program. +// If not, see http://www.gnu.org/licenses/ + +// Official git repository and contact information can be found at +// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. + +#pragma once + +#include +#include +#include + +#include "Common/CommonTypes.h" + +// Source line information decoded from an ELF's DWARF .debug_line section. +// +// Only available where an unstripped ELF is: PRX conversion drops every .debug section, so this +// never applies to a commercial game, and in practice it means homebrew that ships its app.elf +// next to the EBOOT (the same thing the companion symbol loader relies on). +// +// Addresses are stored absolute, relocated to wherever the module was loaded. Unlike SymbolMap, +// which keeps module-relative addresses so a saved .ppsym can be reloaded by a different game that +// pulls in the same module, none of this is ever written anywhere - it's regenerated from the ELF +// on every boot - so there'd be nothing for relative addresses to buy. +struct LineEntry { + u32 address; + // 0 marks the end of a sequence: the address is one past the last instruction the preceding + // rows describe. Without these, a lookup for an address in a gap (a compilation unit built + // without debug info, say) silently reports the last line of an unrelated source file - it + // mis-attributed 70 of 349 functions in one test binary before they were recorded. + u32 line; + u32 fileIndex; +}; + +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); + + // 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 + // for an unload to have to pick apart. + void RemoveModule(u32 moduleStart, u32 moduleSize); + void Clear(); + + bool IsEmpty() const { return modules_.empty(); } + + // The source location of the instruction at this address. False when no loaded module owns the + // address, or when it falls in a gap between sequences. + bool Lookup(u32 address, std::string *file, int *line) const; + + // "file.c:123", or empty if unknown. For status bars and log lines. + std::string LookupString(u32 address) const; + +private: + struct ModuleLines { + u32 start = 0; + u32 size = 0; + std::vector files; + std::vector entries; // Sorted by address. + }; + + const ModuleLines *FindModule(u32 address) const; + + std::vector modules_; +}; + +extern LineInfoMap g_lineInfo; diff --git a/Core/Debugger/WebSocket/BreakpointSubscriber.cpp b/Core/Debugger/WebSocket/BreakpointSubscriber.cpp index 4dbe2d43d0..99071cb1cf 100644 --- a/Core/Debugger/WebSocket/BreakpointSubscriber.cpp +++ b/Core/Debugger/WebSocket/BreakpointSubscriber.cpp @@ -19,6 +19,7 @@ #include "Core/Core.h" #include "Core/Debugger/Breakpoints.h" #include "Core/Debugger/DisassemblyManager.h" +#include "Core/Debugger/LineInfo.h" #include "Core/Debugger/SymbolMap.h" #include "Core/Debugger/WebSocket/BreakpointSubscriber.h" #include "Core/Debugger/WebSocket/WebSocketUtils.h" @@ -54,6 +55,19 @@ void WriteBreakpointHit(JsonWriter &json, const BreakpointHit &hit) { else json.writeString("symbol", symbol); + // Only when the game shipped an unstripped ELF with DWARF in it - see LineInfo.h. Keyed on pc + // rather than address, since for a memory breakpoint the interesting source location is the + // instruction that did the access, not the data it touched. + std::string file; + int line = 0; + if (g_lineInfo.Lookup(hit.pc, &file, &line)) { + json.writeString("file", file); + json.writeInt("line", line); + } else { + json.writeNull("file"); + json.writeNull("line"); + } + if (hit.kind == BreakpointKind::Memory) { json.writeInt("size", hit.size); json.writeString("access", hit.write ? "write" : "read"); diff --git a/Core/Debugger/WebSocket/HLESubscriber.cpp b/Core/Debugger/WebSocket/HLESubscriber.cpp index 1278c6c880..9a54963d85 100644 --- a/Core/Debugger/WebSocket/HLESubscriber.cpp +++ b/Core/Debugger/WebSocket/HLESubscriber.cpp @@ -23,6 +23,7 @@ #include "Core/ELF/ParamSFO.h" #include "Common/File/FileUtil.h" #include "Core/Debugger/DisassemblyManager.h" +#include "Core/Debugger/LineInfo.h" #include "Core/Debugger/SymbolMap.h" #include "Core/Debugger/WebSocket/HLESubscriber.h" #include "Core/Debugger/WebSocket/WebSocketUtils.h" @@ -815,6 +816,9 @@ void WebSocketHLEGameLoadSymbols(DebuggerRequest &req) { // - sp: unsigned integer stack address in this func (beware of alloca().) // - stackSize: integer size of stack frame. // - code: string disassembly of pc, empty if pc isn't readable. +// - file: string source file name, or null when there's no line info for this address. +// - line: integer source line number, or null. Only ever available when the game shipped an +// unstripped ELF - PRX conversion drops DWARF, so this is a homebrew-only luxury. // // A real stack walk needs to recognize the function it starts in, so it comes back empty exactly // when execution has gone somewhere unexpected - a jump through a bad pointer, say - which is @@ -896,6 +900,19 @@ void WebSocketHLEBacktrace(DebuggerRequest &req) { json.writeString("code", ""); } + // Only present when the game shipped an unstripped ELF to read DWARF out of, which in + // practice means homebrew - see Core/Debugger/LineInfo.h. A backtrace is where this + // pays off most: four addresses versus four source locations. + std::string file; + int line = 0; + if (g_lineInfo.Lookup(f.pc, &file, &line)) { + json.writeString("file", file); + json.writeInt("line", line); + } else { + json.writeNull("file"); + json.writeNull("line"); + } + json.pop(); } json.pop(); diff --git a/Core/ELF/ElfReader.cpp b/Core/ELF/ElfReader.cpp index b95940b781..961a83b9bd 100644 --- a/Core/ELF/ElfReader.cpp +++ b/Core/ELF/ElfReader.cpp @@ -27,6 +27,7 @@ #include "Core/MIPS/MIPSTables.h" #include "Core/ELF/ElfReader.h" #include "Core/Debugger/MemBlockInfo.h" +#include "Core/Debugger/LineInfo.h" #include "Core/Debugger/SymbolMap.h" #include "Core/HLE/ErrorCodes.h" #include "Core/HLE/sceKernelMemory.h" @@ -934,6 +935,9 @@ int LoadCompanionElfSymbols(const Path &gameFile, u32 moduleBase, u32 moduleSize if (added > 0) { INFO_LOG(Log::Loader, "Loaded %d symbols from companion ELF '%s'", added, file.name.c_str()); 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); return added; } DEBUG_LOG(Log::Loader, "Companion ELF '%s' skipped: %s", file.name.c_str(), why); diff --git a/Core/HLE/sceKernelModule.cpp b/Core/HLE/sceKernelModule.cpp index fefd8bb69b..1483864e16 100644 --- a/Core/HLE/sceKernelModule.cpp +++ b/Core/HLE/sceKernelModule.cpp @@ -56,6 +56,7 @@ #include "Core/PSPLoaders.h" #include "Core/System.h" #include "Core/MemMapHelpers.h" +#include "Core/Debugger/LineInfo.h" #include "Core/Debugger/SymbolMap.h" #include "Core/HLE/sceKernel.h" #include "Core/HLE/sceKernelModule.h" @@ -210,6 +211,8 @@ PSPModule::~PSPModule() { userMemory.Free(memoryBlockAddr); } g_symbolMap->UnloadModule(memoryBlockAddr, memoryBlockSize); + // Keyed identically, so a module going away takes its own line info and nothing else's. + g_lineInfo.RemoveModule(memoryBlockAddr, memoryBlockSize); } if (modulePtr.ptr) { diff --git a/UI/ImDebugger/ImDisasmView.cpp b/UI/ImDebugger/ImDisasmView.cpp index 0442d6e3a9..a5d061e23b 100644 --- a/UI/ImDebugger/ImDisasmView.cpp +++ b/UI/ImDebugger/ImDisasmView.cpp @@ -16,6 +16,7 @@ #include "Core/MIPS/MIPSAsm.h" #include "Core/HW/Display.h" #include "Core/Reporting.h" +#include "Core/Debugger/LineInfo.h" #include "Core/Debugger/SymbolMap.h" #include "Core/MemMap.h" #include "Common/System/Request.h" @@ -977,6 +978,13 @@ void ImDisasmView::updateStatusBarText() { if (!label.empty()) { statusBarText_ = label; } + + // Appended rather than replacing, since knowing which instruction you're on is still the point. + // Empty for anything without an unstripped ELF to read DWARF from - see LineInfo.h. + const std::string source = g_lineInfo.LookupString(curAddress_); + if (!source.empty()) { + statusBarText_ += " " + source; + } } u32 ImDisasmView::yToAddress(float y) { diff --git a/UWP/CoreUWP/CoreUWP.vcxproj b/UWP/CoreUWP/CoreUWP.vcxproj index 386a560f9f..da01877e66 100644 --- a/UWP/CoreUWP/CoreUWP.vcxproj +++ b/UWP/CoreUWP/CoreUWP.vcxproj @@ -98,6 +98,7 @@ + @@ -375,6 +376,7 @@ + diff --git a/UWP/CoreUWP/CoreUWP.vcxproj.filters b/UWP/CoreUWP/CoreUWP.vcxproj.filters index 7d65f6d17d..5834076ba8 100644 --- a/UWP/CoreUWP/CoreUWP.vcxproj.filters +++ b/UWP/CoreUWP/CoreUWP.vcxproj.filters @@ -11,6 +11,7 @@ + @@ -431,6 +432,7 @@ + diff --git a/android/jni/Android.mk b/android/jni/Android.mk index 9847580b1d..b3d9294826 100644 --- a/android/jni/Android.mk +++ b/android/jni/Android.mk @@ -654,6 +654,7 @@ EXEC_AND_LIB_FILES := \ $(SRC)/Core/WebServer.cpp \ $(SRC)/Core/Debugger/Breakpoints.cpp \ $(SRC)/Core/Debugger/DisassemblyManager.cpp \ + $(SRC)/Core/Debugger/LineInfo.cpp \ $(SRC)/Core/Debugger/MemBlockInfo.cpp \ $(SRC)/Core/Debugger/SymbolMap.cpp \ $(SRC)/Core/Debugger/WebSocket.cpp \ diff --git a/docs/WebSocketDebugger.md b/docs/WebSocketDebugger.md index f259aab430..7ad9503a3d 100644 --- a/docs/WebSocketDebugger.md +++ b/docs/WebSocketDebugger.md @@ -132,6 +132,31 @@ way for periodic GPU stats. `client.config.set` in the same file carries per-connection settings that aren't about broadcasts - currently just `acknowledgeDeferred`, described under "Message protocol" above. +### Source line info + +Where a game shipped an unstripped ELF, PPSSPP decodes its DWARF `.debug_line` +and can map an address to a source file and line. `cpu.breakpoint.hit` and +`cpu.stepping` carry `file`/`line` in the `hit` object, and `hle.backtrace` +carries them per frame - which is where it pays off most: + +``` +08841f98 move sp,fp mesh.zig:163 +0883afa4 li v0,0x0 MenuState.zig:821 +088260d8 andi at,v0,0xFFFF State.zig:40 +0882a27c andi at,v0,0xFFFF engine.zig:468 +``` + +Both fields are `null` when there's no line info for that address, which is the +common case: **PRX conversion strips every `.debug` section**, so this never +applies to a commercial game. In practice it means homebrew that ships its +`app.elf` next to the EBOOT - the same file the companion symbol loader uses - +or a plain `.elf` you built yourself. It follows `bAutoSaveLoadSymbols` along +with the symbols. + +DWARF 2, 3 and 4 are decoded; version 5 units are skipped with a log line rather +than mis-parsed, since it re-encoded the file table. Nothing targeting the PSP +emits it today (psp-gcc produces 2, Zig 4). + ### Emulation speed `game.speed.set` drives two independent things: @@ -211,6 +236,7 @@ Fields common to every kind: | `logged` / `paused` | Which actions it had - `paused` false means the CPU kept running | | `condition` | The condition expression, or `null` | | `symbol` | Symbol at `address`, or `null` - resolved here to save a round trip | +| `file` / `line` | Source location of `pc`, or `null`. See "Source line info" below | | `breakpoint` | `{start, end}` identifying which breakpoint fired. Absent for `"register"`, whose identity is the register, not an address | Extra fields for `"memory"`: diff --git a/libretro/Makefile.common b/libretro/Makefile.common index e0b3ca3dbf..6c49b5bc10 100644 --- a/libretro/Makefile.common +++ b/libretro/Makefile.common @@ -725,6 +725,7 @@ SOURCES_CXX += \ $(COREDIR)/Instance.cpp \ $(COREDIR)/Debugger/Breakpoints.cpp \ $(COREDIR)/Debugger/SymbolMap.cpp \ + $(COREDIR)/Debugger/LineInfo.cpp \ $(COREDIR)/Debugger/MemBlockInfo.cpp \ $(COREDIR)/Dialog/PSPDialog.cpp \ $(COREDIR)/Dialog/PSPGamedataInstallDialog.cpp \