¿Es posible escribir un compilador JIT (en código nativo) completamente en un lenguaje .NET administrado?

84

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?

JD
fuente
No creo que lo haya, si bien puede trabajar en un contexto inseguro a veces en lenguajes administrados, no creo que pueda sintetizar un delegado a partir de un puntero, y ¿de qué otra manera saltaría al código generado?
Damien_The_Unbeliever
@Damien: ¿el código inseguro no te permitiría escribir en un puntero de función?
Henk Holterman
2
Con un título como "cómo transferir dinámicamente el control a código no administrado", es posible que tenga un menor riesgo de ser cerrado. También parece más pertinente. Generar el código no es el problema.
Henk Holterman
8
La idea más simple sería escribir la matriz de bytes en un archivo y dejar que el sistema operativo lo ejecute. Después de todo, necesita un compilador , no un intérprete (lo que también sería posible, pero más complicado).
Vlad
3
Una vez que haya compilado JIT el código que desea, puede usar las API de Win32 para asignar algo de memoria no administrada (marcada como ejecutable), copiar el código compilado en ese espacio de memoria y luego usar el callicódigo de operación IL para llamar al código compilado.
Jack P.

Respuestas:

71

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
Gene Belitski
fuente
A pesar de mi voto positivo, ruego diferir: esto es ejecución de código arbitrario , no JIT - JIT significa " compilación justo a tiempo ", pero no puedo ver el aspecto de "compilación" de este ejemplo de código.
rwong
4
@rwong: el aspecto de "compilación" nunca estuvo dentro del alcance de las preocupaciones de la pregunta original. La capacidad de código administrado para implementar IL -> transformación de código nativo es algo evidente.
Gene Belitski
70

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).

Jack P.
fuente
4
¿No es el objetivo de NoExecute que no puede saltar al código que ha creado usted mismo? En lugar de ser posible salto a código nativo a través de un puntero de función: no es que no es posible saltar a código nativo a través de un puntero de función?
Ian Boyd
Impresionante proyecto, aunque creo que ustedes obtendrían mucha más exposición si lo hicieran gratis para aplicaciones sin fines de lucro. Perderías el tonto del nivel "entusiasta", pero valdría la pena por la mayor exposición de más personas que lo usan (sé que definitivamente lo haría;)) .
BlueRaja - Danny Pflughoeft
@IanBoyd NoExecute es en su mayoría otra forma de evitar problemas por exceso de búfer y problemas relacionados. No es una protección de su propio código, es algo para ayudar a mitigar la ejecución ilegal de código.
Luaan
51

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).

Rasmus Faber
fuente
30

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 _methodPtrcampo, 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 ...)

Tomas Petricek
fuente
1
Buen truco. Tal vez pueda copiar algunas partes del código en esta publicación para evitar problemas posteriores con enlaces rotos. (O simplemente escriba una pequeña descripción en esta publicación).
Felix K.
Obtengo un AccessViolationExceptionsi trato de ejecutar tu ejemplo. Supongo que solo funciona si DEP está deshabilitado.
Rasmus Faber
1
Pero si asigno memoria con el indicador EXECUTE_READWRITE y lo uso en el campo _methodPtr, funciona bien. Mirando a través del código Rotor, parece ser básicamente lo que hace Marshal.GetDelegateForFunctionPointer (), excepto que agrega algunos procesadores adicionales alrededor del código para configurar la pila y manejar la seguridad.
Rasmus Faber
Creo que el enlace está muerto, por desgracia, lo editaría, pero no pude encontrar una reubicación del original.
Abel