Both mean "do not free" as far as the callee is concerned - it only tests the sign - but 0 is passed specifically for base-subobject destruction. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SF5eS5QDNexLksRDeDZvwY
11 KiB
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.
The hidden destructor parameter
The mangling describes the source-level signature, not the generated one. __dt__3SonFv
demangles to Son::~Son(), and that is the right answer for a demangler - but the function the
compiler emitted takes a second argument, and anything that turns a demangled name into a
function signature (a decompiler, a debugger's parameter view) will be wrong about it.
CodeWarrior emits one destructor per class rather than the two or three the Itanium ABI clones,
and distinguishes the cases with a flag. On PSP that flag is a short in a1, and the
destructor returns this in v0. So the real signature is:
Son *__dt__3SonFv(Son *this, short flag);
Decompiled from a real one, the body is:
Son *__dt__3SonFv(Son *this, short flag) {
if (this) { // destructors are null-safe
... restore vtable pointers, destroy members ...
__dt__3DadFv(this + 12, 0); // base subobjects get 0
__dt__3MomFv(this, 0);
if (flag > 0) // "blez" - only a positive flag frees
__dl__FPv(this); // operator delete
}
return this;
}
The values that actually get passed, counted over the call sites in a retail binary:
| Flag | Meaning | Frequency |
|---|---|---|
-1 |
Destroy a complete object without freeing it - a stack object, a member, an explicit ~T() call |
Most calls |
1 |
Destroy and free - what delete compiles to |
A handful |
0 |
Destroy a base-class subobject | A handful |
0 and -1 are a caller-side distinction: the destructors examined here only test the sign, so
both simply mean "don't free", but the compiler still passes 0 specifically when destroying a
base subobject and -1 everywhere else. Treat 0 as "this call is destroying me as somebody's
base" when reading a call site, even though the callee can't tell.
Constructors in these binaries take only this (and also return it).
Note that the vtable slot holds this same function, flag and all, which is how CodeWarrior gets
away without separate deleting and non-deleting destructors: a virtual delete passes 1 through
the vtable.
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 keepsI3dClut__FlushCachefrom turning intoI3dClut(long, ...). eis a real parameter, not a parse failure.Printf__Q26shFont4FontFiiPCceends in varargs;(int, int, const char *, ...)is the correct answer, not a partial one.