È possibile scrivere un compilatore JIT (in codice nativo) interamente in un linguaggio .NET gestito


84

Sto giocando con l'idea di scrivere un compilatore JIT e mi chiedo solo se sia anche teoricamente possibile scrivere il tutto in codice gestito. In particolare, una volta generato l'assembler in un array di byte, come ci salti per iniziare l'esecuzione?

c#  .net  f#  jit 

Non credo che ci sia - mentre a volte puoi lavorare in un contesto non sicuro in linguaggi gestiti, non credo che tu possa sintetizzare un delegato da un puntatore - e in quale altro modo salteresti al codice generato?
Damien_The_Unbeliever

@ Damien: un codice non sicuro non ti permetterebbe di scrivere su un puntatore a funzione?
Henk Holterman

2
Con un titolo come "come trasferire dinamicamente il controllo al codice non gestito" potresti avere un minor rischio di essere chiuso. Sembra anche più pertinente. La generazione del codice non è il problema.
Henk Holterman

8
L'idea più semplice sarebbe quella di annotare l'array di byte in un file e lasciare che il sistema operativo lo esegua. Dopotutto, hai bisogno di un compilatore , non di un interprete (il che sarebbe anche possibile, ma più complicato).
Vlad

3
Dopo aver compilato JIT il codice desiderato, è possibile utilizzare le API Win32 per allocare una certa memoria non gestita (contrassegnata come eseguibile), copiare il codice compilato in quello spazio di memoria, quindi utilizzare il callicodice operativo IL per chiamare il codice compilato.
Jack P.

Risposte:


71

E per la dimostrazione completa del concetto, ecco una traduzione completa dell'approccio di Rasmus a JIT in 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

che esegue felicemente cedendo

Value before: FFFFFFFC
Value after: 7FFFFFFE

Nonostante il mio voto positivo, chiedo la differenza: questa è un'esecuzione di codice arbitraria , non JIT - JIT significa " compilazione just in time ", ma non riesco a vedere l'aspetto della "compilazione" da questo esempio di codice.
rwong

4
@ rwong: l'aspetto della "compilazione" non è mai stato nell'ambito delle domande originarie. La capacità del codice gestito di implementare IL -> trasformazione del codice nativo è piuttosto evidente.
Gene Belitski

70

Si, puoi. In effetti, è il mio lavoro :)

Ho scritto GPU.NET interamente in F # (modulo i nostri unit test) - in realtà disassembla e JITs IL in fase di esecuzione, proprio come fa .NET CLR. Emettiamo codice nativo per qualsiasi dispositivo di accelerazione sottostante che desideri utilizzare; attualmente supportiamo solo le GPU Nvidia, ma ho progettato il nostro sistema per essere retargetabile con un minimo di lavoro, quindi è probabile che supporteremo altre piattaforme in futuro.

Per quanto riguarda le prestazioni, devo ringraziare F #: quando compilato in modalità ottimizzata (con tailcalls), il nostro compilatore JIT stesso è probabilmente veloce quanto il compilatore all'interno di CLR (che è scritto in C ++, IIRC).

Per l'esecuzione, abbiamo il vantaggio di poter passare il controllo ai driver hardware per eseguire il codice jitted; tuttavia, questo non sarebbe più difficile da fare sulla CPU poiché .NET supporta i puntatori a funzione a codice non gestito / nativo (anche se si perde qualsiasi sicurezza normalmente fornita da .NET).


4
Non è il punto centrale di NoExecute che non puoi saltare al codice che hai creato tu stesso? Piuttosto che essere possibile salto di codice nativo tramite un puntatore a funzione: non è vero non è possibile saltare in codice nativo tramite un puntatore a funzione?
Ian Boyd

Progetto fantastico, anche se penso che voi ragazzi otterreste molta più visibilità se lo rendeste gratuito per applicazioni senza scopo di lucro. Perderesti il ​​cambio di moneta dal livello "entusiasta", ma ne varrebbe la pena per la maggiore visibilità da parte di più persone che lo usano (so che lo farei sicuramente;)) !
BlueRaja - Danny Pflughoeft

@IanBoyd NoExecute è principalmente un altro modo per evitare problemi dovuti a sovraccarichi del buffer e problemi correlati. Non è una protezione dal tuo codice, è qualcosa che aiuta a mitigare l'esecuzione di codice illegale.
Luaan

51

Il trucco dovrebbe essere VirtualAlloc con EXECUTE_READWRITE-flag (necessita di P / Invoke) e Marshal.GetDelegateForFunctionPointer .

Ecco una versione modificata dell'esempio rotate integer (nota che qui non è necessario codice non sicuro):

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

Esempio completo (ora funziona sia con X86 che con X64).


30

Utilizzando codice non sicuro, è possibile "hackerare" un delegato e farlo puntare a un codice assembly arbitrario generato e archiviato in un array. L'idea è che il delegato abbia un _methodPtrcampo, che può essere impostato utilizzando Reflection. Ecco un codice di esempio:

Questo è, ovviamente, un trucco sporco che potrebbe smettere di funzionare in qualsiasi momento quando cambia il runtime .NET.

Immagino che, in linea di principio, non sia possibile consentire al codice sicuro completamente gestito di implementare JIT, perché ciò infrange qualsiasi presupposto di sicurezza su cui si basa il runtime. (A meno che, il codice assembly generato sia fornito con una prova verificabile dalla macchina che non viola i presupposti ...)


1
Bel trucco. Forse potresti copiare alcune parti del codice in questo post per evitare problemi successivi con collegamenti interrotti. (Oppure scrivi una piccola descrizione in questo post).
Felix K.

Ottengo un AccessViolationExceptionse provo a eseguire il tuo esempio. Immagino che funzioni solo se DEP è disabilitato.
Rasmus Faber

1
Ma se alloco memoria con il flag EXECUTE_READWRITE e lo uso nel campo _methodPtr funziona bene. Esaminando il codice Rotor, sembra essere fondamentalmente ciò che fa Marshal.GetDelegateForFunctionPointer (), tranne per il fatto che aggiunge alcuni thunk extra attorno al codice per impostare lo stack e gestire la sicurezza.
Rasmus Faber

Penso che il collegamento sia morto, ahimè, lo avrei modificato, ma non sono riuscito a trovare un trasferimento dell'originale.
Abel
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.