什么会导致 P/Invoke 参数在传递时失序?

这是一个特别发生在 ARM 上的问题,而不是在 x86或 x64上。我有一个用户报告的这个问题,并能够通过 Windows IoT 在 Raspberry Pi 2上使用 UWP 复制它。我以前见过这种不匹配调用约定的问题,但是我在 P/Invoke 声明中指定了 Cdecl,并且尝试在本机端显式地添加 _ _ Cdecl,得到了相同的结果。下面是一些信息:

调用声明(参考文献) :

[DllImport(Constants.DllName, CallingConvention = CallingConvention.Cdecl)]
public static extern FLSliceResult FLEncoder_Finish(FLEncoder* encoder, FLError* outError);

C # structs (参考文献) :

internal unsafe partial struct FLSliceResult
{
public void* buf;
private UIntPtr _size;


public ulong size
{
get {
return _size.ToUInt64();
}
set {
_size = (UIntPtr)value;
}
}
}


internal enum FLError
{
NoError = 0,
MemoryError,
OutOfRange,
InvalidData,
EncodeError,
JSONError,
UnknownValue,
InternalError,
NotFound,
SharedKeysStateError,
}


internal unsafe struct FLEncoder
{
}

C 头中的函数(参考文献)

FLSliceResult FLEncoder_Finish(FLEncoder, FLError*);

FLSliceResult 可能会导致一些问题,因为它是通过值返回的,并且在本机端有一些 C + + 内容?

本机端的结构有实际的信息,但是对于 C API,FLEncoder 定义为 作为一个不透明的指针。在 x86和 x64上调用上面的方法时,工作很顺利,但是在 ARM 上,我观察到以下情况。第一个参数的地址是 SECOND 参数的地址,第二个参数是 null (例如,当我记录 C # 端的地址时,我得到了,例如,0x054f59b8和0x0583f3bc,但是在本机端,参数是0x0583f3bc 和0x0000000)。什么原因会导致这样的故障问题?有人知道吗,我被难住了。

下面是我运行以重现的代码:

unsafe {
var enc = Native.FLEncoder_New();
Native.FLEncoder_BeginDict(enc, 1);
Native.FLEncoder_WriteKey(enc, "answer");
Native.FLEncoder_WriteInt(enc, 42);
Native.FLEncoder_EndDict(enc);
FLError err;
NativeRaw.FLEncoder_Finish(enc, &err);
Native.FLEncoder_Free(enc);
}

用下面的代码运行一个 C + + 应用程序是可行的:

auto enc = FLEncoder_New();
FLEncoder_BeginDict(enc, 1);
FLEncoder_WriteKey(enc, FLSTR("answer"));
FLEncoder_WriteInt(enc, 42);
FLEncoder_EndDict(enc);
FLError err;
auto result = FLEncoder_Finish(enc, &err);
FLEncoder_Free(enc);

这种逻辑可能会触发最新的 abc0崩溃,但不幸的是,我还没有想出如何能够可靠地通过 Nuget 提供本地调试符号,使其可以逐步通过(只有从源代码构建所有东西似乎都是这样做的...) ,因此调试有点尴尬,因为本地和托管组件都需要构建。如果有人想尝试,我愿意接受关于如何使这更容易的建议。但是,如果任何人以前经历过这种情况,或有任何想法为什么会发生这种情况,请添加一个答案,谢谢!当然,如果有人想要一个复制的案例(要么是一个容易构建但不提供源代码步骤的案例,要么是一个难以构建的案例) ,那么留下评论,但是如果没有人会使用它,我不想经历一个复制的过程(我不知道在实际的 ARM 上运行 Windows 的东西有多流行)

编辑 有趣的更新: 如果我在 C # 中“伪造”签名并删除第二个参数,那么第一个参数就会通过 OK。

编辑2 第二个有趣的更新: 如果我将 C # FLSliceResult 的 size 定义从 UIntPtr改为 ulong,那么参数就正确地进入了... ... 这是没有意义的,因为 ARM 上的 size_t应该是无符号 int。

[StructLayout(LayoutKind.Sequential, Size = 12)]加入到 C # 的定义中也可以做到这一点,但是为什么呢?对于这个体系结构,C/C + + 中的 sizeof (FLSliceResult)应该返回8。在 C # 中设置相同的大小会导致崩溃,但是如果设置为12就可以了。

EDIT 4 我最小化了测试用例,这样我也可以编写一个 C + + 测试用例。在 C # UWP 中它失败了,但是在 C + + UWP 中它成功了。

编辑5 给你是 C + + 和 C # 的反汇编指令,用于比较(尽管 C # 我不确定要取多少,所以我错误地取了太多)

EDIT 6 进一步的分析表明,在“良好”运行期间,当我撒谎说 C # 上的 struct 是12字节时,返回值被传递给寄存器 r0,另外两个参数通过 r1,r2进入。但是,在运行不好的情况下,这会被移位,这样两个参数通过 r0和 r1进入,并且返回值在其他地方(堆栈指针?)

我查阅了 ARM 体系结构的过程调用标准。我发现这句话: “复合类型大于4字节,或者其大小不能由调用者和 被调用方,存储在内存中的一个地址中,当函数被调用时作为一个额外的参数传递(5.5, 规则 A.4)。用于结果的内存可以在函数调用期间的任何时候进行修改。”这意味着传入 r0是正确的行为,因为额外的参数意味着第一个参数(因为 C 调用约定没有指定参数数量的方法)。我想知道 CLR 是否把这个和另一个关于 基本原则64位数据类型的规则混淆了: “双字大小的基本数据类型(例如,long long、 double 和64位容器化向量)是 以 r0和 r1返回”

好的,有很多证据表明 CLR 在这里做了错误的事情,所以我提交了一份 漏洞报告。我希望有人注意到它之间的所有自动机器人张贴问题的回购:-S。

2186 次浏览

我提交的关于 GH 的问题已经存在了一段时间了。我相信这种行为只是一个 bug,不需要花更多的时间去研究它。