Est-il possible d'écrire un compilateur JIT (en code natif) entièrement dans un langage .NET managé

84

Je joue avec l'idée d'écrire un compilateur JIT et je me demande simplement 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 sauter pour commencer l'exécution?

JD
la source
Je ne crois pas qu'il y en ait - alors que vous pouvez parfois travailler dans un contexte non sécurisé dans des langages gérés, je ne pense pas que vous puissiez synthétiser un délégué à partir d'un pointeur - et comment pourriez-vous passer au code généré autrement?
Damien_The_Unbeliever
@Damien: un code dangereux ne vous permettrait-il pas d'écrire dans un pointeur de fonction?
Henk Holterman
2
Avec un titre comme «comment transférer dynamiquement le contrôle vers du code non managé», vous risquez moins d'être fermé. Cela semble plus pertinent aussi. La génération du code n'est pas le problème.
Henk Holterman
8
L'idée la plus simple serait d'écrire le tableau d'octets dans un fichier et de laisser le système d'exploitation l'exécuter. Après tout, vous avez besoin d'un compilateur , pas d'un interpréteur (ce qui serait également possible, mais plus compliqué).
Vlad
3
Une fois que vous avez compilé JIT le code souhaité, vous pouvez utiliser les API Win32 pour allouer de la mémoire non gérée (marquée comme exécutable), copier le code compilé dans cet espace mémoire, puis utiliser l' calliopcode IL pour appeler le code compilé.
Jack P.

Réponses:

71

Et pour la preuve de concept complète, voici une traduction entièrement capable de l'approche de Rasmus à JIT en 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

qui exécute joyeusement céder

Value before: FFFFFFFC
Value after: 7FFFFFFE
Gene Belitski
la source
Malgré mon vote positif, je ne suis pas d'accord: c'est une exécution de code arbitraire , pas JIT - JIT signifie " compilation juste à temps ", mais je ne vois pas l'aspect "compilation" de cet exemple de code.
rwong
4
@rwong: l'aspect "compilation" n'a jamais été dans la portée des questions d'origine. La capacité du code géré d'implémenter IL -> la transformation du code natif est un peu apparente.
Gene Belitski
70

Oui, vous pouvez. En fait, c'est mon travail :)

J'ai écrit GPU.NET entièrement en F # (modulo nos tests unitaires) - il désassemble en fait et JITs IL au moment de l'exécution, tout comme le .NET CLR le fait. Nous émettons du code natif pour tout périphérique 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 être reciblable avec un minimum de travail, il est donc probable que nous prendrons en charge d'autres plates-formes à l'avenir.

En ce qui concerne les performances, je dois remercier F # - lorsqu'il est compilé en mode optimisé (avec des appels arrière), notre compilateur JIT lui-même est probablement à peu près aussi rapide que le compilateur dans le CLR (qui est écrit en C ++, IIRC).

Pour l'exécution, nous avons l'avantage de pouvoir passer le contrôle aux pilotes matériels pour exécuter le code jitted; cependant, ce ne serait pas plus difficile à faire sur le processeur car .NET prend en charge les pointeurs de fonction vers le code non managé / natif (bien que vous perdriez toute sécurité normalement fournie par .NET).

Jack P.
la source
4
Le but de NoExecute n'est-il pas que vous ne pouvez pas accéder au code que vous avez créé vous-même? Plutôt que de pouvoir passer au code natif via un pointeur de fonction: n'est-il pas possible de passer au code natif via un pointeur de fonction?
Ian Boyd
Projet génial, même si je pense que vous auriez beaucoup plus de visibilité si vous le rendiez gratuit pour les applications à but non lucratif. Vous perdriez le changement de taille du niveau "enthousiaste", mais cela en vaudrait la peine pour une exposition accrue de plus de personnes qui l'utilisent (je sais que je le ferais certainement;)) !
BlueRaja - Danny Pflughoeft
@IanBoyd NoExecute est surtout un autre moyen d'éviter les problèmes liés aux dépassements de tampon et aux problèmes connexes. Ce n'est pas une protection contre votre propre code, c'est quelque chose pour aider à atténuer l'exécution de code illégal.
Luaan
51

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

Voici une version modifiée de l'exemple de rotation d'entier (notez qu'aucun code non sécurisé n'est nécessaire ici):

[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);
 }

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

Rasmus Faber
la source
30

En utilisant du code non sécurisé, vous pouvez «pirater» un délégué et le faire pointer vers un code d'assembly arbitraire que vous avez généré et stocké dans un tableau. L'idée est que le délégué a un _methodPtrchamp, qui peut être défini à l'aide de Reflection. Voici un exemple de code:

C'est, bien sûr, un hack sale qui peut cesser de fonctionner à tout moment lorsque le runtime .NET change.

Je suppose qu'en principe, un code sécurisé entièrement géré ne peut pas être autorisé à implémenter JIT, car cela briserait toutes les hypothèses de sécurité sur lesquelles repose l'exécution. (Sauf si le code d'assemblage généré est livré avec une preuve vérifiable par machine qu'il ne viole pas les hypothèses ...)

Tomas Petricek
la source
1
Bon hack. Vous pourriez peut-être copier certaines parties du code dans cet article pour éviter des problèmes ultérieurs de liens rompus. (Ou écrivez simplement une petite description dans ce post).
Felix K.
J'obtiens un AccessViolationExceptionsi j'essaye d'exécuter votre exemple. Je suppose que cela ne fonctionne que si DEP est désactivé.
Rasmus Faber
1
Mais si j'alloue de la mémoire avec l'indicateur EXECUTE_READWRITE et que je l'utilise dans le champ _methodPtr, cela fonctionne bien. En regardant à travers le code Rotor, cela semble être essentiellement ce que fait Marshal.GetDelegateForFunctionPointer (), sauf qu'il ajoute quelques points supplémentaires autour du code pour configurer la pile et gérer la sécurité.
Rasmus Faber
Je pense que le lien est mort, hélas, je l'éditerais, mais je n'ai pas trouvé de relocalisation de l'original.
Abel