Ist es möglich, einen JIT-Compiler (in nativen Code) vollständig in einer verwalteten .NET-Sprache zu schreiben?

84

Ich spiele mit der Idee, einen JIT-Compiler zu schreiben, und frage mich nur, ob es theoretisch überhaupt möglich ist, das Ganze in verwaltetem Code zu schreiben. Insbesondere, wenn Sie Assembler in ein Byte-Array generiert haben, wie springen Sie hinein, um mit der Ausführung zu beginnen?

JD
quelle
55
Schreiben eines JIT-Compilers in einen JIT-Compiler. Ich mag das. Dies ist JITCEPTION :)
Moo-Juice
2
Mit einem Titel wie "Dynamische Übertragung der Kontrolle auf nicht verwalteten Code" besteht möglicherweise ein geringeres Risiko, geschlossen zu werden. Es sieht auch mehr auf den Punkt. Das Generieren des Codes ist nicht das Problem.
Henk Holterman
8
Die einfachste Idee wäre, das Byte-Array in eine Datei zu schreiben und vom Betriebssystem ausführen zu lassen. Schließlich benötigen Sie einen Compiler , keinen Interpreter (was ebenfalls möglich, aber komplizierter wäre).
Vlad
3
Sobald Sie JIT den gewünschten Code kompiliert haben, können Sie Win32-APIs verwenden, um nicht verwalteten Speicher (als ausführbar markiert) zuzuweisen, den kompilierten Code in diesen Speicherbereich zu kopieren und dann den kompilierten Code mit dem IL- calliOpcode aufzurufen.
Jack P.
2
@ user457104, Sie müssen der Höhepunkt der Party sein :)
Moo-Juice

Antworten:

71

Und für den vollständigen Proof of Concept gibt es hier eine vollständig fähige Übersetzung von Rasmus 'Ansatz für 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

das führt glücklich nach

Value before: FFFFFFFC
Value after: 7FFFFFFE
Gene Belitski
quelle
Trotz meiner positiven Meinung bin ich anderer Meinung: Dies ist eine willkürliche Codeausführung , nicht JIT - JIT bedeutet "Just-in-Time- Kompilierung ", aber ich kann den Aspekt "Kompilierung" in diesem Codebeispiel nicht erkennen.
Rwong
4
@rwong: Der Aspekt "Zusammenstellung" war nie Gegenstand der ursprünglichen Fragen. Die Fähigkeit des verwalteten Codes, IL -> native Code- Transformation zu implementieren , ist offensichtlich.
Gene Belitski
70

Ja, du kannst. In der Tat ist es mein Job :)

Ich habe GPU.NET vollständig in F # geschrieben (modulo unsere Unit-Tests) - es zerlegt und JITs IL zur Laufzeit, genau wie die .NET CLR. Wir geben nativen Code für jedes zugrunde liegende Beschleunigungsgerät aus, das Sie verwenden möchten. Derzeit unterstützen wir nur Nvidia-GPUs, aber ich habe unser System so konzipiert, dass es mit einem Minimum an Arbeit retargetierbar ist, sodass wir wahrscheinlich in Zukunft andere Plattformen unterstützen werden.

Was die Leistung angeht, muss ich mich bei F # bedanken - wenn unser JIT-Compiler im optimierten Modus (mit Tailcalls) kompiliert wird, ist er wahrscheinlich ungefähr so ​​schnell wie der Compiler in der CLR (die in C ++, IIRC geschrieben ist).

Für die Ausführung haben wir den Vorteil, dass wir die Steuerung an Hardwaretreiber übergeben können, um den angepassten Code auszuführen. Dies wäre jedoch auf der CPU nicht schwieriger, da .NET Funktionszeiger auf nicht verwalteten / nativen Code unterstützt (obwohl Sie die Sicherheit verlieren würden, die normalerweise von .NET bereitgestellt wird).

Jack P.
quelle
4
Ist es nicht der springende Punkt von NoExecute, dass Sie nicht zu Code springen können, den Sie selbst erstellt haben? Anstatt über einen Funktionszeiger zum nativen Code springen zu können: Ist es nicht nicht möglich, über einen Funktionszeiger zum nativen Code zu springen?
Ian Boyd
Tolles Projekt, obwohl ich denke, dass ihr viel mehr Aufmerksamkeit bekommen würdet, wenn ihr es für gemeinnützige Anwendungen kostenlos machen würdet. Sie würden den Trottelwechsel von der "Enthusiasten" -Ebene verlieren, aber es würde sich lohnen, wenn mehr Leute ihn verwenden (ich weiß, dass ich das definitiv tun würde;)) !
BlueRaja - Danny Pflughoeft
@IanBoyd NoExecute ist meistens eine weitere Möglichkeit, um Probleme durch Pufferüberläufe und verwandte Probleme zu vermeiden. Es ist kein Schutz vor Ihrem eigenen Code, sondern hilft, die illegale Codeausführung zu verringern.
Luaan
51

Der Trick sollte VirtualAlloc mit dem EXECUTE_READWRITEFlag-Flag (benötigt P / Invoke) und Marshal.GetDelegateForFunctionPointer sein .

Hier ist eine modifizierte Version des Beispiels für rotierende Ganzzahlen (beachten Sie, dass hier kein unsicherer Code benötigt wird):

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

Vollständiges Beispiel (funktioniert jetzt sowohl mit X86 als auch mit X64).

Rasmus Faber
quelle
30

Mit unsicherem Code können Sie einen Delegaten "hacken" und ihn auf einen beliebigen Assemblycode verweisen lassen, den Sie generiert und in einem Array gespeichert haben. Die Idee ist, dass der Delegat ein _methodPtrFeld hat, das mit Reflection festgelegt werden kann. Hier ist ein Beispielcode:

Dies ist natürlich ein schmutziger Hack, der jederzeit nicht mehr funktioniert, wenn sich die .NET-Laufzeit ändert.

Ich vermute, dass vollständig verwalteter sicherer Code JIT im Prinzip nicht implementieren darf, da dies alle Sicherheitsannahmen verletzen würde, auf die sich die Laufzeit stützt. (Es sei denn, der generierte Baugruppencode enthielt einen maschinenprüfbaren Beweis dafür, dass er nicht gegen die Annahmen verstößt ...)

Tomas Petricek
quelle
1
Netter Hack. Vielleicht könnten Sie einige Teile des Codes in diesen Beitrag kopieren, um spätere Probleme mit defekten Links zu vermeiden. (Oder schreiben Sie einfach eine kleine Beschreibung in diesen Beitrag).
Felix K.
Ich bekomme eine, AccessViolationExceptionwenn ich versuche, Ihr Beispiel auszuführen. Ich denke, es funktioniert nur, wenn DEP deaktiviert ist.
Rasmus Faber
1
Aber wenn ich Speicher mit dem EXECUTE_READWRITE-Flag zuweise und diesen im _methodPtr-Feld verwende, funktioniert es einwandfrei. Wenn Sie den Rotor-Code durchsehen, scheint es im Grunde das zu sein, was Marshal.GetDelegateForFunctionPointer () tut, außer dass es einige zusätzliche Thunks um den Code hinzufügt, um den Stapel einzurichten und die Sicherheit zu handhaben.
Rasmus Faber
Ich denke, der Link ist tot, leider würde ich ihn bearbeiten, aber ich konnte keine Verlagerung des Originals finden.
Abel