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:
Henrik RydgårdandClaude Opus 5 committed 2026-08-29 23:23:57 +02:00
1 parent f89a2d4199
commit dfc04f3578
5 files changed
+467 -28

No files matched your search

+220 -20
View File
@@ -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.
+6 -6
View File
@@ -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);
+2 -2
View File
@@ -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;
};
+202
View File
@@ -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.
+37
View File
@@ -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.