【问题标题】:Marshalling IntPtrs within structs in C#在 C# 的结构中编组 IntPtrs
【发布时间】:2012-05-16 10:15:35
【问题描述】:

我打算建模一个 C# 类来解析我作为原始缓冲区接收到的自定义协议数据。缓冲区已经作为字节数组存在。

这个解析器必须处理不同的字段布局,因此一些协议元素是 IntPtrs。

当我在将原始缓冲区映射到结构后尝试访问结构内的指针时,遇到了访问冲突。

我做了一个简单的例子来展示关键部分:

namespace CS_StructMarshalling
{
  class Packet
  {  
    public PacketStruct Fields;

    [StructLayout(LayoutKind.Explicit)]
    public struct PacketStruct
    {
      [FieldOffset(0)] public UInt16 Flags;
      [FieldOffset(2)] public IntPtr Data;
    }

    public Packet()
    {
      Fields = new PacketStruct();
    }

    public void DecompileBinaryBuffer(ref Byte[] byteBuffer)
    {
      int size = Marshal.SizeOf(typeof(PacketStruct)); 
      IntPtr ptr = Marshal.AllocHGlobal(size);

      Marshal.Copy(byteBuffer, 0, ptr, size);

      this.Fields = (PacketStruct)Marshal.PtrToStructure(ptr, typeof(PacketStruct));
      Marshal.FreeHGlobal(ptr);
    }
  }

  class Program
  {
    static void Main(string[] args)
    {
      byte[] rawBuffer = new byte[] { 1, 2, 65, 66, 67, 68, 69, 70 };

      Packet testPkt = new Packet();
      testPkt.DecompileBinaryBuffer(ref rawBuffer);

      UInt16 testFlags = testPkt.Fields.Flags;
      String testData = Marshal.PtrToStringAnsi(testPkt.Fields.Data, 6);
    }
  }
}

我只是试图解决这个问题,但无济于事。据我了解,IntPtr 必须单独编组,但我没有找到任何提示如何以干净的方式完成此操作。

欢迎任何指点。感谢您的宝贵时间。

【问题讨论】:

  • 发送IntPtr 是注定的; 永远无法工作。然而;看起来你只是想序列化一些数据;您是否考虑过诸如 protobuf-net 之类的预滚动二进制序列化 API?

标签: c# struct marshalling intptr


【解决方案1】:

该方法存在一个根本性缺陷,基于可能对 指针 真正含义的误解。托管代码中的指针 IntPtr 只是内存中某个位置的地址。您可以阅读您已经使用的那种代码所指向的内容,例如 Marshal.PtrToStructure()。

指针的一个问题是它们只在一个进程中有效。每个进程都有自己的虚拟内存地址空间。通过托管代码中的附加限制,垃圾收集器可以将对象从一个位置随机移动到另一个位置。有一些解决方法,您可以调用 ReadProcessMemory() 从另一个进程读取数据。您可以通过固定一个对象 GCHandle.Alloc()

来解决垃圾收集器的行为

但这些是不属于自定义序列化方案的解决方法。 ReadProcessMemory() 的一个明显失败模式就是不知道哪个进程拥有数据。或者没有足够的权利使用它。或者在另一台机器上运行的进程。固定指针是有缺陷的,因为不能很好地保证您会取消固定它们。

因此,任何序列化方法都通过扁平化数据来解决这个问题,通过将指针替换为指向的数据来消除指针。并在反序列化器中复活对象图。由 BinaryFormatter、XmlSerializer、DataContractSerializer 和 DataContractJsonSerializer 等类完成​​的工作。还有像马克最喜欢的第 3 方。不要自己写,太多了,因为很难写好。

【讨论】:

  • 感谢您使这一点更清晰。我知道我的代码只是试图对 C 方法进行建模,而 C# 的情况却大不相同。
【解决方案2】:

你已经解压好了;问题是数据;您有以下硬编码数据:

  • 标志 = 513
  • 数据 = 1145258561(如果使用 x86,则为接下来的 4 个字节)

问题很简单:指针值“1145258561”无效,除非可能在该数字来自的早已不复存在的 AppDomain 中。

你不能强行进入不属于你的数据。

此外,在 x64 上它会更快出错,因为缓冲区是 8 个字节,而结构需要 10 个。

另外:您对ref 的使用不正确,您可以通过使用stackalloc 来避免HGlobal

【讨论】:

    【解决方案3】:

    您正在解码字符数据,就好像它是指向字符数据的指针一样,因此它将指向内存中不包含字符数据的任意位置,并且很可能位于地址空间的一部分之外你的程序被允许使用。您根本无法将数据解码为指针,因为它不是指针。

    将数据转换为它们的类型。由于字符是 8 位代码,您需要使用 8 位字符集对其进行解码:

    ushort testFlags = BitConverter.ToUInt16(rawBuffer, 0);
    string testData = Encoding.ASCII.GetString(rawBuffer, 2, rawBuffer.Length - 2);
    

    结果:

    513
    ABCDEF
    

    【讨论】:

    • 抱歉不清楚rawBuffer的作用;它只是进来的测试数据。我想要做的是在 DecompileBinaryBuffer() 之后通过结构语义访问缓冲区内容,即从 testPkt.Fields.Data 读取。
    • @MarSch:这完全没有意义。您基本上要做的是将字节数组编组到内存块并将指针存储在结构中,然后将内存块编组到字节数组以对其进行解码。只需解码您拥有的字节,通过将指针存储在结构中不会获得任何收益。
    • 这并不是毫无意义的,尽管它可能看起来像上面的示例代码。如前所述,缓冲区来自设备,可作为字节数组使用。 Packet 类的整个想法是对数据字段进行人类可读的访问。
    • @MarSch:如果您有任何具有动态长度的字段,则无法将数据编组为结构。如果您想要类中的数据,只需编写代码以读取字节并将它们放入属性中。您可以编写使用反射自动执行此操作的代码,但这通常比仅编写代码本身要多。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-06-27
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多