Skip to main content

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.
  • 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 as if (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.

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).