Decompilation cheatsheet
Useful stuff for when you're stuck, need a refresher or when a function mismatches.
Project tools
A list of useful project tools and the most common options you'll be using.
- Check all decompiled functions for issues:
tools/check
- To compare assembly:
tools/check FUNCTION_NAME
- The function must be listed in data/uking_functions.csv first.
- To do so, search for the name or the address of function you have decompiled, and add its name to the last column.
- Pass the
--source
flag to show source code interleaved with assembly code. - Add the
--inlines
flag to show inline function calls. This is not enabled by default because it usually produces too much output to be useful. - Pass
-mw3
for automatic rebuilds whenever a source file is modified. - For more options, see asm-differ.
- The function must be listed in data/uking_functions.csv first.
- To print progress:
tools/common/progress.py
- Note that progress is only approximate because of inline functions, templating and compiler-generated functions.
- To print AI class decompilation status:
tools/ai_progress.py
- Use this to figure out which AI classes have not been decompiled yet.
- To list symbols:
tools/listsym
(pass --help to see available options)
Inline functions
Want to figure out if you are looking at a common inline function? Click here!
Things to try when a function has major differences
The following actions should help when basic blocks are in the wrong order, or when there are weird issues that involve comparisons or conditionals:
- Invert conditionals.
- Introduce inline functions. Or manually inline code.
- Add or eliminate return statements.
- Duplicate or deduplicate code.
- Independent memory loads/stores can be reordered, so you might need to reorder statements or conditions in the source code. For example,
if (x || y)
might have to be written asif (y || x)
. - Turn if/else into ternaries and vice versa. This doesn't always make a difference, though.
- uintptr_t do not always produce the same code as pointers, even for simple operations such as comparisons.
- Loops:
- Index-based loops
- u32 / s32 can make a difference, in particular for loop unrolling.
<
vs!=
can affect codegen. If the trip count is known at compile-time, Clang will usually change<
into!=
, but you should still try<
first.- In some rare cases, Nintendo will use
!=
instead of<
.
- Iterator loops
- Since we are targeting C++17, the preferred way to iterate over a container is to use a range-based for loop (e.g.
for (auto x : array)
). - Sometimes, making the iterator appear explicitly is required to match the original code. Example:
for (auto it = array.begin(), end = array.end(); it != end; ++it)
- In some rare cases, the end iterator is not kept in a variable, and instead it's recalculated at the end of each iteration. Example:
for (auto it = array.begin(); it != array.end(); ++it)
- Sometimes it is possible to use
<algorithm>
functions (e.g. std::for_each, std::all_of, etc.) for simpler loops. - And in some very rare cases (when dealing with EventFlow for example) it is sometimes required to use
<algorithm>
to match.
- Since we are targeting C++17, the preferred way to iterate over a container is to use a range-based for loop (e.g.
- Index-based loops
Things to try when a function has minor differences
Incorrect comparison flags (getting >= instead of > for example)
- Invert conditionals.
- For integers: make sure you are using the correct signedness.
- For example, HI means that you should be using an unsigned integer.
- For floating-point, keep in mind that
x > 5.0
and!(x <= 5.0)
are not equivalent because of NaN. This can reveal how an if/else statement is supposed to be written.
Swapped CSEL operands
- Invert conditionals. (For ternaries, also swap the ? and : operands, obviously.)
- In some rare cases,
ptr == nullptr
and!ptr
do not generate the same code.
Extraneous function prologue/epilogue: this can happen when returning references. Change the return type to a pointer.
C++ stuff
Classes
Constructors
As explained in the C++ primer, if you see a function that modifies the vtable pointer and/or calls a lot of other constructors, chances are that you are dealing with a constructor.
In C++, most of the code in constructor functions tends to be automatically generated by the compiler. For example, the following function:
void __fastcall ksys::res::Handle::Handle(ksys::res::Handle *this)
{
this->mFlags = 1;
this->mStatus = 0;
this->mUnit = 0LL;
this->vtable = &ksys::res::Handle::vt;
ksys::util::ManagedTaskHandle::ManagedTaskHandle(&this->mTaskHandle);
this->field_40 = 0LL;
this->field_48 = 0LL;
}
is automatically generated based on the class definition:
// irrelevant details were simplified or removed
struct Handle {
u8 mFlags = 1;
Status mStatus = Status::_0;
ResourceUnit* mUnit = nullptr;
util::ManagedTaskHandle mTaskHandle;
sead::ListNode mListNode;
};
Note that the sead::ListNode constructor was inlined here, and that constructor was also automatically generated by the compiler:
class ListNode
{
public:
// ...
ListNode* mPrev = nullptr;
ListNode* mNext = nullptr;
};
Member functions and member variables
C++ non-virtual, non-static member functions are called as if they had the this
pointer as the first argument.
sead::CriticalSection::lock(some_variable);
ksys::res::ResourceUnit::attachHandle(unit, handle);
⬇️
some_variable->lock();
unit->attachHandle(handle);
Member variables are accessed using the this
pointer. In C++, explicitly writing this
is usually unnecessary:
if ( this->mNumCaches <= idx )
cache = this->mCaches;
else
cache = &this->mCaches[idx];
⬇️
if (mNumCaches <= idx)
cache = mCaches;
else
cache = &mCaches[idx];
Custom operator new
sead defines several custom allocation functions. You should #include <basis/seadNew.h>
to ensure they are used for heap allocations.
void* ptr = ::operator new(SOME_SIZE_HERE, heap, 8u);
SomeClass::SomeClass(ptr); //< constructor call
⬇️
auto* ptr = new (heap) SomeClass;
Sometimes, a non-throwing overload of operator new
is used:
void* ptr = ::operator new(SOME_SIZE_HERE, heap, 8u, &std::nothrow);
SomeClass::SomeClass(ptr); //< constructor call
⬇️
auto* ptr = new (heap, std::nothrow) SomeClass;
This also applies for new[]
.
sinit / static initializer / cxa_atexit
- If the second argument of a
_cxa_atexit
call is nullptr and the destructor is a nullsub, the object in question is likely a C-style array (not a std::array or a sead::SafeArray).