mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-01 14:58:14 +00:00
Demangle: handle CodeWarrior templates, function pointers and @-symbols
Checked against two PSP binaries that shipped with intact symbol tables,
which turned up several constructs the format's usual description doesn't
mention:
- Template arguments are written literally inside the length-prefixed name
("39CList<Q38hlScreen5Brwsr13CContentsUnit>"), not with a "__PT" prefix,
and they nest. Function templates put theirs in the base name instead,
followed by the return type.
- A family of "@"-decorated symbols for things with no C++ name: thunks
("@12@__dt__3SonFv"), string literals, function-local statics and their
guard variables. Plus __vt__/__RTTI__/__sinit_, printed in the same style
as the Itanium special names.
- Types are now built as a split declarator, so a pointer to a function
comes out as "int (*)(int)" rather than "int (int) *".
Also stop the lenient pass from turning plain C names with a "__" in them
into nonsense - "I3dClut__FlushCache" became "I3dClut(long, ...)". It now
requires a class qualifier, which costs nothing: over ~10000 symbols the
lenient pass rescued none and only produced those false positives.
Symbol map names go from 128 to 256 characters, since a demangled name
keeps its parameters and templates make short work of 128.
docs/CodeWarriorMangling.md describes the format, marking the parts that
are inferred from cfront rather than attested in a real binary.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SF5eS5QDNexLksRDeDZvwY
This commit is contained in:
1 parent
f89a2d4199
commit
dfc04f3578
5 files changed
+467
-28
No files matched your search
+220
-20
@@ -1210,6 +1210,13 @@ bool DemangleItanium(std::string_view mangled, std::string *out) {
|
||||
// Q<n>-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<Q38hlScreen5Brwsr13CContentsUnit>".
|
||||
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<std::string> 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_<n>_" 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<Pf>__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<Pf>__3stdFPfPf_v@shuffle@0 std::sort<float *>(...)::shuffle
|
||||
//
|
||||
// The @-forms are how CodeWarrior names things that have no C++ name of their own; the
|
||||
// trailing @<n> 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 "@<digits>" 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 "$<n>" 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@<function>@<variable>", 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])) {
|
||||
// "@<this-adjustment>@<function>": 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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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:
|
||||
|
||||
```
|
||||
<basename> __ [<class>] [<cv>] F <parameters> [_ <return type>]
|
||||
```
|
||||
|
||||
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<i,1>__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<count>` 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_<count>_` **(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<Q38hlScreen5Brwsr13CContentsUnit> shList::CList<hlScreen::Brwsr::CContentsUnit>
|
||||
```
|
||||
|
||||
Here `39` is the length of `CList<Q38hlScreen5Brwsr13CContentsUnit>`. 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<TABLE_VAR_MEMBER, TABLE_VAR_SECT_HEADER, void>` |
|
||||
| `47CList<Q26ssTool29tag_<Q26shFont12SysCmdString>>` | `CList<ssTool::tag_<shFont::SysCmdString>>` |
|
||||
|
||||
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<Pf>__3stdFPfPf_v void std::sort<float *>(float *, float *)
|
||||
```
|
||||
|
||||
### Special base names
|
||||
|
||||
| Base name | Meaning |
|
||||
| --- | --- |
|
||||
| `__ct` | Constructor; the name is taken from the class |
|
||||
| `__dt` | Destructor |
|
||||
| `__op<type>` | Conversion operator, e.g. `__opCi` is `operator const int()` |
|
||||
| `__<code>` | 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<n>_` | array of `n` | **(inferred)** |
|
||||
| `F<params>_<ret>` | function | |
|
||||
| `<len><name>` | class | |
|
||||
| `Q<n>...` | 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<index>` | Same type as parameter `<index>`, 1-based |
|
||||
| `N<count><index>` | `<count>` more parameters, each the type of parameter `<index>` |
|
||||
|
||||
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__<class>` | Vtable | `__vt__Q23std9exception` |
|
||||
| `__RTTI__<class>` | Typeinfo record | `__RTTI__Q23std9exception` |
|
||||
| `__sinit_<file>` | Static initializers for a translation unit | `__sinit_hl_app.cpp` |
|
||||
| `__sterm_<file>` | Static destructors | |
|
||||
| `@<n>@<symbol>` | `this`-adjusting thunk, `n` bytes | `@12@__dt__3SonFv` |
|
||||
| `@STRING@<symbol>[@<n>]` | A string literal used inside that function | `@STRING@Get_BGM__Q25hlBHC4SBhcFi@0` |
|
||||
| `@LOCAL@<symbol>@<var>[@<n>]` | A function-local static | `@LOCAL@sort<Pf>__3stdFPfPf_v@shuffle@0` |
|
||||
| `@GUARD@<var>$<n>` | Its "already constructed" flag | `@GUARD@app$16079` |
|
||||
| `@<n>` | An anonymous string constant | `@10046` |
|
||||
|
||||
The trailing `@<n>` 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.
|
||||
|
||||
`@<n>@` 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:
|
||||
|
||||
- `<name>$<digits><file>` - 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.
|
||||
- `<len>@unnamed@<file>@` - 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.
|
||||
@@ -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__Q26shList39CList<Q38hlScreen5Brwsr13CContentsUnit>FPCc",
|
||||
"shList::CList<hlScreen::Brwsr::CContentsUnit>::Init(const char *)" },
|
||||
{ "Reset__Q28shString15CSimpleChar<24>Fv",
|
||||
"shString::CSimpleChar<24>::Reset()" },
|
||||
{ "AddItem__Q26shList47CList<Q26ssTool29tag_<Q26shFont12SysCmdString>>FPQ26ssTool29tag_<Q26shFont12SysCmdString>",
|
||||
"shList::CList<ssTool::tag_<shFont::SysCmdString>>::AddItem(ssTool::tag_<shFont::SysCmdString> *)" },
|
||||
// A function template puts them in the base name instead, and encodes its return type.
|
||||
{ "sort<Pf>__3stdFPfPf_v",
|
||||
"void std::sort<float *>(float *, float *)" },
|
||||
// Compiler-generated symbols that wrap a mangled name rather than being one.
|
||||
{ "__vt__Q23std9exception",
|
||||
"vtable for std::exception" },
|
||||
{ "__RTTI__Q23std9exception",
|
||||
"typeinfo for std::exception" },
|
||||
{ "__sinit_hl_app.cpp",
|
||||
"static initializers for hl_app.cpp" },
|
||||
{ "@12@__dt__3SonFv",
|
||||
"non-virtual thunk (12) to Son::~Son()" },
|
||||
{ "@STRING@what__Q23std9exceptionCFv",
|
||||
"string literal in std::exception::what() const" },
|
||||
{ "@STRING@Get_BGM__Q25hlBHC4SBhcFi@0",
|
||||
"string literal 0 in hlBHC::SBhc::Get_BGM(int)" },
|
||||
{ "@LOCAL@sort<Pf>__3stdFPfPf_v@shuffle@0",
|
||||
"void std::sort<float *>(float *, float *)::shuffle" },
|
||||
{ "@GUARD@app$16079",
|
||||
"guard variable for app$16079" },
|
||||
};
|
||||
|
||||
// SN Systems (SNC/ProDG). Same caveat as above, plus the format itself is reverse
|
||||
@@ -162,6 +195,10 @@ static const char *notMangled[] = {
|
||||
"_Z3fooIXadL_Z1xEEEvv", // An <expression> template argument - no expression parser.
|
||||
"Foo__Bar", // A "__" that isn't a CodeWarrior separator.
|
||||
"a__b__c",
|
||||
// The same, but where the tail happens to start with an "F" and a valid type code, so
|
||||
// only the lack of a class qualifier tells it apart from a real mangled name.
|
||||
"I3dClut__FlushCache",
|
||||
"@10046", // An anonymous string constant: nothing to demangle.
|
||||
"__0", // Too short to be an SN Systems name.
|
||||
// A CodeWarrior template name, truncated by a 127-character symbol table limit. The
|
||||
// length prefixes no longer match what's left, so there's nothing to recover.
|
||||
|
||||
Reference in new issue
Block a user