当进行P/Invoke调用时,参数传递顺序可能会错乱的原因是什么?

79
这是一个特定发生在ARM上的问题,而不是x86或x64。我收到了一位用户报告了此问题,并且我使用Windows IoT上的Raspberry Pi 2通过UWP重现了此问题。我以前见过类似的问题,可能是由于调用约定不匹配引起的,但是我在P/Invoke声明中指定了Cdecl,并尝试在本地端显式添加__cdecl,但结果相同。以下是一些信息:
P/Invoke声明(reference):
[DllImport(Constants.DllName, CallingConvention = CallingConvention.Cdecl)]
public static extern FLSliceResult FLEncoder_Finish(FLEncoder* encoder, FLError* outError);

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
{
}

在C头文件中的函数(reference)

FLSliceResult FLEncoder_Finish(FLEncoder, FLError*);

FLSliceResult可能会引起一些问题,因为它是按值返回的,并在本地端具有一些C++内容?

本地端的结构体具有实际信息,但对于C API,FLEncoder被定义为 一个不透明的指针。 在调用上述方法时,在x86和x64上工作顺畅,但在ARM上,我观察到以下情况。 第一个参数的地址是第二个参数的地址,而第二个参数为空(例如,当我在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);
}

使用以下内容运行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);

这个逻辑会导致最新的开发构建崩溃,但不幸的是我还没有找到可靠的方法通过Nuget提供本地调试符号,以便能够进行步骤调试(只有从源代码构建似乎才能做到这一点...),因此调试有些麻烦,因为需要构建本机和托管组件。如果有人想尝试让这变得更容易,我很乐意听取建议。但如果有人之前遇到过这种情况或对为什么会发生这种情况有任何想法,请添加答案,谢谢!当然,如果有人需要复制案例(无论是易于构建且不提供源代码步骤还是难以构建且提供源代码步骤),请留言,但如果没有人使用它,我不想经历制作一个案例的过程(我不确定在实际ARM上运行Windows程序有多受欢迎)。 编辑 有趣的更新:如果我在C#中“伪造”签名并删除第二个参数,那么第一个参数就可以正常传递。 编辑2 第二个有趣的更新:如果我将C# FLSliceResult定义中的size从 UIntPtr 更改为 ulong ,那么参数就会正确传入...这是没有道理的,因为在ARM上 size_t 应该是unsigned int。

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

编辑4 我简化了测试用例,以便我也可以编写C++测试用例。在C# UWP中失败,在C++ UWP中成功。

编辑5 这里提供了C++和C#的反汇编指令,以便比较(尽管我不确定C#应该采取多少,所以我偏向于采取太多)

编辑6 进一步分析表明,在“好”的运行时,当我撒谎说结构体在C#上是12字节时,返回值通过寄存器r0传递,另外两个参数通过r1、r2传递。然而,在“坏”的运行中,这被移位,使得两个参数通过r0、r1传递,而返回值则在其他地方(堆栈指针?)。

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

编辑 8 好的,有很多证据表明CLR在这里做了错误的事情,所以我提交了一个bug报告。希望在那个仓库中所有自动机器人发布问题之间有人注意到它 :-S。


1
评论不适合延长讨论;此对话已被移至聊天室。 (http://chat.stackoverflow.com/rooms/157727/discussion-on-question-by-borrrden-what-could-cause-p-invoke-arguments-to-be-out) - Andy
已经有60个赞,却没有任何赏金被提供……这很奇怪。 - Mauricio Gracia Gutierrez
6
@MauricioGraciaGutierrez 我想我可以回答这个问题,说“这是JIT引擎中的一个错误”(我假设大多数人来这里点赞是因为他们对错误的解决感兴趣)。 - borrrden
听起来像是大端和小端问题... https://dev59.com/ZnVC5IYBdhLWcg3wqzLV - Proxytype
这个问题可以关闭吗?因为看起来像是一个 bug。 - huysentruitw
1个回答

1
我在GitHub上提交的问题已经放置了相当长的时间。我认为这种行为只是一个错误,不需要再花费更多时间去研究它。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接