web-dev-qa-db-ja.com

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

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

83
Jon Harrop

そして、ここでの概念の完全な証明は、完全に機能する 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
71
Gene Belitski

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

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

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

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

70
Jack P.

トリックは VirtualAlloc with EXECUTE_READWRITE-フラグ(P/Invokeが必要)および Marshal.GetDelegateForFunctionPointer

次に、回転整数の例の修正バージョンを示します(安全でないコードは必要ありません)。

[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の両方で動作するようになりました)。

51
Rasmus Faber

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

もちろん、これは.NETランタイムが変更されたときにいつでも動作を停止する可能性があるダーティーハックです。

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

30
Tomas Petricek