Estoy jugando con la idea de escribir un compilador JIT y me pregunto si es incluso teóricamente posible escribir todo en código administrado. En particular, una vez que haya generado el ensamblador en una matriz de bytes, ¿cómo salta para comenzar la ejecución?
84
calli
código de operación IL para llamar al código compilado.Respuestas:
Y para la prueba de concepto completa, aquí hay una traducción totalmente capaz del enfoque de Rasmus para JIT en 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
que felizmente ejecuta cediendo
Value before: FFFFFFFC Value after: 7FFFFFFE
fuente
Sí tu puedes. De hecho, es mi trabajo :)
He escrito GPU.NET completamente en F # (módulo de nuestras pruebas unitarias); en realidad, se desmonta y JITs IL en tiempo de ejecución, al igual que lo hace .NET CLR. Emitimos código nativo para cualquier dispositivo de aceleración subyacente que desee utilizar; Actualmente solo admitimos GPU de Nvidia, pero he diseñado nuestro sistema para que sea reorientable con un mínimo de trabajo, por lo que es probable que admitamos otras plataformas en el futuro.
En cuanto al rendimiento, tengo que agradecer a F #: cuando se compila en modo optimizado (con tailcalls), nuestro compilador JIT en sí es probablemente tan rápido como el compilador dentro de CLR (que está escrito en C ++, IIRC).
Para la ejecución, tenemos la ventaja de poder pasar el control a los controladores de hardware para ejecutar el código jitted; sin embargo, esto no sería más difícil de hacer en la CPU, ya que .NET admite punteros de función para código nativo / no administrado (aunque perdería cualquier seguridad que normalmente proporciona .NET).
fuente
El truco debería ser VirtualAlloc con
EXECUTE_READWRITE
-flag (necesita P / Invoke) y Marshal.GetDelegateForFunctionPointer .Aquí hay una versión modificada del ejemplo de rotación de entero (tenga en cuenta que aquí no se necesita ningún código inseguro):
[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); }
Ejemplo completo (ahora funciona con X86 y X64).
fuente
Con código inseguro, puede "piratear" un delegado y hacer que apunte a un código ensamblador arbitrario que generó y almacenó en una matriz. La idea es que el delegado tenga un
_methodPtr
campo, que se puede configurar usando Reflection. Aquí hay un código de muestra:Este es, por supuesto, un truco sucio que puede dejar de funcionar en cualquier momento cuando cambie el tiempo de ejecución de .NET.
Supongo que, en principio, no se puede permitir que el código seguro completamente administrado implemente JIT, porque eso rompería cualquier supuesto de seguridad en el que se basa el tiempo de ejecución. (A menos que el código de ensamblaje generado venga con una prueba comprobable por máquina de que no viola las suposiciones ...)
fuente
AccessViolationException
si trato de ejecutar tu ejemplo. Supongo que solo funciona si DEP está deshabilitado.