Files
ppsspp/unittest/TestDemangle.cpp
T
Henrik RydgårdandClaude Opus 5 dfc04f3578 Demangle: handle CodeWarrior templates, function pointers and @-symbols
Checked against two PSP binaries that shipped with intact symbol tables,
which turned up several constructs the format's usual description doesn't
mention:

- Template arguments are written literally inside the length-prefixed name
  ("39CList<Q38hlScreen5Brwsr13CContentsUnit>"), not with a "__PT" prefix,
  and they nest. Function templates put theirs in the base name instead,
  followed by the return type.
- A family of "@"-decorated symbols for things with no C++ name: thunks
  ("@12@__dt__3SonFv"), string literals, function-local statics and their
  guard variables. Plus __vt__/__RTTI__/__sinit_, printed in the same style
  as the Itanium special names.
- Types are now built as a split declarator, so a pointer to a function
  comes out as "int (*)(int)" rather than "int (int) *".

Also stop the lenient pass from turning plain C names with a "__" in them
into nonsense - "I3dClut__FlushCache" became "I3dClut(long, ...)". It now
requires a class qualifier, which costs nothing: over ~10000 symbols the
lenient pass rescued none and only produced those false positives.

Symbol map names go from 128 to 256 characters, since a demangled name
keeps its parameters and templates make short work of 128.

docs/CodeWarriorMangling.md describes the format, marking the parts that
are inferred from cfront rather than attested in a real binary.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SF5eS5QDNexLksRDeDZvwY
2026-08-29 23:23:57 +02:00

276 lines
14 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" },
};
// Metrowerks CodeWarrior (cfront-derived). No reference demangler was available to check
// these against, so they're what our own parser produces - readable and correctly qualified,
// but not claimed to be byte-identical to what CodeWarrior's own tools would print.
static const DemangleCase codeWarriorCases[] = {
{ "__as__9ANIMEDataFRC9ANIMEData",
"ANIMEData::operator=(const ANIMEData &)" },
{ "__SetupFrameInfo__FP12ThrowContextP13ExceptionInfo",
"__SetupFrameInfo(ThrowContext *, ExceptionInfo *)" },
{ "getDistance__6KzUtilFP7st_unitP7st_unit",
"KzUtil::getDistance(st_unit *, st_unit *)" },
{ "__ad__3stdFQ33std10ctype_base4maskQ33std10ctype_base4mask",
"std::operator&(std::ctype_base::mask, std::ctype_base::mask)" },
{ "__ml__10DTVector3dCFf",
"DTVector3d::operator*(float) const" },
{ "__ct__Q23std12basic_stringFv",
"std::basic_string::basic_string()" },
{ "__dt__6KzUtilFv",
"KzUtil::~KzUtil()" },
{ "__op4Type__3FooCFv",
"Foo::operator Type() const" },
{ "__vc__5ArrayFi",
"Array::operator[](int)" },
{ "__nw__FUi",
"operator new(unsigned int)" },
{ "draw__3FooFA10_iRCf",
"Foo::draw(int [10], const float &)" },
// "N<count><index>" repeats an earlier parameter, "T<index>" refers back to one.
{ "foo__FPCcUiN21",
"foo(const char *, unsigned int, const char *, const char *)" },
{ "sort__FPiT1",
"sort(int *, int *)" },
// A static data member: no "F", so no parameter list.
{ "count__Q23foo3bar",
"foo::bar::count" },
// A parameter type we can't decode still gets to keep its name.
{ "weird__3FooFZZZ",
"Foo::weird(...)" },
// A pointer to a function needs the declarator wrapped, not just a "*" stuck on.
{ "__call_static_initializers__FPPFv_vPPFv_v",
"__call_static_initializers(void (**)(), void (**)())" },
{ "GetCalcHeapSize__Q26hlSave14SaveDataBufferFPFi_i",
"hlSave::SaveDataBuffer::GetCalcHeapSize(int (*)(int))" },
// Template arguments are spelled inside the length-prefixed name, and are themselves
// mangled types (or plain integers, for a non-type parameter).
{ "Init__Q26shList39CList<Q38hlScreen5Brwsr13CContentsUnit>FPCc",
"shList::CList<hlScreen::Brwsr::CContentsUnit>::Init(const char *)" },
{ "Reset__Q28shString15CSimpleChar<24>Fv",
"shString::CSimpleChar<24>::Reset()" },
{ "AddItem__Q26shList47CList<Q26ssTool29tag_<Q26shFont12SysCmdString>>FPQ26ssTool29tag_<Q26shFont12SysCmdString>",
"shList::CList<ssTool::tag_<shFont::SysCmdString>>::AddItem(ssTool::tag_<shFont::SysCmdString> *)" },
// A function template puts them in the base name instead, and encodes its return type.
{ "sort<Pf>__3stdFPfPf_v",
"void std::sort<float *>(float *, float *)" },
// Compiler-generated symbols that wrap a mangled name rather than being one.
{ "__vt__Q23std9exception",
"vtable for std::exception" },
{ "__RTTI__Q23std9exception",
"typeinfo for std::exception" },
{ "__sinit_hl_app.cpp",
"static initializers for hl_app.cpp" },
{ "@12@__dt__3SonFv",
"non-virtual thunk (12) to Son::~Son()" },
{ "@STRING@what__Q23std9exceptionCFv",
"string literal in std::exception::what() const" },
{ "@STRING@Get_BGM__Q25hlBHC4SBhcFi@0",
"string literal 0 in hlBHC::SBhc::Get_BGM(int)" },
{ "@LOCAL@sort<Pf>__3stdFPfPf_v@shuffle@0",
"void std::sort<float *>(float *, float *)::shuffle" },
{ "@GUARD@app$16079",
"guard variable for app$16079" },
};
// SN Systems (SNC/ProDG). Same caveat as above, plus the format itself is reverse
// engineered from a small sample - see the comments in Demangle.cpp.
static const DemangleCase snSystemsCases[] = {
{ "__0f5DstdIbad_castEwhatvK",
"std::bad_cast::what() const" },
{ "__0F5DstdJTerminatev",
"std::Terminate()" },
{ "__0F5INTskMenuKEventInit_6LtagSTaskHdl",
"NTskMenu::EventInit_(tagSTaskHdl)" },
};
// 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.
"Foo__Bar", // A "__" that isn't a CodeWarrior separator.
"a__b__c",
// The same, but where the tail happens to start with an "F" and a valid type code, so
// only the lack of a class qualifier tells it apart from a real mangled name.
"I3dClut__FlushCache",
"@10046", // An anonymous string constant: nothing to demangle.
"__0", // Too short to be an SN Systems name.
// A CodeWarrior template name, truncated by a 127-character symbol table limit. The
// length prefixes no longer match what's left, so there's nothing to recover.
"__distance<Q23std164__wrap_iterator<Q23std100vector<Q210Metrowerks20range_map_entry<w,w>,Q23std47allocator<Q210Metrowerks20rang",
};
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 DemangleCase &testCase : codeWarriorCases) {
DemangledSymbol sym;
if (!DemangleCodeWarrior(testCase.mangled, &sym)) {
printf("Failed to demangle %s\n", testCase.mangled);
return false;
}
const std::string out = sym.ToString();
const std::string expected = testCase.expected;
EXPECT_EQ_STR(out, expected);
// The parts have to add up to the same thing the wrapper prints.
const std::string full = DemangleSymbolName(testCase.mangled);
EXPECT_EQ_STR(full, expected);
}
for (const DemangleCase &testCase : snSystemsCases) {
DemangledSymbol sym;
if (!DemangleSNSystems(testCase.mangled, &sym)) {
printf("Failed to demangle %s\n", testCase.mangled);
return false;
}
const std::string out = sym.ToString();
const std::string expected = testCase.expected;
EXPECT_EQ_STR(out, expected);
const std::string full = DemangleSymbolName(testCase.mangled);
EXPECT_EQ_STR(full, expected);
}
// The split-up form is there for callers that want more than the printed string.
{
DemangledSymbol sym;
EXPECT_TRUE(DemangleCodeWarrior("__ml__10DTVector3dCFf", &sym));
EXPECT_EQ_STR(sym.name, std::string("DTVector3d::operator*"));
EXPECT_EQ_STR(sym.parameters, std::string("float"));
EXPECT_EQ_STR(sym.qualifiers, std::string("const"));
EXPECT_TRUE(sym.isFunction);
EXPECT_TRUE(DemangleCodeWarrior("count__Q23foo3bar", &sym));
EXPECT_FALSE(sym.isFunction);
}
for (const char *name : notMangled) {
std::string out = "unchanged";
EXPECT_FALSE(DemangleItanium(name, &out));
EXPECT_TRUE(out == "unchanged");
DemangledSymbol sym;
EXPECT_FALSE(DemangleCodeWarrior(name, &sym));
EXPECT_FALSE(DemangleSNSystems(name, &sym));
// 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;
}