-
Notifications
You must be signed in to change notification settings - Fork 186
Structures
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.
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]andraxlooks 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.
You need to be attached to a process first.
Open it from:
Memory ViewTools -> 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.
Most structure work goes like this:
- Find one value that belongs to an object, usually with a normal scan.
- Use
Find out what accesses this addressor nearby memory to figure out the object base. - Create a structure for that object type.
- Add fields at the offsets you understand.
- View the structure at the object base.
- Keep expanding the structure as you learn more offsets.
- 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.
Click New in the Structures window. The editor asks for a structure name and a list of members.
Good names are boring names:
PlayerEnemyInventoryWeaponGameManagerTransform
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.
A member is one field inside the structure. It has:
-
Name: what you want to call it, likehealth,position_x,ammo,inventory_ptr -
Offset (hex): byte offset from the structure base -
Kind:Value,PointerorInline
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.
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.
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.
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.
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.
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:
0x7f1234560000libgame.so+0x4F2A10$rax*(long*)(libgame.so+0x18)-
player_manager->current_playerif 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.
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.
Pointer and inline members can be expanded.
For Pointer members, PINCE:
- Reads the pointer value at
base + offset. - Uses that pointer value as the child base.
- Builds the child structure under it.
For Inline members, PINCE:
- Uses
base + offsetas the child base. - 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.
You can add a structure to the address table from two places.
From the Structures window:
- Select the structure.
- Click
Add to address table. - Enter the base address.
From Structure view:
- Open the structure at an address.
- 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
.pctsession - You want to share a table with structure rows already laid out
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 structureis 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.
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.
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
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.
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.
Say you found a player object at 0x7f1234560000 and you know this much:
- health is an
Int32at0x10 - mana is a
Float32at0x14 - position is an inline
Vector3at0x20 - 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.
There are a few common ways to fill out a structure.
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.
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.
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.
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 inStructure 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.
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.
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.
A few layouts come up a lot.
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.
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.
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.
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.
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
Pointerfor inline data - You used
Inlinefor 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 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.