From dfc04f3578643ba4e16b6eb8cec39c52cfa3c3a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sat, 29 Aug 2026 23:23:57 +0200 Subject: [PATCH] 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"), 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 Claude-Session: https://claude.ai/code/session_01SF5eS5QDNexLksRDeDZvwY --- Common/Data/Text/Demangle.cpp | 240 +++++++++++++++++++++++++++++++--- Core/Debugger/SymbolMap.cpp | 12 +- Core/Debugger/SymbolMap.h | 4 +- docs/CodeWarriorMangling.md | 202 ++++++++++++++++++++++++++++ unittest/TestDemangle.cpp | 37 ++++++ 5 files changed, 467 insertions(+), 28 deletions(-) create mode 100644 docs/CodeWarriorMangling.md diff --git a/Common/Data/Text/Demangle.cpp b/Common/Data/Text/Demangle.cpp index 6e1fd406b1..0236bc42f5 100644 --- a/Common/Data/Text/Demangle.cpp +++ b/Common/Data/Text/Demangle.cpp @@ -1210,6 +1210,13 @@ bool DemangleItanium(std::string_view mangled, std::string *out) { // Q-introduced chain of them, and the parameters are cfront type codes. Rough by design: // the goal is a correctly qualified name plus a plausible parameter list, not byte-exact // agreement with any particular demangler. +// +// Two places deviate from cfront, both worked out from real PSP binaries rather than from +// the Macintosh ABI document (which is wrong about them): template arguments are written +// literally as "<...>" inside the length-prefixed name rather than with a "__PT" prefix, and +// there is a whole family of "@"-decorated names for symbols the compiler generates itself. +// See DemangleCodeWarriorSpecial below for those, and docs/CodeWarriorMangling.md for a +// description of the whole format. namespace { @@ -1253,6 +1260,16 @@ static bool IsDigit(char c) { return c >= '0' && c <= '9'; } +// A type, split around where a declarator name would go, so a pointer to a function or an +// array can be wrapped: "int (*)(char)" is pre="int (*", post=")(char)". Plain types leave +// post empty. Same idea as Decl above, kept separate because the two manglings share nothing. +struct CWDecl { + std::string pre; + std::string post; + + std::string str() const { return pre + post; } +}; + class CodeWarriorParser { public: // strict: fail outright on a parameter we can't decode, rather than printing "...". @@ -1267,13 +1284,18 @@ public: // Standalone entry point, for the type after a "__op" conversion-operator name. std::string ParseWholeType(); + // Standalone entry point, for a template function's arguments, which are spelled out in + // the name rather than in the part after the "__". + std::string ParseTemplateArgs(std::string_view args); + private: char Peek() const { return pos_ < s_.size() ? s_[pos_] : 0; } bool Fail() { failed_ = true; return false; } int ParseNumber(); std::string ParseIdentifier(); std::string ParseQualifiedName(); - std::string ParseType(); + CWDecl ParseDecl(); + std::string ParseType() { return ParseDecl().str(); } bool ParseParams(std::string *out); std::string_view s_; @@ -1309,9 +1331,56 @@ std::string CodeWarriorParser::ParseIdentifier() { } std::string name(s_.substr(pos_, len)); pos_ += len; + // A class template's arguments live inside the length-prefixed name, spelled with the + // same type codes as everywhere else: "39CList". + const size_t lt = name.find('<'); + if (lt != std::string::npos && name.back() == '>') { + const std::string_view args = std::string_view(name).substr(lt + 1, name.size() - lt - 2); + name = name.substr(0, lt) + ParseTemplateArgs(args); + } return name; } +// The inside of a "<...>", as a comma-separated list of types and integer literals. Returns +// the whole thing including the brackets, and falls back to the raw text if any argument +// doesn't parse - half a decoded argument list is worse than the mangled one. +std::string CodeWarriorParser::ParseTemplateArgs(std::string_view args) { + std::vector parts; + size_t start = 0; + int depth = 0; + for (size_t i = 0; i <= args.size(); i++) { + if (i < args.size()) { + if (args[i] == '<') + depth++; + else if (args[i] == '>') + depth--; + // Only a top-level comma separates arguments; a nested "<...>" has its own. + if (args[i] != ',' || depth != 0) + continue; + } + const std::string_view arg = args.substr(start, i - start); + start = i + 1; + if (arg.empty()) + return "<" + std::string(args) + ">"; + // All digits is a non-type argument ("CSimpleChar<24>"); anything else is a type. + size_t digits = arg[0] == '-' ? 1 : 0; + while (digits < arg.size() && IsDigit(arg[digits])) + digits++; + if (digits == arg.size()) { + parts.push_back(std::string(arg)); + continue; + } + CodeWarriorParser sub(arg, true); + const std::string type = sub.ParseWholeType(); + if (type.empty()) + return "<" + std::string(args) + ">"; + parts.push_back(type); + } + std::string out; + JoinInto(out, parts, ", "); + return "<" + out + ">"; +} + // "9ANIMEData", or "Q33std10ctype_base4mask" for a qualified one. The count after Q is a // single digit, with "Q__" for the rare case of ten or more components. std::string CodeWarriorParser::ParseQualifiedName() { @@ -1344,37 +1413,47 @@ std::string CodeWarriorParser::ParseQualifiedName() { return out; } -std::string CodeWarriorParser::ParseType() { +CWDecl CodeWarriorParser::ParseDecl() { if (failed_ || depth_ > 64) { Fail(); - return ""; + return CWDecl(); } depth_++; - std::string result; + CWDecl result; const char c = Peek(); switch (c) { case 'P': case 'R': + { pos_++; - result = ParseType() + (c == 'P' ? " *" : " &"); + result = ParseDecl(); + const char sigil = c == 'P' ? '*' : '&'; + if (result.post.empty()) { + result.pre += ' '; + } + // Pointing at a function or an array, the sigil goes inside the parens the inner + // type left open: "int (*)(char)", not "int ()(char) *". + result.pre += sigil; break; + } case 'C': case 'V': { pos_++; const char *qual = c == 'C' ? "const" : "volatile"; - const std::string inner = ParseType(); + result = ParseDecl(); // "char * const", but "const char *" - the qualifier binds to whatever came before. - if (!inner.empty() && (inner.back() == '*' || inner.back() == '&')) - result = inner + " " + qual; + if (!result.pre.empty() && (result.pre.back() == '*' || result.pre.back() == '&')) + result.pre += std::string(" ") + qual; else - result = std::string(qual) + " " + inner; + result.pre = std::string(qual) + " " + result.pre; break; } case 'U': case 'S': pos_++; - result = std::string(c == 'U' ? "unsigned " : "signed ") + ParseType(); + result = ParseDecl(); + result.pre = std::string(c == 'U' ? "unsigned " : "signed ") + result.pre; break; case 'A': { @@ -1385,14 +1464,14 @@ std::string CodeWarriorParser::ParseType() { break; } pos_++; - result = ParseType() + " [" + std::to_string(count) + "]"; + result = ParseDecl(); + result.post = " [" + std::to_string(count) + "]" + result.post; break; } case 'F': { - // A function type: parameters, then "_" and the return type. Printed without the - // declarator gymnastics a real demangler does, so a pointer to one comes out as - // "int () *" rather than "int (*)()". + // A function type: parameters, then "_" and the return type. The parens around the + // declarator are left open for a P or R to put its sigil in - see above. pos_++; std::string args; if (!ParseParams(&args)) @@ -1402,7 +1481,8 @@ std::string CodeWarriorParser::ParseType() { pos_++; ret = ParseType(); } - result = ret + " (" + args + ")"; + result.pre = ret + " ("; + result.post = ")(" + args + ")"; break; } case 'T': @@ -1413,26 +1493,26 @@ std::string CodeWarriorParser::ParseType() { if (index < 1 || (size_t)index > params_.size()) Fail(); else - result = params_[index - 1]; + result.pre = params_[index - 1]; break; } case 'e': pos_++; - result = "..."; + result.pre = "..."; break; default: if (c == 'Q' || IsDigit(c)) { - result = ParseQualifiedName(); + result.pre = ParseQualifiedName(); } else if (const char *basic = CWBasicType(c)) { pos_++; - result = basic; + result.pre = basic; } else { Fail(); } break; } depth_--; - return failed_ ? "" : result; + return failed_ ? CWDecl() : result; } bool CodeWarriorParser::ParseParams(std::string *out) { @@ -1541,6 +1621,13 @@ static std::string CodeWarriorName(std::string_view name, const std::string &qua } if (base.empty()) base = std::string(name); + // A function template spells its arguments out in the name: "sort__3stdFPfPf_v". + const size_t lt = base.find('<'); + if (lt != std::string::npos && base.back() == '>') { + CodeWarriorParser argParser("", true); + const std::string_view args = std::string_view(base).substr(lt + 1, base.size() - lt - 2); + base = base.substr(0, lt) + argParser.ParseTemplateArgs(args); + } return qualifier.empty() ? base : qualifier + "::" + base; } @@ -1549,6 +1636,12 @@ static bool TryCodeWarriorSplit(std::string_view mangled, size_t sep, bool stric DemangledSymbol sym; if (!parser.ParseAfterSeparator(&sym)) return false; + // A plain C name with a "__" in it ("I3dClut__FlushCache") can look like an unqualified + // function whose parameters happen not to decode, so in the lenient pass - where the + // parameters are allowed not to decode - insist on a class qualifier as evidence that + // this really is a mangled name. + if (!strict && parser.qualifier().empty()) + return false; sym.name = CodeWarriorName(mangled.substr(0, sep), parser.qualifier()); *out = sym; return true; @@ -1556,7 +1649,114 @@ static bool TryCodeWarriorSplit(std::string_view mangled, size_t sep, bool stric } // namespace +// A compiler-generated symbol that carries a mangled name inside it, rather than being one: +// +// __vt__3Son vtable for Son +// __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@Init__Q25shTbb3TbbFPvPci@0 string literal 0 in shTbb::Tbb::Init(...) +// @LOCAL@sort__3stdFPfPf_v@shuffle@0 std::sort(...)::shuffle +// +// The @-forms are how CodeWarrior names things that have no C++ name of their own; the +// trailing @ distinguishes several of them within the same function. Returns false if +// this isn't one of them, leaving the caller to demangle the name as an ordinary symbol. +static bool DemangleCodeWarriorSpecial(std::string_view mangled, DemangledSymbol *out) { + // Splits off a trailing "@" discriminator, which is only there when a function + // has more than one of whatever this is. Then demangles what's left of the front half. + auto splitIndex = [](std::string_view s, DemangledSymbol *inner, std::string *index) { + const size_t at = s.rfind('@'); + if (at != std::string_view::npos && at + 1 < s.size() && + s.find_first_not_of("0123456789", at + 1) == std::string_view::npos) { + *index = std::string(s.substr(at + 1)) + " "; + s = s.substr(0, at); + } + return DemangleCodeWarrior(s, inner); + }; + + static const struct { const char *prefix; const char *text; } kinds[] = { + {"__vt__", "vtable for "}, + {"__RTTI__", "typeinfo for "}, + {"__sinit_", "static initializers for "}, + {"__sterm_", "static destructors for "}, + }; + for (const auto &kind : kinds) { + const size_t len = strlen(kind.prefix); + if (mangled.size() <= len || mangled.compare(0, len, kind.prefix) != 0) + continue; + const std::string_view rest = mangled.substr(len); + out->name = kind.text; + if (kind.prefix[2] == 's') { + // The static init/term functions are named after a source file, not a class. + out->name += std::string(rest); + } else { + CodeWarriorParser parser(rest, true); + DemangledSymbol type; + if (!parser.ParseAfterSeparator(&type) || type.isFunction || parser.qualifier().empty()) + return false; + out->name += parser.qualifier(); + } + out->isFunction = false; + return true; + } + + if (mangled.compare(0, 8, "@STRING@") == 0) { + // A string literal inside a function - typically the __FILE__ an assert expanded to. + DemangledSymbol inner; + std::string index; + if (!splitIndex(mangled.substr(8), &inner, &index)) + return false; + out->name = "string literal " + index + "in " + inner.ToString(); + out->isFunction = false; + return true; + } + if (mangled.compare(0, 7, "@GUARD@") == 0) { + // The "has this local static been constructed yet" flag. The name is the variable's, + // undecorated apart from the "$" that CodeWarrior gives every function-local one. + out->name = "guard variable for " + std::string(mangled.substr(7)); + out->isFunction = false; + return true; + } + if (mangled.compare(0, 7, "@LOCAL@") == 0) { + // A function-local static: "@LOCAL@@", plus the usual index. + std::string_view body = mangled.substr(7); + size_t at = body.rfind('@'); + if (at != std::string_view::npos && at + 1 < body.size() && + body.find_first_not_of("0123456789", at + 1) == std::string_view::npos) + body = body.substr(0, at); + at = body.rfind('@'); + if (at == std::string_view::npos) + return false; + DemangledSymbol inner; + if (!DemangleCodeWarrior(body.substr(0, at), &inner)) + return false; + out->name = inner.ToString() + "::" + std::string(body.substr(at + 1)); + out->isFunction = false; + return true; + } + if (mangled[0] == '@' && IsDigit(mangled[1])) { + // "@@": a thunk for a base other than the first. + const size_t at = mangled.find('@', 1); + if (at == std::string_view::npos) + return false; + const std::string_view offset = mangled.substr(1, at - 1); + if (offset.find_first_not_of("0123456789") != std::string_view::npos) + return false; + DemangledSymbol inner; + if (!DemangleCodeWarrior(mangled.substr(at + 1), &inner)) + return false; + out->name = "non-virtual thunk (" + std::string(offset) + ") to " + inner.ToString(); + out->isFunction = false; + return true; + } + return false; +} + bool DemangleCodeWarrior(std::string_view mangled, DemangledSymbol *out) { + if (mangled.size() > 2 && (mangled[0] == '@' || mangled[0] == '_') && + DemangleCodeWarriorSpecial(mangled, out)) + return true; + // Names can themselves start with underscores ("__SetupFrameInfo__F..."), and can // contain "__" further in, so every candidate separator gets tried. Strict first, so a // split that decodes completely wins over one that only decodes its name. diff --git a/Core/Debugger/SymbolMap.cpp b/Core/Debugger/SymbolMap.cpp index 0af8cd9849..a82a1f1ccf 100644 --- a/Core/Debugger/SymbolMap.cpp +++ b/Core/Debugger/SymbolMap.cpp @@ -141,9 +141,9 @@ bool SymbolMap::LoadSymbolMap(const Path &filename) { int moduleIndex = 0; int typeInt = ST_NONE; SymbolType type; - char name[128] = {0}; + char name[256] = {0}; - if (sscanf(line, ".module %x %08x %08x %127c", (unsigned int *)&moduleIndex, &address, &size, name) >= 3) { + if (sscanf(line, ".module %x %08x %08x %255c", (unsigned int *)&moduleIndex, &address, &size, name) >= 3) { // Found a module definition. ModuleEntry mod; mod.index = moduleIndex; @@ -155,7 +155,7 @@ bool SymbolMap::LoadSymbolMap(const Path &filename) { continue; } - const int matched = sscanf(line, "%08x %08x %x %i %127c", &address, &size, &vaddress, &typeInt, name); + const int matched = sscanf(line, "%08x %08x %x %i %255c", &address, &size, &vaddress, &typeInt, name); if (matched < 1) continue; type = (SymbolType) typeInt; @@ -316,8 +316,8 @@ bool SymbolMap::LoadNocashSym(const Path &filename) { return false; while (!feof(f)) { - char line[256], value[256] = {0}; - char *p = fgets(line, 256, f); + char line[512], value[256] = {0}; + char *p = fgets(line, sizeof(line), f); if (p == NULL) break; @@ -1262,7 +1262,7 @@ void SymbolMap::SetLabelName(const char* name, u32 address) { auto label = labels.find(symbolKey); if (label != labels.end()) { truncate_cpy(label->second.name, name); - label->second.name[127] = 0; + label->second.name[255] = 0; // Refresh the active item if it exists. auto active = activeLabels.find(address); diff --git a/Core/Debugger/SymbolMap.h b/Core/Debugger/SymbolMap.h index b9cec14566..786c8e06b4 100644 --- a/Core/Debugger/SymbolMap.h +++ b/Core/Debugger/SymbolMap.h @@ -217,7 +217,7 @@ private: struct LabelEntry { u32 addr; int module; - char name[128]; + char name[256]; }; struct DataEntry { @@ -232,7 +232,7 @@ private: int index; u32 start; u32 size; - char name[128]; + char name[256]; // 0 = unknown. See AddModule. u32 crc = 0; }; diff --git a/docs/CodeWarriorMangling.md b/docs/CodeWarriorMangling.md new file mode 100644 index 0000000000..41abc3312d --- /dev/null +++ b/docs/CodeWarriorMangling.md @@ -0,0 +1,202 @@ +# Metrowerks CodeWarrior C++ symbol mangling (PSP) + +Some PSP titles were built with Metrowerks CodeWarrior rather than the SDK's GCC, so their +symbols aren't Itanium-mangled and `_Z`-based demanglers do nothing with them. PPSSPP +demangles them in `Common/Data/Text/Demangle.cpp` (`DemangleCodeWarrior`), which is what makes +the symbol map readable for those games. + +The scheme is a descendant of the AT&T cfront mangling, and the basics match the Macintosh C++ +ABI. It deviates in enough places that following that document alone produces wrong answers, so +this is a description of what PSP binaries actually contain. + +## How this was worked out + +From two PSP executables that shipped with intact symbol tables - one small C++ test program, +one large application of about 10,000 symbols - by demangling every symbol and checking the +result against the disassembly and against the bytes each data symbol points at. + +Constructs the corpus did not contain are marked **(inferred)** below. They come from the cfront +scheme, are implemented, and are believed right, but nothing here proves them. + +## Overall shape + +A mangled symbol is: + +``` + __ [] [] F [_ ] +``` + +with the class, the cv-qualifiers, the `F` and everything after it all optional. A few examples, +building up: + +| Mangled | Demangled | +| --- | --- | +| `run_tests__Fv` | `run_tests()` | +| `getDistance__6KzUtilFP7st_unitP7st_unit` | `KzUtil::getDistance(st_unit *, st_unit *)` | +| `what__Q23std9exceptionCFv` | `std::exception::what() const` | +| `count__Q23foo3bar` | `foo::bar::count` | + +The last one has no `F`, so it isn't a function at all - it's a static data member, and there is +no parameter list to print. A name with neither a class nor an `F` is not mangled. + +### Finding the separator + +The `__` separator is genuinely ambiguous. A base name can itself start with underscores +(`__SetupFrameInfo__FP12ThrowContext...`), contain a `__` of its own +(`__TableUnit__SetValue__5shTbbF...`), and plain C code in the same binary is full of names +like `I3dCacheManager__GarbageCollect` that are not mangled at all. + +There is no way to resolve this from the grammar; PPSSPP tries every `__` in the symbol and keeps +the first split whose right-hand side parses completely. Only if none does will it accept a split +whose parameters didn't decode - and then only when a class qualifier is present, because +otherwise every C name with a double underscore in it decodes as a garbage function signature. + +## Names + +An identifier is written as its length in decimal followed by its characters: `6KzUtil`. + +A qualified name is `Q` followed by that many identifiers: `Q23std9exception` is +`std::exception` (2 components, `3std` and `9exception`). The count is a single digit; ten or +more components are written `Q__` **(inferred)**. + +### Template arguments + +Template arguments are written literally between `<` and `>`, *inside* the length-prefixed name - +the length covers the whole thing, brackets included. (The Macintosh ABI document describes a +`__PT` prefix instead. PSP binaries do not use it.) + +``` +39CList shList::CList +``` + +Here `39` is the length of `CList`. Each argument is either a +mangled type, or a plain integer for a non-type parameter, separated by commas: + +| Mangled | Demangled | +| --- | --- | +| `15CSimpleChar<24>` | `CSimpleChar<24>` | +| `61ForwardIterator<16TABLE_VAR_MEMBER,21TABLE_VAR_SECT_HEADER,v>` | `ForwardIterator` | +| `47CList>` | `CList>` | + +They nest, so a parser must match brackets rather than scanning for the next `>`, and split +arguments only on top-level commas. + +A *function* template puts its arguments in the base name instead, and - unlike a normal function - +encodes its return type after a trailing `_`: + +``` +sort__3stdFPfPf_v void std::sort(float *, float *) +``` + +### Special base names + +| Base name | Meaning | +| --- | --- | +| `__ct` | Constructor; the name is taken from the class | +| `__dt` | Destructor | +| `__op` | Conversion operator, e.g. `__opCi` is `operator const int()` | +| `__` | Overloaded operator, see below | + +Operator codes are the cfront set: `nw` `dl` `nwa` `dla` for `new`/`delete`, `pl` `mi` `ml` `dv` +`md` for arithmetic, `apl` `ami` `amu` `adv` `amd` `aad` `aor` `aer` `als` `ars` for the compound +assignments, `eq` `ne` `lt` `gt` `le` `ge` for comparisons, `aa` `oo` `nt` for logic, `ad` `or` +`er` `co` `ls` `rs` for bitwise, and `as` `pp` `mm` `cl` `vc` `rf` `rm` `cm` for +`=` `++` `--` `()` `[]` `->` `->*` `,`. + +So `__as__9ANIMEDataFRC9ANIMEData` is `ANIMEData::operator=(const ANIMEData &)`. + +## Types + +Type codes are read left to right, each one modifying what follows. + +| Code | Type | | +| --- | --- | --- | +| `v` | `void` | | +| `b` | `bool` | | +| `c` | `char` | | +| `s` | `short` | | +| `i` | `int` | | +| `l` | `long` | | +| `x` | `long long` | | +| `f` | `float` | | +| `d` | `double` | | +| `r` | `long double` | **(inferred)** | +| `w` | `wchar_t` | **(inferred)** - a Metrowerks addition to the cfront set | +| `e` | `...` | varargs, always last | +| `P` | pointer to | | +| `R` | reference to | | +| `C` | `const` | | +| `V` | `volatile` | | +| `U` | `unsigned` | | +| `S` | `signed` | **(inferred)** | +| `A_` | array of `n` | **(inferred)** | +| `F_` | function | | +| `` | class | | +| `Q...` | qualified class | | + +`PCc` is `const char *`; `CPc` is `char * const`. A pointer to a function or an array needs the +declarator wrapped rather than a `*` appended - `PFi_i` is `int (*)(int)`, and `PPFv_v` is +`void (**)()`. + +Two forms back-reference an earlier parameter of the same function **(inferred)**: + +| Form | Meaning | +| --- | --- | +| `T` | Same type as parameter ``, 1-based | +| `N` | `` more parameters, each the type of parameter `` | + +So `foo__FPCcUiN21` is `foo(const char *, unsigned int, const char *, const char *)`. + +### cv-qualifiers on the function + +A `C` or `V` sits between the class and the `F`, and qualifies the member function rather than a +parameter: `InitRun__Q28shCamera11TStillParamVFv` is +`shCamera::TStillParam::InitRun() volatile`. + +## Compiler-generated symbols + +CodeWarrior emits a family of symbols for things with no C++ name of their own. These wrap a +mangled name rather than being one, and none of them are described in the ABI document. + +| Form | Meaning | Example | +| --- | --- | --- | +| `__vt__` | Vtable | `__vt__Q23std9exception` | +| `__RTTI__` | Typeinfo record | `__RTTI__Q23std9exception` | +| `__sinit_` | Static initializers for a translation unit | `__sinit_hl_app.cpp` | +| `__sterm_` | Static destructors | | +| `@@` | `this`-adjusting thunk, `n` bytes | `@12@__dt__3SonFv` | +| `@STRING@[@]` | A string literal used inside that function | `@STRING@Get_BGM__Q25hlBHC4SBhcFi@0` | +| `@LOCAL@@[@]` | A function-local static | `@LOCAL@sort__3stdFPfPf_v@shuffle@0` | +| `@GUARD@$` | Its "already constructed" flag | `@GUARD@app$16079` | +| `@` | An anonymous string constant | `@10046` | + +The trailing `@` on `@STRING@` and `@LOCAL@` is a discriminator, present only when a function +has more than one of them - so a parser has to treat it as optional, and must not mistake the +`@` before a `@LOCAL@` variable name for it. + +`@@` thunks really are thunks: the one above is 8 bytes of code that does `addiu a0, a0, -12` +and jumps, i.e. adjusts `this` for a secondary base. `@STRING@` symbols point at ordinary string +data - usually the `__FILE__` an assertion expanded to. + +## Other decorations + +Two more show up inside otherwise ordinary names. Neither needs decoding, but both are worth +recognising: + +- `$` - a class or variable with internal linkage, tagged with where it was + declared: `ClutScreen$11229hl_cplayer_effect_renderer_cpp`. These make for very long symbols, + since the tag repeats everywhere the type appears. +- `@unnamed@@` - used as a qualifier for file-scope statics, i.e. an unnamed namespace. + `ARWMENU_MODEL_NAME__34@unnamed@hl_editor_arrow_menu_cpp@` is a data symbol in one. + +## Hazards + +- **A truncated name is unrecoverable.** Some symbol tables cap names (127 characters is a common + limit), and templates blow past that easily. Once a name is cut, the length prefixes no longer + match what's left and nothing after the cut can be trusted - better to reject the symbol than + to print a plausible-looking guess. +- **Not everything with a `__` is mangled.** C code linked into the same binary uses `__` as a + word separator freely. Requiring a class qualifier before accepting a partial parse is what + keeps `I3dClut__FlushCache` from turning into `I3dClut(long, ...)`. +- **`e` is a real parameter, not a parse failure.** `Printf__Q26shFont4FontFiiPCce` ends in + varargs; `(int, int, const char *, ...)` is the correct answer, not a partial one. diff --git a/unittest/TestDemangle.cpp b/unittest/TestDemangle.cpp index 63d51f6e1e..e38d9fc766 100644 --- a/unittest/TestDemangle.cpp +++ b/unittest/TestDemangle.cpp @@ -133,6 +133,39 @@ static const DemangleCase codeWarriorCases[] = { // 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__Q26shList39CListFPCc", + "shList::CList::Init(const char *)" }, + { "Reset__Q28shString15CSimpleChar<24>Fv", + "shString::CSimpleChar<24>::Reset()" }, + { "AddItem__Q26shList47CList>FPQ26ssTool29tag_", + "shList::CList>::AddItem(ssTool::tag_ *)" }, + // A function template puts them in the base name instead, and encodes its return type. + { "sort__3stdFPfPf_v", + "void std::sort(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__3stdFPfPf_v@shuffle@0", + "void std::sort(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 @@ -162,6 +195,10 @@ static const char *notMangled[] = { "_Z3fooIXadL_Z1xEEEvv", // An 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.