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 \