JITコンパイラを(ネイティブコードに)完全にマネージド.NET言語で作成することは可能ですか?


84

私はJITコンパイラを書くという考えをいじっていて、すべてをマネージコードで書くことが理論的にも可能かどうか疑問に思っています。特に、アセンブラをバイト配列に生成したら、どのようにアセンブラにジャンプして実行を開始しますか?


マネージド言語では安全でないコンテキストで作業できることもありますが、ポインターからデリゲートを合成できるは思いません。生成されたコードに他にどのようにジャンプしますか?
damien_The_Unbeliever 2012年

@Damien:安全でないコードでは、関数ポインターに書き込むことができませんか?
ヘンクホルターマン2012年

2
「制御をアンマネージコードに動的に転送する方法」のようなタイトルを使用すると、クローズされるリスクが低くなる可能性があります。それも要点に見えます。コードの生成は問題ではありません。
ヘンクホルターマン2012年

8
最も簡単なアイデアは、バイト配列をファイルに書き留めて、OSに実行させることです。結局のところ、インタプリタではなくコンパイラが必要です(これも可能ですが、より複雑です)。
vlad 2012年

3
必要なコードをJITでコンパイルしたら、Win32 APIを使用してアンマネージメモリ(実行可能としてマーク)を割り当て、コンパイルしたコードをそのメモリスペースにコピーし、ILcalliオペコードを使用してコンパイルしたコードを呼び出すことができます。
ジャックP.

回答:


71

そして、ここでの概念実証の完全な証拠として、RasmusのJITへのアプローチをF#に完全に変換することができます。

open System
open System.Runtime.InteropServices

type AllocationType =
    | COMMIT=0x1000u

type MemoryProtection =
    | EXECUTE_READWRITE=0x40u

type FreeType =
    | DECOMMIT = 0x4000u

[<DllImport("kernel32.dll", SetLastError=true)>]
extern IntPtr VirtualAlloc(IntPtr lpAddress, UIntPtr dwSize, AllocationType flAllocationType, MemoryProtection flProtect);

[<DllImport("kernel32.dll", SetLastError=true)>]
extern bool VirtualFree(IntPtr lpAddress, UIntPtr dwSize, FreeType freeType);

let JITcode: byte[] = [|0x55uy;0x8Buy;0xECuy;0x8Buy;0x45uy;0x08uy;0xD1uy;0xC8uy;0x5Duy;0xC3uy|]

[<UnmanagedFunctionPointer(CallingConvention.Cdecl)>] 
type Ret1ArgDelegate = delegate of (uint32) -> uint32

[<EntryPointAttribute>]
let main (args: string[]) =
    let executableMemory = VirtualAlloc(IntPtr.Zero, UIntPtr(uint32(JITcode.Length)), AllocationType.COMMIT, MemoryProtection.EXECUTE_READWRITE)
    Marshal.Copy(JITcode, 0, executableMemory, JITcode.Length)
    let jitedFun = Marshal.GetDelegateForFunctionPointer(executableMemory, typeof<Ret1ArgDelegate>) :?> Ret1ArgDelegate
    let mutable test = 0xFFFFFFFCu
    printfn "Value before: %X" test
    test <- jitedFun.Invoke test
    printfn "Value after: %X" test
    VirtualFree(executableMemory, UIntPtr.Zero, FreeType.DECOMMIT) |> ignore
    0

それは喜んで譲歩を実行します

Value before: FFFFFFFC
Value after: 7FFFFFFE

私の賛成にもかかわらず、私は違うように頼みます。これは任意のコード実行であり、JITではありません。JITは「ジャストインタイムコンパイル」を意味しますが、このコード例からは「コンパイル」の側面を見ることができません。
rwong 2015

4
@rwong:「コンパイル」の側面は、元の質問の懸念の範囲内にはありませんでした。IL->ネイティブコード変換を実装するマネージコード機能は、ちょっと明らかです。
ジーンベリツキ2015

70

はい、できます。実際、それは私の仕事です:)

GPU.NETを完全にF#で記述しました(単体テストを法として)-。NET CLRと同じように、実行時に実際に分解してILをJITします。使用したい基盤となるアクセラレーションデバイスのネイティブコードを発行します。現在、Nvidia GPUのみをサポートしていますが、最小限の作業で再ターゲット可能になるようにシステムを設計したため、将来的には他のプラットフォームもサポートする可能性があります。

パフォーマンスに関しては、F#に感謝します。最適化モード(テールコールあり)でコンパイルすると、JITコンパイラ自体はおそらくCLR内のコンパイラ(C ++、IIRCで記述)とほぼ同じくらい高速になります。

実行する場合、ハードウェアドライバーに制御を渡して、jittedコードを実行できるという利点があります。ただし、.NETはアンマネージ/ネイティブコードへの関数ポインターをサポートしているため、これをCPUで行うのはそれほど難しくありません(ただし、通常.NETによって提供される安全性/セキュリティは失われます)。


4
NoExecuteの要点は、自分で作成したコードにジャンプできないということではありませんか?関数ポインタを介してネイティブコードにジャンプするのではなく、関数ポインタを介してネイティブコードにジャンプすることできませんか?
イアン・ボイド

素晴らしいプロジェクトですが、非営利のアプリケーションのために無料にした場合、皆さんはもっと多くの露出を得るだろうと思います。「マニア」層からのチャンプチェンジは失われますが、それを使用するより多くの人々からの露出が増えるので、それだけの価値があります(私は間違いなくそうするでしょう;))
BlueRaja-Danny Pflughoeft 2012

@IanBoyd NoExecuteは、ほとんどの場合、バッファオーバーランや関連する問題によるトラブルを回避するためのもう1つの方法です。これは、独自のコードからの保護ではなく、不正なコード実行を軽減するのに役立つものです。
ルアン2016年

51

トリックは、-flag(P / Invokeが必要)とMarshal.GetDelegateForFunctionPointerを指定したVirtualAllocであるEXECUTE_READWRITE必要があります。

整数の回転の例の修正バージョンを次に示します(ここでは安全でないコードは必要ないことに注意してください)。

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate uint Ret1ArgDelegate(uint arg1);

public static void Main(string[] args){
    // Bitwise rotate input and return it.
    // The rest is just to handle CDECL calling convention.
    byte[] asmBytes = new byte[]
    {        
      0x55,             // push ebp
      0x8B, 0xEC,       // mov ebp, esp 
      0x8B, 0x45, 0x08, // mov eax, [ebp+8]
      0xD1, 0xC8,       // ror eax, 1
      0x5D,             // pop ebp 
      0xC3              // ret
    };

    // Allocate memory with EXECUTE_READWRITE permissions
    IntPtr executableMemory = 
        VirtualAlloc(
            IntPtr.Zero, 
            (UIntPtr) asmBytes.Length,    
            AllocationType.COMMIT,
            MemoryProtection.EXECUTE_READWRITE
        );

    // Copy the machine code into the allocated memory
    Marshal.Copy(asmBytes, 0, executableMemory, asmBytes.Length);

    // Create a delegate to the machine code.
    Ret1ArgDelegate del = 
        (Ret1ArgDelegate) Marshal.GetDelegateForFunctionPointer(
            executableMemory, 
            typeof(Ret1ArgDelegate)
        );

    // Call it
    uint n = (uint)0xFFFFFFFC;
    n = del(n);
    Console.WriteLine("{0:x}", n);

    // Free the memory
    VirtualFree(executableMemory, UIntPtr.Zero, FreeType.DECOMMIT);
 }

完全な例(X86とX64の両方で動作するようになりました)。


30

安全でないコードを使用すると、デリゲートを「ハッキング」して、生成して配列に格納した任意のアセンブリコードを指すようにすることができます。デリゲートには、_methodPtrReflectionを使用して設定できるフィールドがあるという考え方です。サンプルコードは次のとおりです。

もちろん、これは汚いハックであり、.NETランタイムが変更されるといつでも機能しなくなる可能性があります。

原則として、完全に管理された安全なコードでJITを実装することはできません。これは、ランタイムが依存するセキュリティの前提を破ることになるためです。(ただし、生成されたアセンブリコードには、前提条件に違反していないことを示すマシンチェック可能な証明が付属しています...)


1
いいハック。たぶん、コードの一部をこの投稿にコピーして、後でリンク切れの問題が発生しないようにすることができます。(または、この投稿に簡単な説明を書いてください)。
Felix K.

AccessViolationExceptionあなたの例を実行しようとすると、私はを取得します。DEPが無効になっている場合にのみ機能すると思います。
ラスマスフェイバー2012年

1
しかし、EXECUTE_READWRITEフラグを使用してメモリを割り当て、それを_methodPtrフィールドで使用すると、正常に機能します。ローターコードを見ると、スタックを設定してセキュリティを処理するためにコードの周りにいくつかの余分なサンクを追加することを除いて、基本的にMarshal.GetDelegateForFunctionPointer()が行うことのようです。
ラスマスフェイバー2012年

リンクが切れていると思いますが、残念ながら編集しますが、元のリンクの再配置が見つかりませんでした。
アベル
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.