Czy można napisać kompilator JIT (do kodu natywnego) w całości w zarządzanym języku .NET

84

Bawię się pomysłem napisania kompilatora JIT i zastanawiam się tylko, czy teoretycznie jest możliwe napisanie całości w kodzie zarządzanym. W szczególności, po wygenerowaniu asemblera w tablicy bajtów, jak wskoczyć do niej, aby rozpocząć wykonywanie?

JD
źródło
Nie wierzę, że tak jest - chociaż czasami możesz pracować w niebezpiecznym kontekście w językach zarządzanych, nie wierzę, że możesz zsyntetyzować delegata ze wskaźnika - a jak inaczej przeskoczysz do wygenerowanego kodu?
Damien_The_Unbeliever
@Damien: czy niebezpieczny kod nie pozwoliłby pisać do wskaźnika funkcji?
Henk Holterman
2
Z tytułem typu „jak dynamicznie przenieść kontrolę do niezarządzanego kodu” możesz mieć mniejsze ryzyko zamknięcia. Wygląda też bardziej na temat. Generowanie kodu nie jest problemem.
Henk Holterman
8
Najprostszym pomysłem byłoby zapisanie tablicy bajtów do pliku i pozwolenie systemowi operacyjnemu na jej uruchomienie. W końcu potrzebujesz kompilatora , a nie interpretera (co również byłoby możliwe, ale bardziej skomplikowane).
Vlad
3
Po skompilowaniu JIT kodu, który chcesz, możesz użyć interfejsów API Win32 do przydzielenia części niezarządzanej pamięci (oznaczonej jako wykonywalna), skopiuj skompilowany kod do tej przestrzeni pamięci, a następnie użyj callikodu operacyjnego IL, aby wywołać skompilowany kod.
Jack P.

Odpowiedzi:

71

A pełny dowód słuszności koncepcji jest tutaj w pełni zdolnym tłumaczeniem podejścia Rasmusa do JIT na 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

to szczęśliwie wykonuje ustępstwa

Value before: FFFFFFFC
Value after: 7FFFFFFE
Gene Belitski
źródło
Pomimo mojego poparcia, błagam o inne zdanie: to jest wykonanie dowolnego kodu , a nie JIT - JIT oznacza „ kompilację w samą porę ”, ale nie widzę aspektu „kompilacji” z tego przykładu kodu.
rwong
4
@rwong: Aspekt „kompilacji” nigdy nie był przedmiotem pierwotnych pytań. Możliwość implementacji transformacji kodu natywnego IL -> kodu zarządzanego jest pozorna.
Gene Belitski
70

Tak, możesz. Właściwie to moja praca :)

Napisałem GPU.NET całkowicie w F # (modulo nasze testy jednostkowe) - faktycznie deasembluje i JITs IL w czasie wykonywania, tak jak robi .NET CLR. Emitujemy kod natywny dla dowolnego podstawowego urządzenia akceleracyjnego, którego chcesz użyć; obecnie obsługujemy tylko procesory graficzne Nvidia, ale zaprojektowałem nasz system tak, aby można go było ponownie zaadresować przy minimalnym nakładzie pracy, więc prawdopodobnie w przyszłości będziemy obsługiwać inne platformy.

Jeśli chodzi o wydajność, to muszę podziękować za F # - po skompilowaniu w trybie zoptymalizowanym (z wywołaniami końcowymi) nasz kompilator JIT jest prawdopodobnie tak szybki, jak kompilator w środowisku CLR (który jest napisany w C ++, IIRC).

W przypadku wykonywania korzystamy z możliwości przekazania kontroli do sterowników sprzętu, aby uruchomić jitted kod; nie byłoby to jednak trudniejsze do zrobienia na procesorze, ponieważ .NET obsługuje wskaźniki funkcji do niezarządzanego / natywnego kodu (chociaż straciłbyś wszelkie bezpieczeństwo / ochronę zwykle zapewniane przez .NET).

Jack P.
źródło
4
Czy nie chodzi o to, że NoExecute nie możesz przejść do kodu, który sam stworzyłeś? Zamiast można przeskoczyć do natywnego kodu poprzez wskaźnik funkcji: nie jest to nie możliwe, aby przejść do natywnego kodu poprzez wskaźnik funkcji?
Ian Boyd,
Świetny projekt, chociaż myślę, że uzyskalibyście dużo większą ekspozycję, gdybyście zrobili to za darmo dla aplikacji non-profit. Straciłbyś zmianę z poziomu „entuzjastów”, ale byłoby to warte zachodu ze względu na zwiększoną widoczność większej liczby osób, które go używają (wiem, że zdecydowanie bym tak zrobił;)) !
BlueRaja - Danny Pflughoeft
@IanBoyd NoExecute to przede wszystkim kolejny sposób na uniknięcie problemów związanych z przepełnieniem bufora i powiązanymi problemami. To nie jest ochrona przed własnym kodem, to coś, co pomaga złagodzić nielegalne wykonywanie kodu.
Luaan
51

Sztuką powinien być VirtualAlloc z EXECUTE_READWRITEFLAG (potrzebuje P / Invoke) i Marshal.GetDelegateForFunctionPointer .

Oto zmodyfikowana wersja przykładu rotacji liczb całkowitych (pamiętaj, że nie jest potrzebny żaden niebezpieczny kod):

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

Pełny przykład (teraz działa z X86 i X64).

Rasmus Faber
źródło
30

Używając niebezpiecznego kodu, możesz „zhakować” delegata i wskazać dowolny kod asemblera, który został wygenerowany i przechowywany w tablicy. Chodzi o to, że delegat ma _methodPtrpole, które można ustawić za pomocą Odbicia. Oto przykładowy kod:

Jest to oczywiście brudny hack, który może przestać działać w dowolnym momencie, gdy zmieni się środowisko wykonawcze .NET.

Wydaje mi się, że w zasadzie w pełni zarządzany bezpieczny kod nie może być dopuszczony do implementacji JIT, ponieważ złamałoby to wszelkie założenia dotyczące bezpieczeństwa, na których opiera się środowisko wykonawcze. (Chyba że wygenerowany kod asemblera został dostarczony z możliwym do sprawdzenia maszynowym dowodem, że nie narusza założeń ...)

Tomas Petricek
źródło
1
Niezły hack. Może mógłbyś skopiować niektóre części kodu do tego posta, aby uniknąć późniejszych problemów z niedziałającymi linkami. (Lub po prostu napisz mały opis w tym poście).
Felix K.
Dostaję, AccessViolationExceptionjeśli spróbuję dać ci przykład. Myślę, że działa tylko wtedy, gdy DEP jest wyłączone.
Rasmus Faber
1
Ale jeśli przydzielę pamięć za pomocą flagi EXECUTE_READWRITE i użyję tego w polu _methodPtr, działa dobrze. Przeglądając kod Rotora, wydaje się, że jest to w zasadzie to, co robi Marshal.GetDelegateForFunctionPointer (), z wyjątkiem tego, że dodaje kilka dodatkowych elementów do kodu w celu skonfigurowania stosu i obsługi bezpieczeństwa.
Rasmus Faber
Myślę, że link jest martwy, niestety zmieniłbym go, ale nie mogłem znaleźć przeniesienia oryginału.
Abel