私はJITコンパイラを書くという考えをいじっていて、すべてをマネージコードで書くことが理論的にも可能かどうか疑問に思っています。特に、アセンブラをバイト配列に生成したら、どのようにアセンブラにジャンプして実行を開始しますか?
calli
オペコードを使用してコンパイルしたコードを呼び出すことができます。
私はJITコンパイラを書くという考えをいじっていて、すべてをマネージコードで書くことが理論的にも可能かどうか疑問に思っています。特に、アセンブラをバイト配列に生成したら、どのようにアセンブラにジャンプして実行を開始しますか?
calli
オペコードを使用してコンパイルしたコードを呼び出すことができます。
回答:
そして、ここでの概念実証の完全な証拠として、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
はい、できます。実際、それは私の仕事です:)
GPU.NETを完全にF#で記述しました(単体テストを法として)-。NET CLRと同じように、実行時に実際に分解してILをJITします。使用したい基盤となるアクセラレーションデバイスのネイティブコードを発行します。現在、Nvidia GPUのみをサポートしていますが、最小限の作業で再ターゲット可能になるようにシステムを設計したため、将来的には他のプラットフォームもサポートする可能性があります。
パフォーマンスに関しては、F#に感謝します。最適化モード(テールコールあり)でコンパイルすると、JITコンパイラ自体はおそらくCLR内のコンパイラ(C ++、IIRCで記述)とほぼ同じくらい高速になります。
実行する場合、ハードウェアドライバーに制御を渡して、jittedコードを実行できるという利点があります。ただし、.NETはアンマネージ/ネイティブコードへの関数ポインターをサポートしているため、これをCPUで行うのはそれほど難しくありません(ただし、通常.NETによって提供される安全性/セキュリティは失われます)。
トリックは、-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の両方で動作するようになりました)。
安全でないコードを使用すると、デリゲートを「ハッキング」して、生成して配列に格納した任意のアセンブリコードを指すようにすることができます。デリゲートには、_methodPtr
Reflectionを使用して設定できるフィールドがあるという考え方です。サンプルコードは次のとおりです。
もちろん、これは汚いハックであり、.NETランタイムが変更されるといつでも機能しなくなる可能性があります。
原則として、完全に管理された安全なコードでJITを実装することはできません。これは、ランタイムが依存するセキュリティの前提を破ることになるためです。(ただし、生成されたアセンブリコードには、前提条件に違反していないことを示すマシンチェック可能な証明が付属しています...)
AccessViolationException
あなたの例を実行しようとすると、私はを取得します。DEPが無効になっている場合にのみ機能すると思います。