Est-il possible d’écrire un compilateur JIT (au code natif) entièrement dans un langage .NET géré?

Je joue avec l’idée d’écrire un compilateur JIT et je me demande s’il est même théoriquement possible d’écrire le tout en code managé. En particulier, une fois que vous avez généré l’assembleur dans un tableau d’octets, comment y entrez-vous pour commencer l’exécution?

Et pour la preuve de concept complète, voici une traduction de l’approche de JIT en F # entièrement compatible avec Rasmus.

 open System open System.Runtime.InteropServices type AllocationType = | COMMIT=0x1000u type MemoryProtection = | EXECUTE_READWRITE=0x40u type FreeType = | DECOMMIT = 0x4000u [] extern IntPtr VirtualAlloc(IntPtr lpAddress, UIntPtr dwSize, AllocationType flAllocationType, MemoryProtection flProtect); [] extern bool VirtualFree(IntPtr lpAddress, UIntPtr dwSize, FreeType freeType); let JITcode: byte[] = [|0x55uy;0x8Buy;0xECuy;0x8Buy;0x45uy;0x08uy;0xD1uy;0xC8uy;0x5Duy;0xC3uy|] [] type Ret1ArgDelegate = delegate of (uint32) -> uint32 [] let main (args: ssortingng[]) = 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 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 

qui exécute avec bonheur

 Value before: FFFFFFFC Value after: 7FFFFFFE 

Oui, vous pouvez. En fait, c’est mon travail 🙂

J’ai écrit GPU.NET entièrement en F # (modulo nos tests unitaires) – il démonte et JITs IL au moment de l’exécution, comme le fait le CLR .NET. Nous émettons du code natif pour tout appareil d’accélération sous-jacent que vous souhaitez utiliser. Actuellement, nous ne prenons en charge que les GPU Nvidia, mais j’ai conçu notre système pour qu’il soit redirigé avec un minimum de travail, il est donc probable que nous supportons d’autres plates-formes à l’avenir.

En ce qui concerne les performances, je dois remercier F #: compilé en mode optimisé (avec les appels reçus), notre compilateur JIT lui-même est probablement aussi rapide que le compilateur du CLR (écrit en C ++, IIRC).

Pour l’exécution, nous avons l’avantage de pouvoir transmettre le contrôle aux pilotes de matériel pour exécuter le code jitted; Cependant, cela ne serait pas plus difficile sur le processeur puisque .NET prend en charge les pointeurs de fonctions vers un code non géré / natif (bien que vous perdiez toute sécurité / sécurité normalement fournie par .NET).

L’astuce devrait être VirtualAlloc avec EXECUTE_READWRITE -flag (nécessite P / Invoke) et Marshal.GetDelegateForFunctionPointer .

Voici une version modifiée de l’exemple de rotation de l’entier entier (notez qu’aucun code dangereux n’est requirejs ici):

 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate uint Ret1ArgDelegate(uint arg1); public static void Main(ssortingng[] 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); } 

Exemple complet (fonctionne désormais avec X86 et X64).

En utilisant un code non sécurisé, vous pouvez “pirater” un délégué et le diriger vers un code d’assemblage arbitraire que vous avez généré et stocké dans un tableau. L’idée est que délégué a un champ _methodPtr , qui peut être défini à l’aide de Reflection. Voici un exemple de code:

  • Inline x86 ASM en C #

Il s’agit, bien sûr, d’un piratage sale qui peut cesser de fonctionner à tout moment lorsque le runtime .NET change.

Je suppose que, en principe, un code sécurisé entièrement géré ne peut pas être implémenté dans JIT, car cela briserait toute hypothèse de sécurité sur laquelle repose le runtime. (Sauf si le code assembleur généré est accompagné d’une preuve vérifiable par ordinateur qu’il ne viole pas les hypothèses …)