-
Notifications
You must be signed in to change notification settings - Fork 186
Structures
Structures (or Structure Types) let you define custom data layouts and apply them to memory addresses. If you've ever reverse engineered a game struct or class and wanted to see all its fields laid out nicely with their current values, this is your bread n butter.
It's basically like creating your own mini type system on top of raw memory. You tell PINCE "at this address, there's an int at offset 0, a float at offset 4, a pointer at offset 8" and so on and the Structure view will read and display all those values for you, updating in real time as the game runs.
- You need to be attached to a process first.
- After that, open
Memory View. - Go to
Tools -> Structures.
This opens the Structures window where you can manage your structure definitions.
Click New in the Structures window to open the Structure Editor (titled Edit structure). You'll need to give your structure a name and then add members to it.
Each member represents a field in your structure. Click Add to create a new member, which opens the Member Editor (titled Member). You'll see these options:
-
Name: What you want to call this field (likehealth,position_x,ptr_to_inventory). -
Offset (hex): The byte offset from the structure's base address. You can enter it in hex with or without the0xprefix. When editing an existing member, the field shows the offset with the0xprefix. -
Kind: What type of member this is:-
Value: A primitive value (int, float, string, etc.) -
Pointer: A pointer to another structure (dereferences the pointer at this offset) -
Inline: Another structure embedded directly at this offset (no pointer dereference)
-
The Pointer and Inline kinds are disabled if no structures exist yet. You need to create at least one structure before you can reference it from another.
For value members, you pick the data type from the Value type dropdown:
-
Int8,Int16,Int32,Int64: Integer types. Pick a representation from theReprdropdown (Unsigned,SignedorHex). The Repr dropdown only appears for integer types, not floats. -
Float32,Float64: Float types. No repr option since floats don't have signed/unsigned variants. -
String_ASCII,String_UTF8,String_UTF16,String_UTF32: String types. Set theLengthfield. The Length field only appears for string and byte array types. -
ByteArray: Raw bytes with a specified length.
You can also set the Endianness (Host, Little or Big) for any value member. Endianness affects UTF-16 and UTF-32 string encoding by selecting the appropriate little-endian or big-endian codec variant. ASCII and UTF-8 ignore endianness.
Note: The data model supports a zero-termination flag but the manual Member Editor doesn't expose it. Zero termination is always on for manually created members. (The Mono/IL2CPP Dissector's auto-generated System.String structure sets zero termination off for the chars member, but you can't change this from the UI.)
These let you nest structures. Say you have a Player structure with a field that points to an Inventory structure:
- First create the
Inventorystructure with its own members. - Then in your
Playerstructure, add a member at the right offset. - Set Kind to
Pointer(if the field holds an address) orInline(if the inventory data is embedded directly). - Select
Inventoryfrom theStructuredropdown.
In the Structure Editor's member tree, pointer members display as ptr → StructName and inline members as inline → StructName in the Type column. The same prefixes are shown in the Structure View.
If you reference a structure that later gets deleted, the member keeps its link. The deleted structure's name is still shown in the Structure dropdown when editing that member, so you don't lose the reference silently.
Once you've defined a structure, you can view it at any address:
- Select the structure in the Structures window.
- Click
View at address…. - Enter the base address in the dialog that pops up.
Base address formats
The field accepts any GDB expression, not only a raw hex value:
- absolute address:
0x7f1234abcd- symbol:
g_player- module-relative address:
libgame.so+0x4F2A10(resolved against the process's loaded modules)- pointer dereference:
*(long*)(libgame.so+0x18)to base the structure on whatever a pointer currently points toThe same formats apply when adding to the address table and when changing a base by double-clicking a structure row's address column.
Right-click on any entry in the address table and hover over View as structure. You'll see a submenu listing all your defined structures. Pick one and the Structure View opens at that entry's address.
This is handy when you've already found an interesting address and want to interpret it as a known structure without having to copy the address over manually. The submenu is hidden for script entries, struct rows and when no row is selected.
The Structure View window (titled Structure view) opens showing all your members with their current values. The view has four columns:
-
Offset: The byte offset from the base address. -
Name: Your member name. -
Type: The data type or nested structure name (withptr →orinline →prefix for nested structs). -
Value: The current value read from memory. Shows??if the memory read fails.
The Structure View polls memory at the same interval as the address table, so values update automatically as the game runs. You can double-click any value in the Value column to edit it directly. The change is written to memory immediately and that single item is refreshed right away without waiting for the next poll.
Polling pauses while the Structure View is hidden or closed, so it doesn't waste CPU when you're not looking at it.
If you have pointer or inline members, you'll see an expand arrow next to them. Click it to drill down into the nested structure and see its fields. This works recursively up to 16 levels deep.
For pointer members, PINCE reads the pointer value at that offset and uses it as the base address for the nested structure. If the pointer is null, the expansion just shows nothing. For inline members, the bytes at that offset are interpreted directly as the nested structure.
You can push an entire structure's members into the address table as a group:
- Select your structure in the Structures window.
- Click
Add to address table. - Enter the base address (see Base address formats under Viewing a Structure).
Click the Add to address table button at the bottom of the Structure View window. This uses the view's current base address, so you don't need to re-enter it.
All the structure's members appear in the address table as a collapsible group, with their offsets shown as relative addresses (like +0x10). Nested structures become sub-groups. Pointer members are materialized as pointer chains so the address table can resolve them. This is handy when you want to monitor or freeze multiple related values at once.
When you add a structure to the address table, the parent row is a special type that only holds an address, it doesn't have an editable value or type like normal entries. The child rows underneath it are the actual members with their values.
You can change the base address of a structure row by double-clicking its address column. This updates all the child rows' addresses automatically since they're calculated as offsets from the base.
Structure rows have some limitations compared to normal entries:
- No freeze: The freeze checkbox on a structure row just unchecks itself as you can't freeze the parent row. Individual child members can still be frozen though.
-
No "View as structure": Right-clicking a structure row doesn't show the
View as structuresubmenu since it's already a structure. -
Relative addresses: Child rows show their offset as the address (like
+0x10) rather than the resolved absolute address. The resolved address is used internally for reads and writes.
Note: A pointer-dereference base (e.g. *(long*)(libgame.so+0x18)) is resolved when the row is added and re-resolved on a manual table refresh. It does not automatically follow a pointer that moves on its own. This is done to avoid the cost of a GDB call on each automatic address table refresh which happens very frequently (every 0.5s by default).
-
Edit: Modify an existing structure's name or members. Double-clicking a structure in the list also opens the editor. -
Delete: Remove a structure definition. You'll get a confirmation prompt askingDelete structure '{}'?where the name is shown in single quotes. Deleting a structure doesn't affect any address table entries you've already created from it.
In the Structure Editor, you can reorder members with the Up and Down buttons or remove them with Remove. You can also click Edit to modify an individual member (this opens the Member Editor pre-loaded with that member's settings).
If you try to save a structure with an empty name, you'll get Structure name cannot be empty. If you try to use a name that already exists, you'll get A structure with this name already exists.
Your structure definitions are saved alongside your address table when you save a session (.pct file). This means when you load a session later, all your structures come back with it.
If you share a .pct file with someone, they'll get your structures too, which is useful for distributing cheat tables that rely on specific structure layouts.
Let's say you've found a player object at 0x7f1234560000 and you know from reverse engineering that it has:
- Health (int32) at offset 0x10
- Mana (float32) at offset 0x14
- Position X (float32) at offset 0x20
- Position Y (float32) at offset 0x24
- A pointer to an Inventory struct at offset 0x30
You'd create a Player structure like this:
Player
├── health 0x10 Int32
├── mana 0x14 Float32
├── position_x 0x20 Float32
├── position_y 0x24 Float32
└── inventory 0x30 ptr → Inventory
Then create an Inventory structure with whatever fields you've found there:
Inventory
├── gold 0x08 Int32
├── item_count 0x0C Int32
└── items_ptr 0x10 ptr → ItemArray
Now when you view the Player structure at 0x7f1234560000, you see all the player's stats and you can expand inventory to see the nested Inventory fields. If you expand items_ptr, it follows that pointer too and shows the item array structure.
If you're not sure about the exact offsets, you can use the address table and Memory Viewer to figure them out. Add a variable at a known offset, then use Find out what accesses this address to see what code touches it. The disassembly will show you the offset being used (like [rax+0x14]).
The structure's size field is mostly informational, it doesn't affect how members are read. But it's useful to keep track of how big your structure is, especially if you're dealing with arrays of structures. The size is only set automatically by the Mono/IL2CPP Dissector when exporting structures, the manual editor doesn't set it.
You can reuse the same structure definition across multiple addresses. If you have 5 enemies all using the same Enemy struct layout, just create one Enemy structure and view it at each enemy's base address.
If you're working with a Mono or IL2CPP game, the Mono/IL2CPP Dissector can automatically generate structure definitions from the game's managed classes:
-
Export as structure: Right-click on a class to create a structure definition with all the fields already filled in. Opens the Structure Editor for confirmation. -
Dissect as structure: Right-click on a class with a singleton pattern (likePlayer.Instance) or on a live instance in the Find Instances window to immediately open the Structure View on that object.
Games often use common patterns you can recognize:
- Object headers: Most objects start with a vtable pointer (8 bytes on 64-bit, 4 on 32-bit) followed by a sync block or reference count.
- Arrays: Managed arrays usually have a length field followed by the elements. Native arrays might just be a pointer and size stored separately.
- Strings: UTF-16 strings often have a length prefix. C-style strings are just null-terminated byte sequences.