web-dev-qa-db-ja.com

P / Invoke引数が渡されたときに順序が狂う原因は何ですか?

これは、x86またはx64ではなく、ARMで特に発生する問題です。この問題はユーザーから報告され、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);
_

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では次のことを確認しています。最初の引数のアドレスはSECOND引数のアドレスであり、2番目の引数は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);
}
_

以下を使用して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を実行するのがどれほど一般的かはわかりません)

[〜#〜] edit [〜#〜]興味深い更新:C#で署名を「偽造」し、2番目のパラメーターを削除すると、最初の1つはOKです。

EDIT 22番目の興味深い更新:サイズのC#FLSliceResult定義をUIntPtrからulongに変更すると、引数正しく入ります... ARMの_size_t_はunsigned intである必要があるため、これは意味がありません。

EDIT 3C#の定義に[StructLayout(LayoutKind.Sequential, Size = 12)]を追加することでもこの作業ができますが、なぜですか?このアーキテクチャのC/C++のsizeof(FLSliceResult)は、必要に応じて8を返します。 C#で同じサイズを設定するとクラッシュしますが、12に設定すると動作します。

EDIT 4同様にC++テストケースを作成できるように、テストケースを最小化しました。 C#UWPでは失敗しますが、C++ UWPでは成功します。

EDIT 5ここ は、比較のためにC++とC#の両方の逆アセンブルされた命令です(ただし、C#の量はわかりませんが)取るので、私は取りすぎの側で間違っていた)

EDIT 6さらに分析すると、嘘をついてC#で構造体が12バイトであると言う「良い」実行中に、戻り値がレジスタr0、他の2つの引数はr1、r2を介して入ります。ただし、悪い実行では、これは2つの引数がr0、r1を介して入り、戻り値が他のどこかにあるようにシフトされます(スタックポインター?)

EDIT 7ARMアーキテクチャの手続き呼び出し標準 を参照しました。私はこの引用を見つけました:「4バイトより大きい、またはサイズが呼び出し元と呼び出し先の両方で静的に決定できない複合型は、関数が呼び出されたときに追加の引数として渡されたアドレスのメモリに保存されます(§5.5、ルールA .4)。結果に使用されるメモリは、関数呼び出し中の任意の時点で変更できます。これは、追加の引数が最初の引数を意味するため、r0に渡すことが正しい動作であることを意味します(Cの呼び出し規約には引数の数を指定する方法がないため)。 CLRがこれをfundamental64ビットデータ型に関する別のルールと混同しているのだろうか:「ダブルワードサイズの基本データ型(例:long long、double、および64ビットのコンテナー化されたベクトル)がr0およびr1に返されます。」

EDIT 8OkここでCLRが間違ったことをしていることを示す多くの証拠があるので、私は バグレポート 。そのレポに問題を投稿するすべての自動化されたボットの間で誰かがそれに気づくことを願っています:-S.

79
borrrden

私がGHに提出した問題は、かなり長い間そこにありました。この動作は単なるバグであり、これを調べるのに時間を費やす必要はないと思います。

1
borrrden