Skip to content

Recovering types and data structures ​

A decompiler shows a function. The data it works on shows as param_1 + 0x24. Most of the work of making machine code readable is giving those offsets names and types: the structures, classes, arrays, lists and enumerations of the original program. Once a structure is named, every function that uses it reads better at once. This page lists where type information comes from, the shapes that common structures leave in code, and the layouts of the C++ standard containers. Names go in labels/fields.tsv (structure, offset, name, type, evidence), which the tools and the next session read.

Where types come from ​

Use the best source that exists, in this order:

SourceWhat it gives
Debug symbolsEverything: a PDB from a demo or a leaked build, Borland Turbo Debugger records (DOS games), DWARF in a Linux or Mac build, a linker map file. A related build with symbols (a demo, a port, a sequel on the same engine) carries over with match_functions.py
RTTIClass names and the class tree. MSVC writes a type descriptor named .?AVName@@ for each class with virtual functions. GCC and Clang write _ZTS names. Ghidra applies MSVC's RTTI on its own
Mangled namesExports and imports with C++ names give each parameter's type
The program's own textAssertions with expressions (pUnit->m_health > 0), log messages that print fields, serialisers that write field names to text files, script bindings that register fields by name
Data files and savesA record in a file is often the structure in memory, written whole. Its layout fixes the order and sizes of the fields
The codeEvery access to a field: its offset, its size, its type, and whether it is read or written. The rest of this page

Reading a structure from its uses ​

A structure merged from the accesses of three functions

  • The size. The call that allocates an object passes its size: operator new(0x148), malloc(0x30), or size * count for an array. A memset or memcpy of an object gives the size too.
  • The constructor. It writes most fields once, in order, with their starting values. The first write is often the vtable pointer, which names the class. Destructors free the fields that own memory, in reverse order.
  • The serialiser. A load or save function reads or writes each field in order, with its type: ReadInt, ReadFloat, ReadString. It is the best single source of the field list.
  • Every use. tools/triage/decomp_query.py layout FN[:VAR] ... collects the accesses to one pointer in several functions, and prints each offset with its sizes, types, reads, writes and users, then a draft structure with the gaps marked. Give it the constructor, the serialiser and the main users. Ghidra's own "Auto Create Structure" does the same for one function.
  • Two types at one offset. A field read as both float and int is a union, a value copied as raw bits, or a decompiler guess. A field read as a byte and as a word may be a set of flags.

Shapes in the code ​

StructureWhat it looks like
Array of structuresAn index multiplied by the structure's size (imul, or lea and shifts for sizes like 12 or 24) and added to a base. The size is the stride
Several parallel arraysThe same index into several bases, each scaled by its own element size
Linked listA loop that loads a pointer from a fixed offset of the current node until it reaches null. The offset is the next field
TreeTwo child pointers and often a parent pointer, with a loop going left or right by a comparison. A colour byte next to them makes it a red-black tree
Hash tableA hash of the key, reduced by % or & with the bucket count, an index into an array of bucket heads, then a list walk comparing keys
Bit fields and flagsA shift and a mask ((x >> 5) & 7), or tests of single bits. Name each bit
Tagged unionA switch on one field, with each case reading the same later offsets as different types
EnumerationComparisons with small constants, switches, and tables indexed by the value (often a table of name strings, which names every value)
Fixed pointShifts by 8, 12 or 16 after multiplications, and multiplications by 256 or 65536 (Compiled code)
StringA pointer passed to string functions, or a small buffer passed with its size

The C++ standard containers ​

The layouts of vector, string, list and a map node in 64-bit MSVC release builds

Games written in C++ use the standard library's containers, and their layouts repeat in every structure that holds one. These are the release-build layouts of 64-bit programs. A 32-bit build has the same fields with 4-byte pointers. Check the layout for your compiler with a small test program built with the same compiler version.

ContainerMicrosoft (MSVC 2015 and later)GNU (libstdc++)
std::vector<T>Three pointers: first element, one past the last, end of the capacity. The count is (last - first) / sizeof(T)The same three pointers
std::stringA 16-byte union (the text itself when it is 15 characters or fewer, else a pointer to it), then the length and the capacity: 32 bytes. A capacity above 15 means the pointer is in useA pointer, the length, then a 16-byte buffer for short text: 32 bytes. Before GCC 5, one pointer to the text, with the length, capacity and reference count stored just before the text
std::list<T>A pointer to a sentinel node, then the count. Each node: next, previous, valueThe sentinel node itself, then the count
std::map, std::setA pointer to the head node, then the count. Each node: left, parent, right, a colour byte, an "is sentinel" byte, then the valueThe header node inside the container, then the count. Each node: colour, parent, left, right, then the value
std::unordered_mapA list of all the elements, then a vector of bucket pointers, a mask and a maximum indexAn array of bucket pointers, the bucket count, a list head, the count, and the rehash policy
std::shared_ptr<T>Two pointers: the object and its control block (the counts)The same
std::unique_ptr<T>One pointerOne pointer

Debug builds of MSVC programs add a pointer at the start of each container for iterator checking, which moves every field by 8 bytes. Older compilers have their own layouts: Visual C++ 6.0 programs from the late 1990s use a different std::string. A game's own containers, which many engines wrote to avoid the standard library, have their own layouts: find them with the shapes above.

Working through a program ​

  1. Apply what exists: symbols, RTTI, known library structures. In Ghidra, parse a header of the library's types when you have one (DirectX, Windows, the C runtime).
  2. Pick the structures that matter most: the ones passed to the most functions, and the ones behind the values you are porting.
  3. For each, gather its constructor, serialiser and users, run layout, and write the structure in Ghidra's data type manager. Apply it to the parameters and globals that point to it. Re-export, and the pseudocode reads unit->health in every function.
  4. Name each field from evidence (a string, a saved value, a value watched in memory), and record it in labels/fields.tsv with the evidence.
  5. Leave unknown fields as field_24, never as a guess presented as a fact.

The port takes the types it needs, with the same sizes and signedness. Where the original writes a structure to a file or reads it raw, the port must also keep its exact layout, including padding.

Code: Apache 2.0. Content: CC0 1.0. No game material: bring your own copy.