Можно ли написать JIT-компилятор (в нативный код) полностью на управляемом языке .NET - PullRequest
82 голосов
/ 04 марта 2012

Мне нравится идея написания JIT-компилятора, и мне просто интересно, возможно ли вообще теоретически написать все это в управляемом коде. В частности, как только вы сгенерировали ассемблер в байтовом массиве, как перейти к нему, чтобы начать выполнение?

Ответы [ 4 ]

70 голосов
/ 05 марта 2012

И для полного подтверждения концепции здесь приведен полностью способный перевод подхода Расмуса к JIT в 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

, который успешно выполняется, давая

Value before: FFFFFFFC
Value after: 7FFFFFFE
70 голосов
/ 04 марта 2012

Да, вы можете.Фактически, это моя работа:)

Я написал GPU.NET полностью на F # (по модулю наших модульных тестов) - он на самом деле разбирает и JITs IL во время выполнения, так же, как и .NET CLR,Мы генерируем нативный код для любого устройства ускорения, которое вы хотите использовать;в настоящее время мы поддерживаем только графические процессоры Nvidia, но я разработал нашу систему так, чтобы она была перенастраиваемой с минимумом работы, поэтому, вероятно, мы будем поддерживать другие платформы в будущем.

Что касается производительности, я хочу поблагодарить F #- при компиляции в оптимизированном режиме (с использованием tailcalls) наш JIT-компилятор, вероятно, работает примерно так же быстро, как компилятор в CLR (который написан на C ++, IIRC).

Для выполнения у нас есть преимуществоспособность передавать управление драйверам аппаратного обеспечения для запуска объединенного кода;тем не менее, это не составит труда сделать с процессором, так как .NET поддерживает указатели функций на неуправляемый / нативный код (хотя вы потеряете всю безопасность, обычно предоставляемую .NET).

50 голосов
/ 05 марта 2012

Трюк должен быть VirtualAlloc с флагом EXECUTE_READWRITE (требуется P / Invoke) и Marshal.GetDelegateForFunctionPointer .

Вот модифицированная версия примера вращения целого (обратите внимание, что здесь небезопасный код не требуется):

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

Полный пример (теперь работает как с X86, так и с X64).

30 голосов
/ 04 марта 2012

Используя небезопасный код, вы можете «взломать» делегата и заставить его указывать на произвольный код сборки, который вы сгенерировали и сохранили в массиве. Идея состоит в том, что делегат имеет поле _methodPtr, которое можно установить с помощью Reflection. Вот пример кода:

Это, конечно, грязный хак, который может перестать работать в любое время, когда изменяется среда выполнения .NET. * * 1010

Я полагаю, что в принципе полностью управляемый безопасный код не может реализовывать JIT, потому что это нарушит любые предположения безопасности, на которые опирается среда выполнения. (Если сгенерированный ассемблерный код не сопровождался проверяемым компьютером доказательством того, что он не нарушает предположения ...)

...