【问题标题】:What could cause P/Invoke arguments to be out of order when passed?什么可能导致 P/Invoke 参数在传递时出现乱序?
【发布时间】:2017-11-22 07:08:22
【问题描述】:

这是一个专门在 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# 结构 (reference):

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++ 内容?

native 端的结构有实际信息,但是对于 C API,FLEncoder 定义为as an opaque pointer。在 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);

这个逻辑可以用最新的developer build 触发崩溃,但不幸的是,我还没有弄清楚如何通过 Nuget 可靠地提供本机调试符号,以便可以单步执行(似乎只能从源代码构建所有内容)这样做...)所以调试有点尴尬,因为需要构建本机和托管组件。如果有人想尝试,我愿意接受有关如何使这更容易的建议。但是,如果有人以前经历过这种情况或对为什么会发生这种情况有任何想法,请添加答案,谢谢!当然,如果有人想要一个复制案例(一个易于构建但不提供源步进的案例或难以构建的案例),请发表评论,但我不想经历制作一个如果没有人会使用它(我不确定在实际 ARM 上运行 Windows 的东西有多受欢迎)

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

EDIT 2 第二个有趣的更新:如果我将 C# FLSliceResult 的大小定义从 UIntPtr 更改为 ulong,那么参数会正确输入......这没有意义,因为 @ ARM 上的 987654338@ 应该是无符号整数。

EDIT 3[StructLayout(LayoutKind.Sequential, Size = 12)] 添加到 C# 中的定义中也可以实现这一点,但为什么呢?此架构的 C/C++ 中的 sizeof(FLSliceResult) 应返回 8。在 C# 中设置相同的大小会导致崩溃,但将其设置为 12 可以正常工作。

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

EDIT 5 Here 是用于比较的 C++ 和 C# 的反汇编指令(虽然 C# 我不确定要拿多少,所以我错误地拿了太多)

EDIT 6 进一步分析表明,在“良好”运行期间,当我撒谎并说结构在 C# 上是 12 字节时,返回值与其他两个参数一起传递给寄存器 r0通过 r1、r2 进入。然而,在糟糕的运行中,这被转移了,因此两个 args 通过 r0、r1 进入,并且返回值在其他地方(堆栈指针?)

EDIT 7 我咨询了Procedure Call Standard for the ARM Architecture。我找到了这句话:“一个大于 4 个字节的复合类型,或者它的大小不能由调用者和 被调用者,存储在内存中的地址处,该地址在调用函数时作为额外参数传递(第 5.5 节, 规则 A.4)。用于结果的内存可以在函数调用期间的任何时候修改。”这意味着传递到 r0 是正确的行为,因为额外的参数意味着第一个参数(因为 C 调用约定没有办法指定参数的数量)。我想知道 CLR 是否将此与另一条关于 基本 64 位数据类型的规则混淆:“双字大小的基本数据类型(例如,long long、double 和 64 -bit 容器化向量)是 在 r0 和 r1 中返回。”

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

【问题讨论】:

  • 评论不用于扩展讨论;这个对话是moved to chat
  • 60 次点赞,但没有提供任何赏金......这很奇怪
  • @MauricioGraciaGutierrez 我想我可以用“这是 JIT 引擎中的一个错误”来回答这个问题(我假设大多数人来这里投票是因为他们对解决该错误感兴趣)跨度>
  • 听起来像大大小小的印度问题...stackoverflow.com/questions/217980/…
  • 这个问题好像是个bug可以关闭吗?

标签: c# windows uwp arm pinvoke


【解决方案1】:

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-06-14
    • 2013-10-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-07-27
    • 2020-03-08
    相关资源
    最近更新 更多