From 2f5bb829f3c1b780e7d0d595b3dc8f851210abcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sat, 29 Aug 2026 23:44:35 +0200 Subject: [PATCH] Demangle: rewrite the SN Systems demangler The old one was reverse engineered from a handful of symbols and got the shape of the format wrong - it required a digit right after the kind character, which most real symbols don't have. Measured against a PSP executable that shipped with its symbol table intact, it decoded 238 of 4662 mangled symbols, most of those incorrectly. Worked out properly from that binary, the format turns out to be: __0 [_ ] [] where the kind character (member function, free function, operator, data) is the only thing that says how many name components follow, since nothing separates the last one from the first parameter. Lengths are letters (A = 0, a = 26); "5" marks an enclosing namespace; "7...._" is a template argument list, with "4" plus a compact integer for a non-type argument and "9A" for a back-reference to one; "T" and "N" repeat an earlier parameter; a trailing "K" is const and a trailing "T" is a static member function. Also handles __TID_/__T_ (the two halves of a class's RTTI) and __sti__ (a translation unit's static initializers). That decodes 4661 of the 4662. The one holdout is an STL symbol whose template argument is a reference to a member of another template. Declarator wrapping is shared with the CodeWarrior demangler now, so pointers to arrays come out as "short (**)[64]" in both. docs/SNSystemsMangling.md describes the format, marking what's inferred rather than attested. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01SF5eS5QDNexLksRDeDZvwY --- Common/Data/Text/Demangle.cpp | 571 ++++++++++++++++++++++++++++++---- Common/Data/Text/Demangle.h | 7 +- docs/SNSystemsMangling.md | 205 ++++++++++++ unittest/TestDemangle.cpp | 53 ++++ 4 files changed, 777 insertions(+), 59 deletions(-) create mode 100644 docs/SNSystemsMangling.md diff --git a/Common/Data/Text/Demangle.cpp b/Common/Data/Text/Demangle.cpp index 0236bc42f5..afb4e51c2f 100644 --- a/Common/Data/Text/Demangle.cpp +++ b/Common/Data/Text/Demangle.cpp @@ -1270,6 +1270,13 @@ struct CWDecl { std::string str() const { return pre + post; } }; +// Whether a declarator's prefix is already open for a "*" or "&" to be written into it - that +// is, whether it came from a function type ("int ("), a pointer to one ("int (*"), or a +// pointer to member ("int (Foo::"). +static bool EndsDeclarator(const std::string &pre) { + return !pre.empty() && (pre.back() == '(' || pre.back() == '*' || pre.back() == ':'); +} + class CodeWarriorParser { public: // strict: fail outright on a parameter we can't decode, rather than printing "...". @@ -1430,9 +1437,13 @@ CWDecl CodeWarriorParser::ParseDecl() { const char sigil = c == 'P' ? '*' : '&'; if (result.post.empty()) { result.pre += ' '; + } else if (!EndsDeclarator(result.pre)) { + // An array, which needs parens of its own: "short (*)[64]", not "short * [64]". + result.pre += " ("; + result.post = ")" + result.post; } - // Pointing at a function or an array, the sigil goes inside the parens the inner - // type left open: "int (*)(char)", not "int ()(char) *". + // Otherwise the inner type is a function or a pointer to one, and has already opened + // the parens for us: "int (*)(char)", not "int ()(char) *". result.pre += sigil; break; } @@ -1775,76 +1786,522 @@ bool DemangleCodeWarrior(std::string_view mangled, DemangledSymbol *out) { return false; } -// SN Systems (SNC / ProDG), a much more compact scheme: +// SN Systems (SNC / ProDG), a much more compact scheme, and unrelated to cfront: // -// __0f5DstdIbad_castEwhatvK -> std::bad_cast::what() const +// __0fLCHeapMemoryFAlloci -> CHeapMemory::Alloc(int) // -// "__0", a kind character, a tag digit, then the name as a chain of components whose lengths -// are *letters* (A = 0, so D = 3 and J = 9), then the parameters, then any cv-qualifier. +// "__0", a kind character, then the name as a chain of components whose lengths are written +// as *letters* (A = 0, so F = 5 and a = 26), then the parameters, then any qualifier. What +// the name consists of follows from the kind: a member function is class + function, a free +// function is just the function, and a "5" before a component makes it a namespace. // -// Reverse engineered from a handful of real symbols, so parts of this are educated guesses: -// the tag digit (5 for a function, 6 for a class type used as a parameter), and 'f' vs 'F' -// for a non-static member function vs. a free or static one. Neither affects the name, which -// is the part worth trusting. -bool DemangleSNSystems(std::string_view mangled, DemangledSymbol *out) { - if (mangled.size() < 6 || mangled.compare(0, 3, "__0") != 0) - return false; - size_t pos = 3; - const char kind = mangled[pos++]; - if (kind != 'f' && kind != 'F') - return false; - if (!IsDigit(mangled[pos])) - return false; - pos++; +// See docs/SNSystemsMangling.md for the format; the parts that are guesses are flagged there +// and in the comments below. - std::vector parts; - while (pos < mangled.size() && mangled[pos] >= 'A' && mangled[pos] <= 'Z') { - const size_t len = (size_t)(mangled[pos] - 'A'); - pos++; - if (pos + len > mangled.size()) - return false; - parts.push_back(std::string(mangled.substr(pos, len))); - pos += len; +namespace { + +// Operator codes, matched against the two characters after the class name ("nw" and "dl" take +// an optional "a" for the array forms). +static const CWOperator snOperators[] = { + {"nw", "new"}, {"dl", "delete"}, + {"apl", "+="}, {"ami", "-="}, {"amu", "*="}, {"adv", "/="}, {"amd", "%="}, + {"aad", "&="}, {"aor", "|="}, {"aer", "^="}, {"als", "<<="}, {"ars", ">>="}, + {"pl", "+"}, {"mi", "-"}, {"ml", "*"}, {"dv", "/"}, {"md", "%"}, + {"eq", "=="}, {"ne", "!="}, {"lt", "<"}, {"gt", ">"}, {"le", "<="}, {"ge", ">="}, + {"aa", "&&"}, {"oo", "||"}, {"nt", "!"}, {"co", "~"}, {"er", "^"}, + {"ad", "&"}, {"or", "|"}, {"ls", "<<"}, {"rs", ">>"}, {"as", "="}, + {"pp", "++"}, {"mm", "--"}, {"cl", "()"}, {"vc", "[]"}, {"rf", "->"}, + {"rm", "->*"}, {"cm", ","}, +}; + +static const char *SNBasicType(char c) { + switch (c) { + case 'v': return "void"; + case 'c': return "char"; + case 's': return "short"; + case 'i': return "int"; + case 'l': return "long"; + case 'x': return "long long"; + case 'f': return "float"; + case 'd': return "double"; + case 'r': return "long double"; + case 'b': return "bool"; + case 'w': return "wchar_t"; + case 'e': return "..."; + default: return nullptr; } - if (parts.empty()) - return false; +} - DemangledSymbol sym; - sym.isFunction = true; - std::vector params; - while (pos < mangled.size()) { - const char c = mangled[pos]; - if (c == 'v') { - // void, i.e. no parameters at all. - pos++; - continue; +// Lengths, and the small numbers in template arguments, are single letters: A-Z is 0-25 and +// a-z is 26-51. Returns -1 for anything else. +static int SNLetterValue(char c) { + if (c >= 'A' && c <= 'Z') + return c - 'A'; + if (c >= 'a' && c <= 'z') + return c - 'a' + 26; + return -1; +} + +class SNSystemsParser { +public: + explicit SNSystemsParser(std::string_view s) : s_(s) {} + + bool Parse(DemangledSymbol *out); + + // Standalone entry point, for the bare type name in a "__TID_"/"__T_" symbol. Those spell + // an enclosing namespace as just another component rather than marking it with a "5", so + // components are read until the symbol runs out. + std::string ParseWholeType() { + const std::string type = ParseType(); + return (failed_ || pos_ != s_.size()) ? "" : type; + } + + std::string ParseWholeName() { + std::vector parts; + while (pos_ < s_.size()) { + parts.push_back(ParseName(nullptr)); + if (failed_) + return ""; } - if (c == 'K' && pos + 1 == mangled.size()) { - sym.qualifiers = "const"; - pos++; - continue; + std::string out; + JoinInto(out, parts, "::"); + return out; + } + +private: + char Peek(size_t ahead = 0) const { return pos_ + ahead < s_.size() ? s_[pos_ + ahead] : 0; } + bool ConsumeIf(const char *str) { + const size_t len = strlen(str); + if (s_.compare(pos_, len, str) != 0) + return false; + pos_ += len; + return true; + } + void Fail() { failed_ = true; } + + std::string ParseIdentifier(); + std::string ParseNumber(); + std::string ParseName(std::vector *templateArgs); + std::string ParseTemplateArgs(std::vector *args); + CWDecl ParseDecl(); + std::string ParseType() { return ParseDecl().str(); } + std::string ParseParams(); + + std::string_view s_; + size_t pos_ = 0; + bool failed_ = false; + int depth_ = 0; + // The template arguments of the name this symbol belongs to, for resolving "9" back- + // references in the parameters and return type. + std::vector templateArgs_; + // Parameter types so far, for resolving the "T" and "N" back-references. + std::vector params_; +}; + +std::string SNSystemsParser::ParseIdentifier() { + const int len = SNLetterValue(Peek()); + if (len < 0 || pos_ + 1 + (size_t)len > s_.size()) { + Fail(); + return ""; + } + pos_++; + std::string name(s_.substr(pos_, len)); + pos_ += len; + return name; +} + +// A non-negative integer, in one of two forms: a run of '0's each worth 52 followed by a +// letter for the remainder (so "0M" is 64), or "8" plus a length letter and that many decimal +// digits, which is how values too big for the first form are written ("8E1024" is 1024). +std::string SNSystemsParser::ParseNumber() { + if (Peek() == '8') { + pos_++; + const int len = SNLetterValue(Peek()); + if (len <= 0 || pos_ + 1 + (size_t)len > s_.size()) { + Fail(); + return ""; } - if (IsDigit(c) && pos + 1 < mangled.size() && - mangled[pos + 1] >= 'A' && mangled[pos + 1] <= 'Z') { - // A tagged, length-prefixed type name. - const size_t len = (size_t)(mangled[pos + 1] - 'A'); - if (pos + 2 + len > mangled.size()) - return false; - params.push_back(std::string(mangled.substr(pos + 2, len))); - pos += 2 + len; - continue; + pos_++; + std::string digits(s_.substr(pos_, len)); + pos_ += len; + if (digits.find_first_not_of("0123456789") != std::string::npos) { + Fail(); + return ""; } - // Anything else is a type code we haven't seen yet. Keep the name, admit to not - // knowing the rest. - params.push_back("..."); + return digits; + } + int value = 0; + while (Peek() == '0') { + pos_++; + value += 52; + if (value > 1000000) { + Fail(); + return ""; + } + } + const int digit = SNLetterValue(Peek()); + if (digit < 0) { + Fail(); + return ""; + } + pos_++; + return std::to_string(value + digit); +} + +// "7" then a list of arguments, terminated by "_". An argument is a type, or "4" and an +// integer for a non-type parameter. +std::string SNSystemsParser::ParseTemplateArgs(std::vector *args) { + pos_++; + while (Peek() != '_') { + if (!Peek() || failed_) { + Fail(); + return ""; + } + if (Peek() == '4') { + pos_++; + args->push_back(ParseNumber()); + } else { + args->push_back(ParseType()); + } + if (failed_) + return ""; + } + pos_++; + std::string out; + JoinInto(out, *args, ", "); + return "<" + out + ">"; +} + +// Any number of "5"-introduced enclosing scopes, then the name itself, then its template +// arguments if it has any. +std::string SNSystemsParser::ParseName(std::vector *templateArgs) { + std::vector parts; + while (Peek() == '5') { + pos_++; + parts.push_back(ParseIdentifier()); + if (failed_) + return ""; + } + parts.push_back(ParseIdentifier()); + if (failed_) + return ""; + if (Peek() == '7') { + std::vector args; + const std::string printed = ParseTemplateArgs(&args); + if (failed_) + return ""; + parts.back() += printed; + if (templateArgs && !args.empty()) + *templateArgs = args; + } + std::string out; + JoinInto(out, parts, "::"); + return out; +} + +CWDecl SNSystemsParser::ParseDecl() { + if (failed_ || depth_ > 64) { + Fail(); + return CWDecl(); + } + depth_++; + CWDecl result; + const char c = Peek(); + switch (c) { + case 'P': + case 'R': + { + pos_++; + result = ParseDecl(); + const char sigil = c == 'P' ? '*' : '&'; + if (result.post.empty()) { + result.pre += ' '; + } else if (!EndsDeclarator(result.pre)) { + // An array, which needs parens of its own: "short (*)[64]", not "short * [64]". + result.pre += " ("; + result.post = ")" + result.post; + } + // Otherwise the inner type is a function or a pointer to one, and has already opened + // the parens for us: "int (*)(char)", not "int ()(char) *". + result.pre += sigil; break; } - JoinInto(sym.name, parts, "::"); - JoinInto(sym.parameters, params, ", "); + case 'C': + case 'V': + pos_++; + result = ParseDecl(); + result.pre = std::string(c == 'C' ? "const " : "volatile ") + result.pre; + break; + case 'U': + case 'S': + pos_++; + result = ParseDecl(); + result.pre = std::string(c == 'U' ? "unsigned " : "signed ") + result.pre; + break; + case '6': + pos_++; + result.pre = ParseName(nullptr); + break; + case 'A': + { + pos_++; + const std::string count = ParseNumber(); + if (failed_) + break; + result = ParseDecl(); + result.post = " [" + count + "]" + result.post; + break; + } + case 'F': + { + // A function type: parameters, then "_" and the return type. The parens around the + // declarator are left open for a P, R or M to fill in - see above. + pos_++; + std::vector parts; + while (Peek() && Peek() != '_') { + const std::string type = ParseType(); + if (failed_) + break; + if (type != "void") + parts.push_back(type); + } + if (failed_ || Peek() != '_') { + Fail(); + break; + } + pos_++; + const std::string ret = ParseType(); + if (failed_) + break; + std::string args; + JoinInto(args, parts, ", "); + result.pre = ret + " ("; + result.post = ")(" + args + ")"; + break; + } + case 'M': + { + // Pointer to member: the class, then the member's type. The enclosing P supplies the + // "*", so this only has to name the class inside the parens. + pos_++; + const std::string cls = ParseName(nullptr); + if (failed_) + break; + result = ParseDecl(); + if (result.post.empty()) + result.pre += " " + cls + "::"; + else + result.pre += cls + "::"; + break; + } + case '9': + { + // A reference to one of the enclosing name's template arguments: the index (1-based), + // then what appears to be a nesting level, always "A" in practice. + pos_++; + const int index = SNLetterValue(Peek()); + pos_++; + if (SNLetterValue(Peek()) < 0) { + Fail(); + break; + } + pos_++; + if (index < 1 || (size_t)index > templateArgs_.size()) + Fail(); + else + result.pre = templateArgs_[index - 1]; + break; + } + default: + if (const char *basic = SNBasicType(c)) { + pos_++; + result.pre = basic; + } else { + Fail(); + } + break; + } + depth_--; + return failed_ ? CWDecl() : result; +} + +// Parameters run until the return type ("_"), a qualifier, or the end of the symbol. +std::string SNSystemsParser::ParseParams() { + while (pos_ < s_.size() && Peek() != 'K' && Peek() != '_') { + if (Peek() == 'T') { + // "T": the same type as parameter . A "T" with nothing usable after + // it is the static marker instead, which ends the list. + const int index = SNLetterValue(Peek(1)); + if (index < 1 || (size_t)index > params_.size()) + break; + pos_ += 2; + params_.push_back(params_[index - 1]); + continue; + } + if (Peek() == 'N') { + // "N": more parameters, each the type of parameter . + const int count = SNLetterValue(Peek(1)); + const int index = SNLetterValue(Peek(2)); + if (count >= 1 && index >= 1 && (size_t)index <= params_.size()) { + pos_ += 3; + for (int i = 0; i < count; i++) + params_.push_back(params_[index - 1]); + continue; + } + } + const std::string type = ParseType(); + if (failed_) + return ""; + params_.push_back(type); + } + // A lone "void" is how a parameterless function is spelled. + if (params_.size() == 1 && params_[0] == "void") + params_.clear(); + std::string out; + JoinInto(out, params_, ", "); + return out; +} + +bool SNSystemsParser::Parse(DemangledSymbol *out) { + if (!ConsumeIf("__0")) + return false; + const char kind = Peek(); + pos_++; + + std::string name; + bool isOperator = false; + if (kind == 'O') { + // A global operator: no class, just the code. + isOperator = true; + } else if (kind == 'f' || kind == 'F' || kind == 'o' || kind == 'd') { + name = ParseName(&templateArgs_); + if (failed_) + return false; + isOperator = kind == 'o'; + } else { + return false; + } + + if (isOperator) { + // "ct" and "dt" are the constructor and destructor rather than operators; the class + // name isn't repeated, so it has to be taken from the qualifier. + const std::string_view code = s_.substr(pos_, 2); + const std::string base = BaseName(name); + std::string op; + if (code == "ct" && !base.empty()) { + op = base; + } else if (code == "dt" && !base.empty()) { + op = "~" + base; + } else { + for (const CWOperator &candidate : snOperators) { + if (code != candidate.code) + continue; + op = std::string("operator") + (candidate.name[0] >= 'a' ? " " : "") + candidate.name; + // "nwa" and "dla" are the array forms. + if (s_.compare(pos_ + 2, 1, "a") == 0 && (code == "nw" || code == "dl")) { + pos_++; + op += "[]"; + } + break; + } + if (op.empty()) + return false; + } + pos_ += 2; + name = name.empty() ? op : name + "::" + op; + } else if (kind == 'f' || kind == 'd') { + // A member: the name so far was the class, and its own name follows. + std::vector memberArgs; + const std::string member = ParseName(&memberArgs); + if (failed_) + return false; + if (!memberArgs.empty()) + templateArgs_ = memberArgs; + name += "::" + member; + } + + DemangledSymbol sym; + sym.name = name; + if (kind == 'd') { + // Data, so there's nothing after the name. + sym.isFunction = false; + if (pos_ != s_.size()) + return false; + *out = sym; + return true; + } + + // An unidentified marker that shows up between the name and the parameters on a couple of + // template members. It doesn't affect the name, so skip it rather than giving up. + ConsumeIf("__S"); + + sym.isFunction = true; + sym.parameters = ParseParams(); + if (failed_) + return false; + if (Peek() == '_') { + // Templates encode their return type, same as in the other manglings. + pos_++; + sym.returnType = ParseType(); + if (failed_) + return false; + } + if (Peek() == 'K') { + pos_++; + sym.qualifiers = "const"; + } else if (Peek() == 'T') { + // A static member function - it's what "operator new" and every thread entry point and + // callback in the corpus this was worked out from carries, and it never appears on a + // free function, which wouldn't need marking. + pos_++; + sym.returnType = sym.returnType.empty() ? "static" : "static " + sym.returnType; + } + if (pos_ != s_.size()) + return false; *out = sym; return true; } +} // namespace + +bool DemangleSNSystems(std::string_view mangled, DemangledSymbol *out) { + if (mangled.size() > 3 && mangled.compare(0, 3, "__0") == 0) { + SNSystemsParser parser(mangled); + return parser.Parse(out); + } + + // Symbols the compiler generates for a type, using the same name encoding. + static const struct { const char *prefix; const char *text; } kinds[] = { + {"__TID_", "type id for "}, + {"__T_", "typeinfo for "}, + {"__sti__", "static initializers 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; + out->name = kind.text; + out->isFunction = false; + if (kind.prefix[2] == 's') { + // The static initializer is named after a source file, not a type. + out->name += std::string(mangled.substr(len)); + return true; + } + SNSystemsParser parser(mangled.substr(len)); + std::string name = parser.ParseWholeName(); + if (name.empty()) { + // It can also be a plain type rather than a class - "__TID_v" is void's. + SNSystemsParser typeParser(mangled.substr(len)); + name = typeParser.ParseWholeType(); + } + if (name.empty()) + return false; + out->name += name; + return true; + } + return false; +} + std::string DemangledSymbol::ToString() const { std::string out; if (!returnType.empty()) diff --git a/Common/Data/Text/Demangle.h b/Common/Data/Text/Demangle.h index 94b0d7dad2..1d45a027df 100644 --- a/Common/Data/Text/Demangle.h +++ b/Common/Data/Text/Demangle.h @@ -41,7 +41,8 @@ bool DemangleItanium(std::string_view mangled, std::string *out); struct DemangledSymbol { std::string name; // Qualified, no parameters: "ANIMEData::operator=". std::string parameters; // What goes between the parens. Empty means "()". - std::string returnType; // Usually empty - neither format encodes it in most cases. + std::string returnType; // Usually empty - only templates encode one. Also carries the + // "static" of an SN Systems static member function. std::string qualifiers; // "const" and friends, printed after the parameter list. bool isFunction = false; // False for data symbols, where there are no parens at all. @@ -49,9 +50,11 @@ struct DemangledSymbol { }; // Metrowerks CodeWarrior: "getDistance__6KzUtilFP7st_unitP7st_unit". +// See docs/CodeWarriorMangling.md. bool DemangleCodeWarrior(std::string_view mangled, DemangledSymbol *out); -// SN Systems (SNC/ProDG): "__0f5DstdIbad_castEwhatvK". +// SN Systems (SNC/ProDG): "__0fLCHeapMemoryFAlloci". +// See docs/SNSystemsMangling.md. bool DemangleSNSystems(std::string_view mangled, DemangledSymbol *out); // Convenience wrapper: tries all three manglings and returns the demangled name, or a diff --git a/docs/SNSystemsMangling.md b/docs/SNSystemsMangling.md new file mode 100644 index 0000000000..71d2cbfcc7 --- /dev/null +++ b/docs/SNSystemsMangling.md @@ -0,0 +1,205 @@ +# SN Systems (SNC / ProDG) C++ symbol mangling (PSP) + +A number of PSP titles were built with SN Systems' compiler rather than the SDK's GCC. Its C++ +symbols look nothing like the Itanium mangling, and nothing like the cfront-derived one +CodeWarrior uses either - they are much more compact, and encode lengths as letters rather than +digits. PPSSPP demangles them in `Common/Data/Text/Demangle.cpp` (`DemangleSNSystems`). + +## How this was worked out + +From a single PSP executable that shipped with its symbol table intact: 4,662 mangled symbols +out of 11,338, covering an application, an STL, a JPEG decoder and a sound library. Everything +below is attested there except where marked **(inferred)**, which means the code implements it +by analogy with the codes that are attested and nothing in that binary proves it. + +The description is complete enough to decode 4,661 of the 4,662. The one holdout is an STL +symbol whose template argument is a reference to a member of another template. + +## Overall shape + +``` +__0 [_ ] [] +``` + +The kind character says what the symbol is, and - importantly - how many name components to +read, since nothing else delimits them: + +| Kind | Meaning | Name components | +| --- | --- | --- | +| `f` | Member function | class, then function | +| `F` | Free function | function | +| `o` | Member operator, constructor or destructor | class, then a two-letter code | +| `O` | Global operator | just the two-letter code | +| `d` | Data | class, then variable | + +``` +__0fLCHeapMemoryFAlloci CHeapMemory::Alloc(int) +__0FHGetRandv GetRand() +__0oLCHeapMemorydtv CHeapMemory::~CHeapMemory() +__0OnwUi operator new(unsigned int) +__0dLCHeapMemoryG__vtbl CHeapMemory::__vtbl +``` + +## Names + +An identifier is a **letter giving its length**, then that many characters. `A` is 0, so `F` is +5 and `L` is 11; lowercase continues the run, with `a` = 26 up to `z` = 51. + +``` +LCHeapMemory -> L = 11 -> "CHeapMemory" +FAlloc -> F = 5 -> "Alloc" +bMatrixScaleRotationXZYTrans -> b = 27 +``` + +A `5` before a component makes it an enclosing scope, and any number of them can stack up +before the component the kind character called for: + +``` +__0f5FsoundMCSoundPlayerPGetPlayerObjectUi + ^ namespace "sound", class "CSoundPlayer", function "GetPlayerObject" + sound::CSoundPlayer::GetPlayerObject(unsigned int) +``` + +### Template arguments + +`7` opens a template argument list and `_` closes it. Each argument is a type, or `4` and an +integer for a non-type parameter. They nest. + +``` +6KTFixString748D512_ TFixString<512> +6FTRect7f_ TRect +6KTFixVector76KTFixString7400Y_4g_ TFixVector, 32> +``` + +`9A` refers back to one of the enclosing name's template arguments, 1-based, so `9BA` is +the first. The trailing letter appears to be a nesting level and is always `A` in practice. + +``` +__0FDmax7f_RC9BATB_RC9BA const float & max(const float &, const float &) +``` + +### Integers + +Non-type template arguments and array bounds use a compact encoding: a run of `0`s, each worth +52, followed by a letter for the remainder. + +| Written | Value | | +| --- | --- | --- | +| `Q` | 16 | | +| `g` | 32 | (lowercase, so 26 + 6) | +| `0M` | 64 | 52 + 12 | +| `00Y` | 128 | 104 + 24 | +| `0000w` | 256 | 208 + 48 | + +Values that would need too many `0`s switch to an escape: `8`, a length letter, then that many +decimal digits. `8D512` is 512 and `8E1024` is 1024. + +### Operators + +For kinds `o` and `O` the name ends with a two-letter code instead of a length-prefixed +identifier. `ct` and `dt` are the constructor and destructor - the class name isn't repeated, +so it has to be taken from the qualifier. `nw` and `dl` take an optional trailing `a` for the +array forms. + +Attested: `ct` `dt` `as` `eq` `ne` `ls` `nw` `nwa` `dl` `dla`. The rest of the usual set +(`pl` `mi` `ml` `dv` `md`, the compound assignments, comparisons, `cl`, `vc`, `rf`, …) is +implemented by analogy **(inferred)**. + +## Types + +| 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)** | +| `e` | `...` | varargs | +| `P` | pointer to | | +| `R` | reference to | | +| `C` | `const` | | +| `V` | `volatile` | **(inferred)** | +| `U` | `unsigned` | | +| `S` | `signed` | **(inferred)** | +| `A` | array of `n` | | +| `F_` | function | | +| `M` | pointer to member of `` | | +| `6` | class, struct or enum | | +| `9A` | template argument `` | | + +Note that a class type in a parameter is introduced by `6`, while the symbol's own name isn't - +the kind character has already said a name is coming. + +Declarators need wrapping rather than suffixing: `PFv_v` is `void (*)()`, `PPA0Ms` is +`short (**)[64]`, and `PMF…_…` is `ret (Class::*)(args)`. + +### Parameter back-references + +Two forms repeat an earlier parameter of the same function, both indexed 1-based with letters: + +| Form | Meaning | +| --- | --- | +| `T` | Same type as parameter `` | +| `N` | `` more parameters, each the type of parameter `` | + +``` +__0FKRadixSortSPUiNCBUi + RadixSortS(unsigned int *, unsigned int *, unsigned int *, unsigned int) +``` + +`NCB` is "two more of parameter 1". Note that `T` is also the static marker below, so a `T` +whose following letter isn't a valid parameter index ends the list rather than repeating one. + +## Return types and qualifiers + +A `_` after the parameters introduces the return type. Only templates carry one - the same rule +as the other manglings, since two instantiations can differ in return type alone. + +After that, a trailing `K` marks a const member function. A trailing `T` marks a **static** +member function: it appears only on kinds `f` and `o`, never on a free function (which wouldn't +need marking), and every member `operator new`/`operator delete` in the corpus carries it, +which is exactly the set of member functions that are implicitly static. + +One more marker, `__S`, turns up between the name and the parameters on a couple of template +members. What it means is unknown; it doesn't affect the name, so the demangler skips it. + +## Compiler-generated symbols + +| Form | Meaning | +| --- | --- | +| `__TID_` | Type id - a one-byte object whose *address* is the type's identity | +| `__T_` | Typeinfo record - 20 bytes, pointing at the name and the base class | +| `__sti___cpp` | Static initializers for a translation unit | +| `::__vtbl` | Vtable, as an ordinary `__0d` data symbol | + +`__TID_` and `__T_` are the two halves of the RTTI for one class, and which is which is +inferred from their sizes rather than from anything that names them. Both take a type rather +than a symbol, and - unlike everything above - spell an enclosing namespace as just another +component instead of marking it with a `5`: + +``` +__TID_DstdIbad_cast type id for std::bad_cast +__TID_v type id for void +``` + +Secondary vtables for multiple inheritance use a different scheme again, with decimal lengths: +`__vtbl___10CRefObject__5FsoundNCSoundRequest`. Only a handful exist and PPSSPP leaves them +alone. + +## Hazards + +- **A lot of C runtime code links into the same binary**, and much of it starts with a double + underscore (`__adddf3`, `__malloc_lock`, `__sti`). Rejecting a symbol that doesn't parse + completely is what keeps those intact. +- **The kind character is the only thing that says where the name ends.** There is no separator + between the last name component and the first parameter, and uppercase letters are both + lengths and type codes, so a parser that guesses will read the wrong thing and still consume + a plausible number of characters. +- **`T` is overloaded** - a parameter back-reference in the middle of the list, and the static + marker at the end. diff --git a/unittest/TestDemangle.cpp b/unittest/TestDemangle.cpp index e38d9fc766..47148a2ac1 100644 --- a/unittest/TestDemangle.cpp +++ b/unittest/TestDemangle.cpp @@ -177,6 +177,57 @@ static const DemangleCase snSystemsCases[] = { "std::Terminate()" }, { "__0F5INTskMenuKEventInit_6LtagSTaskHdl", "NTskMenu::EventInit_(tagSTaskHdl)" }, + // A member function, and one in a namespace. The kind character decides how many name + // components there are; a "5" marks an extra enclosing one. + { "__0fLCHeapMemoryFAlloci", + "CHeapMemory::Alloc(int)" }, + { "__0f5FsoundMCSoundPlayerPGetPlayerObjectUi", + "sound::CSoundPlayer::GetPlayerObject(unsigned int)" }, + { "__0fLCHeapMemoryKGetUseSizevK", + "CHeapMemory::GetUseSize() const" }, + // Constructors, destructors and operators, member and global. + { "__0oLCHeapMemoryctv", + "CHeapMemory::CHeapMemory()" }, + { "__0oLCHeapMemorydtv", + "CHeapMemory::~CHeapMemory()" }, + { "__0OnwUi", + "operator new(unsigned int)" }, + // operator new is implicitly static, which is what the trailing "T" marks. + { "__0oECMsgnwaUiT", + "static CMsg::operator new[](unsigned int)" }, + // Data, which has nothing after the name at all. + { "__0dLCHeapMemoryG__vtbl", + "CHeapMemory::__vtbl" }, + // "T" repeats an earlier parameter, "N" repeats it several times, + // and "9A" refers back to a template argument. + { "__0FDmax7f_RC9BATB_RC9BA", + "const float & max(const float &, const float &)" }, + { "__0FFClamp7i_RC9BANCB_RC9BA", + "const int & Clamp(const int &, const int &, const int &)" }, + { "__0FKRadixSortSPUiNCBUi", + "RadixSortS(unsigned int *, unsigned int *, unsigned int *, unsigned int)" }, + // Non-type template arguments: "0" is worth 52 each, and "8" escapes to decimal. + { "__0dKTFixString748D512_G__vtbl", + "TFixString<512>::__vtbl" }, + { "__0fLCFileListup74000s_LGetListFilei_R6KTFixString748D512_", + "TFixString<512> & CFileListup<200>::GetListFile(int)" }, + // Declarators have to be wrapped, not just suffixed. + { "__0F5DstdNset_terminatePFv_v", + "std::set_terminate(void (*)())" }, + { "__0FKdecode_mcuP6Wjpeg_decompress_structPPA0Ms", + "decode_mcu(jpeg_decompress_struct *, short (**) [64])" }, + { "__0FS_AfxDispatchCmdMsgP6KCCmdTargetUiiPMKCCmdTargetFR6KCCmdTarget_vPvTCP6SAFX_CMDHANDLERINFO", + "_AfxDispatchCmdMsg(CCmdTarget *, unsigned int, int, void (CCmdTarget::*)(CCmdTarget &), void *, unsigned int, AFX_CMDHANDLERINFO *)" }, + // Compiler-generated symbols. These spell an enclosing namespace as a plain component + // rather than marking it with a "5". + { "__TID_LCHeapMemory", + "type id for CHeapMemory" }, + { "__T_DstdIbad_cast", + "typeinfo for std::bad_cast" }, + { "__TID_v", + "type id for void" }, + { "__sti__CHeapMemory_cpp", + "static initializers for CHeapMemory_cpp" }, }; // Things that aren't Itanium-mangled names, or that we deliberately don't try to parse. @@ -199,6 +250,8 @@ static const char *notMangled[] = { // 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. + "__adddf3", // A C runtime name that starts like an SN Systems one. + "__sti", "__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.