Я играю с идеей написания JIT-компилятора, и мне просто интересно, возможно ли даже теоретически написать все в управляемом коде. В частности, как только вы сгенерировали ассемблер в массив байтов, как вы в него вскакиваете, чтобы начать выполнение?
Возможно ли записать компилятор JIT (на собственный код) полностью на управляемом языке .NET
Ответ 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, потому что это нарушит любые предположения безопасности, на которые полагается среда выполнения. (Разве что сгенерированный ассемблерный код пришел с проверенным машиной доказательством того, что он не нарушает предположений...)