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:
Stenzek committed 2026-11-07 01:17:36 +10:00
1 parent 33245ebe56
commit 2015c40d2f
18 files changed
+1153 -129

No files matched your search

+144 -93
View File
@@ -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);
}
+15 -3
View File
@@ -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;
+42 -14
View File
@@ -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 -2
View File
@@ -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;
};
+214 -15
View File
@@ -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)
{
+29 -2
View File
@@ -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;
};
+53
View File
@@ -51,8 +51,19 @@
<addaction name="actionToggleBreakpoint"/>
<addaction name="actionClearBreakpoints"/>
</widget>
<widget class="QMenu" name="menuSymbols">
<property name="title">
<string>&amp;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 &amp;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>&amp;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>&amp;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>&amp;Disable Symbols</string>
</property>
</action>
<action name="actionClose">
<property name="icon">
<iconset resource="resources/duckstation-qt.qrc">
+1
View File
@@ -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
+354
View File
@@ -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
+1
View File
@@ -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>
+2
View File
@@ -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
+38
View File
@@ -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)
+18
View File
@@ -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;
};
+190
View File
@@ -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, &section_first, &section_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;
}
+37
View File
@@ -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
+2
View File
@@ -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" />
+2
View File
@@ -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" />