Skip to content

Structures

brkzlr edited this page Jun 25, 2026 · 6 revisions

Structures let you describe a chunk of memory once, then view it at any address. If you have a player object, enemy object, inventory entry, projectile, UI state, whatever, a structure is the thing that turns raw bytes into named fields you can actually read.

The important bit is that structures do not find the object for you. They are a layout. You still need some base address, pointer, singleton, symbol or scan result first. Once you have that base, PINCE can show base + 0x10 as health, base + 0x14 as mana, base + 0x30 as a pointer to another structure and so on.

When structures are useful

Use structures when you have more than one value that belongs to the same object.

Typical cases:

  • You found health, then nearby memory also has armor, ammo, coordinates or state flags
  • A pointer scanner result lands at an object base and you want to label its fields
  • A function accesses stuff like [rax+0x10], [rax+0x14], [rax+0x30] and rax looks like an object pointer
  • The Mono/IL2CPP Dissector exported a managed class and you want to inspect live instances
  • You have several enemies using the same layout and want to reuse the same definition at different addresses

A structure is best for understanding layout. The address table is best for keeping a few important values around, freezing them, editing them quickly or saving a table. You can use both together.

Open the structure tools

You need to be attached to a process first.

Open it from:

  • Memory View
  • Tools -> Structures

This opens the Structures window. That window is only for managing structure definitions. To actually look at memory through a structure, you use View at address or add the structure to the address table.

The normal workflow

Most structure work goes like this:

  1. Find one value that belongs to an object, usually with a normal scan.
  2. Use Find out what accesses this address or nearby memory to figure out the object base.
  3. Create a structure for that object type.
  4. Add fields at the offsets you understand.
  5. View the structure at the object base.
  6. Keep expanding the structure as you learn more offsets.
  7. Add the useful fields to the address table when you want to monitor, freeze or share them.

Do not try to finish the whole structure in one go. Start with 2 or 3 fields you trust. Bad offsets make the whole thing confusing very quickly.

Creating a structure

Click New in the Structures window. The editor asks for a structure name and a list of members.

Good names are boring names:

  • Player
  • Enemy
  • Inventory
  • Weapon
  • GameManager
  • Transform

Structure names must not be empty and must be unique. If you try to reuse a name, PINCE shows A structure with this name already exists.

Members

A member is one field inside the structure. It has:

  • Name: what you want to call it, like health, position_x, ammo, inventory_ptr
  • Offset (hex): byte offset from the structure base
  • Kind: Value, Pointer or Inline

Offsets are always relative to the base address you view the structure at. If the base is 0x10000000 and a field offset is 0x10, PINCE reads that field from 0x10000010.

The order in the editor is only the display order. Offsets decide where the data is read from. Keeping members sorted by offset is still usually easier to read.

The offset box accepts hex with or without 0x. If something looks wrong, check for typoed offsets first.

Value members

Use Value for normal values stored directly inside the object.

Useful value types:

  • Int8, Int16, Int32, Int64: integers
  • Float32, Float64: floats
  • String_ASCII, String_UTF8, String_UTF16, String_UTF32: strings
  • ByteArray: raw bytes

For integer types, Repr controls how the value is displayed:

  • Unsigned: normal positive integer view
  • Signed: signed integer view
  • Hex: hex display, good for flags, handles, pointers stored as integers or unknown values

For strings and byte arrays, set Length. For strings, that length is the max amount PINCE reads. For ByteArray, it is the number of bytes.

Endianness can be Host, Little or Big. Most normal x86/x64 Linux and WINE targets use little endian, so Host is usually fine. Use Big only when you know the data is stored that way.

Manual string members are zero terminated internally. The manual member editor does not expose a zero-termination checkbox. The data model supports it though. Auto-created structures can use it. For example the Mono/IL2CPP System.String structure uses a non-zero-terminated chars member.

Pointer members

Use Pointer when the field stores an address to another object.

Example layout:

Player base
+0x10  health
+0x30  pointer to Inventory

For this, inventory at 0x30 should be a Pointer member that references the Inventory structure. PINCE reads the pointer-sized value at player_base + 0x30, then uses that as the base of the nested Inventory.

Pointer size follows the target process architecture:

  • 32-bit target: 4 byte pointer
  • 64-bit target: 8 byte pointer

If the pointer is null or unreadable, expanding it shows nothing. That usually means the object does not exist right now, the offset is wrong or the pointer is only valid in some game states.

Inline members

Use Inline when another structure is embedded directly inside the current one.

Example layout:

Player base
+0x10  health
+0x20  position struct starts here
+0x20  position.x
+0x24  position.y
+0x28  position.z

Here position is not a pointer. The position data starts at player_base + 0x20, so it should be an Inline member that references a Vector3 structure.

The quick rule:

  • Pointer: the field contains an address
  • Inline: the field is the nested object

Pointer and Inline are disabled if there are no structures to reference yet. Create the child structure first, then reference it from the parent.

Renaming and deleting referenced structures

If one structure references another structure, that reference is stored by name.

This means:

  • Deleting a referenced structure does not delete members that point to it
  • Editing that member later still shows the old structure name, so the reference is not silently changed
  • Renaming a structure does not automatically update other structures that referenced the old name

So if you rename Inventory to PlayerInventory, check any Pointer or Inline members that used Inventory before.

Viewing a structure at an address

After creating a structure, select it in the Structures window and click View at address.

The address can be more than a raw hex address. It goes through PINCE/GDB expression handling, so useful examples are:

  • 0x7f1234560000
  • libgame.so+0x4F2A10
  • $rax
  • *(long*)(libgame.so+0x18)
  • player_manager->current_player if GDB can resolve it in your target

See GDB Expressions for more examples.

The Structure view window shows:

  • Offset: offset from the structure base
  • Name: member name
  • Type: value type or nested structure type
  • Value: current value read from memory

?? means PINCE could not read that value. Common reasons are an invalid base, wrong offset, unmapped memory, a null pointer or the object being freed.

The base address line in Structure view is read-only. If you want the same structure at another base, open a new view. If you added it to the address table, you can edit the parent structure row address there.

Live updates

Structure view refreshes on the same interval as the address table.

Values update while the window is visible. Polling stops when the view is hidden or closed, so it is not constantly reading memory in the background.

Double-click a value in the Value column to edit it. PINCE parses the new value using that member type, writes it immediately, then refreshes that one field.

Nested structure rows themselves are not editable values. Edit the actual child value under them.

Expanding nested structures

Pointer and inline members can be expanded.

For Pointer members, PINCE:

  1. Reads the pointer value at base + offset.
  2. Uses that pointer value as the child base.
  3. Builds the child structure under it.

For Inline members, PINCE:

  1. Uses base + offset as the child base.
  2. Builds the child structure under it without dereferencing anything.

Nested expansion goes up to 16 levels. That limit is there so bad recursive structures do not keep expanding forever.

One caveat: once a pointer member is expanded in Structure view, the child rows use the address that was read at expansion time. The visible child values refresh, but the expansion is not rebuilt every tick. If the pointer itself changes, collapse/reopen a fresh view or re-add it to the address table if needed.

Adding structures to the address table

You can add a structure to the address table from two places.

From the Structures window:

  1. Select the structure.
  2. Click Add to address table.
  3. Enter the base address.

From Structure view:

  1. Open the structure at an address.
  2. Click Add to address table.

This creates a parent structure row with child rows for the members.

This is useful when:

  • You want to freeze a field
  • You want to keep a few fields while closing the structure view
  • You want table entries saved in a .pct session
  • You want to share a table with structure rows already laid out

Structure rows

The parent structure row is only a container. It has an address, but it is not a readable value.

Because of that:

  • You cannot freeze the parent structure row
  • You cannot edit the parent row value or type
  • View as structure is not shown for the parent row because it is already a structure row
  • Child rows are the real readable and writable values

Child rows show relative addresses like +0x10. Internally PINCE resolves them against the parent structure row address.

If the child is from a Pointer member, PINCE stores it as a pointer chain from the parent base. In the table this lets the child resolve through the pointer instead of being stuck at one absolute address.

You can change the structure base by double-clicking the parent row address. Child rows update from the new base on refresh.

Pointer bases in the address table

If you use a pointer-dereference expression as the parent base, like:

*(long*)(libgame.so+0x18)

PINCE resolves it when the row is added. It can be re-resolved when you manually refresh the address table with the refresh button.

It does not call GDB for that base on every automatic table refresh. The table refreshes often, so doing a GDB expression every tick would be expensive.

If you need a base that follows a changing pointer all the time, it is usually better to make it a proper pointer chain entry instead of a plain dereference expression.

Viewing an address table row as a structure

If you already have an address table entry, right-click it and use View as structure.

PINCE opens a submenu with your defined structures. Pick one and it opens Structure view using that row's resolved address as the base.

This is handy when a scan result or pointer scan result looks like an object base and you want to quickly test a layout.

The submenu is hidden for:

  • script entries
  • structure parent rows
  • cases where there are no structures defined
  • cases where no row is selected

Editing and deleting structures

In the Structures window:

  • Edit: edit the selected structure
  • double-click a structure: also edits it
  • Delete: deletes the selected structure definition

In the structure editor:

  • Add: add a member
  • Edit: edit selected member
  • Remove: remove selected member
  • Up / Down: reorder members in the display

Deleting a structure definition does not remove address table rows that were already created from it. Those rows are normal saved table rows at that point.

Saving structures

Structure definitions are saved in .pct sessions together with the address table.

A saved session can include:

  • structure definitions
  • address table rows made from structures
  • bookmarks
  • notes
  • process name

A session is not a live memory snapshot. You still need to attach to the target again and the addresses still need to make sense for that run.

If you share a .pct file, the other person gets the structure definitions too. That is useful for tables that rely on named layouts.

Practical example

Say you found a player object at 0x7f1234560000 and you know this much:

  • health is an Int32 at 0x10
  • mana is a Float32 at 0x14
  • position is an inline Vector3 at 0x20
  • inventory pointer is at 0x30

Create Vector3 first:

Vector3
+0x00  x  Float32
+0x04  y  Float32
+0x08  z  Float32

Create Inventory next:

Inventory
+0x08  gold        Int32
+0x0C  item_count  Int32
+0x10  items_ptr   Int64(h)

Then create Player:

Player
+0x10  health    Int32
+0x14  mana      Float32
+0x20  position  inline -> Vector3
+0x30  inventory ptr -> Inventory

Now view Player at:

0x7f1234560000

You should see health and mana directly. Expanding position reads x, y and z from inside the player object. Expanding inventory reads the pointer stored at player + 0x30, then shows the Inventory fields from that address.

If the player base changes every run, do not hardcode 0x7f1234560000 forever. Use a pointer scan, module-relative expression, symbol or Mono singleton if one exists.

Finding offsets

There are a few common ways to fill out a structure.

From nearby memory

If you found health, browse the memory region around it. Nearby values often belong to the same object.

For example, if health is at player + 0x10, nearby fields might be:

+0x14  max_health
+0x18  armor
+0x20  x position
+0x24  y position
+0x28  z position

Do not assume every nearby value belongs to the same object. Allocators, padding, old values and unrelated objects can sit nearby too.

From instructions

Use Find out what accesses this address on a known field. If you see instructions like:

mov eax, [rax+0x10]
movss xmm0, [rax+0x20]
mov rcx, [rax+0x30]

rax is probably the object base in that code path. The offsets are 0x10, 0x20 and 0x30.

This is often the cleanest way to build a structure because the game code is telling you how it uses the object.

From pointer scan results

Pointer scan results often end at a field, not the object base. If the final offset is 0x10 and that points to health, the object base may be the resolved address minus 0x10.

Once you know the base, structures help you check whether the rest of the object layout makes sense.

From Mono/IL2CPP

For Mono and IL2CPP games, the Mono/IL2CPP Dissector can create structures from managed classes.

Useful actions there:

  • Export as structure: creates a structure definition from a class
  • Dissect as structure: opens a live object in Structure view
  • Find Instances: finds live objects, then lets you dissect one as a structure

Auto-exported structures are a starting point. Some fields may be object references, inline value types, byte arrays or unknown padding depending on what the runtime can report.

Strings and byte arrays

String fields need a length. If the length is too short, the string is cut off. If it is too long, PINCE reads more memory than needed and the display may include junk after the real text.

For C-style strings, zero termination usually stops the read at the first null terminator.

For managed System.String, the Mono/IL2CPP exporter creates a special System.String layout. Its character data is UTF-16 and not treated as a normal null-terminated C string. When materialized into the address table, PINCE tries to read the managed string length and adjust the preview length, capped to avoid silly reads.

Use ByteArray when you do not know the type yet or when the field is raw data. It is also useful for padding, flags, GUID-like data, inline buffers and values you want to inspect before naming properly.

Structure size

Manual structures do not have a size box that affects reads. PINCE reads each member by its own offset and type.

So structure size is mostly something you track mentally from the largest field:

last field offset + last field size

This matters when you are dealing with arrays of structures. If Enemy is 0x80 bytes, then enemy array entries might be:

enemy_array + 0x00
enemy_array + 0x80
enemy_array + 0x100

The Mono/IL2CPP exporter can infer more layout information because the runtime gives it field offsets, inherited fields and value type details.

Common patterns

A few layouts come up a lot.

Object header

Objects often start with header fields before the values you care about.

Native C++ objects often start with a vtable pointer:

+0x00  vtable pointer
+0x08  first real field on many 64-bit layouts

Managed objects usually have runtime header data first. For example a 64-bit managed string uses fields like vtable pointer, sync data, length, then UTF-16 chars.

Pointer plus count

Arrays and containers often store a pointer and a count separately:

+0x10  items_ptr
+0x18  item_count
+0x1C  capacity

The pointer may lead to an array, the count tells you how many entries are valid.

Inline vectors

Positions, rotations, colors and velocities are often inline floats:

+0x20  x
+0x24  y
+0x28  z

These are usually Inline only if you want a reusable Vector3 structure. Otherwise three Float32 value members are fine.

Flags

State is often packed into a small integer:

+0x40  flags  Int32(h)

Use hex display for these. Bit flags are easier to see in hex than signed decimal.

Common mistakes

If a structure looks wrong, check these first:

  • Base address is the field address, not the object base
  • Offset is decimal in your notes but you entered it as hex
  • You used Pointer for inline data
  • You used Inline for a field that actually stores an address
  • The target is 32-bit but you assumed 8 byte pointers
  • The object moved or got freed
  • You are looking at a stale pointer
  • String length is too short or too long
  • The value is big endian data but the member uses host endian
  • You renamed a referenced structure and old members still point to the old name

When in doubt, go back to one known field. View the base in the hex viewer, check base + offset, then add fields one at a time.

A good structure while reversing

A useful structure does not need every field.

A good early structure might have:

  • known fields with correct names
  • unknown fields named like unk_18, unk_1C, unk_30
  • pointer fields marked as hex until you know what they point to
  • byte arrays for unknown chunks
  • comments in your own notes about which instruction confirmed each offset

It is better to have a small structure you trust than a huge one full of guessed fields.

Clone this wiki locally