使用P / Invoke Interop助手时数据结构和堆栈损坏

时间:2017-01-06 19:01:36

标签: c# c pinvoke calling-convention packing

我在C库中有一个结构:

#pragma pack(push, packing)
#pragma pack(1)

typedef struct
{
    unsigned int  ipAddress;
    unsigned char aMacAddress[6];
    unsigned int  nodeId;
} tStructToMarshall;

__declspec(dllexport) int SetCommunicationParameters(tStructToMarshall parameters);

此代码使用cl /LD /Zi Communication.c编译,以生成用于调试的DLL和PDB文件。

要从.Net应用程序使用此代码,我使用P/Invoke Interop Assistant为包装器DLL生成C#代码:

Screenshot of P/Invoke Interop Assistant GUI translating snippet from C to C#

这导致显示的C#包装器,我修改它使用正确的DLL而不是"<unkown>"。另外,我确实想要aMacAddress的字节数组,而不是字符串(虽然我理解这通常会有用):

[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential, CharSet = System.Runtime.InteropServices.CharSet.Ansi)]
public struct tStructToMarshall
{
    /// unsigned int
    public uint ipAddress;
    /// unsigned char[6]
    [System.Runtime.InteropServices.MarshalAsAttribute(System.Runtime.InteropServices.UnmanagedType.ByValTStr, SizeConst = 6)]
    public byte[] aMacAddress;
        // ^^^^^^ Was "string"
    /// unsigned int
    public uint nodeId;
}

public partial class NativeMethods
{
    internal const string DllName = "lib/Communication.dll";

    /// Return Type: int
    ///parameters: tStructToMarshall->Anonymous_75c92899_b50d_4bea_a217_a69989a8d651
    [System.Runtime.InteropServices.DllImportAttribute(DllName, EntryPoint = "SetCommunicationParameters")]
                                                    // ^^^^^^^ Was "<unknown>"
    public static extern int SetCommunicationParameters(tStructToMarshall parameters);
}

我有两个问题: 1.当我将结构的值设置为非零值并查找节点ID时,它会被破坏或损坏。 IP地址和MAC地址都很好,但是数组之后的任何结构成员(包括其他数据类型)都被破坏,即使我指定了一位数值,也会在C输出中显示非常大的数字。 2.当我调用该方法时,我收到一条错误消息:

  

对PInvoke函数的调用''使堆栈失衡。这很可能是因为托管PInvoke签名与非托管目标签名不匹配。检查PInvoke签名的调用约定和参数是否与目标非托管签名匹配。

尝试调用不带参数的方法不会生成此异常。而且我很确定它与目标签名相匹配,因为这就是我生成它的方式!

如何解决这些问题?

1 个答案:

答案 0 :(得分:2)

1。结构腐败

这种腐败&#39;是由对齐问题引起的。互操作助手忽略#pragma pack(1)指令,并使用默认值described here

  

使用以下规则对齐类型实例的字段:

     
      
  • 类型的对齐方式是其最大元素(1,2,4,8等,字节)的大小或指定的包装大小,以较小者为准。

  •   
  • 每个字段必须与其自身大小的字段(1,2,4,8等,字节)或类型的对齐方式对齐,以较小者为准。由于类型的默认对齐方式是其最大元素的大小(大于或等于所有其他字段长度),这通常意味着字段按其大小对齐。例如,即使一个类型中的最大字段是64位(8字节)整数或者Pack字段设置为8,字节字段在1字节边界上对齐,Int16字段在2字节边界上对齐,并且Int32字段在4字节边界上对齐。

  •   
  • 在字段之间添加填充以满足对齐要求。
  •   

您在C中指定字段应在1字节边界上对齐。但是,你的C#代码假设那里没有填充,特别是在你的6字节结构之后:

使用IP地址0x01ABCDEF,MAC地址{0x01,0x02,0x03,0x04,0x05,0x06}和节点ID 0x00000001,内存看起来像这样(忽略了字符串问题,如果你得到它就不会有问题对齐权):

Byte   Value   C expects           .NET Expects:
0      0x01    \                   \
1      0xAB     } IP Address        } IP Address
2      0xCD     |                   |
3      0xEF    /                   /
4      0x01    } aMacAddress[0]    } aMacAddress[0]
5      0x02    } aMacAddress[1]    } aMacAddress[1]
6      0x03    } aMacAddress[2]    } aMacAddress[2]
7      0x04    } aMacAddress[3]    } aMacAddress[3]
8      0x05    } aMacAddress[4]    } aMacAddress[4]
9      0x06    } aMacAddress[5]    } aMacAddress[5]
10     0x00    \                   } Padding
11     0x00     } Node ID          } Padding
12     0x00     |                  \
13     0x01    /                    } Node ID
14     0x??    } Unititialized      |
15     0x??    } Unititialized     /

请注意,.NET期望节点ID(4字节值)从地址12开始,可以被4整除。它实际上使用了未初始化的内存,这会导致错误的结果。

修复:

将命名参数Pack=1添加到您对StructLayoutAttribute的调用中:

[System.Runtime.InteropServices.StructLayoutAttribute(
    System.Runtime.InteropServices.LayoutKind.Sequential, Pack=1, CharSet = System.Runtime.InteropServices.CharSet.Ansi)]
                                                      //  ^^^^^^ - Here

2。堆栈不平衡

这是由不同的calling conventions引起的。当您使用参数调用方法时,这些参数将进入堆栈。在某些调用约定下,调用者在方法返回后清理堆栈。在其他情况下,被调用函数会在返回之前清除。

使用cl编译未注释的函数时,它使用cdecl约定,该约定声明:

  

来电者清理堆栈。这样就可以使用varargs调用函数,这使得它适用于接受可变数量参数的方法,例如printf

因此是C编译器的一个很好的默认值。将函数导入.NET时,它使用stdcall约定,该约定声明:

  

被调用者清理堆栈。这是使用平台调用调用非托管函数的默认约定。

这在Windows API中使用(可能是P / Invoke最常用的库),因此是P / Invoke的一个很好的默认值,但两者不兼容。

several other questions中对此进行了一点描述(可能是因为它有一个与您的结构损坏不同的Googleable错误消息)并且已被回答here

修复:

CallingConvention = CallingConvention.Cdecl添加到您的DllImportAttribute:

[System.Runtime.InteropServices.DllImportAttribute(DllName, EntryPoint = "SetCommunicationParameters", CallingConvention = CallingConvention.Cdecl)]