Files
archived-pcsx2/pcsx2/DebugTools/SymbolImporter.cpp

677 lines
19 KiB
C++

// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "SymbolImporter.h"
#include "CDVD/CDVD.h"
#include "DebugInterface.h"
#include "Elfheader.h"
#include "MIPSAnalyst.h"
#include "VMManager.h"
#include "common/Console.h"
#include "common/Error.h"
#include "common/FileSystem.h"
#include "common/Path.h"
#include "common/StringUtil.h"
#include "common/Threading.h"
#include <ccc/ast.h>
#include <ccc/elf.h>
#include <ccc/importer_flags.h>
#include <ccc/symbol_file.h>
#include <demangle.h>
SymbolImporter R5900SymbolImporter(R5900SymbolGuardian);
struct DefaultBuiltInType
{
const char* name;
ccc::ast::BuiltInClass bclass;
};
static const std::vector<DefaultBuiltInType> DEFAULT_BUILT_IN_TYPES = {
{"char", ccc::ast::BuiltInClass::UNQUALIFIED_8},
{"signed char", ccc::ast::BuiltInClass::SIGNED_8},
{"unsigned char", ccc::ast::BuiltInClass::UNSIGNED_8},
{"short", ccc::ast::BuiltInClass::SIGNED_16},
{"unsigned short", ccc::ast::BuiltInClass::UNSIGNED_16},
{"int", ccc::ast::BuiltInClass::SIGNED_32},
{"unsigned int", ccc::ast::BuiltInClass::UNSIGNED_32},
{"unsigned", ccc::ast::BuiltInClass::UNSIGNED_32},
{"long", ccc::ast::BuiltInClass::SIGNED_64},
{"unsigned long", ccc::ast::BuiltInClass::UNSIGNED_64},
{"long long", ccc::ast::BuiltInClass::SIGNED_64},
{"unsigned long long", ccc::ast::BuiltInClass::UNSIGNED_64},
{"bool", ccc::ast::BuiltInClass::BOOL_8},
{"float", ccc::ast::BuiltInClass::FLOAT_32},
{"double", ccc::ast::BuiltInClass::FLOAT_64},
{"void", ccc::ast::BuiltInClass::VOID_TYPE},
{"s8", ccc::ast::BuiltInClass::SIGNED_8},
{"u8", ccc::ast::BuiltInClass::UNSIGNED_8},
{"s16", ccc::ast::BuiltInClass::SIGNED_16},
{"u16", ccc::ast::BuiltInClass::UNSIGNED_16},
{"s32", ccc::ast::BuiltInClass::SIGNED_32},
{"u32", ccc::ast::BuiltInClass::UNSIGNED_32},
{"s64", ccc::ast::BuiltInClass::SIGNED_64},
{"u64", ccc::ast::BuiltInClass::UNSIGNED_64},
{"s128", ccc::ast::BuiltInClass::SIGNED_128},
{"u128", ccc::ast::BuiltInClass::UNSIGNED_128},
{"f32", ccc::ast::BuiltInClass::FLOAT_32},
{"f64", ccc::ast::BuiltInClass::FLOAT_64},
};
static void error_callback(const ccc::Error& error, ccc::ErrorLevel level)
{
switch (level)
{
case ccc::ERROR_LEVEL_ERROR:
Console.Error("Error while importing symbol table: %s", error.message.c_str());
break;
case ccc::ERROR_LEVEL_WARNING:
Console.Warning("Warning while importing symbol table: %s", error.message.c_str());
break;
}
}
SymbolImporter::SymbolImporter(SymbolGuardian& guardian)
: m_guardian(guardian)
{
ccc::set_custom_error_callback(error_callback);
}
void SymbolImporter::OnElfChanged(std::vector<u8> elf, const std::string& elf_file_name)
{
Reset();
if (EmuConfig.DebuggerAnalysis.RunCondition == DebugAnalysisCondition::NEVER)
{
m_symbol_table_loaded_on_boot = false;
return;
}
if (!m_debugger_open && EmuConfig.DebuggerAnalysis.RunCondition == DebugAnalysisCondition::IF_DEBUGGER_IS_OPEN)
{
m_symbol_table_loaded_on_boot = false;
return;
}
AnalyseElf(std::move(elf), elf_file_name, EmuConfig.DebuggerAnalysis, true);
m_symbol_table_loaded_on_boot = true;
}
void SymbolImporter::OnElfLoadedInMemory()
{
{
std::lock_guard lock(m_elf_loaded_in_memory_mutex);
m_elf_loaded_in_memory = true;
}
m_elf_loaded_in_memory_condition_variable.notify_one();
}
void SymbolImporter::OnDebuggerOpened()
{
m_debugger_open = true;
if (EmuConfig.DebuggerAnalysis.RunCondition == DebugAnalysisCondition::NEVER)
return;
if (m_symbol_table_loaded_on_boot)
return;
LoadAndAnalyseElf(EmuConfig.DebuggerAnalysis);
}
void SymbolImporter::OnDebuggerClosed()
{
m_debugger_open = false;
}
void SymbolImporter::Reset()
{
ShutdownWorkerThread();
m_guardian.ReadWrite([&](ccc::SymbolDatabase& database) {
database.clear();
m_builtin_types.clear();
ccc::Result<ccc::SymbolSourceHandle> source = database.get_symbol_source("Built-In");
if (!source.success())
return;
// Create some built-in data type symbols so that users still have some
// types to use even if there isn't a symbol table loaded. Maybe in the
// future we could add PS2-specific types like DMA tags here too.
for (const DefaultBuiltInType& default_type : DEFAULT_BUILT_IN_TYPES)
{
ccc::Result<ccc::DataType*> symbol = database.data_types.create_symbol(default_type.name, *source, nullptr);
if (!symbol.success())
return;
std::unique_ptr<ccc::ast::BuiltIn> type = std::make_unique<ccc::ast::BuiltIn>();
type->name = default_type.name;
type->size_bytes = ccc::ast::builtin_class_size(default_type.bclass);
type->bclass = default_type.bclass;
(*symbol)->set_type(std::move(type));
m_builtin_types.emplace(default_type.name, (*symbol)->handle());
}
});
}
void SymbolImporter::LoadAndAnalyseElf(Pcsx2Config::DebugAnalysisOptions options)
{
if (!VMManager::HasValidVM())
return;
const std::string& elf_path = VMManager::GetCurrentELF();
Error error;
ElfObject elfo;
if (elf_path.empty() || !cdvdLoadElf(&elfo, elf_path, false, &error))
{
if (!elf_path.empty())
Console.Error(fmt::format("Failed to read ELF for symbol import: {}: {}", elf_path, error.GetDescription()));
return;
}
AnalyseElf(elfo.ReleaseData(), elf_path, options, false);
}
struct SymbolImporterThreadParameters
{
std::vector<u8> elf;
std::string elf_file_name;
std::string nocash_path;
Pcsx2Config::DebugAnalysisOptions options;
bool wait_until_elf_is_loaded;
};
void SymbolImporter::AnalyseElf(
std::vector<u8> elf,
const std::string& elf_file_name,
Pcsx2Config::DebugAnalysisOptions options,
bool wait_until_elf_is_loaded)
{
if (!VMManager::HasValidVM())
return;
// Search for a .sym file to load symbols from.
std::string nocash_path;
CDVD_SourceType source_type = CDVDsys_GetSourceType();
if (source_type == CDVD_SourceType::Iso)
{
std::string iso_file_path = CDVDsys_GetFile(source_type);
std::string::size_type n = iso_file_path.rfind('.');
if (n == std::string::npos)
nocash_path = iso_file_path + ".sym";
else
nocash_path = iso_file_path.substr(0, n) + ".sym";
}
SymbolImporterThreadParameters parameters;
parameters.elf = std::move(elf);
parameters.elf_file_name = elf_file_name;
parameters.nocash_path = std::move(nocash_path);
parameters.options = std::move(options);
parameters.wait_until_elf_is_loaded = wait_until_elf_is_loaded;
ShutdownWorkerThread();
m_import_thread = std::thread([this, params = std::move(parameters)]() {
Threading::SetNameOfCurrentThread("Symbol Worker");
ccc::Result<ccc::ElfFile> parsed_elf = ccc::ElfFile::parse(std::move(params.elf));
if (!parsed_elf.success())
{
ccc::report_error(parsed_elf.error());
return;
}
ccc::ElfSymbolFile symbol_file(std::move(*parsed_elf), std::move(params.elf_file_name));
ccc::SymbolDatabase temp_database;
ImportSymbols(
temp_database,
symbol_file,
params.nocash_path,
params.options,
m_builtin_types,
&m_interrupt_import_thread);
if (m_interrupt_import_thread)
return;
if (params.options.GenerateFunctionHashes)
{
ElfMemoryReader reader(symbol_file.elf());
SymbolGuardian::GenerateFunctionHashes(temp_database, reader);
}
if (m_interrupt_import_thread)
return;
if (params.wait_until_elf_is_loaded && params.options.FunctionScanMode == DebugFunctionScanMode::SCAN_MEMORY)
{
// Wait for the entry point to start compiling on the CPU thread so
// we know the functions we want to scan are loaded in memory.
std::unique_lock lock(m_elf_loaded_in_memory_mutex);
m_elf_loaded_in_memory_condition_variable.wait(lock,
[this]() { return m_elf_loaded_in_memory; });
if (m_interrupt_import_thread)
return;
}
m_guardian.ReadWrite([&](ccc::SymbolDatabase& database) {
ClearExistingSymbols(database, params.options);
if (m_interrupt_import_thread)
return;
database.merge_from(temp_database);
if (m_interrupt_import_thread)
return;
// The function scanner has to be run on the main database so that
// functions created before the importer was run are still
// considered. Otherwise, duplicate functions will be created.
ScanForFunctions(database, symbol_file, params.options);
});
});
}
void SymbolImporter::ShutdownWorkerThread()
{
if (m_import_thread.joinable())
{
m_interrupt_import_thread = true;
// Make sure the import thread is woken up so we can shut it down.
{
std::lock_guard lock(m_elf_loaded_in_memory_mutex);
m_elf_loaded_in_memory = true;
}
m_elf_loaded_in_memory_condition_variable.notify_one();
m_import_thread.join();
m_interrupt_import_thread = false;
}
{
std::lock_guard lock(m_elf_loaded_in_memory_mutex);
m_elf_loaded_in_memory = false;
}
}
void SymbolImporter::ClearExistingSymbols(ccc::SymbolDatabase& database, const Pcsx2Config::DebugAnalysisOptions& options)
{
std::vector<ccc::SymbolSourceHandle> sources_to_destroy;
for (const ccc::SymbolSource& source : database.symbol_sources)
{
bool should_destroy = ShouldClearSymbolsFromSourceByDefault(source.name());
if (!options.AutomaticallySelectSymbolsToClear)
for (const DebugSymbolSource& source_config : options.SymbolSources)
if (source_config.Name == source.name())
should_destroy = source_config.ClearDuringAnalysis;
if (should_destroy)
sources_to_destroy.emplace_back(source.handle());
}
for (ccc::SymbolSourceHandle handle : sources_to_destroy)
database.destroy_symbols_from_source(handle, true);
}
bool SymbolImporter::ShouldClearSymbolsFromSourceByDefault(const std::string& source_name)
{
return source_name.find("Symbol Table") != std::string::npos ||
source_name == "ELF Section Headers" ||
source_name == "Function Scanner" ||
source_name == "Nocash Symbols";
}
void SymbolImporter::ImportSymbols(
ccc::SymbolDatabase& database,
const ccc::ElfSymbolFile& elf,
const std::string& nocash_path,
const Pcsx2Config::DebugAnalysisOptions& options,
const std::map<std::string, ccc::DataTypeHandle>& builtin_types,
const std::atomic_bool* interrupt)
{
u32 importer_flags =
ccc::NO_MEMBER_FUNCTIONS |
ccc::NO_OPTIMIZED_OUT_FUNCTIONS |
ccc::UNIQUE_FUNCTIONS;
if (options.DemangleParameters)
importer_flags |= ccc::DEMANGLE_PARAMETERS;
ccc::DemanglerFunctions demangler;
if (options.DemangleSymbols)
{
demangler.cplus_demangle = cplus_demangle;
demangler.cplus_demangle_opname = cplus_demangle_opname;
}
if (options.ImportSymbolsFromELF)
{
ccc::Result<std::vector<std::unique_ptr<ccc::SymbolTable>>> symbol_tables = elf.get_all_symbol_tables();
if (!symbol_tables.success())
{
ccc::report_error(symbol_tables.error());
}
else
{
ccc::Result<ccc::ModuleHandle> module_handle = ccc::import_symbol_tables(
database, *symbol_tables, elf.name(), ccc::Address(), importer_flags, demangler, interrupt);
if (!module_handle.success())
{
ccc::report_error(module_handle.error());
}
}
}
if (!nocash_path.empty() && options.ImportSymFileFromDefaultLocation)
{
ccc::Result<bool> nocash_result = ImportNocashSymbols(database, nocash_path, 0, builtin_types);
if (!nocash_result.success())
{
Console.Error("Failed to import symbol file '%s': %s",
nocash_path.c_str(), nocash_result.error().message.c_str());
}
}
ImportExtraSymbols(database, options, builtin_types, importer_flags, demangler, interrupt);
Console.WriteLn("Imported %d symbols.", database.symbol_count());
return;
}
void SymbolImporter::ImportExtraSymbols(
ccc::SymbolDatabase& database,
const Pcsx2Config::DebugAnalysisOptions& options,
const std::map<std::string, ccc::DataTypeHandle>& builtin_types,
u32 importer_flags,
const ccc::DemanglerFunctions& demangler,
const std::atomic_bool* interrupt)
{
MipsExpressionFunctions expression_functions(&r5900Debug, &database, true);
for (const DebugExtraSymbolFile& extra_symbol_file : options.ExtraSymbolFiles)
{
if (*interrupt)
return;
std::string path = Path::ToNativePath(extra_symbol_file.Path);
if (!Path::IsAbsolute(path))
path = Path::Combine(EmuFolders::GameSettings, path);
if (!extra_symbol_file.Condition.empty())
{
u64 expression_result = 0;
std::string error;
if (!parseExpression(extra_symbol_file.Condition.c_str(), &expression_functions, expression_result, error))
{
Console.Error("Failed to evaluate condition expression '%s' while importing extra symbol file '%s': %s",
extra_symbol_file.Condition.c_str(), path.c_str(), error.c_str());
}
if (!expression_result)
continue;
}
ccc::Address base_address;
if (!extra_symbol_file.BaseAddress.empty())
{
u64 expression_result = 0;
std::string error;
if (!parseExpression(extra_symbol_file.BaseAddress.c_str(), &expression_functions, expression_result, error))
{
Console.Error("Failed to evaluate base address expression '%s' while importing extra symbol file '%s': %s",
extra_symbol_file.BaseAddress.c_str(), path.c_str(), error.c_str());
}
base_address = static_cast<u32>(expression_result);
}
if (StringUtil::EndsWithNoCase(path, ".sym"))
{
ccc::Result<bool> nocash_result = ImportNocashSymbols(
database, path, base_address.get_or_zero(), builtin_types);
if (!nocash_result.success())
{
Console.Error("Failed to import symbol file '%s': %s",
extra_symbol_file.Path.c_str(), nocash_result.error().message.c_str());
}
if (!*nocash_result)
Console.Error("Cannot open symbol file '%s'.", path.c_str());
continue;
}
std::optional<std::vector<u8>> image = FileSystem::ReadBinaryFile(path.c_str());
if (!image.has_value())
{
Console.Error("Failed to read extra symbol file '%s'.", path.c_str());
continue;
}
std::string file_name(Path::GetFileName(path));
ccc::Result<std::unique_ptr<ccc::SymbolFile>> symbol_file = ccc::parse_symbol_file(
std::move(*image), file_name.c_str());
if (!symbol_file.success())
{
ccc::report_error(symbol_file.error());
continue;
}
ccc::Result<std::vector<std::unique_ptr<ccc::SymbolTable>>> symbol_tables =
(*symbol_file)->get_all_symbol_tables();
if (!symbol_tables.success())
{
ccc::report_error(symbol_tables.error());
continue;
}
ccc::Result<ccc::ModuleHandle> module_handle = ccc::import_symbol_tables(
database, *symbol_tables, (*symbol_file)->name(), base_address, importer_flags, demangler, interrupt);
if (!module_handle.success())
{
ccc::report_error(module_handle.error());
continue;
}
}
}
ccc::Result<bool> SymbolImporter::ImportNocashSymbols(
ccc::SymbolDatabase& database,
const std::string& file_path,
u32 base_address,
const std::map<std::string, ccc::DataTypeHandle>& builtin_types)
{
auto file = FileSystem::OpenManagedCFile(file_path.c_str(), "r");
if (!file)
return false;
ccc::Result<ccc::SymbolSourceHandle> source = database.get_symbol_source("Nocash Symbols");
CCC_RETURN_IF_ERROR(source);
while (!feof(file.get()))
{
char line[256], value[256] = {0};
char* p = fgets(line, 256, file.get());
if (p == NULL)
break;
if (char* end = strchr(line, '\n'))
*end = '\0';
u32 address;
if (sscanf(line, "%08x %255s", &address, value) != 2)
continue;
if (address == 0 && strcmp(value, "0") == 0)
continue;
address += base_address;
if (value[0] == '.')
{
// data directives
char* s = strchr(value, ':');
if (s != NULL)
{
*s = 0;
u32 size = 0;
if (sscanf(s + 1, "%04x", &size) != 1)
continue;
std::unique_ptr<ccc::ast::Node> type;
if (StringUtil::Strcasecmp(value, ".byt") == 0)
type = GetBuiltInType("u8", ccc::ast::BuiltInClass::UNSIGNED_8, builtin_types);
else if (StringUtil::Strcasecmp(value, ".wrd") == 0)
type = GetBuiltInType("u16", ccc::ast::BuiltInClass::UNSIGNED_16, builtin_types);
else if (StringUtil::Strcasecmp(value, ".dbl") == 0)
type = GetBuiltInType("u32", ccc::ast::BuiltInClass::UNSIGNED_32, builtin_types);
else if (StringUtil::Strcasecmp(value, ".asc") == 0)
type = GetBuiltInType("char", ccc::ast::BuiltInClass::UNQUALIFIED_8, builtin_types);
else
continue;
ccc::Result<ccc::GlobalVariable*> global_variable = database.global_variables.create_symbol(
line, address, *source, nullptr);
CCC_RETURN_IF_ERROR(global_variable);
if (type->size_bytes == (s32)size)
{
(*global_variable)->set_type(std::move(type));
}
else
{
std::unique_ptr<ccc::ast::Array> array = std::make_unique<ccc::ast::Array>();
array->size_bytes = (s32)size;
array->element_type = std::move(type);
array->element_count = size / array->element_type->size_bytes;
(*global_variable)->set_type(std::move(array));
}
}
}
else
{ // labels
u32 size = 1;
char* seperator = strchr(value, ',');
if (seperator != NULL)
{
*seperator = 0;
sscanf(seperator + 1, "%08x", &size);
}
if (size != 1)
{
ccc::Result<ccc::Function*> function = database.functions.create_symbol(value, address, *source, nullptr);
CCC_RETURN_IF_ERROR(function);
(*function)->set_size(size);
}
else
{
ccc::Result<ccc::Label*> label = database.labels.create_symbol(value, address, *source, nullptr);
CCC_RETURN_IF_ERROR(label);
}
}
}
return true;
}
std::unique_ptr<ccc::ast::Node> SymbolImporter::GetBuiltInType(
const std::string& name,
ccc::ast::BuiltInClass bclass,
const std::map<std::string, ccc::DataTypeHandle>& builtin_types)
{
auto type = builtin_types.find(name);
if (type != builtin_types.end())
{
std::unique_ptr<ccc::ast::TypeName> type_name = std::make_unique<ccc::ast::TypeName>();
type_name->size_bytes = ccc::ast::builtin_class_size(bclass);
type_name->data_type_handle = type->second;
return type_name;
}
std::unique_ptr<ccc::ast::BuiltIn> built_in = std::make_unique<ccc::ast::BuiltIn>();
built_in->size_bytes = ccc::ast::builtin_class_size(bclass);
built_in->bclass = bclass;
return built_in;
}
void SymbolImporter::ScanForFunctions(
ccc::SymbolDatabase& database, const ccc::ElfSymbolFile& elf, const Pcsx2Config::DebugAnalysisOptions& options)
{
MipsExpressionFunctions expression_functions(&r5900Debug, &database, true);
u32 start_address = 0;
u32 end_address = 0;
if (options.CustomFunctionScanRange)
{
u64 expression_result = 0;
std::string error;
if (!parseExpression(options.FunctionScanStartAddress.c_str(), &expression_functions, expression_result, error))
{
Console.Error("Failed to evaluate start address expression '%s' while scanning for functions: %s",
options.FunctionScanStartAddress.c_str(), error.c_str());
return;
}
start_address = static_cast<u32>(expression_result);
if (!parseExpression(options.FunctionScanEndAddress.c_str(), &expression_functions, expression_result, error))
{
Console.Error("Failed to evaluate end address expression '%s' while scanning for functions: %s",
options.FunctionScanEndAddress.c_str(), error.c_str());
return;
}
end_address = static_cast<u32>(expression_result);
}
else
{
const ccc::ElfProgramHeader* entry_segment = elf.elf().entry_point_segment();
if (!entry_segment)
return;
start_address = entry_segment->vaddr;
end_address = entry_segment->vaddr + entry_segment->filesz;
}
switch (options.FunctionScanMode)
{
case DebugFunctionScanMode::SCAN_ELF:
{
ElfMemoryReader reader(elf.elf());
MIPSAnalyst::ScanForFunctions(database, reader, start_address, end_address, options.GenerateFunctionHashes);
break;
}
case DebugFunctionScanMode::SCAN_MEMORY:
{
MIPSAnalyst::ScanForFunctions(database, r5900Debug, start_address, end_address, options.GenerateFunctionHashes);
break;
}
case DebugFunctionScanMode::SKIP:
{
break;
}
}
}