使用 P/Invoke 互操作助手时的数据结构和堆栈损坏

Data structure and stack corruption when using the P/Invoke Interop Assistant

我在 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# 代码:

这导致显示的 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. 当我调用该方法时,出现错误消息:

A call to PInvoke function '' has unbalanced the stack. This is likely because the managed PInvoke signature does not match the unmanaged target signature. Check that the calling convention and parameters of the PInvoke signature match the target unmanaged signature.

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

我该如何解决这些问题?

1。结构损坏

这 'corruption' 是由对齐问题引起的。互操作助手忽略 #pragma pack(1) 指令,并使用默认值 described here.

The fields of a type instance are aligned by using the following rules:

  • The alignment of the type is the size of its largest element (1, 2, 4, 8, etc., bytes) or the specified packing size, whichever is smaller.

  • Each field must align with fields of its own size (1, 2, 4, 8, etc., bytes) or the alignment of the type, whichever is smaller. Because the default alignment of the type is the size of its largest element, which is greater than or equal to all other field lengths, this usually means that fields are aligned by their size. For example, even if the largest field in a type is a 64-bit (8-byte) integer or the Pack field is set to 8, Byte fields align on 1-byte boundaries, Int16 fields align on 2-byte boundaries, and Int32 fields align on 4-byte boundaries.

  • Padding is added between fields to satisfy the alignment requirements.

您已在 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 开始,地址 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不同造成的。当您调用带有参数的方法时,这些参数进入堆栈。在某些调用约定下,调用者在方法 returns 之后清理堆栈。在其他情况下,调用的函数在返回之前进行清理。

当您使用 cl 编译未注释的函数时,它使用 cdecl 约定,其中规定:

The caller cleans the stack. This enables calling functions with varargs, which makes it appropriate to use for methods that accept a variable number of parameters, such as printf.

因此对于 C 编译器来说是一个很好的默认值。将函数导入 .NET 时,它使用 stdcall 约定,其中规定:

The callee cleans the stack. This is the default convention for calling unmanaged functions with platform invoke.

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

这在 several other questions (probably because it has a Googleable error message, unlike your struct corruption) and is answered here 中有一些描述。

修复:

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

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