mirror of
https://github.com/stenzek/duckstation.git
synced 2026-11-07 09:36:00 -10:00
Qt: Add symbol support to debugger
Loads ELF/GNU ld map symbols next to the game or from a chosen file, showing labels and branch targets.
This commit is contained in:
18 files changed
+1153
-129
No files matched your search
@@ -13,8 +13,10 @@
|
||||
#include "common/log.h"
|
||||
#include "common/small_string.h"
|
||||
|
||||
#include <QtCore/QSignalBlocker>
|
||||
#include <QtCore/QTimer>
|
||||
#include <QtGui/QFontDatabase>
|
||||
#include <QtGui/QHelpEvent>
|
||||
#include <QtGui/QKeySequence>
|
||||
#include <QtGui/QMouseEvent>
|
||||
#include <QtGui/QPainter>
|
||||
@@ -23,6 +25,7 @@
|
||||
#include <QtWidgets/QApplication>
|
||||
#include <QtWidgets/QScrollBar>
|
||||
#include <QtWidgets/QStyleOption>
|
||||
#include <QtWidgets/QToolTip>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
@@ -45,16 +48,9 @@ DebuggerCodeView::DebuggerCodeView(QWidget* parent) : QAbstractScrollArea(parent
|
||||
|
||||
// Connect scroll bar
|
||||
connect(verticalScrollBar(), &QScrollBar::valueChanged, this, [this](int value) {
|
||||
const VirtualMemoryAddress new_top = getAddressForRow(value);
|
||||
// Clamp to valid range to prevent wrapping
|
||||
if (new_top >= m_code_region_start && new_top < m_code_region_end)
|
||||
{
|
||||
m_top_address = new_top;
|
||||
updateVisibleRange();
|
||||
// Recalculate arrows when scroll position changes
|
||||
calculateBranchArrows();
|
||||
viewport()->update();
|
||||
}
|
||||
m_top_row = value;
|
||||
calculateBranchArrows();
|
||||
viewport()->update();
|
||||
});
|
||||
|
||||
setFocusPolicy(Qt::StrongFocus);
|
||||
@@ -147,20 +143,78 @@ bool DebuggerCodeView::hasBreakpointAtAddress(VirtualMemoryAddress address) cons
|
||||
return std::find(m_breakpoints.begin(), m_breakpoints.end(), address) != m_breakpoints.end();
|
||||
}
|
||||
|
||||
void DebuggerCodeView::setSymbols(const SymbolFile::SymbolList* symbols)
|
||||
{
|
||||
m_symbols = symbols;
|
||||
rebuildLayout();
|
||||
calculateBranchArrows();
|
||||
viewport()->update();
|
||||
}
|
||||
|
||||
void DebuggerCodeView::rebuildLayout()
|
||||
{
|
||||
const VirtualMemoryAddress top_address = getAddressForRow(m_top_row);
|
||||
m_rows.clear();
|
||||
for (VirtualMemoryAddress address = m_code_region_start; address < m_code_region_end;
|
||||
address += CPU::INSTRUCTION_SIZE)
|
||||
{
|
||||
const VirtualMemoryAddress symbol_address =
|
||||
m_current_segment == CPU::Segment::KSEG2 ? address : CPU::VirtualAddressToPhysical(address);
|
||||
if (m_symbols && !SymbolFile::GetSymbolsAtAddress(*m_symbols, symbol_address).empty())
|
||||
m_rows.push_back({address, true});
|
||||
m_rows.push_back({address, false});
|
||||
}
|
||||
m_top_row = getRowForAddress(top_address);
|
||||
if (m_top_row > 0 && m_rows[m_top_row - 1].is_label)
|
||||
m_top_row--;
|
||||
updateScrollBars();
|
||||
}
|
||||
|
||||
QString DebuggerCodeView::getSymbolLabel(VirtualMemoryAddress address) const
|
||||
{
|
||||
QStringList names;
|
||||
if (m_symbols)
|
||||
{
|
||||
const VirtualMemoryAddress symbol_address =
|
||||
CPU::GetSegmentForAddress(address) == CPU::Segment::KSEG2 ? address : CPU::VirtualAddressToPhysical(address);
|
||||
for (const SymbolFile::Symbol& symbol : SymbolFile::GetSymbolsAtAddress(*m_symbols, symbol_address))
|
||||
{
|
||||
const QString name = QString::fromStdString(symbol.name);
|
||||
names.push_back(name);
|
||||
}
|
||||
}
|
||||
return names.join(u", "_s) + QLatin1Char(':');
|
||||
}
|
||||
|
||||
bool DebuggerCodeView::viewportEvent(QEvent* event)
|
||||
{
|
||||
if (event->type() == QEvent::ToolTip)
|
||||
{
|
||||
const auto* help = static_cast<QHelpEvent*>(event);
|
||||
const int row = m_top_row + help->pos().y() / m_row_height;
|
||||
if (row >= 0 && row < static_cast<int>(m_rows.size()) && m_rows[row].is_label)
|
||||
QToolTip::showText(help->globalPos(), getSymbolLabel(m_rows[row].address), viewport());
|
||||
else
|
||||
QToolTip::hideText();
|
||||
return true;
|
||||
}
|
||||
return QAbstractScrollArea::viewportEvent(event);
|
||||
}
|
||||
|
||||
int DebuggerCodeView::getRowForAddress(VirtualMemoryAddress address) const
|
||||
{
|
||||
if (address < m_code_region_start)
|
||||
if (m_rows.empty())
|
||||
return 0;
|
||||
if (address >= m_code_region_end)
|
||||
return static_cast<int>((m_code_region_end - m_code_region_start) / CPU::INSTRUCTION_SIZE) - 1;
|
||||
|
||||
return static_cast<int>((address - m_code_region_start) / CPU::INSTRUCTION_SIZE);
|
||||
address = std::clamp(address, m_code_region_start, m_code_region_end - CPU::INSTRUCTION_SIZE);
|
||||
const auto it = std::ranges::upper_bound(m_rows, address, {}, &CodeRow::address);
|
||||
return static_cast<int>(it - m_rows.begin()) - 1;
|
||||
}
|
||||
|
||||
VirtualMemoryAddress DebuggerCodeView::getAddressForRow(int row) const
|
||||
{
|
||||
const VirtualMemoryAddress address = m_code_region_start + (static_cast<u32>(row) * CPU::INSTRUCTION_SIZE);
|
||||
return std::clamp(address, m_code_region_start, m_code_region_end - CPU::INSTRUCTION_SIZE);
|
||||
if (m_rows.empty())
|
||||
return m_code_region_start;
|
||||
return m_rows[std::clamp(row, 0, static_cast<int>(m_rows.size()) - 1)].address;
|
||||
}
|
||||
|
||||
bool DebuggerCodeView::updateRegion(VirtualMemoryAddress address)
|
||||
@@ -186,7 +240,7 @@ bool DebuggerCodeView::updateRegion(VirtualMemoryAddress address)
|
||||
m_current_segment = segment;
|
||||
m_current_code_region = region.value();
|
||||
|
||||
updateScrollBars();
|
||||
rebuildLayout();
|
||||
viewport()->update();
|
||||
return true;
|
||||
}
|
||||
@@ -196,34 +250,19 @@ void DebuggerCodeView::scrollToAddress(VirtualMemoryAddress address, bool center
|
||||
address &= ~3u;
|
||||
ensureAddressVisible(address);
|
||||
|
||||
VirtualMemoryAddress old_top = m_top_address;
|
||||
|
||||
const int target_row = getRowForAddress(address);
|
||||
const int visible_rows = getVisibleRowCount();
|
||||
if (center)
|
||||
m_top_row = std::max(0, target_row - visible_rows / 2);
|
||||
else if (target_row < m_top_row || target_row >= m_top_row + visible_rows)
|
||||
{
|
||||
const int visible_rows = getVisibleRowCount();
|
||||
const int target_row = getRowForAddress(address);
|
||||
const int top_row = std::max(0, target_row - visible_rows / 2);
|
||||
m_top_address = getAddressForRow(top_row);
|
||||
}
|
||||
else
|
||||
{
|
||||
const VirtualMemoryAddress first_visible = getFirstVisibleAddress();
|
||||
const VirtualMemoryAddress last_visible = getLastVisibleAddress();
|
||||
|
||||
if (address < first_visible || address > last_visible)
|
||||
{
|
||||
m_top_address = address;
|
||||
}
|
||||
}
|
||||
|
||||
// Only recalculate arrows if scroll position actually changed
|
||||
if (m_top_address != old_top)
|
||||
{
|
||||
calculateBranchArrows();
|
||||
m_top_row = target_row;
|
||||
if (visible_rows > 1 && m_top_row > 0 && m_rows[m_top_row - 1].is_label)
|
||||
m_top_row--;
|
||||
}
|
||||
|
||||
updateScrollBars();
|
||||
updateVisibleRange();
|
||||
calculateBranchArrows();
|
||||
viewport()->update();
|
||||
}
|
||||
|
||||
@@ -267,7 +306,7 @@ void DebuggerCodeView::setSelectionEndAddress(VirtualMemoryAddress address)
|
||||
|
||||
VirtualMemoryAddress DebuggerCodeView::getAddressAtPoint(const QPoint& point) const
|
||||
{
|
||||
const int top_row = getRowForAddress(m_top_address);
|
||||
const int top_row = m_top_row;
|
||||
const int clicked_row = top_row + (point.y() / m_row_height);
|
||||
return getAddressForRow(clicked_row);
|
||||
}
|
||||
@@ -280,9 +319,10 @@ void DebuggerCodeView::paintEvent(QPaintEvent* event)
|
||||
const QRect visible_rect = event->rect();
|
||||
|
||||
// Calculate which rows are visible based on scroll position
|
||||
const int top_row = getRowForAddress(m_top_address);
|
||||
const int top_row = m_top_row;
|
||||
const int first_visible_row = top_row + (visible_rect.top() / m_row_height);
|
||||
const int last_visible_row = top_row + ((visible_rect.bottom() + m_row_height - 1) / m_row_height);
|
||||
const int last_visible_row =
|
||||
std::min(static_cast<int>(m_rows.size()) - 1, top_row + visible_rect.bottom() / m_row_height);
|
||||
|
||||
painter.fillRect(visible_rect, palette().base());
|
||||
|
||||
@@ -296,10 +336,26 @@ void DebuggerCodeView::paintEvent(QPaintEvent* event)
|
||||
if (y + m_row_height < visible_rect.top() || y > visible_rect.bottom())
|
||||
continue;
|
||||
|
||||
const bool is_selected = isAddressSelected(address);
|
||||
const bool is_pc = (address == m_last_pc);
|
||||
|
||||
drawInstruction(painter, address, y, is_selected, is_pc);
|
||||
if (m_rows[row].is_label)
|
||||
{
|
||||
painter.save();
|
||||
painter.fillRect(QRect(ADDRESS_COLUMN_START, y, viewport()->width() - ADDRESS_COLUMN_START, m_row_height),
|
||||
palette().alternateBase());
|
||||
painter.setPen(palette().mid().color());
|
||||
painter.drawLine(ADDRESS_COLUMN_START, y, viewport()->width(), y);
|
||||
QFont label_font = font();
|
||||
label_font.setBold(true);
|
||||
painter.setFont(label_font);
|
||||
painter.setPen(palette().link().color());
|
||||
const QString label = painter.fontMetrics().elidedText(
|
||||
getSymbolLabel(address), Qt::ElideRight, std::max(0, viewport()->width() - ADDRESS_COLUMN_START - 4));
|
||||
painter.drawText(ADDRESS_COLUMN_START + 2, y + painter.fontMetrics().ascent() + 1, label);
|
||||
painter.restore();
|
||||
}
|
||||
else
|
||||
{
|
||||
drawInstruction(painter, address, y, isAddressSelected(address), address == m_last_pc);
|
||||
}
|
||||
}
|
||||
|
||||
drawBranchArrows(painter, visible_rect);
|
||||
@@ -416,16 +472,28 @@ void DebuggerCodeView::drawInstruction(QPainter& painter, VirtualMemoryAddress a
|
||||
start_pos = end_pos;
|
||||
}
|
||||
|
||||
str.clear();
|
||||
if (is_pc)
|
||||
{
|
||||
str.clear();
|
||||
CPU::DisassembleInstructionComment(&str, address, instruction_bits);
|
||||
if (!str.empty())
|
||||
const CPU::Instruction instruction{instruction_bits};
|
||||
if (m_symbols && CPU::IsDirectBranchInstruction(instruction))
|
||||
{
|
||||
VirtualMemoryAddress target = CPU::GetDirectBranchTarget(instruction, address);
|
||||
if (CPU::GetSegmentForAddress(target) != CPU::Segment::KSEG2)
|
||||
target = CPU::VirtualAddressToPhysical(target);
|
||||
if (const SymbolFile::Symbol* symbol = SymbolFile::FindSymbol(*m_symbols, target))
|
||||
{
|
||||
painter.setPen(address_color);
|
||||
painter.drawText(COMMENT_COLUMN_START, y, QtUtils::StringViewToQString(str));
|
||||
str.append_format("{}<{}", str.empty() ? "" : "; ", symbol->name);
|
||||
if (target != symbol->address)
|
||||
str.append_format("+0x{:X}", target - symbol->address);
|
||||
str.append('>');
|
||||
}
|
||||
}
|
||||
if (!str.empty())
|
||||
{
|
||||
painter.setPen(address_color);
|
||||
painter.drawText(COMMENT_COLUMN_START, y, QtUtils::StringViewToQString(str));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -443,8 +511,9 @@ void DebuggerCodeView::calculateBranchArrows()
|
||||
|
||||
// Expand search range to include arrows that might be partially visible
|
||||
const VirtualMemoryAddress search_start =
|
||||
(first_visible > 64 * CPU::INSTRUCTION_SIZE) ? first_visible - (64 * CPU::INSTRUCTION_SIZE) : m_code_region_start;
|
||||
const VirtualMemoryAddress search_end = std::min(last_visible + (64 * CPU::INSTRUCTION_SIZE), m_code_region_end);
|
||||
std::max(m_code_region_start, first_visible - std::min(first_visible, 64 * CPU::INSTRUCTION_SIZE));
|
||||
const VirtualMemoryAddress search_end =
|
||||
last_visible + std::min(m_code_region_end - last_visible, 64 * CPU::INSTRUCTION_SIZE);
|
||||
|
||||
for (VirtualMemoryAddress addr = search_start; addr < search_end; addr += CPU::INSTRUCTION_SIZE)
|
||||
{
|
||||
@@ -477,7 +546,7 @@ void DebuggerCodeView::drawBranchArrows(QPainter& painter, const QRect& visible_
|
||||
if (m_branch_arrows.empty())
|
||||
return;
|
||||
|
||||
const int top_row = getRowForAddress(m_top_address);
|
||||
const int top_row = m_top_row;
|
||||
const int arrow_left = 2;
|
||||
const int arrow_right = ARROW_COLUMN_WIDTH - 2;
|
||||
const int viewport_height = viewport()->height();
|
||||
@@ -631,6 +700,12 @@ void DebuggerCodeView::mouseDoubleClickEvent(QMouseEvent* event)
|
||||
{
|
||||
if (event->button() == Qt::LeftButton)
|
||||
{
|
||||
const int row = m_top_row + event->pos().y() / m_row_height;
|
||||
if (row < 0 || row >= static_cast<int>(m_rows.size()) || m_rows[row].is_label)
|
||||
{
|
||||
event->accept();
|
||||
return;
|
||||
}
|
||||
const VirtualMemoryAddress address = getAddressAtPoint(event->pos());
|
||||
|
||||
const int x = event->pos().x();
|
||||
@@ -695,8 +770,11 @@ void DebuggerCodeView::keyPressEvent(QKeyEvent* event)
|
||||
return;
|
||||
}
|
||||
|
||||
const int total_rows = static_cast<int>((m_code_region_end - m_code_region_start) / CPU::INSTRUCTION_SIZE);
|
||||
const int new_row = std::clamp(getRowForAddress(m_selected_address) + row_delta, 0, total_rows - 1);
|
||||
if (m_rows.empty())
|
||||
return;
|
||||
int new_row = std::clamp(getRowForAddress(m_selected_address) + row_delta, 0, static_cast<int>(m_rows.size()) - 1);
|
||||
if (row_delta < 0 && new_row > 0 && m_rows[new_row].is_label)
|
||||
new_row--;
|
||||
const VirtualMemoryAddress new_address = getAddressForRow(new_row);
|
||||
scrollToAddress(new_address, false);
|
||||
if (event->modifiers() & Qt::ShiftModifier)
|
||||
@@ -711,25 +789,7 @@ void DebuggerCodeView::wheelEvent(QWheelEvent* event)
|
||||
const int delta = event->angleDelta().y();
|
||||
const int steps = delta / 120; // Standard wheel step
|
||||
|
||||
const u32 instruction_steps = static_cast<u32>(std::abs(steps) * CPU::INSTRUCTION_SIZE * 3);
|
||||
VirtualMemoryAddress old_top = m_top_address;
|
||||
VirtualMemoryAddress new_top = (steps >= 0) ?
|
||||
(instruction_steps >= m_top_address) ? 0 : (m_top_address - instruction_steps) :
|
||||
(m_top_address + instruction_steps);
|
||||
|
||||
// Clamp to valid range
|
||||
new_top = std::clamp(new_top, m_code_region_start, m_code_region_end - CPU::INSTRUCTION_SIZE);
|
||||
m_top_address = new_top;
|
||||
|
||||
// Only recalculate arrows if scroll position actually changed
|
||||
if (m_top_address != old_top)
|
||||
{
|
||||
calculateBranchArrows();
|
||||
}
|
||||
|
||||
updateScrollBars();
|
||||
updateVisibleRange();
|
||||
viewport()->update();
|
||||
verticalScrollBar()->setValue(verticalScrollBar()->value() - steps * 3);
|
||||
|
||||
event->accept();
|
||||
}
|
||||
@@ -738,41 +798,32 @@ void DebuggerCodeView::resizeEvent(QResizeEvent* event)
|
||||
{
|
||||
QAbstractScrollArea::resizeEvent(event);
|
||||
updateScrollBars();
|
||||
updateVisibleRange();
|
||||
// Recalculate arrows on resize since visible range changes
|
||||
calculateBranchArrows();
|
||||
}
|
||||
|
||||
void DebuggerCodeView::updateScrollBars()
|
||||
{
|
||||
const int total_rows = static_cast<int>((m_code_region_end - m_code_region_start) / CPU::INSTRUCTION_SIZE);
|
||||
const int visible_rows = getVisibleRowCount();
|
||||
const int max_value = std::max(0, total_rows - visible_rows);
|
||||
|
||||
const int max_value = std::max(0, static_cast<int>(m_rows.size()) - visible_rows);
|
||||
m_top_row = std::clamp(m_top_row, 0, max_value);
|
||||
const QSignalBlocker blocker(verticalScrollBar());
|
||||
verticalScrollBar()->setRange(0, max_value);
|
||||
verticalScrollBar()->setPageStep(visible_rows);
|
||||
|
||||
const int current_row = getRowForAddress(m_top_address);
|
||||
verticalScrollBar()->setValue(current_row);
|
||||
}
|
||||
|
||||
void DebuggerCodeView::updateVisibleRange()
|
||||
{
|
||||
m_visible_rows = getVisibleRowCount();
|
||||
verticalScrollBar()->setValue(m_top_row);
|
||||
}
|
||||
|
||||
int DebuggerCodeView::getVisibleRowCount() const
|
||||
{
|
||||
return viewport()->height() / m_row_height;
|
||||
return std::max(1, viewport()->height() / m_row_height);
|
||||
}
|
||||
|
||||
VirtualMemoryAddress DebuggerCodeView::getFirstVisibleAddress() const
|
||||
{
|
||||
return m_top_address;
|
||||
return getAddressForRow(m_top_row);
|
||||
}
|
||||
|
||||
VirtualMemoryAddress DebuggerCodeView::getLastVisibleAddress() const
|
||||
{
|
||||
const int visible_rows = getVisibleRowCount();
|
||||
return m_top_address + (visible_rows * CPU::INSTRUCTION_SIZE);
|
||||
return getAddressForRow(m_top_row + (viewport()->height() + m_row_height - 1) / m_row_height - 1);
|
||||
}
|
||||
@@ -7,6 +7,8 @@
|
||||
#include "core/cpu_core.h"
|
||||
#include "core/types.h"
|
||||
|
||||
#include "util/symbol_file.h"
|
||||
|
||||
#include <QtCore/QPoint>
|
||||
#include <QtGui/QPixmap>
|
||||
#include <QtWidgets/QAbstractScrollArea>
|
||||
@@ -44,6 +46,7 @@ public:
|
||||
void ensureAddressVisible(VirtualMemoryAddress address);
|
||||
void updateBreakpointList(const CPU::BreakpointList& bps);
|
||||
void clearBreakpoints();
|
||||
void setSymbols(const SymbolFile::SymbolList* symbols);
|
||||
VirtualMemoryAddress getPC() const { return m_last_pc; }
|
||||
bool hasBreakpointAtAddress(VirtualMemoryAddress address) const;
|
||||
|
||||
@@ -55,6 +58,7 @@ Q_SIGNALS:
|
||||
void contextMenuRequested(const QPoint& point, VirtualMemoryAddress address);
|
||||
|
||||
protected:
|
||||
bool viewportEvent(QEvent* event) override;
|
||||
void paintEvent(QPaintEvent* event) override;
|
||||
void mousePressEvent(QMouseEvent* event) override;
|
||||
void mouseMoveEvent(QMouseEvent* event) override;
|
||||
@@ -86,16 +90,23 @@ private:
|
||||
bool is_forward;
|
||||
};
|
||||
|
||||
struct CodeRow
|
||||
{
|
||||
VirtualMemoryAddress address;
|
||||
bool is_label;
|
||||
};
|
||||
|
||||
// Address/row conversion methods
|
||||
int getRowForAddress(VirtualMemoryAddress address) const;
|
||||
VirtualMemoryAddress getAddressForRow(int row) const;
|
||||
|
||||
// Memory region management
|
||||
bool updateRegion(VirtualMemoryAddress address);
|
||||
void rebuildLayout();
|
||||
QString getSymbolLabel(VirtualMemoryAddress address) const;
|
||||
|
||||
// Drawing methods
|
||||
void updateScrollBars();
|
||||
void updateVisibleRange();
|
||||
void calculateBranchArrows();
|
||||
void drawBranchArrows(QPainter& painter, const QRect& visible_rect);
|
||||
void drawInstruction(QPainter& painter, VirtualMemoryAddress address, int y, bool is_selected, bool is_pc);
|
||||
@@ -118,8 +129,9 @@ private:
|
||||
QPixmap m_breakpoint_pixmap;
|
||||
|
||||
// Scroll state
|
||||
VirtualMemoryAddress m_top_address = 0;
|
||||
int m_visible_rows = 0;
|
||||
int m_top_row = 0;
|
||||
std::vector<CodeRow> m_rows;
|
||||
const SymbolFile::SymbolList* m_symbols = nullptr; // Owned by DebuggerWindow.
|
||||
|
||||
// Code region state (from DebuggerCodeModel)
|
||||
Bus::MemoryRegion m_current_code_region = Bus::MemoryRegion::Count;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
|
||||
|
||||
#include "debuggermodels.h"
|
||||
#include "debuggerwindow.h"
|
||||
#include "qtutils.h"
|
||||
|
||||
#include "core/bios_types.h"
|
||||
@@ -205,7 +206,14 @@ void DebuggerStackModel::invalidateView()
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
DebuggerCallStackModel::DebuggerCallStackModel(QObject* parent /*= nullptr*/) : QAbstractTableModel(parent)
|
||||
static VirtualMemoryAddress GetSymbolAddress(VirtualMemoryAddress address)
|
||||
{
|
||||
// Symbols are stored as physical addresses, except for KSEG2.
|
||||
return (CPU::GetSegmentForAddress(address) == CPU::Segment::KSEG2) ? address : CPU::VirtualAddressToPhysical(address);
|
||||
}
|
||||
|
||||
DebuggerCallStackModel::DebuggerCallStackModel(const SymbolFile::SymbolList* symbols, QObject* parent /*= nullptr*/)
|
||||
: QAbstractTableModel(parent), m_symbols(symbols)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -223,7 +231,7 @@ int DebuggerCallStackModel::columnCount(const QModelIndex& parent /*= QModelInde
|
||||
|
||||
QVariant DebuggerCallStackModel::data(const QModelIndex& index, int role /*= Qt::DisplayRole*/) const
|
||||
{
|
||||
if (!index.isValid() || role != Qt::DisplayRole || index.row() < 0 ||
|
||||
if (!index.isValid() || (role != Qt::DisplayRole && role != Qt::ToolTipRole) || index.row() < 0 ||
|
||||
static_cast<size_t>(index.row()) >= m_frames.size())
|
||||
{
|
||||
return {};
|
||||
@@ -233,9 +241,20 @@ QVariant DebuggerCallStackModel::data(const QModelIndex& index, int role /*= Qt:
|
||||
switch (index.column())
|
||||
{
|
||||
case 0:
|
||||
return QString::asprintf("0x%08X", frame.address);
|
||||
{
|
||||
const SymbolFile::Symbol* symbol = getFrameSymbol(frame);
|
||||
if (!symbol)
|
||||
return (role == Qt::DisplayRole) ? QVariant(QString::asprintf("0x%08X", frame.address)) : QVariant();
|
||||
|
||||
const u32 offset = GetSymbolAddress(frame.address) - symbol->address;
|
||||
QString text = (offset == 0) ? QString::fromStdString(symbol->name) :
|
||||
QString::asprintf("%s+0x%X", symbol->name.c_str(), offset);
|
||||
if (role == Qt::ToolTipRole)
|
||||
text += QString::asprintf(": 0x%08X", frame.address);
|
||||
return text;
|
||||
}
|
||||
case 1:
|
||||
return QString::asprintf("0x%08X", frame.stack_pointer);
|
||||
return (role == Qt::DisplayRole) ? QVariant(QString::asprintf("0x%08X", frame.stack_pointer)) : QVariant();
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
@@ -277,6 +296,12 @@ void DebuggerCallStackModel::clear()
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
void DebuggerCallStackModel::invalidateView()
|
||||
{
|
||||
beginResetModel();
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
const CPU::CallStack::Frame* DebuggerCallStackModel::getFrame(const QModelIndex& index) const
|
||||
{
|
||||
if (!index.isValid() || index.row() < 0 || static_cast<size_t>(index.row()) >= m_frames.size())
|
||||
@@ -285,6 +310,11 @@ const CPU::CallStack::Frame* DebuggerCallStackModel::getFrame(const QModelIndex&
|
||||
return &m_frames[static_cast<size_t>(index.row())];
|
||||
}
|
||||
|
||||
const SymbolFile::Symbol* DebuggerCallStackModel::getFrameSymbol(const CPU::CallStack::Frame& frame) const
|
||||
{
|
||||
return SymbolFile::FindSymbol(*m_symbols, GetSymbolAddress(frame.address));
|
||||
}
|
||||
|
||||
DebuggerThreadsModel::DebuggerThreadsModel(QObject* parent /*= nullptr*/) : QAbstractItemModel(parent)
|
||||
{
|
||||
}
|
||||
@@ -502,7 +532,7 @@ std::optional<VirtualMemoryAddress> DebuggerThreadsModel::getThreadPC(const QMod
|
||||
return m_threads[static_cast<size_t>(index.row())].pc;
|
||||
}
|
||||
|
||||
DebuggerAddBreakpointDialog::DebuggerAddBreakpointDialog(QWidget* parent /*= nullptr*/) : QDialog(parent)
|
||||
DebuggerAddBreakpointDialog::DebuggerAddBreakpointDialog(DebuggerWindow* parent) : QDialog(parent), m_window(parent)
|
||||
{
|
||||
m_ui.setupUi(this);
|
||||
connect(m_ui.buttonBox, &QDialogButtonBox::accepted, this, &DebuggerAddBreakpointDialog::okClicked);
|
||||
@@ -514,23 +544,21 @@ void DebuggerAddBreakpointDialog::okClicked()
|
||||
{
|
||||
const QString address_str = m_ui.address->text();
|
||||
m_address = 0;
|
||||
bool ok = false;
|
||||
|
||||
if (!address_str.isEmpty())
|
||||
if (!address_str.trimmed().isEmpty())
|
||||
{
|
||||
if (address_str.startsWith("0x"))
|
||||
m_address = address_str.mid(2).toUInt(&ok, 16);
|
||||
else
|
||||
m_address = address_str.toUInt(&ok, 16);
|
||||
|
||||
if (!ok)
|
||||
const std::optional<VirtualMemoryAddress> address = m_window->parseAddress(address_str);
|
||||
if (!address.has_value())
|
||||
{
|
||||
QtUtils::AsyncMessageBox(
|
||||
this, QMessageBox::Critical, windowTitle(),
|
||||
QCoreApplication::translate("DebuggerWindow", "Invalid address. It should be in hex (0x12345678 or 12345678)"));
|
||||
QCoreApplication::translate("DebuggerWindow",
|
||||
"Invalid address. It should be a symbol name or in hex (0x12345678 or 12345678)."));
|
||||
return;
|
||||
}
|
||||
|
||||
m_address = address.value();
|
||||
|
||||
if (m_ui.read->isChecked())
|
||||
m_type = CPU::BreakpointType::Read;
|
||||
else if (m_ui.write->isChecked())
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
#include "core/cpu_types.h"
|
||||
#include "core/cpu_call_stack.h"
|
||||
|
||||
#include "util/symbol_file.h"
|
||||
|
||||
#include <QtCore/QAbstractItemModel>
|
||||
#include <QtCore/QAbstractListModel>
|
||||
#include <QtCore/QAbstractTableModel>
|
||||
@@ -20,6 +22,8 @@
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
class DebuggerWindow;
|
||||
|
||||
class DebuggerRegistersModel final : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
@@ -64,7 +68,7 @@ class DebuggerCallStackModel final : public QAbstractTableModel
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit DebuggerCallStackModel(QObject* parent = nullptr);
|
||||
explicit DebuggerCallStackModel(const SymbolFile::SymbolList* symbols, QObject* parent = nullptr);
|
||||
~DebuggerCallStackModel() override;
|
||||
|
||||
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
|
||||
@@ -73,12 +77,15 @@ public:
|
||||
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
|
||||
|
||||
const CPU::CallStack::Frame* getFrame(const QModelIndex& index) const;
|
||||
const SymbolFile::Symbol* getFrameSymbol(const CPU::CallStack::Frame& frame) const;
|
||||
|
||||
void updateValues();
|
||||
void clear();
|
||||
void invalidateView();
|
||||
|
||||
private:
|
||||
CPU::CallStack::FrameList m_frames;
|
||||
const SymbolFile::SymbolList* m_symbols; // Owned by DebuggerWindow.
|
||||
};
|
||||
|
||||
class DebuggerThreadsModel final : public QAbstractItemModel
|
||||
@@ -120,7 +127,7 @@ class DebuggerAddBreakpointDialog final : public QDialog
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit DebuggerAddBreakpointDialog(QWidget* parent = nullptr);
|
||||
explicit DebuggerAddBreakpointDialog(DebuggerWindow* parent);
|
||||
~DebuggerAddBreakpointDialog() override;
|
||||
|
||||
u32 getAddress() const { return m_address; }
|
||||
@@ -130,6 +137,7 @@ private:
|
||||
void okClicked();
|
||||
|
||||
Ui::DebuggerAddBreakpointDialog m_ui;
|
||||
DebuggerWindow* m_window;
|
||||
u32 m_address = 0;
|
||||
CPU::BreakpointType m_type = CPU::BreakpointType::Execute;
|
||||
};
|
||||
@@ -61,11 +61,12 @@ DebuggerWindow::DebuggerWindow(QWidget* parent /* = nullptr */)
|
||||
{
|
||||
m_game_serial = QtHost::GetCurrentGameSerial();
|
||||
loadGameSettings(false);
|
||||
loadSymbols(QtHost::GetCurrentGamePath(), QtHost::GetCurrentExeOverride());
|
||||
|
||||
if (QtHost::IsSystemPaused())
|
||||
onSystemPaused();
|
||||
else
|
||||
onSystemStarted();
|
||||
setUIEnabled(false, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -75,14 +76,21 @@ DebuggerWindow::DebuggerWindow(QWidget* parent /* = nullptr */)
|
||||
|
||||
DebuggerWindow::~DebuggerWindow() = default;
|
||||
|
||||
void DebuggerWindow::onSystemStarted()
|
||||
void DebuggerWindow::onSystemStarted(const QString& path, const QString& serial, const QString& title, GameHash hash,
|
||||
const QString& exe_override)
|
||||
{
|
||||
if (const QString& serial = QtHost::GetCurrentGameSerial(); m_game_serial != serial)
|
||||
Q_UNUSED(serial);
|
||||
Q_UNUSED(title);
|
||||
Q_UNUSED(hash);
|
||||
|
||||
if (m_game_serial != serial)
|
||||
{
|
||||
m_game_serial = serial;
|
||||
loadGameSettings(true);
|
||||
}
|
||||
|
||||
// Boot doesn't send systemGameChanged.
|
||||
loadSymbols(path, exe_override);
|
||||
setUIEnabled(false, true);
|
||||
|
||||
// RAM size and region pointers aren't valid until the system has started.
|
||||
@@ -92,22 +100,27 @@ void DebuggerWindow::onSystemStarted()
|
||||
void DebuggerWindow::onSystemDestroyed()
|
||||
{
|
||||
m_game_serial.clear();
|
||||
loadGameSettings(false);
|
||||
m_symbols.clear();
|
||||
updateSymbolUI();
|
||||
refreshBreakpointList({});
|
||||
m_call_stack_model->clear();
|
||||
m_threads_model->clear();
|
||||
setUIEnabled(false, false);
|
||||
}
|
||||
|
||||
void DebuggerWindow::onSystemGameChanged(const QString& path, const QString& serial, const QString& title)
|
||||
void DebuggerWindow::onSystemGameChanged(const QString& path, const QString& serial, const QString& title,
|
||||
GameHash hash, const QString& exe_override)
|
||||
{
|
||||
Q_UNUSED(path);
|
||||
Q_UNUSED(title);
|
||||
Q_UNUSED(hash);
|
||||
|
||||
if (m_game_serial == serial)
|
||||
return;
|
||||
|
||||
m_game_serial = serial;
|
||||
loadGameSettings(true);
|
||||
if (m_game_serial != serial)
|
||||
{
|
||||
m_game_serial = serial;
|
||||
loadGameSettings(true);
|
||||
}
|
||||
loadSymbols(path, exe_override);
|
||||
}
|
||||
|
||||
void DebuggerWindow::onSystemPaused()
|
||||
@@ -130,6 +143,121 @@ void DebuggerWindow::reportMessage(const QString& message)
|
||||
m_ui.statusbar->showMessage(message, 0);
|
||||
}
|
||||
|
||||
bool DebuggerWindow::loadSymbolsFile(std::string path, Error* error)
|
||||
{
|
||||
SymbolFile::SymbolList symbols;
|
||||
if (!SymbolFile::Load(path.c_str(), &symbols, error))
|
||||
return false;
|
||||
if (symbols.empty())
|
||||
{
|
||||
Error::SetStringView(error, "File does not contain usable symbols.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cache physical addresses so cached and uncached code use the same labels.
|
||||
for (SymbolFile::Symbol& symbol : symbols)
|
||||
{
|
||||
if (CPU::GetSegmentForAddress(symbol.address) != CPU::Segment::KSEG2)
|
||||
symbol.address = CPU::VirtualAddressToPhysical(symbol.address);
|
||||
}
|
||||
|
||||
SymbolFile::SortSymbols(&symbols);
|
||||
m_symbols = std::move(symbols);
|
||||
m_symbol_path = std::move(path);
|
||||
updateSymbolUI();
|
||||
reportMessage(tr("Loaded %n symbol(s) from %1.", nullptr, static_cast<int>(m_symbols.size()))
|
||||
.arg(QtUtils::StringViewToQStringView(m_symbol_path)));
|
||||
return true;
|
||||
}
|
||||
|
||||
void DebuggerWindow::loadSymbols(const QString& path, const QString& exe_override)
|
||||
{
|
||||
m_symbols.clear();
|
||||
updateSymbolUI();
|
||||
if (m_symbol_source == SymbolSource::Disabled)
|
||||
return;
|
||||
|
||||
if (m_symbol_source == SymbolSource::Manual)
|
||||
{
|
||||
if (Error error; !loadSymbolsFile(m_symbol_path, &error))
|
||||
{
|
||||
WARNING_LOG("Failed to load symbols from '{}': {}", m_symbol_path, error.GetDescription());
|
||||
reportMessage(tr("Failed to load symbols from %1: %2")
|
||||
.arg(QtUtils::StringViewToQStringView(m_symbol_path))
|
||||
.arg(QtUtils::StringViewToQStringView(error.GetDescription())));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Prefer symbols next to a sideloaded executable over the disc's.
|
||||
m_symbol_path.clear();
|
||||
const std::string base_path = (exe_override.isEmpty() ? path : exe_override).toStdString();
|
||||
if (!base_path.empty())
|
||||
{
|
||||
std::vector<std::string> candidates;
|
||||
if (StringUtil::EqualNoCase(Path::GetExtension(base_path), "elf"))
|
||||
candidates.push_back(base_path);
|
||||
const std::string elf_path = Path::ReplaceExtension(base_path, "elf");
|
||||
if (candidates.empty() || candidates.back() != elf_path)
|
||||
candidates.push_back(elf_path);
|
||||
candidates.push_back(Path::ReplaceExtension(base_path, "map"));
|
||||
|
||||
for (const std::string& candidate : candidates)
|
||||
{
|
||||
if (!FileSystem::FileExists(candidate.c_str()))
|
||||
continue;
|
||||
Error error;
|
||||
if (loadSymbolsFile(candidate, &error))
|
||||
return;
|
||||
WARNING_LOG("Ignoring symbol file '{}': {}", candidate, error.GetDescription());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DebuggerWindow::updateSymbolUI()
|
||||
{
|
||||
m_ui.codeView->setSymbols(&m_symbols);
|
||||
m_call_stack_model->invalidateView();
|
||||
m_ui.actionAutomaticSymbols->setChecked(m_symbol_source == SymbolSource::Automatic);
|
||||
m_ui.actionDisableSymbols->setChecked(m_symbol_source == SymbolSource::Disabled);
|
||||
m_ui.actionReloadSymbols->setEnabled(m_symbol_source != SymbolSource::Disabled);
|
||||
}
|
||||
|
||||
std::optional<VirtualMemoryAddress> DebuggerWindow::parseAddress(const QString& str) const
|
||||
{
|
||||
const QString trimmed = str.trimmed();
|
||||
const QByteArray name = trimmed.toUtf8();
|
||||
if (const auto it =
|
||||
std::ranges::find(m_symbols, std::string_view(name.constData(), name.size()), &SymbolFile::Symbol::name);
|
||||
it != m_symbols.end())
|
||||
{
|
||||
// Symbols are stored as physical addresses, go to the cached mirror.
|
||||
return (CPU::GetSegmentForAddress(it->address) == CPU::Segment::KSEG2) ?
|
||||
it->address :
|
||||
CPU::PhysicalAddressToVirtual(it->address, CPU::Segment::KSEG0);
|
||||
}
|
||||
|
||||
bool ok;
|
||||
const uint value = (trimmed.startsWith("0x"_L1) ? trimmed.mid(2) : trimmed).toUInt(&ok, 16);
|
||||
return ok ? std::optional<VirtualMemoryAddress>(value) : std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<VirtualMemoryAddress> DebuggerWindow::promptForAddress(const QString& label)
|
||||
{
|
||||
const QString text = QInputDialog::getText(this, windowTitle(), label);
|
||||
if (text.trimmed().isEmpty())
|
||||
return std::nullopt;
|
||||
|
||||
const std::optional<VirtualMemoryAddress> address = parseAddress(text);
|
||||
if (!address.has_value())
|
||||
{
|
||||
QtUtils::MessageBoxCritical(this, windowTitle(),
|
||||
tr("Invalid address. It should be a symbol name or in hex (0x12345678 or 12345678)."));
|
||||
}
|
||||
|
||||
return address;
|
||||
}
|
||||
|
||||
void DebuggerWindow::timerRefresh()
|
||||
{
|
||||
m_ui.memoryView->forceRefresh();
|
||||
@@ -189,8 +317,7 @@ void DebuggerWindow::onGoToPCTriggered()
|
||||
|
||||
void DebuggerWindow::onGoToAddressTriggered()
|
||||
{
|
||||
std::optional<VirtualMemoryAddress> address =
|
||||
QtUtils::PromptForHexValue(this, windowTitle(), tr("Enter code address:"));
|
||||
const std::optional<VirtualMemoryAddress> address = promptForAddress(tr("Enter code address or symbol:"));
|
||||
if (!address.has_value())
|
||||
return;
|
||||
|
||||
@@ -200,8 +327,7 @@ void DebuggerWindow::onGoToAddressTriggered()
|
||||
|
||||
void DebuggerWindow::onDumpAddressTriggered()
|
||||
{
|
||||
std::optional<VirtualMemoryAddress> address =
|
||||
QtUtils::PromptForHexValue(this, windowTitle(), tr("Enter memory address:"));
|
||||
const std::optional<VirtualMemoryAddress> address = promptForAddress(tr("Enter memory address or symbol:"));
|
||||
if (!address.has_value())
|
||||
return;
|
||||
|
||||
@@ -725,6 +851,46 @@ void DebuggerWindow::onMemorySearchStringChanged(const QString&)
|
||||
m_next_memory_search_address = 0;
|
||||
}
|
||||
|
||||
void DebuggerWindow::onAutomaticSymbolsTriggered()
|
||||
{
|
||||
m_symbol_source = SymbolSource::Automatic;
|
||||
saveGameSettings();
|
||||
loadSymbols(QtHost::GetCurrentGamePath(), QtHost::GetCurrentExeOverride());
|
||||
}
|
||||
|
||||
void DebuggerWindow::onLoadSymbolsTriggered()
|
||||
{
|
||||
const QString path = QFileDialog::getOpenFileName(this, tr("Load Symbols"), QString::fromStdString(m_symbol_path),
|
||||
tr("Symbol Files (*.elf *.map);;All Files (*)"));
|
||||
if (path.isEmpty())
|
||||
return;
|
||||
|
||||
if (Error error; !loadSymbolsFile(path.toStdString(), &error))
|
||||
{
|
||||
QtUtils::AsyncMessageBox(
|
||||
this, QMessageBox::Critical, windowTitle(),
|
||||
tr("Failed to load symbols: %1").arg(QtUtils::StringViewToQString(error.GetDescription())));
|
||||
return;
|
||||
}
|
||||
|
||||
m_symbol_source = SymbolSource::Manual;
|
||||
saveGameSettings();
|
||||
updateSymbolUI();
|
||||
}
|
||||
|
||||
void DebuggerWindow::onReloadSymbolsTriggered()
|
||||
{
|
||||
loadSymbols(QtHost::GetCurrentGamePath(), QtHost::GetCurrentExeOverride());
|
||||
}
|
||||
|
||||
void DebuggerWindow::onDisableSymbolsTriggered()
|
||||
{
|
||||
m_symbol_source = SymbolSource::Disabled;
|
||||
m_symbol_path.clear();
|
||||
saveGameSettings();
|
||||
loadSymbols({}, {});
|
||||
}
|
||||
|
||||
void DebuggerWindow::closeEvent(QCloseEvent* event)
|
||||
{
|
||||
g_core_thread->disconnect(this);
|
||||
@@ -802,6 +968,10 @@ void DebuggerWindow::connectSignals()
|
||||
connect(m_ui.actionToggleBreakpoint, &QAction::triggered, this, &DebuggerWindow::onToggleBreakpointTriggered);
|
||||
connect(m_ui.actionClearBreakpoints, &QAction::triggered, this, &DebuggerWindow::onClearBreakpointsTriggered);
|
||||
connect(m_ui.actionClose, &QAction::triggered, this, &DebuggerWindow::close);
|
||||
connect(m_ui.actionAutomaticSymbols, &QAction::triggered, this, &DebuggerWindow::onAutomaticSymbolsTriggered);
|
||||
connect(m_ui.actionLoadSymbols, &QAction::triggered, this, &DebuggerWindow::onLoadSymbolsTriggered);
|
||||
connect(m_ui.actionReloadSymbols, &QAction::triggered, this, &DebuggerWindow::onReloadSymbolsTriggered);
|
||||
connect(m_ui.actionDisableSymbols, &QAction::triggered, this, &DebuggerWindow::onDisableSymbolsTriggered);
|
||||
connect(m_ui.codeView, &DebuggerCodeView::addressActivated, this, &DebuggerWindow::onCodeViewAddressActivated);
|
||||
connect(m_ui.codeView, &DebuggerCodeView::toggleBreakpointActivated, this,
|
||||
&DebuggerWindow::onCodeViewToggleBreakpointActivated);
|
||||
@@ -841,7 +1011,7 @@ void DebuggerWindow::createModels()
|
||||
m_stack_model = new DebuggerStackModel(this);
|
||||
m_ui.stackView->setModel(m_stack_model);
|
||||
|
||||
m_call_stack_model = new DebuggerCallStackModel(this);
|
||||
m_call_stack_model = new DebuggerCallStackModel(&m_symbols, this);
|
||||
m_ui.callStackView->setModel(m_call_stack_model);
|
||||
QtUtils::SetColumnWidthsForTreeView(m_ui.callStackView, {-1, 100});
|
||||
|
||||
@@ -861,6 +1031,7 @@ void DebuggerWindow::setUIEnabled(bool enabled, bool allow_pause)
|
||||
const bool read_only_views = (enabled || allow_pause);
|
||||
|
||||
m_ui.actionPause->setEnabled(allow_pause);
|
||||
m_ui.menuSymbols->setEnabled(allow_pause);
|
||||
|
||||
// Disable all UI elements that depend on execution state
|
||||
m_ui.codeView->setEnabled(read_only_views);
|
||||
@@ -1148,6 +1319,8 @@ void DebuggerWindow::loadGameSettings(bool clear_existing)
|
||||
{
|
||||
saveGameSettings();
|
||||
m_settings.reset();
|
||||
m_symbol_source = SymbolSource::Automatic;
|
||||
m_symbol_path.clear();
|
||||
|
||||
if (!clear_existing)
|
||||
return;
|
||||
@@ -1158,6 +1331,8 @@ void DebuggerWindow::loadGameSettings(bool clear_existing)
|
||||
return;
|
||||
}
|
||||
|
||||
m_symbol_source = SymbolSource::Automatic;
|
||||
m_symbol_path.clear();
|
||||
m_settings = std::make_unique<INISettingsInterface>(
|
||||
Path::Combine(EmuFolders::GameSettings, fmt::format("{}.debugger.ini", m_game_serial.toStdString())));
|
||||
if (Error error; FileSystem::FileExists(m_settings->GetPath().c_str()))
|
||||
@@ -1173,6 +1348,17 @@ void DebuggerWindow::loadGameSettings(bool clear_existing)
|
||||
}
|
||||
}
|
||||
|
||||
const std::string symbol_source = m_settings->GetStringValue("Symbols", "Mode");
|
||||
if (symbol_source == "Manual")
|
||||
{
|
||||
m_symbol_source = SymbolSource::Manual;
|
||||
m_symbol_path = m_settings->GetStringValue("Symbols", "Path");
|
||||
}
|
||||
else if (symbol_source == "Disabled")
|
||||
{
|
||||
m_symbol_source = SymbolSource::Disabled;
|
||||
}
|
||||
|
||||
using QueuedBreakpoint = std::tuple<CPU::BreakpointType, VirtualMemoryAddress, bool>;
|
||||
std::vector<QueuedBreakpoint> queued_bps;
|
||||
|
||||
@@ -1225,6 +1411,19 @@ void DebuggerWindow::saveGameSettings()
|
||||
if (!m_settings)
|
||||
return;
|
||||
|
||||
if (m_symbol_source == SymbolSource::Automatic)
|
||||
{
|
||||
m_settings->ClearSection("Symbols");
|
||||
}
|
||||
else
|
||||
{
|
||||
m_settings->SetStringValue("Symbols", "Mode", (m_symbol_source == SymbolSource::Manual) ? "Manual" : "Disabled");
|
||||
if (m_symbol_source == SymbolSource::Manual)
|
||||
m_settings->SetStringValue("Symbols", "Path", m_symbol_path);
|
||||
else
|
||||
m_settings->DeleteValue("Symbols", "Path");
|
||||
}
|
||||
|
||||
// update the breakpoint list, we can yoink it back from the UI to avoid storing it again
|
||||
if (const int count = m_ui.breakpointsWidget->topLevelItemCount(); count > 0)
|
||||
{
|
||||
|
||||
@@ -8,12 +8,15 @@
|
||||
#include "core/cpu_core.h"
|
||||
#include "core/types.h"
|
||||
|
||||
#include "util/symbol_file.h"
|
||||
|
||||
#include <QtCore/QTimer>
|
||||
#include <QtWidgets/QMainWindow>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
class INISettingsInterface;
|
||||
class Error;
|
||||
|
||||
namespace Bus {
|
||||
enum class MemoryRegion;
|
||||
@@ -35,6 +38,9 @@ public:
|
||||
void reportMessage(const QString& message);
|
||||
void updateBreakpointHitCounts(const CPU::BreakpointList& bps);
|
||||
|
||||
/// Parses a hex address or symbol name.
|
||||
std::optional<VirtualMemoryAddress> parseAddress(const QString& str) const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void closed();
|
||||
|
||||
@@ -42,6 +48,13 @@ protected:
|
||||
void closeEvent(QCloseEvent* event) override;
|
||||
|
||||
private:
|
||||
enum class SymbolSource
|
||||
{
|
||||
Automatic,
|
||||
Manual,
|
||||
Disabled,
|
||||
};
|
||||
|
||||
void saveWindowState();
|
||||
void restoreWindowState();
|
||||
void setupAdditionalUi();
|
||||
@@ -66,10 +79,16 @@ private:
|
||||
void removeBreakpoint(CPU::BreakpointType type, u32 address);
|
||||
void loadGameSettings(bool clear_existing);
|
||||
void saveGameSettings();
|
||||
void loadSymbols(const QString& path, const QString& exe_override);
|
||||
bool loadSymbolsFile(std::string path, Error* error);
|
||||
void updateSymbolUI();
|
||||
std::optional<VirtualMemoryAddress> promptForAddress(const QString& label);
|
||||
|
||||
void onSystemStarted();
|
||||
void onSystemStarted(const QString& path, const QString& serial, const QString& title, GameHash hash,
|
||||
const QString& exe_override);
|
||||
void onSystemDestroyed();
|
||||
void onSystemGameChanged(const QString& path, const QString& serial, const QString& title);
|
||||
void onSystemGameChanged(const QString& path, const QString& serial, const QString& title, GameHash hash,
|
||||
const QString& exe_override);
|
||||
void onSystemPaused();
|
||||
void onSystemResumed();
|
||||
|
||||
@@ -100,6 +119,10 @@ private:
|
||||
void onThreadItemDoubleClicked(const QModelIndex& index);
|
||||
void onMemorySearchTriggered();
|
||||
void onMemorySearchStringChanged(const QString&);
|
||||
void onAutomaticSymbolsTriggered();
|
||||
void onLoadSymbolsTriggered();
|
||||
void onReloadSymbolsTriggered();
|
||||
void onDisableSymbolsTriggered();
|
||||
|
||||
Ui::DebuggerWindow m_ui;
|
||||
|
||||
@@ -115,5 +138,9 @@ private:
|
||||
PhysicalMemoryAddress m_next_memory_search_address = 0;
|
||||
|
||||
QString m_game_serial;
|
||||
|
||||
std::unique_ptr<INISettingsInterface> m_settings;
|
||||
SymbolFile::SymbolList m_symbols;
|
||||
std::string m_symbol_path;
|
||||
SymbolSource m_symbol_source = SymbolSource::Automatic;
|
||||
};
|
||||
@@ -51,8 +51,19 @@
|
||||
<addaction name="actionToggleBreakpoint"/>
|
||||
<addaction name="actionClearBreakpoints"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuSymbols">
|
||||
<property name="title">
|
||||
<string>&Symbols</string>
|
||||
</property>
|
||||
<addaction name="actionAutomaticSymbols"/>
|
||||
<addaction name="actionLoadSymbols"/>
|
||||
<addaction name="actionReloadSymbols"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionDisableSymbols"/>
|
||||
</widget>
|
||||
<addaction name="menu_Debugger"/>
|
||||
<addaction name="menuBreakpoints"/>
|
||||
<addaction name="menuSymbols"/>
|
||||
</widget>
|
||||
<widget class="QStatusBar" name="statusbar"/>
|
||||
<widget class="QToolBar" name="toolBar">
|
||||
@@ -410,6 +421,48 @@
|
||||
<string>F9</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionAutomaticSymbols">
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="resources/duckstation-qt.qrc">
|
||||
<normaloff>:/icons/monochrome/svg/magic-line.svg</normaloff>:/icons/monochrome/svg/magic-line.svg</iconset>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Use &Automatic Symbols</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionLoadSymbols">
|
||||
<property name="icon">
|
||||
<iconset resource="resources/duckstation-qt.qrc">
|
||||
<normaloff>:/icons/monochrome/svg/folder-open-line.svg</normaloff>:/icons/monochrome/svg/folder-open-line.svg</iconset>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>&Load Symbols...</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionReloadSymbols">
|
||||
<property name="icon">
|
||||
<iconset resource="resources/duckstation-qt.qrc">
|
||||
<normaloff>:/icons/monochrome/svg/refresh-line.svg</normaloff>:/icons/monochrome/svg/refresh-line.svg</iconset>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>&Reload Symbols</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionDisableSymbols">
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="resources/duckstation-qt.qrc">
|
||||
<normaloff>:/icons/monochrome/svg/file-forbid-line.svg</normaloff>:/icons/monochrome/svg/file-forbid-line.svg</iconset>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>&Disable Symbols</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionClose">
|
||||
<property name="icon">
|
||||
<iconset resource="resources/duckstation-qt.qrc">
|
||||
|
||||
@@ -9,6 +9,7 @@ add_executable(util-tests
|
||||
cd_image_tests.cpp
|
||||
core_audio_stream_tests.cpp
|
||||
elf_parser_tests.cpp
|
||||
symbol_file_tests.cpp
|
||||
cue_parser_tests.cpp
|
||||
image_tests.cpp
|
||||
ini_settings_interface_tests.cpp
|
||||
|
||||
@@ -0,0 +1,354 @@
|
||||
// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin <[email protected]>
|
||||
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
|
||||
|
||||
#include "util/elf_file.h"
|
||||
#include "util/symbol_file.h"
|
||||
|
||||
#include "common/error.h"
|
||||
#include "common/file_system.h"
|
||||
#include "common/path.h"
|
||||
#include "common/scoped_guard.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <functional>
|
||||
|
||||
namespace {
|
||||
|
||||
ELFFile::DataArray CreateSymbolELF()
|
||||
{
|
||||
ELFFile::DataArray data(2048);
|
||||
std::memset(data.data(), 0, data.size());
|
||||
auto& hdr = *reinterpret_cast<ELFFile::Elf32_Ehdr*>(data.data());
|
||||
std::memcpy(hdr.e_ident, "\177ELF\1\1\1", 7);
|
||||
hdr.e_type = ELFFile::ET_EXEC;
|
||||
hdr.e_machine = ELFFile::EM_MIPS;
|
||||
hdr.e_version = 1;
|
||||
hdr.e_ehsize = sizeof(hdr);
|
||||
hdr.e_shoff = 64;
|
||||
hdr.e_shentsize = sizeof(ELFFile::Elf32_Shdr);
|
||||
hdr.e_shnum = 5;
|
||||
auto* sections = reinterpret_cast<ELFFile::Elf32_Shdr*>(data.data() + hdr.e_shoff);
|
||||
sections[1].sh_type = ELFFile::SHT_NOBITS;
|
||||
sections[1].sh_flags = ELFFile::SHF_ALLOC;
|
||||
sections[2].sh_type = ELFFile::SHT_SYMTAB;
|
||||
sections[2].sh_offset = 512;
|
||||
sections[2].sh_entsize = sizeof(ELFFile::Elf32_Sym);
|
||||
sections[2].sh_link = 3;
|
||||
sections[3].sh_type = ELFFile::SHT_STRTAB;
|
||||
sections[3].sh_offset = 1024;
|
||||
sections[3].sh_size = 1;
|
||||
sections[4].sh_type = ELFFile::SHT_PROGBITS; // Non-allocated debug section.
|
||||
auto* symbols = reinterpret_cast<ELFFile::Elf32_Sym*>(data.data() + sections[2].sh_offset);
|
||||
size_t count = 1;
|
||||
const auto add = [&](const char* name, u32 address, u32 size, u8 info, u16 section) {
|
||||
const u32 offset = sections[3].sh_size;
|
||||
const u32 length = static_cast<u32>(std::strlen(name) + 1);
|
||||
std::memcpy(data.data() + sections[3].sh_offset + offset, name, length);
|
||||
sections[3].sh_size += length;
|
||||
symbols[count++] = {offset, address, size, info, 0, section};
|
||||
};
|
||||
add("_start", 0x80010000, 0, 0x12, 1);
|
||||
add("main", 0x8001005C, 9904, 0x12, 1);
|
||||
add("tick", 0x800139F0, 52, 0x02, 1);
|
||||
add("main_alias", 0x8001005C, 9904, 0x22, 1);
|
||||
add("data", 0x8001A800, 4, 0x11, 1);
|
||||
add("_bss_init", 0x80010044, 0, 0, 1);
|
||||
add("undefined", 0, 0, 0x12, 0);
|
||||
add("file.c", 0, 0, 4, 0xFFF1);
|
||||
add("RAM_SIZE", 0x200000, 0, 0x10, 0xFFF1);
|
||||
add("common", 4, 4, 0x11, 0xFFF2);
|
||||
add("debug", 0x10, 4, 0x12, 4);
|
||||
add("section", 0x80010000, 0, 3, 1);
|
||||
sections[2].sh_size = static_cast<u32>(count * sizeof(ELFFile::Elf32_Sym));
|
||||
return data;
|
||||
}
|
||||
|
||||
ELFFile::Elf32_Shdr& Section(ELFFile::DataArray& data, u32 index)
|
||||
{
|
||||
const auto& hdr = *reinterpret_cast<const ELFFile::Elf32_Ehdr*>(data.data());
|
||||
return *reinterpret_cast<ELFFile::Elf32_Shdr*>(data.data() + hdr.e_shoff + hdr.e_shentsize * index);
|
||||
}
|
||||
|
||||
ELFFile::Elf32_Sym& Symbol(ELFFile::DataArray& data, u32 index)
|
||||
{
|
||||
return *reinterpret_cast<ELFFile::Elf32_Sym*>(data.data() + Section(data, 2).sh_offset +
|
||||
sizeof(ELFFile::Elf32_Sym) * index);
|
||||
}
|
||||
|
||||
bool ParseELF(ELFFile::DataArray data, SymbolFile::SymbolList* symbols, Error* error)
|
||||
{
|
||||
ELFFile elf;
|
||||
if (!elf.Open(std::move(data), error))
|
||||
return false;
|
||||
SymbolFile::ParseELF(elf, symbols);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Reduced from gpu-quad-dreq.map, retaining the structures that can otherwise
|
||||
// be mistaken for symbols. No dependency on the external test executable.
|
||||
constexpr std::string_view MAP = R"(
|
||||
Discarded input sections
|
||||
.text.discarded 0x00000000 0x20 unused.o
|
||||
0x00000000 discarded_label
|
||||
Memory Configuration
|
||||
ram 0x80010000 0x001f0000 xrw
|
||||
Linker script and memory map
|
||||
LOAD gpu-quad-dreq.o
|
||||
0x00200000 RAM_SIZE = DEFINED (RAM_SIZE)?RAM_SIZE:0x200000
|
||||
.PSX_EXE_Header
|
||||
0x8000f800 0x800
|
||||
0x8000f800 0x1 BYTE 0x50
|
||||
0x80010000 . = (. + 0x7c8)
|
||||
.text 0x80010000 0x8c30
|
||||
.start 0x80010000 0x5c crt0.o
|
||||
0x80010000 _start
|
||||
.text.startup
|
||||
0x8001005c 0x26b0 gpu-quad-dreq.o
|
||||
0x8001005c main
|
||||
0x80012bf4 operator new(unsigned int)
|
||||
0x80012c14 operator delete(void*, unsigned int)
|
||||
0x80018c30 . = ALIGN (0x10)
|
||||
*fill* 0x80018c24 0xc
|
||||
.text_memcpy 0x80018c30 0x28
|
||||
.text_memcpy 0x80018c30 0x28 memory-s.o
|
||||
0x80018c30 memcpy
|
||||
.rodata 0x80018c64 0x166c
|
||||
.rodata.str1.4
|
||||
0x80018c64 0x11d4 xprintf.o
|
||||
0x5f (size before relaxing)
|
||||
.data 0x8001a2d0 0x530
|
||||
0x8001a464 superTestInstance
|
||||
0x8001a800 __bss_start = .
|
||||
/DISCARD/
|
||||
0x80010000 discarded_again
|
||||
.debug_info 0x00000000 0x100
|
||||
0x00000010 debug_label
|
||||
Cross Reference Table
|
||||
0x80010000 cross_reference
|
||||
)";
|
||||
|
||||
TEST(SymbolFile, ELFSymbolsOwnNamesAndKeepLocalWeakAndZeroSize)
|
||||
{
|
||||
SymbolFile::SymbolList symbols;
|
||||
Error error;
|
||||
ASSERT_TRUE(ParseELF(CreateSymbolELF(), &symbols, &error)) << error.GetDescription();
|
||||
ASSERT_EQ(symbols.size(), 6u);
|
||||
EXPECT_EQ(symbols[0].name, "_start");
|
||||
EXPECT_EQ(symbols[0].size, 0u);
|
||||
EXPECT_EQ(symbols[1].name, "main");
|
||||
EXPECT_EQ(symbols[1].address, 0x8001005Cu);
|
||||
EXPECT_EQ(symbols[1].size, 9904u);
|
||||
EXPECT_EQ(symbols[2].name, "tick");
|
||||
EXPECT_EQ(symbols[2].size, 52u);
|
||||
EXPECT_EQ(symbols[3].name, "main_alias");
|
||||
EXPECT_EQ(symbols[4].size, 0u);
|
||||
EXPECT_EQ(symbols[5].name, "_bss_init");
|
||||
}
|
||||
|
||||
TEST(SymbolFile, ELFDynamicAndStrippedSymbols)
|
||||
{
|
||||
auto data = CreateSymbolELF();
|
||||
Section(data, 2).sh_type = ELFFile::SHT_DYNSYM;
|
||||
SymbolFile::SymbolList symbols;
|
||||
Error error;
|
||||
ASSERT_TRUE(ParseELF(std::move(data), &symbols, &error));
|
||||
EXPECT_EQ(symbols.size(), 6u);
|
||||
data = CreateSymbolELF();
|
||||
reinterpret_cast<ELFFile::Elf32_Ehdr*>(data.data())->e_shnum = 0;
|
||||
symbols.clear();
|
||||
ASSERT_TRUE(ParseELF(std::move(data), &symbols, &error));
|
||||
EXPECT_TRUE(symbols.empty());
|
||||
}
|
||||
|
||||
TEST(SymbolFile, ELFSkipsMalformedTables)
|
||||
{
|
||||
using Mutator = std::function<void(ELFFile::DataArray&)>;
|
||||
const Mutator cases[] = {
|
||||
[](auto& data) { reinterpret_cast<ELFFile::Elf32_Ehdr*>(data.data())->e_shoff = 0xFFFFFFF0; },
|
||||
[](auto& data) { reinterpret_cast<ELFFile::Elf32_Ehdr*>(data.data())->e_shentsize = 1; },
|
||||
[](auto& data) { Section(data, 2).sh_offset = 0xFFFFFFF0; },
|
||||
[](auto& data) { Section(data, 2).sh_entsize = 0; },
|
||||
[](auto& data) { Section(data, 2).sh_size = 0xFFFFFFFF; },
|
||||
[](auto& data) { Section(data, 2).sh_link = 99; },
|
||||
[](auto& data) { Section(data, 3).sh_type = ELFFile::SHT_PROGBITS; },
|
||||
[](auto& data) { Section(data, 3).sh_size = 0xFFFFFFFF; },
|
||||
};
|
||||
for (size_t i = 0; i < std::size(cases); i++)
|
||||
{
|
||||
SCOPED_TRACE(i);
|
||||
auto data = CreateSymbolELF();
|
||||
cases[i](data);
|
||||
SymbolFile::SymbolList symbols;
|
||||
Error error;
|
||||
EXPECT_TRUE(ParseELF(std::move(data), &symbols, &error));
|
||||
EXPECT_TRUE(symbols.empty());
|
||||
}
|
||||
|
||||
// Bad individual entries are skipped, unterminated names are cut off at the end of the table.
|
||||
auto data = CreateSymbolELF();
|
||||
Symbol(data, 2).st_name = Section(data, 3).sh_size;
|
||||
Symbol(data, 3).st_shndx = 99;
|
||||
Section(data, 3).sh_size = Symbol(data, 6).st_name + 6;
|
||||
SymbolFile::SymbolList symbols;
|
||||
Error error;
|
||||
ASSERT_TRUE(ParseELF(std::move(data), &symbols, &error));
|
||||
ASSERT_EQ(symbols.size(), 4u);
|
||||
EXPECT_EQ(symbols[0].name, "_start");
|
||||
EXPECT_EQ(symbols[3].name, "_bss_i");
|
||||
}
|
||||
|
||||
TEST(SymbolFile, GNUMapSymbolsAndFalsePositives)
|
||||
{
|
||||
SymbolFile::SymbolList symbols;
|
||||
Error error;
|
||||
ASSERT_TRUE(SymbolFile::ParseMap(MAP, &symbols, &error));
|
||||
ASSERT_EQ(symbols.size(), 6u);
|
||||
EXPECT_EQ(symbols[0].name, "_start");
|
||||
EXPECT_EQ(symbols[0].address, 0x80010000u);
|
||||
EXPECT_EQ(symbols[1].name, "main");
|
||||
EXPECT_EQ(symbols[1].address, 0x8001005Cu);
|
||||
EXPECT_EQ(symbols[2].name, "operator new(unsigned int)");
|
||||
EXPECT_EQ(symbols[3].name, "operator delete(void*, unsigned int)");
|
||||
EXPECT_EQ(symbols[4].name, "memcpy");
|
||||
EXPECT_EQ(symbols[4].address, 0x80018C30u);
|
||||
EXPECT_EQ(symbols[5].name, "superTestInstance");
|
||||
|
||||
// Sizes run to the next symbol or the end of the output section.
|
||||
EXPECT_EQ(symbols[0].size, 0x5Cu);
|
||||
EXPECT_EQ(symbols[1].size, 0x2B98u);
|
||||
EXPECT_EQ(symbols[2].size, 0x20u);
|
||||
EXPECT_EQ(symbols[3].size, 0x601Cu);
|
||||
EXPECT_EQ(symbols[4].size, 0x28u);
|
||||
EXPECT_EQ(symbols[5].size, 0x39Cu);
|
||||
}
|
||||
|
||||
TEST(SymbolFile, GNUMapSizesWithWrappedSectionsAndAliases)
|
||||
{
|
||||
SymbolFile::SymbolList symbols;
|
||||
Error error;
|
||||
ASSERT_TRUE(SymbolFile::ParseMap("Linker script and memory map\n"
|
||||
".text.a_very_long_section_name\n"
|
||||
" 0x80010000 0x100\n"
|
||||
" 0x80010040 second\n"
|
||||
" 0x80010000 first\n"
|
||||
" 0x80010000 first_alias\n"
|
||||
".data 0x80020000 0x10 load address 0x80010100\n"
|
||||
" 0x80020008 variable\n",
|
||||
&symbols, &error));
|
||||
ASSERT_EQ(symbols.size(), 4u);
|
||||
EXPECT_EQ(symbols[0].name, "first");
|
||||
EXPECT_EQ(symbols[0].size, 0x40u);
|
||||
EXPECT_EQ(symbols[1].name, "first_alias");
|
||||
EXPECT_EQ(symbols[1].size, 0x40u);
|
||||
EXPECT_EQ(symbols[2].name, "second");
|
||||
EXPECT_EQ(symbols[2].size, 0xC0u);
|
||||
EXPECT_EQ(symbols[3].name, "variable");
|
||||
EXPECT_EQ(symbols[3].size, 0x8u);
|
||||
}
|
||||
|
||||
TEST(SymbolFile, GNUMapCRLFTabsAliasesAndUnalignedLabels)
|
||||
{
|
||||
SymbolFile::SymbolList symbols;
|
||||
Error error;
|
||||
ASSERT_TRUE(SymbolFile::ParseMap("Linker script and memory map\r\n.text\t0x80010000 0x20\r\n"
|
||||
"\t0x80010000\tmain\r\n\t0x80010000\talias\r\n\t0x80010001\tbyte_label",
|
||||
&symbols, &error));
|
||||
ASSERT_EQ(symbols.size(), 3u);
|
||||
EXPECT_EQ(symbols[1].name, "alias");
|
||||
EXPECT_EQ(symbols[1].size, 1u);
|
||||
EXPECT_EQ(symbols[2].address, 0x80010001u);
|
||||
EXPECT_EQ(symbols[2].size, 0x1Fu);
|
||||
}
|
||||
|
||||
TEST(SymbolFile, GNUMapErrors)
|
||||
{
|
||||
for (const std::string_view text : {"random text", "Linker script and memory map\n.text\n 0x100000000 overflow"})
|
||||
{
|
||||
SymbolFile::SymbolList symbols;
|
||||
Error error;
|
||||
EXPECT_FALSE(SymbolFile::ParseMap(text, &symbols, &error));
|
||||
EXPECT_TRUE(error.IsValid());
|
||||
}
|
||||
SymbolFile::SymbolList symbols;
|
||||
Error error;
|
||||
ASSERT_TRUE(SymbolFile::ParseMap("Linker script and memory map\n", &symbols, &error));
|
||||
EXPECT_TRUE(symbols.empty());
|
||||
}
|
||||
|
||||
TEST(SymbolFile, GNUMapAssignmentsDoNotHideOperatorNames)
|
||||
{
|
||||
SymbolFile::SymbolList symbols;
|
||||
Error error;
|
||||
ASSERT_TRUE(SymbolFile::ParseMap("Linker script and memory map\n.text\n"
|
||||
" 0x80010000 foo = (.)\n"
|
||||
" 0x80010000 [!provide] PROVIDE (foo = .)\n"
|
||||
" 0x80010000 PROVIDE (foo = .)\n"
|
||||
" 0x80010000 operator==(Thing const&, Thing const&)\n"
|
||||
" 0x80010004 Thing::operator=(Thing const&)\n",
|
||||
&symbols, &error));
|
||||
ASSERT_EQ(symbols.size(), 2u);
|
||||
EXPECT_EQ(symbols[0].name, "operator==(Thing const&, Thing const&)");
|
||||
EXPECT_EQ(symbols[1].name, "Thing::operator=(Thing const&)");
|
||||
}
|
||||
|
||||
TEST(SymbolFile, LoadDetectsFileContents)
|
||||
{
|
||||
std::string path;
|
||||
auto fp =
|
||||
FileSystem::OpenTemporaryManagedCFile(Path::Combine(FileSystem::GetWorkingDirectory(), "symbol-file-test"), &path);
|
||||
ASSERT_TRUE(fp);
|
||||
fp.reset();
|
||||
const ScopedGuard cleanup([&path]() { FileSystem::DeleteFile(path.c_str()); });
|
||||
const auto data = CreateSymbolELF();
|
||||
ASSERT_TRUE(FileSystem::WriteBinaryFile(path.c_str(), data.cspan()));
|
||||
SymbolFile::SymbolList symbols;
|
||||
Error error;
|
||||
ASSERT_TRUE(SymbolFile::Load(path.c_str(), &symbols, &error));
|
||||
EXPECT_EQ(symbols.size(), 6u);
|
||||
ASSERT_TRUE(FileSystem::WriteStringToFile(path.c_str(), MAP));
|
||||
symbols.clear();
|
||||
ASSERT_TRUE(SymbolFile::Load(path.c_str(), &symbols, &error));
|
||||
ASSERT_EQ(symbols.size(), 6u);
|
||||
EXPECT_EQ(symbols[2].name, "operator new(unsigned int)");
|
||||
}
|
||||
|
||||
TEST(SymbolFile, SortGroupsAliasesAndKeepsLargestDuplicate)
|
||||
{
|
||||
SymbolFile::SymbolList symbols = {
|
||||
{0x1004, 0, "next"},
|
||||
{0x1000, 0, "function"},
|
||||
{0x1000, 32, "function"},
|
||||
{0x1000, 0, "alias"},
|
||||
};
|
||||
SymbolFile::SortSymbols(&symbols);
|
||||
ASSERT_EQ(symbols.size(), 3u);
|
||||
const auto labels = SymbolFile::GetSymbolsAtAddress(symbols, 0x1000);
|
||||
ASSERT_EQ(labels.size(), 2u);
|
||||
EXPECT_EQ(labels[0].name, "alias");
|
||||
EXPECT_EQ(labels[1].name, "function");
|
||||
EXPECT_EQ(labels[1].size, 32u);
|
||||
EXPECT_TRUE(SymbolFile::GetSymbolsAtAddress(symbols, 0x1008).empty());
|
||||
}
|
||||
|
||||
TEST(SymbolFile, LookupUsesExactLabelsAndFunctionSizes)
|
||||
{
|
||||
SymbolFile::SymbolList symbols = {
|
||||
{0x1000, 32, "func"}, {0x1000, 0, "alias"}, {0x1008, 0, "label"}, {0x1020, 8, "next"}, {0x1040, 0, "data"},
|
||||
};
|
||||
SymbolFile::SortSymbols(&symbols);
|
||||
EXPECT_EQ(SymbolFile::FindSymbol(symbols, 0x1000)->name, "alias");
|
||||
EXPECT_EQ(SymbolFile::FindSymbol(symbols, 0x1004)->name, "func");
|
||||
EXPECT_EQ(SymbolFile::FindSymbol(symbols, 0x1008)->name, "label");
|
||||
EXPECT_EQ(SymbolFile::FindSymbol(symbols, 0x100C)->name, "func");
|
||||
EXPECT_EQ(SymbolFile::FindSymbol(symbols, 0x1024)->name, "next");
|
||||
EXPECT_EQ(SymbolFile::FindSymbol(symbols, 0x1028), nullptr);
|
||||
EXPECT_EQ(SymbolFile::FindSymbol(symbols, 0x1040)->name, "data");
|
||||
EXPECT_EQ(SymbolFile::FindSymbol(symbols, 0x1044), nullptr);
|
||||
EXPECT_EQ(SymbolFile::FindSymbol(symbols, 0xFFC), nullptr);
|
||||
EXPECT_EQ(SymbolFile::FindSymbol({}, 0x1000), nullptr);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -13,6 +13,7 @@
|
||||
<ClCompile Include="object_archive_tests.cpp" />
|
||||
<ClCompile Include="shiftjis_tests.cpp" />
|
||||
<ClCompile Include="stub_host.cpp" />
|
||||
<ClCompile Include="symbol_file_tests.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\dep\googletest\googletest.vcxproj">
|
||||
|
||||
@@ -12,5 +12,6 @@
|
||||
<ClCompile Include="cd_image_tests.cpp" />
|
||||
<ClCompile Include="shiftjis_tests.cpp" />
|
||||
<ClCompile Include="core_audio_stream_tests.cpp" />
|
||||
<ClCompile Include="symbol_file_tests.cpp" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -40,6 +40,8 @@ add_library(util
|
||||
dyn_sqlite.h
|
||||
elf_file.cpp
|
||||
elf_file.h
|
||||
symbol_file.cpp
|
||||
symbol_file.h
|
||||
gpu_device.cpp
|
||||
gpu_device.h
|
||||
gpu_framebuffer_manager.h
|
||||
|
||||
@@ -254,6 +254,44 @@ bool ELFFile::IsValidElfHeader(const std::span<const u8> data, Error* error /*=
|
||||
return IsValidElfHeader(reinterpret_cast<const Elf32_Ehdr&>(*data.data()), error);
|
||||
}
|
||||
|
||||
void ELFFile::EnumerateSymbols(SymbolCallback callback) const
|
||||
{
|
||||
const u32 section_count = GetSectionCount();
|
||||
for (u32 i = 0; i < section_count; i++)
|
||||
{
|
||||
const Elf32_Shdr* table = GetSectionHeader(i);
|
||||
if (!table || (table->sh_type != SHT_SYMTAB && table->sh_type != SHT_DYNSYM) ||
|
||||
table->sh_entsize < sizeof(Elf32_Sym) || table->sh_offset > m_data.size() ||
|
||||
table->sh_size > (m_data.size() - table->sh_offset))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const Elf32_Shdr* strings = GetSectionHeader(table->sh_link);
|
||||
if (!strings || strings->sh_type != SHT_STRTAB || strings->sh_offset > m_data.size() ||
|
||||
strings->sh_size > (m_data.size() - strings->sh_offset))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::string_view string_data(reinterpret_cast<const char*>(&m_data[strings->sh_offset]), strings->sh_size);
|
||||
for (size_t offset = 0; (offset + sizeof(Elf32_Sym)) <= table->sh_size; offset += table->sh_entsize)
|
||||
{
|
||||
const Elf32_Sym& sym = *reinterpret_cast<const Elf32_Sym*>(&m_data[table->sh_offset + offset]);
|
||||
const u8 type = sym.st_info & 0xFu;
|
||||
const Elf32_Shdr* section = GetSectionHeader(sym.st_shndx);
|
||||
if (type > STT_FUNC || !section || !(section->sh_flags & SHF_ALLOC) || sym.st_name >= string_data.size())
|
||||
continue;
|
||||
|
||||
const std::string_view::size_type end = string_data.find('\0', sym.st_name);
|
||||
const std::string_view name =
|
||||
string_data.substr(sym.st_name, ((end != std::string_view::npos) ? end : string_data.size()) - sym.st_name);
|
||||
if (!name.empty())
|
||||
callback(sym.st_value, sym.st_size, type, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool ELFFile::IsValidElfHeader(const Elf32_Ehdr& header, Error* error /* = nullptr */)
|
||||
{
|
||||
if (std::memcmp(header.e_ident, EXPECTED_ELF_HEADER, sizeof(EXPECTED_ELF_HEADER)) != 0)
|
||||
|
||||
@@ -35,6 +35,10 @@ public:
|
||||
static constexpr u32 SHT_SHLIB = 10;
|
||||
static constexpr u32 SHT_DYNSYM = 11;
|
||||
static constexpr u32 SHT_NUM = 12;
|
||||
static constexpr u32 SHF_ALLOC = 2;
|
||||
static constexpr u8 STT_NOTYPE = 0;
|
||||
static constexpr u8 STT_OBJECT = 1;
|
||||
static constexpr u8 STT_FUNC = 2;
|
||||
static constexpr u32 PT_NULL = 0;
|
||||
static constexpr u32 PT_LOAD = 1;
|
||||
static constexpr u32 PT_DYNAMIC = 2;
|
||||
@@ -91,6 +95,16 @@ public:
|
||||
u32 p_align;
|
||||
};
|
||||
|
||||
struct Elf32_Sym
|
||||
{
|
||||
u32 st_name;
|
||||
u32 st_value;
|
||||
u32 st_size;
|
||||
u8 st_info;
|
||||
u8 st_other;
|
||||
u16 st_shndx;
|
||||
};
|
||||
|
||||
public:
|
||||
ELFFile();
|
||||
~ELFFile();
|
||||
@@ -115,6 +129,10 @@ public:
|
||||
MoveOnlyFunction<bool(std::span<const u8> data, u32 dest_vaddr, u32 dest_size, Error* error)>;
|
||||
bool LoadExecutableSections(LoadExecutableSectionCallback callback, Error* error) const;
|
||||
|
||||
// Names refer to the file buffer and must be copied if retained by the caller.
|
||||
using SymbolCallback = MoveOnlyFunction<void(u32 address, u32 size, u8 type, std::string_view name)>;
|
||||
void EnumerateSymbols(SymbolCallback callback) const;
|
||||
|
||||
private:
|
||||
DataArray m_data;
|
||||
};
|
||||
@@ -0,0 +1,190 @@
|
||||
// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin <[email protected]>
|
||||
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
|
||||
|
||||
#include "symbol_file.h"
|
||||
#include "elf_file.h"
|
||||
|
||||
#include "common/error.h"
|
||||
#include "common/file_system.h"
|
||||
#include "common/string_util.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
void SymbolFile::ParseELF(const ELFFile& elf, SymbolList* symbols)
|
||||
{
|
||||
elf.EnumerateSymbols([symbols](u32 address, u32 size, u8 type, std::string_view name) {
|
||||
symbols->push_back({address, (type == ELFFile::STT_FUNC) ? size : 0, std::string(name)});
|
||||
});
|
||||
}
|
||||
|
||||
bool SymbolFile::ParseMap(std::string_view text, SymbolList* symbols, Error* error)
|
||||
{
|
||||
// "0xSTART 0xSIZE", returns the end address of the section.
|
||||
const auto parse_section_end = [](std::string_view str) -> u32 {
|
||||
const size_t split = str.find_first_of(" \t");
|
||||
if (!str.starts_with("0x") || split == std::string_view::npos)
|
||||
return 0;
|
||||
std::string_view size_str = StringUtil::StripWhitespace(str.substr(split));
|
||||
size_str = size_str.substr(0, size_str.find_first_of(" \t"));
|
||||
const std::optional<u32> start = StringUtil::FromChars<u32>(str.substr(2, split - 2), 16);
|
||||
const std::optional<u32> size =
|
||||
size_str.starts_with("0x") ? StringUtil::FromChars<u32>(size_str.substr(2), 16) : std::nullopt;
|
||||
return (start.has_value() && size.has_value()) ? (start.value() + size.value()) : 0;
|
||||
};
|
||||
|
||||
// Maps don't record symbol sizes, so assume each runs until the next symbol or the end of its section.
|
||||
size_t section_first = symbols->size();
|
||||
u32 section_end = 0;
|
||||
const auto finish_section = [symbols, §ion_first, §ion_end]() {
|
||||
std::stable_sort(symbols->begin() + section_first, symbols->end(),
|
||||
[](const Symbol& lhs, const Symbol& rhs) { return lhs.address < rhs.address; });
|
||||
u32 next_address = section_end;
|
||||
for (size_t i = symbols->size(); i > section_first;)
|
||||
{
|
||||
i--;
|
||||
Symbol& symbol = (*symbols)[i];
|
||||
if ((i + 1) < symbols->size() && (*symbols)[i + 1].address != symbol.address)
|
||||
next_address = (*symbols)[i + 1].address;
|
||||
symbol.size = (next_address > symbol.address) ? (next_address - symbol.address) : 0;
|
||||
}
|
||||
section_first = symbols->size();
|
||||
};
|
||||
|
||||
bool found_map = false;
|
||||
bool in_section = false;
|
||||
bool section_wrapped = false;
|
||||
while (!text.empty())
|
||||
{
|
||||
const size_t newline = text.find('\n');
|
||||
const std::string_view line = text.substr(0, newline);
|
||||
text = (newline == std::string_view::npos) ? std::string_view() : text.substr(newline + 1);
|
||||
const std::string_view trimmed = StringUtil::StripWhitespace(line);
|
||||
if (trimmed == "Linker script and memory map")
|
||||
{
|
||||
found_map = true;
|
||||
continue;
|
||||
}
|
||||
if (!found_map || trimmed.empty())
|
||||
continue;
|
||||
if (trimmed == "Cross Reference Table")
|
||||
break;
|
||||
|
||||
// Output sections start in column zero, everything else is indented.
|
||||
if (line.front() != ' ' && line.front() != '\t')
|
||||
{
|
||||
finish_section();
|
||||
in_section = trimmed.starts_with('.') && !trimmed.starts_with(".debug");
|
||||
const size_t name_end = trimmed.find_first_of(" \t");
|
||||
section_wrapped = (name_end == std::string_view::npos);
|
||||
section_end = section_wrapped ? 0 : parse_section_end(StringUtil::StripWhitespace(trimmed.substr(name_end)));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Long section names put the address and size on the next line.
|
||||
if (section_wrapped)
|
||||
{
|
||||
section_wrapped = false;
|
||||
section_end = parse_section_end(trimmed);
|
||||
if (section_end != 0)
|
||||
continue;
|
||||
}
|
||||
|
||||
// Symbol lines are "0xADDRESS name". Skip input section contributions (second number), assignments, etc.
|
||||
if (!in_section || !trimmed.starts_with("0x"))
|
||||
continue;
|
||||
const size_t split = trimmed.find_first_of(" \t");
|
||||
if (split == std::string_view::npos)
|
||||
continue;
|
||||
const std::string_view name = StringUtil::StripWhitespace(trimmed.substr(split));
|
||||
if (name.empty() || name.starts_with("0x") || name.starts_with('(') || name.find(" = ") != std::string_view::npos)
|
||||
continue;
|
||||
|
||||
const std::optional<u32> address = StringUtil::FromChars<u32>(trimmed.substr(2, split - 2), 16);
|
||||
if (!address.has_value())
|
||||
{
|
||||
Error::SetStringView(error, "Invalid address in GNU linker map.");
|
||||
return false;
|
||||
}
|
||||
symbols->push_back({address.value(), 0, std::string(name)});
|
||||
}
|
||||
finish_section();
|
||||
if (!found_map)
|
||||
{
|
||||
Error::SetStringView(error, "Not a GNU linker map (missing linker script and memory map).");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SymbolFile::Load(const char* path, SymbolList* symbols, Error* error)
|
||||
{
|
||||
auto fp = FileSystem::OpenManagedCFile(path, "rb", error);
|
||||
if (!fp)
|
||||
return false;
|
||||
|
||||
const s64 size = FileSystem::FSize64(fp.get(), error);
|
||||
if (size < 0)
|
||||
return false;
|
||||
if (size >= 32 * 1024 * 1024)
|
||||
{
|
||||
Error::SetStringView(error, "Symbol file is too large.");
|
||||
return false;
|
||||
}
|
||||
ELFFile::DataArray data(static_cast<size_t>(size));
|
||||
if (size > 0 && std::fread(data.data(), data.size(), 1, fp.get()) != 1)
|
||||
{
|
||||
Error::SetErrno(error, "Failed to read symbol file: ", errno);
|
||||
return false;
|
||||
}
|
||||
if (data.size() >= 4 && std::memcmp(data.data(), "\177ELF", 4) == 0)
|
||||
{
|
||||
ELFFile elf;
|
||||
if (!elf.Open(std::move(data), error))
|
||||
return false;
|
||||
ParseELF(elf, symbols);
|
||||
return true;
|
||||
}
|
||||
return ParseMap(std::string_view(reinterpret_cast<const char*>(data.data()), data.size()), symbols, error);
|
||||
}
|
||||
|
||||
void SymbolFile::SortSymbols(SymbolList* symbols)
|
||||
{
|
||||
std::ranges::sort(*symbols, [](const Symbol& lhs, const Symbol& rhs) {
|
||||
if (lhs.address != rhs.address)
|
||||
return lhs.address < rhs.address;
|
||||
if (lhs.name != rhs.name)
|
||||
return lhs.name < rhs.name;
|
||||
return lhs.size > rhs.size;
|
||||
});
|
||||
symbols->erase(std::unique(symbols->begin(), symbols->end(),
|
||||
[](const Symbol& lhs, const Symbol& rhs) {
|
||||
return lhs.address == rhs.address && lhs.name == rhs.name;
|
||||
}),
|
||||
symbols->end());
|
||||
}
|
||||
|
||||
std::span<const SymbolFile::Symbol> SymbolFile::GetSymbolsAtAddress(const SymbolList& symbols, u32 address)
|
||||
{
|
||||
const auto first = std::ranges::lower_bound(symbols, address, {}, &Symbol::address);
|
||||
const auto last = std::ranges::upper_bound(first, symbols.end(), address, {}, &Symbol::address);
|
||||
return std::span(first, last);
|
||||
}
|
||||
|
||||
const SymbolFile::Symbol* SymbolFile::FindSymbol(const SymbolList& symbols, u32 address)
|
||||
{
|
||||
auto it = std::ranges::lower_bound(symbols, address, {}, &Symbol::address);
|
||||
if (it != symbols.end() && it->address == address)
|
||||
return &*it;
|
||||
|
||||
// Skip labels, functions don't nest, so the closest preceding sized symbol is the only candidate.
|
||||
while (it != symbols.begin())
|
||||
{
|
||||
--it;
|
||||
if (it->size != 0)
|
||||
return ((address - it->address) < it->size) ? &*it : nullptr;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin <[email protected]>
|
||||
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "common/types.h"
|
||||
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
class ELFFile;
|
||||
class Error;
|
||||
|
||||
namespace SymbolFile {
|
||||
|
||||
struct Symbol
|
||||
{
|
||||
u32 address;
|
||||
u32 size; // Function size, or zero for labels/unknown sizes.
|
||||
std::string name;
|
||||
};
|
||||
using SymbolList = std::vector<Symbol>;
|
||||
|
||||
// Parsed symbols are appended to the list.
|
||||
void ParseELF(const ELFFile& elf, SymbolList* symbols);
|
||||
bool ParseMap(std::string_view text, SymbolList* symbols, Error* error);
|
||||
bool Load(const char* path, SymbolList* symbols, Error* error);
|
||||
|
||||
void SortSymbols(SymbolList* symbols);
|
||||
|
||||
// Lookups require a sorted list. Exact labels take precedence over function ranges, which are assumed not to nest.
|
||||
std::span<const Symbol> GetSymbolsAtAddress(const SymbolList& symbols, u32 address);
|
||||
const Symbol* FindSymbol(const SymbolList& symbols, u32 address);
|
||||
|
||||
} // namespace SymbolFile
|
||||
@@ -103,6 +103,7 @@
|
||||
<ClInclude Include="spirv_module.h" />
|
||||
<ClInclude Include="sqlite_helpers.h" />
|
||||
<ClInclude Include="state_wrapper.h" />
|
||||
<ClInclude Include="symbol_file.h" />
|
||||
<ClInclude Include="texture_decompress.h" />
|
||||
<ClInclude Include="translation.h" />
|
||||
<ClInclude Include="vulkan_builders.h" />
|
||||
@@ -213,6 +214,7 @@
|
||||
<ClCompile Include="spirv_module.cpp" />
|
||||
<ClCompile Include="sqlite_helpers.cpp" />
|
||||
<ClCompile Include="state_wrapper.cpp" />
|
||||
<ClCompile Include="symbol_file.cpp" />
|
||||
<ClCompile Include="texture_decompress.cpp" />
|
||||
<ClCompile Include="translation.cpp" />
|
||||
<ClCompile Include="vulkan_builders.cpp" />
|
||||
|
||||
@@ -90,6 +90,7 @@
|
||||
<ClInclude Include="dyn_libwebp.h" />
|
||||
<ClInclude Include="dyn_libzip.h" />
|
||||
<ClInclude Include="input_manager_private.h" />
|
||||
<ClInclude Include="symbol_file.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="state_wrapper.cpp" />
|
||||
@@ -178,6 +179,7 @@
|
||||
<ClCompile Include="sqlite_helpers.cpp" />
|
||||
<ClCompile Include="zip_helpers.cpp" />
|
||||
<ClCompile Include="dyn_libs.cpp" />
|
||||
<ClCompile Include="symbol_file.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="metal_shaders.metal" />
|
||||
|
||||
Reference in new issue
Block a user