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.