2012-03-04 12 views
82

Mi sto prendendo gioco dell'idea di scrivere un compilatore JIT e mi sto solo chiedendo se è anche teoricamente possibile scrivere l'intera cosa nel codice gestito. In particolare, una volta generato l'assemblatore in un array di byte, come ci si butta dentro per iniziare l'esecuzione?È possibile scrivere un compilatore JIT (in codice nativo) interamente in un linguaggio .NET gestito

+55

Scrivi compilatore JIT in un Compilatore JIT Mi piace. Questo è JITCEPTION :) –

+0

Non credo che ci sia - mentre si può lavorare in un contesto non sicuro, a volte nei linguaggi gestiti, non credo * si può sintetizzare un delegato da un puntatore - e in quale altro modo si salta al codice generato? –

+0

@Danni: il codice non sicuro non consente di scrivere su un puntatore di funzione? –

risposta

69

E per la piena prova di concetto: ecco un pienamente in grado traduzione di approccio 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 
+1

Wow! Ottima risposta, grazie! –

+0

Nonostante il mio upvote, mi permetto di dissentire: questa è ** esecuzione di codice arbitrario **, non JIT - JIT significa "just in time ** compilation **", ma non riesco a vedere l'aspetto "compilation" da questo codice esempio. – rwong

+4

@rwong: l'aspetto della "compilazione" non rientrava mai nell'ambito delle preoccupazioni relative alle domande originali. La capacità del codice gestito di implementare la trasformazione * IL -> codice nativo * è piuttosto evidente. –

30

Utilizzando un codice non sicuro, è possibile "hackerare" un delegato e farlo puntare a un codice assembly arbitrario che è stato generato e memorizzato in un array. L'idea è che il delegato abbia un campo _methodPtr, che può essere impostato usando Reflection. Ecco alcuni esempi di codice:

Questo è, naturalmente, un hack sporco che potrebbe smettere di funzionare in qualsiasi momento quando cambia .NET runtime.

Immagino che, in linea di principio, il codice di sicurezza completamente gestito non possa essere in grado di implementare JIT, perché ciò interromperà qualsiasi ipotesi di sicurezza su cui si basa il runtime. (A meno che il codice assembly generato sia stato fornito con una prova controllabile dalla macchina che non viola le ipotesi ...)

+1

Bel trucco. Forse potresti copiare alcune parti del codice in questo post per evitare problemi successivi con link non funzionanti. (Oppure scrivi una piccola descrizione in questo post). –

+0

Ottengo un 'AccessViolationException' se cerco di eseguire il tuo esempio. Immagino che funzioni solo se DEP è disabilitato. –

+1

Ma se alloco memoria con il flag EXECUTE_READWRITE e lo utilizzo nel campo _methodPtr, funziona correttamente. Guardando attraverso il codice rotore, sembra essere fondamentalmente ciò che Marshal.GetDelegateForFunctionPointer() fa, tranne che aggiunge alcuni thunk extra attorno al codice per configurare lo stack e gestire la sicurezza. –

69

Sì, è possibile. In effetti, è il mio lavoro :)

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

Per quanto riguarda le prestazioni, devo ringraziare F - quando compilato in modalità ottimizzata (con tailcall), il compilatore JIT stesso è probabilmente più veloce del compilatore all'interno del 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 di funzioni al codice non gestito/nativo (anche se si perderebbe qualsiasi sicurezza/sicurezza normalmente fornita da .NET).

+4

Non è l'intero punto di NoExecute che non puoi saltare al codice che hai creato tu stesso? Piuttosto che essere possibile passare al codice nativo attraverso un puntatore a funzione: non è ** non ** possibile passare al codice nativo attraverso un puntatore a funzione? –

+0

Sembra un progetto molto interessante. –

+0

Un progetto fantastico, anche se penso che voi otterreste molta più visibilità se la rendeste gratuita per le applicazioni senza scopo di lucro. Perderesti il ​​chump-change dal livello "appassionato", ma ne varrà la pena per la maggiore esposizione da parte di più persone che lo usano * (lo so che lo farei sicuramente;)) *! –

50

Il trucco deve essere VirtualAlloc con il EXECUTE_READWRITE -flag (richiede P/Invoke) e Marshal.GetDelegateForFunctionPointer.

Ecco una versione modificata dell'esempio intero rotazione (si noti che nessun codice non sicuro è necessario qui):

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

Full example (ora funziona sia con X86 e X64).

+1

Fantastico, grazie! –

Problemi correlati