Files
ppsspp/unittest/TestDemangle.cpp
Henrik RydgårdandClaude Opus 5 049bcd5483 Demangle C++ symbol names when loading ELF symbols
C++ homebrew has an unreadable symbol table -
everything is _ZN10PxRenderer7DrawImmE... - which makes the disassembly and
symbol list nearly useless. Add an Itanium C++ ABI demangler and run ELF
symbols through it on load, in both ElfReader::LoadSymbols (unstripped EXECs,
which is what a CMake pspdev EBOOT actually contains) and the companion-ELF
path.

The demangling standard is called Itanium for historical reasons - it
was defined for Itanium but ended up being almost universally
applicable.

Written from scratch rather than using __cxa_demangle, which doesn't exist on
MSVC/UWP, or vendoring LLVM's demangler, whose license doesn't fit. Anything
unrecognized (arbitrary constant expressions, decltype) aborts the parse and
the caller gets the original mangled name back, so a caller never sees a
half-parsed result. Recursion is depth-capped since the input comes from a
file we didn't write.

Checked against c++filt as an oracle: of 1089 mangled symbols in a real C++
homebrew EBOOT, one differs; of 55189 from libstdc++/libLLVM/cc1plus, 22
differ and 413 are declined. Fuzzed with 220k mutated and random inputs under
ASan/UBSan.

Also adds a right-click menu to the ImDebugger symbol list.

Note that SymbolMap stores names in char[128], so the longest STL names get
truncated in the UI. Still far more readable than the mangled form.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01X3DbkJ8ShYiXU7q5Tv1LZu
2026-08-26 08:12:45 +02:00

141 lines
7.8 KiB
C++

// Copyright (c) 2026- PPSSPP Project.
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 2.0 or later versions.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License 2.0 for more details.
// A copy of the GPL 2.0 should have been included with the program.
// If not, see http://www.gnu.org/licenses/
// Official git repository and contact information can be found at
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
#include <string>
#include "Common/Data/Text/Demangle.h"
#include "UnitTest.h"
struct DemangleCase {
const char *mangled;
const char *expected;
};
// Expected values are what GNU c++filt prints, so they can be regenerated or checked by
// piping the left column through it.
static const DemangleCase demangleCases[] = {
{ "_ZN12_GLOBAL__N_1L12ExitCallbackEiiPv",
"(anon)::ExitCallback(int, int, void*)" },
{ "_ZN12_GLOBAL__N_14poolD1Ev",
"(anon)::pool::~pool()" },
{ "_ZN12_GLOBAL__N_14pool4freeEPv.constprop.0",
"(anon)::pool::free(void*) [clone .constprop.0]" },
{ "_ZNK5Thing3fooEv",
"Thing::foo() const" },
{ "_Z3fooPFivE",
"foo(int (*)())" },
{ "_Z3fooRA10_i",
"foo(int (&) [10])" },
{ "_Z3fooM3BarFivE",
"foo(int (Bar::*)())" },
{ "_Z3foorVKi",
"foo(int const volatile restrict)" },
{ "_Z3fooSs",
"foo(std::basic_string<char, std::char_traits<char>, std::allocator<char> >)" },
{ "_Z3fooSbIcE",
"foo(std::basic_string<char>)" },
{ "_ZNSt3mapIiiSt4lessIiESaISt4pairIKiiEEE3endEv",
"std::map<int, int, std::less<int>, std::allocator<std::pair<int const, int> > >::end()" },
{ "_ZNSsC1EOSs",
"std::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string(std::basic_string<char, std::char_traits<char>, std::allocator<char> >&&)" },
{ "_ZNSsC1IPKcEET_S2_RKSaIcE",
"std::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string<char const*>(char const*, char const*, std::allocator<char> const&)" },
{ "_ZNKSt6vectorIhSaIhEE11_M_data_ptrIhEEPT_S4_",
"unsigned char* std::vector<unsigned char, std::allocator<unsigned char> >::_M_data_ptr<unsigned char>(unsigned char*) const" },
{ "_ZNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEE12_M_constructIPKcEEvT_S8_St20forward_iterator_tag.isra.0",
"void std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_construct<char const*>(char const*, char const*, std::forward_iterator_tag) [clone .isra.0]" },
{ "_ZNKSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEE12find_last_ofERKS4_j",
"std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::find_last_of(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, unsigned int) const" },
{ "_ZZNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEE12_M_constructIPKcEEvT_S8_St20forward_iterator_tagEN6_GuardD1Ev",
"std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_construct<char const*>(char const*, char const*, std::forward_iterator_tag)::_Guard::~_Guard()" },
{ "_ZN9__gnu_cxxeqIPcSsEEbRKNS_17__normal_iteratorIT_T0_EES7_",
"bool __gnu_cxx::operator==<char*, std::basic_string<char, std::char_traits<char>, std::allocator<char> > >(__gnu_cxx::__normal_iterator<char*, std::basic_string<char, std::char_traits<char>, std::allocator<char> > > const&, __gnu_cxx::__normal_iterator<char*, std::basic_string<char, std::char_traits<char>, std::allocator<char> > > const&)" },
{ "_ZSt4sortIPiEvT_S1_",
"void std::sort<int*>(int*, int*)" },
{ "_ZTV3Foo",
"vtable for Foo" },
{ "_ZTIN3foo3barE",
"typeinfo for foo::bar" },
{ "_ZThn8_N3Foo3barEv",
"non-virtual thunk to Foo::bar()" },
{ "_ZGVZN3foo3barEvE5thing",
"guard variable for foo::bar()::thing" },
{ "_ZTIFbRlE",
"typeinfo for bool (long&)" },
{ "_ZNSt8ios_base7failureB5cxx11D0Ev",
"std::ios_base::failure[abi:cxx11]::~failure()" },
{ "_ZZ3foovENKUliE0_clEi",
"foo()::{lambda(int)#2}::operator()(int) const" },
{ "_ZN4llvm10AccelTableINS_20DWARF5AccelTableDataEE7addNameIJRKNS_3DIEEjbEEEvNS_23DwarfStringPoolEntryRefEDpOT_",
"void llvm::AccelTable<llvm::DWARF5AccelTableData>::addName<llvm::DIE const&, unsigned int, bool>(llvm::DwarfStringPoolEntryRef, llvm::DIE const&, unsigned int&&, bool&&)" },
{ "_ZNSt6vectorIN10PxRenderer10PspTextureESaIS1_EE12emplace_backIJS1_EEERS1_DpOT_",
"PxRenderer::PspTexture& std::vector<PxRenderer::PspTexture, std::allocator<PxRenderer::PspTexture> >::emplace_back<PxRenderer::PspTexture>(PxRenderer::PspTexture&&)" },
{ "_ZNK4llvm15SpecialCaseList14inSectionBlameERKNS_9StringMapINS1_INS0_7MatcherENS_15MallocAllocatorEEES3_EENS_9StringRefES8_S8_",
"llvm::SpecialCaseList::inSectionBlame(llvm::StringMap<llvm::StringMap<llvm::SpecialCaseList::Matcher, llvm::MallocAllocator>, llvm::MallocAllocator> const&, llvm::StringRef, llvm::StringRef, llvm::StringRef) const" },
{ "_ZN4llvm5RTLIB22getOutlineAtomicHelperERA5_A4_KNS0_7LibcallENS_14AtomicOrderingEm",
"llvm::RTLIB::getOutlineAtomicHelper(llvm::RTLIB::Libcall const (&) [5][4], llvm::AtomicOrdering, unsigned long)" },
{ "_ZNSt8_Rb_treeIN4llvm11logicalview10LVLineKindESt4pairIKS2_MNS1_6LVLineEKFbvEESt10_Select1stIS8_ESt4lessIS2_ESaIS8_EEaSERKSE_",
"std::_Rb_tree<llvm::logicalview::LVLineKind, std::pair<llvm::logicalview::LVLineKind const, bool (llvm::logicalview::LVLine::*)() const>, std::_Select1st<std::pair<llvm::logicalview::LVLineKind const, bool (llvm::logicalview::LVLine::*)() const> >, std::less<llvm::logicalview::LVLineKind>, std::allocator<std::pair<llvm::logicalview::LVLineKind const, bool (llvm::logicalview::LVLine::*)() const> > >::operator=(std::_Rb_tree<llvm::logicalview::LVLineKind, std::pair<llvm::logicalview::LVLineKind const, bool (llvm::logicalview::LVLine::*)() const>, std::_Select1st<std::pair<llvm::logicalview::LVLineKind const, bool (llvm::logicalview::LVLine::*)() const> >, std::less<llvm::logicalview::LVLineKind>, std::allocator<std::pair<llvm::logicalview::LVLineKind const, bool (llvm::logicalview::LVLine::*)() const> > > const&)" },
{ "_ZlsRSoRK4Vec3",
"operator<<(std::basic_ostream<char, std::char_traits<char> >&, Vec3 const&)" },
{ "_ZNK3Foocv3BarEv",
"Foo::operator Bar() const" },
};
// Things that aren't Itanium-mangled names, or that we deliberately don't try to parse.
// All of these have to be reported as "not demangled" rather than half-parsed.
static const char *notMangled[] = {
"",
"_Z",
"main",
"sceKernelDelayThread",
"z_un_088c00f0",
"_GLOBAL__sub_I__ZN9__gnu_cxx9__freeresEv",
"_ZZZ",
"_ZN3foo", // Unterminated nested name.
"_ZN3foo3barES9_", // Substitution past the end of the table.
"_Z3fooIiEvT0_", // Template parameter past the end of the argument list.
"_Z3fooIXadL_Z1xEEEvv", // An <expression> template argument - no expression parser.
};
bool TestDemangle() {
for (const DemangleCase &testCase : demangleCases) {
std::string out;
if (!DemangleItanium(testCase.mangled, &out)) {
printf("Failed to demangle %s\n", testCase.mangled);
return false;
}
const std::string expected = testCase.expected;
EXPECT_EQ_STR(out, expected);
}
for (const char *name : notMangled) {
std::string out = "unchanged";
EXPECT_FALSE(DemangleItanium(name, &out));
EXPECT_TRUE(out == "unchanged");
// The convenience wrapper hands back the original in that case.
const std::string passedThrough = DemangleSymbolName(name);
const std::string original = name;
EXPECT_EQ_STR(passedThrough, original);
}
// Deeply nested types must fail on the depth cap rather than blowing the stack.
std::string out;
DemangleItanium("_Z3foo" + std::string(10000, 'P') + "i", &out);
return true;
}