Преобразование байтового массива в управляемую структуру
Обновление: ответы на этот вопрос помогли мне написать код открытого проекта AlicanC Modern Warfare 2 Tool на GitHub. Вы можете увидеть, как я читаю эти пакеты в MW2Packets.cs и расширениях, которые я кодировал для чтения данных с прямым порядком байтов в Extensions.cs.
Я собираю UDP-пакеты Call of Duty: Modern Warfare 2, используя Pcap.Net в моем приложении C#. Я получаю byte[]
из библиотеки. Я пытался разобрать его как строку, но это не сработало.
byte[]
У меня есть общий заголовок пакета, затем другой заголовок, относящийся к типу пакета, затем информация о каждом игроке в лобби.
Полезный человек проверил некоторые пакеты для меня и придумал эти структуры:
// Fields are big endian unless specified otherwise.
struct packet_header
{
uint16_t magic;
uint16_t packet_size;
uint32_t unknown1;
uint32_t unknown2;
uint32_t unknown3;
uint32_t unknown4;
uint16_t unknown5;
uint16_t unknown6;
uint32_t unknown7;
uint32_t unknown8;
cstring_t packet_type; // \0 terminated string
};
// Fields are little endian unless specified otherwise.
struct header_partystate //Header for the "partystate" packet type
{
uint32_t unknown1;
uint8_t unknown2;
uint8_t player_entry_count;
uint32_t unknown4;
uint32_t unknown5;
uint32_t unknown6;
uint32_t unknown7;
uint8_t unknown8;
uint32_t unknown9;
uint16_t unknown10;
uint8_t unknown11;
uint8_t unknown12[9];
uint32_t unknown13;
uint32_t unknown14;
uint16_t unknown15;
uint16_t unknown16;
uint32_t unknown17[10];
uint32_t unknown18;
uint32_t unknown19;
uint8_t unknown20;
uint32_t unknown21;
uint32_t unknown22;
uint32_t unknown23;
};
// Fields are little endian unless specified otherwise.
struct player_entry
{
uint8_t player_id;
// The following fields may not actually exist in the data if it's an empty entry.
uint8_t unknown1[3];
cstring_t player_name;
uint32_t unknown2;
uint64_t steam_id;
uint32_t internal_ip;
uint32_t external_ip;
uint16_t unknown3;
uint16_t unknown4;
uint32_t unknown5;
uint32_t unknown6;
uint32_t unknown7;
uint32_t unknown8;
uint32_t unknown9;
uint32_t unknown10;
uint32_t unknown11;
uint32_t unknown12;
uint16_t unknown13;
uint8_t unknown14[???]; // Appears to be a bit mask, sometimes the length is zero, sometimes it's one. (First entry is always zero?)
uint8_t unknown15;
uint32_t unknown16;
uint16_t unknown17;
uint8_t unknown18[???]; // Most of the time this is 4 bytes, other times it is 3 bytes.
};
Я воссоздал структуру заголовка пакета в моем приложении C# следующим образом:
[StructLayout(LayoutKind.Sequential, Pack=1)]
struct PacketHeader
{
public UInt16 magic;
public UInt16 packetSize;
public UInt32 unknown1;
public UInt32 unknown2;
public UInt32 unknown3;
public UInt32 unknown4;
public UInt16 unknown5;
public UInt16 unknown6;
public UInt32 unknown7;
public UInt32 unknown8;
public String packetType;
}
Затем я попытался создать структуру для заголовка "partystate", но я получил сообщение об ошибке fixed
ключевое слово небезопасно:
[StructLayout(LayoutKind.Sequential, Pack=1)]
struct PartyStateHeader
{
UInt32 unknown1;
Byte unknown2;
Byte playerEntryCount;
UInt32 unknown4;
UInt32 unknown5;
UInt32 unknown6;
UInt32 unknown7;
Byte unknown8;
UInt32 unknown9;
UInt16 unknown10;
Byte unknown11;
fixed Byte unknown12[9];
UInt32 unknown13;
UInt32 unknown14;
UInt16 unknown15;
UInt16 unknown16;
fixed UInt32 unknown17[10];
UInt32 unknown18;
UInt32 unknown19;
Byte unknown20;
UInt32 unknown21;
UInt32 unknown22;
UInt32 unknown23;
}
Я не мог ничего сделать для записей игрока из-за изменяющегося размера unknown14
а также unknown18
, (Записи игрока являются наиболее важными.)
Теперь, так или иначе, я должен бросить byte[]
Я должен к этим PacketHeader
структур. К сожалению, это не так просто, как (PacketHeader)bytes
, Я попробовал этот метод, который я нашел в Интернете, но он бросил AccessViolationException
:
GCHandle handle = GCHandle.Alloc(bytes, GCHandleType.Pinned);
PacketHeader packetHeader = (PacketHeader)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(PacketHeader));
Как мне этого добиться?
7 ответов
Я бы превратил массив байтов в поток памяти. Затем создайте экземпляр двоичного читателя в этом потоке. А затем определите вспомогательные функции, которые берут двоичный считыватель и анализируют один класс.
Встроенный BinaryReader
В классе всегда используется порядок байтов.
Я бы использовал классы вместо структур здесь.
class PacketHeader
{
uint16_t magic;
uint16_t packet_size;
uint32_t unknown1;
uint32_t unknown2;
uint32_t unknown3;
uint32_t unknown4;
uint16_t unknown5;
uint16_t unknown6;
uint32_t unknown7;
uint32_t unknown8;
string packet_type; // replaced with a real string
};
PacketHeader ReadPacketHeader(BinaryReader reader)
{
var result=new PacketHeader();
result.magic = reader.ReadInt16();
...
result.packet_type=ReadCString();//Some helper function you might need to define yourself
return result;
}
// Я нашел это по адресу: http://code.cheesydesign.com/?p=572 (я еще не тестировал, но // на первый взгляд это будет хорошо работать.)
/// <summary>
/// Reads in a block from a file and converts it to the struct
/// type specified by the template parameter
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="reader"></param>
/// <returns></returns>
private static T FromBinaryReader<T>(BinaryReader reader)
{
// Read in a byte array
byte[] bytes = reader.ReadBytes(Marshal.SizeOf(typeof(T)));
// Pin the managed memory while, copy it out the data, then unpin it
GCHandle handle = GCHandle.Alloc(bytes, GCHandleType.Pinned);
T theStructure = (T)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(T));
handle.Free();
return theStructure;
}
Вот как я это сделал:
using System;
using System.Runtime.InteropServices;
public static object GetObjectFromBytes(byte[] buffer, Type objType)
{
object obj = null;
if ((buffer != null) && (buffer.Length > 0))
{
IntPtr ptrObj = IntPtr.Zero;
try
{
int objSize = Marshal.SizeOf(objType);
if (objSize > 0)
{
if (buffer.Length < objSize)
throw new Exception(String.Format("Buffer smaller than needed for creation of object of type {0}", objType));
ptrObj = Marshal.AllocHGlobal(objSize);
if (ptrObj != IntPtr.Zero)
{
Marshal.Copy(buffer, 0, ptrObj, objSize);
obj = Marshal.PtrToStructure(ptrObj, objType);
}
else
throw new Exception(String.Format("Couldn't allocate memory to create object of type {0}", objType));
}
}
finally
{
if (ptrObj != IntPtr.Zero)
Marshal.FreeHGlobal(ptrObj);
}
}
return obj;
}
И в определении структуры я не использовал fixed
регион, вместо этого я использовал MarshalAs
атрибут, если стандартная сортировка не работает. Это то, что вам, вероятно, понадобится для строки.
Вы бы использовали эту функцию следующим образом:
PacketHeader ph = (PacketHeader)GetObjectFromBytes(buffer, typeof(PacketHeader));
Редактировать: я не видел вашего BigEndian "ограничение" в примере кода. Это решение будет работать только если байты LittleEndian.
Редактировать 2: в строке вашего примера вы бы украсили его:
[MarshalAs(UnmanagedType.LPStr)]
В массивах я бы использовал что-то вроде этого для массива размера n:
[MarshalAs(UnmanagedType.ByValArray, SizeConst = n)]
Для тех, кто имеет доступ к функциям C# 7.3, я использую этот небезопасный код для "сериализации" в байты:
public static class Serializer
{
public static unsafe byte[] Serialize<T>(T value) where T : unmanaged
{
byte[] buffer = new byte[sizeof(T)];
fixed (byte* bufferPtr = buffer)
{
Buffer.MemoryCopy(&value, bufferPtr, sizeof(T), sizeof(T));
}
return buffer;
}
public static unsafe T Deserialize<T>(byte[] buffer) where T : unmanaged
{
T result = new T();
fixed (byte* bufferPtr = buffer)
{
Buffer.MemoryCopy(bufferPtr, &result, sizeof(T), sizeof(T));
}
return result;
}
}
unmanaged
Тип может быть структурой (простая структура без ссылочных типов, рассматриваемых как управляемые структуры) или нативным типом, таким как int
, short
, так далее.
Мой подход другой. Я не хотел копировать ни одного байта.
Я просто хотел использовать их, модифицировать некоторые из них и использовать измененный массив byte[] в другом месте как byte[].
После копания гугла и stackru я решил зайти в unsafe/fixed.
Там, играя с кодом, я нашел быстрый код без копирования.
Это код отладки / теста. Проверьте это в режиме отладки.
Помните, что таким образом вы не делаете копию и работаете с необработанными данными byte[].
Любое изменение в структуре будет отражаться в изменении массива byte[] и наоборот.
++ ПРОВЕРЕНО ++ РАБОТАЕТ
//FOR DEBUG/TEST ONLY
using System.Runtime.InteropServices;
namespace ByteStructCast1
{
class Program
{
[StructLayout(LayoutKind.Sequential, Pack = 1)]
unsafe struct StructTest//4B
{
[MarshalAs(UnmanagedType.U2)]
public ushort item1;//2B
public fixed byte item2[2];//2B =2x 1B
}
static void Main(string[] args)
{
//managed byte array
byte[] DB1 = new byte[7];//7B more than we need. byte buffer usually is greater.
DB1[0] = 2;//test data |> LITTLE ENDIAN
DB1[1] = 0;//test data |
DB1[2] = 3;//test data
DB1[3] = 4;//test data
unsafe //OK we are going to pin unmanaged struct to managed byte array
{
fixed(byte* db1 = DB1) //db1 is pinned pointer to DB1 byte[] array
{
//StructTest t1 = *(StructTest*)db1; //does not change DB1/db1
//t1.item1 = 11; //does not change DB1/db1
db1[0] = 22; //does CHANGE DB1/db1
DB1[0] = 33; //does CHANGE DB1/db1
StructTest* ptest = (StructTest*)db1; //does CHANGE DB1/db1
ptest->item1 = 44; //does CHANGE DB1/db1
ptest->item2[0]++; //does CHANGE DB1/db1
ptest->item2[1]--; //does CHANGE DB1/db1
}
}
}
}
}
Ну, у тебя действительно есть две задачи. Во-первых, это по существу интерпретировать byte[] как struct, а во-вторых, иметь дело с возможными различными порядками байтов.
Итак, они несколько расходятся. AFAIK, если вы хотите использовать маршалинг - он будет просто интерпретировать байты, как если бы это была управляемая структура. Таким образом, преобразование из одного порядка в другой оставлено вам. Это не сложно сделать, но это не будет автоматически.
Итак, чтобы интерпретировать byte[] как struct, у вас должно быть что-то вроде этого:
[StructLayout(LayoutKind.Sequential)]
internal struct X
{
public int IntValue;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3, ArraySubType = UnmanagedType.U1)]
public byte[] Array;
}
static void Main(string[] args)
{
byte[] data = {1, 0, 0, 0, 9, 8, 7}; // IntValue = 1, Array = {9,8,7}
IntPtr ptPoit = Marshal.AllocHGlobal(data.Length);
Marshal.Copy(data, 0, ptPoit, data.Length);
var x = (X) Marshal.PtrToStructure(ptPoit, typeof (X));
Marshal.FreeHGlobal(ptPoit);
Console.WriteLine("x.IntValue = {0}", x.IntValue);
Console.WriteLine("x.Array = ({0}, {1}, {2})", x.Array[0], x.Array[1], x.Array[2]);
}
Итак, первые 4 байта идут в IntValue (1,0,0,0) -> [little endian] -> 1 Следующие 3 байта идут непосредственно в массив.
Если вы хотите BigEndian, вы должны сделать это самостоятельно:
int LittleToBigEndian(int littleEndian)
{
byte[] buf = BitConverter.GetBytes(littleEndian).Reverse().ToArray();
return BitConverter.ToInt32(buf, 0);
}
Это немного беспорядочно, так что, вероятно, вам будет лучше придерживаться своего пользовательского парсера, который берет байты один за другим из исходного байта [] и заполнять ваш класс данных без StructLayout и другого встроенного взаимодействия.
Чтобы преобразовать байтовый массив в строку, вы делаете это;
byte [] dBytes = ...
string str;
System.Text.UTF8Encoding enc = new System.Text.UTF8Encoding();
str = enc.GetString(dBytes);
И преобразовать строку обратно в байтовый массив
public static byte[] StrToByteArray(string str)
{
System.Text.UTF8Encoding encoding=new System.Text.UTF8Encoding();
return encoding.GetBytes(str);
}
Теперь прочитайте вашу строку и посмотрите, что ваши данные.