什么可能导致P/Invoke参数在传递时出现故障?

bor*_*den 80 c# windows pinvoke arm uwp

这是一个特别在ARM上发生的问题,而不是在x86或x64上.我有这个用户报告的问题,并且能够通过Windows IoT在Raspberry Pi 2上使用UWP重现它.我在使用不匹配的调用约定之前已经看到过这种问题,但是我在P/Invoke声明中指定了Cdecl,我尝试在原生端显式添加__cdecl并获得相同的结果.这是一些信息:

P/Invoke声明(参考):

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

C#结构(参考):

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
{
}
Run Code Online (Sandbox Code Playgroud)

C头中的函数(参考)

FLSliceResult FLEncoder_Finish(FLEncoder, FLError*);
Run Code Online (Sandbox Code Playgroud)

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

本机端的结构具有实际信息,但对于C API,FLEncoder被定义为不透明指针.在x86和x64上调用上面的方法时,事情顺利进行,但在ARM上,我观察到以下内容.第一个参数的地址是SECOND参数的地址,第二个参数是null(例如,当我在C#端记录地址时,我得到,例如,0x054f59b8和0x0583f3bc,但是然后在本机端参数是0x0583f3bc和0x00000000).什么可能导致这种乱序问题?有没有人有任何想法,因为我很难过......

这是我重现的代码:

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);
}
Run Code Online (Sandbox Code Playgroud)

使用以下代码运行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);
Run Code Online (Sandbox Code Playgroud)

这个逻辑可以通过最新的开发人员构建触发崩溃,但遗憾的是我还没有想出如何通过Nuget可靠地提供本机调试符号,以便它可以逐步完成(仅从源代码构建所有内容似乎就是这样做... .)所以调试有点尴尬,因为需要构建本机和托管组件.如果有人想尝试,我愿意接受如何使这更容易的建议.但如果有人以前经历过这个或者对于为什么会发生这种情况有任何想法,请添加答案,谢谢!当然,如果有人想要一个复制案例(要么很容易构建一个不提供源步骤的案例,要么难以构建一个那样做)然后留下评论但是我不想经历一个制作过程如果没有人会使用它(我不确定在实际的ARM上运行Windows的流行程度是多少)

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

编辑2第二个有趣的更新:如果我将C#FLSliceResult的大小定义从那时UIntPtr改为,ulong则参数正确进入...这没有意义,因为size_t在ARM上应该是unsigned int.

编辑3添加[StructLayout(LayoutKind.Sequential, Size = 12)]到C#中的定义也可以使这个工作,但是为什么?用于此体系结构的C/C++中的sizeof(FLSliceResult)返回8.在C#中设置相同的大小会导致崩溃,但将其设置为12会使其工作.

编辑4我最小化了测试用例,以便我也可以编写一个C++测试用例.在C#UWP中,它失败了,但在C++ UWP中它成功了.

编辑5 这里有用于比较的C++和C#的反汇编指令(虽然C#我不知道要花多少钱所以我错了太多了)

编辑6进一步分析显示,在"好"运行期间,当我撒谎并说结构在C#上为12字节时,返回值传递给寄存器r0,其他两个args通过r1,r2进入.然而,在糟糕的运行中,这被转移,以便两个args通过r0,r1进入并且返回值是在其他地方(堆栈指针?)

编辑7我参考了ARM体系结构过程调用标准.我发现这句话:"一个大于4个字节的复合类型,或者调用者和被调用者无法静态确定其大小,在调用该函数时作为额外参数传递的地址存储在内存中(§5.5,规则A) .4).在函数调用期间的任何时候都可以修改用于结果的内存." 这意味着传入r0是正确的行为,因为额外的参数意味着第一个(因为C调用约定没有办法指定参数的数量).我想知道CLR是否将此与另一条关于基本 64位数据类型的规则混淆:"在r0和r1中返回双字大小的基本数据类型(例如,长长,双和64位容器化向量)."

编辑8好的,有很多证据表明CLR在这里做错了,所以我提交了一份错误报告.我希望有人注意到在该回购邮件上发布问题的所有自动机器人之间:-S.

bor*_*den 1

我在 GH 上提交的问题已经存在相当长一段时间了。我相信这种行为只是一个错误,不需要再花时间去研究它。