Возможно ли записать компилятор JIT (на собственный код) полностью на управляемом языке .NET

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

Ответ 1

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

Ответ 2

Да, вы можете. На самом деле это моя работа:)

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

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

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

Ответ 3

Трюк должен быть VirtualAlloc с EXECUTE_READWRITE -flag (требуется 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).

Ответ 4

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

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

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