Эта страница про то, как описывать данные из памяти в виде C#-структур так, чтобы Memory API читал их правильно.
Если коротко:
Read<T>(...)LayoutKind.ExplicitMarshalAs, используйте ReadManaged<T>(...)Официальные ссылки по теме:
Из reverse engineering-материалов особенно полезны:
Memory API умеет читать структуры двумя разными путями:
Read<T>(...) / TryRead<T>(...) для простых структурReadManaged<T>(...) / TryReadManaged<T>(...) для структур со строками, MarshalAs и похожими вещамиВыигрывает почти всегда тот путь, который проще:
MarshalAs, строк или фиксированных managed-массивов, используйте ReadManaged<T>(...)Sequential и ExplicitLayoutKind.SequentialПодходит, когда поля реально лежат подряд в том же порядке, что и в native-типе.
using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Sequential)]
public struct PlayerStats
{
public int Health;
public float Speed;
public long TargetId;
}
Это хороший вариант, когда:
LayoutKind.ExplicitПодходит, когда offsets известны заранее и вы хотите зафиксировать их вручную.
using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Explicit, Size = 0x20)]
public struct PlayerData
{
[FieldOffset(0x0)] public int Health;
[FieldOffset(0x8)] public float Speed;
[FieldOffset(0x10)] public long TargetId;
}
Именно этот стиль обычно удобнее всего в reverse engineering, потому что вы буквально переносите offsets из SDK, REClass, Cheat Engine или из дизасма в C#.
Pack и выравниваниеДаже если поля идут "по порядку", итоговый layout может сломаться из-за выравнивания.
Пример:
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct PackedHeader
{
public byte Type;
public int Size;
}
Если в оригинальном native-типе был #pragma pack(push, 1), а в C# вы забыли Pack = 1, offsets уедут.
Практический совет:
PackExplicitpack честноPack вам пока ни о чем не говорит, не страшно: в большинстве RE-сценариев проще использовать Explicit и выставить offsets вручнуюДля полей-указателей обычно лучше использовать:
nint / nuintIntPtr / UIntPtruint / ulong, если layout жёстко завязан на 32/64 bitЕсли вы описываете 64-bit игру, не храните указатели в int.
[StructLayout(LayoutKind.Sequential)]
public struct ActorNode64
{
public nuint Next;
public nuint Prev;
public int Id;
}
Если же вы сознательно разбираете 32-bit layout, можно фиксировать это и типом:
[StructLayout(LayoutKind.Sequential)]
public struct ActorNode32
{
public uint Next;
public uint Prev;
public int Id;
}
Здесь есть три основных маршрута.
fixedСамый прямой unmanaged-вариант:
[StructLayout(LayoutKind.Sequential)]
public unsafe struct FixedArrayStruct
{
public int Header;
public fixed float Values[4];
}
Это быстро и хорошо подходит для Read<T>(...), но требует unsafe.
InlineArrayСовременный и аккуратный вариант для inline-массивов:
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
[InlineArray(4)]
public struct Float4
{
private float _element0;
}
[StructLayout(LayoutKind.Sequential)]
public struct InlineArrayStruct
{
public int Header;
public Float4 Values;
}
Это тоже хорошо стыкуется с быстрым маршрутом чтения.
[MarshalAs(UnmanagedType.ByValArray)]Подходит, когда нужен более гибкий маршрут:
[StructLayout(LayoutKind.Sequential)]
public struct ManagedArrayStruct
{
public int Header;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
public byte[] Data;
}
Такую структуру уже лучше читать через:
var value = process.Memory.ReadManaged<ManagedArrayStruct>(addr);
Если строка хранится фиксированным полем внутри структуры, это почти всегда сценарий для ReadManaged<T>(...).
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct NameEntry
{
public int Id;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
public string Name;
}
Чтение:
var entry = process.Memory.ReadManaged<NameEntry>(addr);
Log.Info(entry.Name);
Важно:
ByValTStr зависит от CharSet у самой структурыUnicode это будет UTF-16-представлениеAnsi это будет однобайтная строкаRead<T>(...)В MemoryAccessor есть полезные helper'ы, чтобы не держать offsets руками в десяти местах:
SizeOf<T>()OffsetOf<T>(...)OffsetOf(type, memberName)NameOf<T>(...)FieldOf<T>(...)var size = process.Memory.SizeOf<PlayerData>();
var targetOffset = process.Memory.OffsetOf<PlayerData>(x => x.TargetId);
Это удобно, когда:
Но practical rule здесь простой:
[MarshalAs]-layout'ов чаще проще читать структуру целиком через ReadManaged<T>(...), чем строить ручной offset mathЕсли структура выглядит как обычный кусок памяти без строк и managed-полей:
intfloatlongfixedInlineArrayтогда лучше:
StructLayoutstructRead<T>(...)Если структура содержит:
stringbyte[] через ByValArrayByValTStrтогда лучше:
ReadManaged<T>(...)TryReadManaged<T>(...)TryWriteManaged<T>(...)class вместо structStructLayoutPackstring или managed-массив как обычную простую структуруЕсли видите "все значения читаются, но как будто сдвинуты", почти всегда проблема либо в Pack, либо в размере указателя, либо в том, что sequential-описание не совпало с оригиналом.
Если у вас есть только offsets из reverse engineering:
LayoutKind.ExplicitSizeSequentialЕсли у вас есть исходный native header:
StructLayoutPack, если он был