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

84

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

JD
источник
Я не верю, что есть - хотя вы можете иногда работать в небезопасном контексте на управляемых языках, я не верю, что вы можете синтезировать делегат из указателя - а как еще вы могли бы перейти к сгенерированному коду?
Damien_The_Unbeliever
@Damien: разве небезопасный код не позволит вам писать в указатель функции?
Хенк Холтерман,
2
С таким заголовком, как «как динамически передавать управление неуправляемому коду», вы можете снизить риск закрытия. Это тоже выглядит более подходящим. Проблема не в генерации кода.
Хенк Холтерман,
8
Простейшей идеей было бы записать массив байтов в файл и позволить ОС запустить его. В конце концов, вам нужен компилятор , а не интерпретатор (что тоже возможно, но посложнее).
Влад
3
После того, как вы JIT скомпилировали нужный код, вы можете использовать Win32 API для выделения некоторой неуправляемой памяти (помеченной как исполняемая), скопировать скомпилированный код в это пространство памяти, а затем использовать calliкод операции IL для вызова скомпилированного кода.
Джек П.

Ответы:

71

И для полного доказательства концепции вот полностью работоспособный перевод подхода Расмуса к 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
Джин Белицкий
источник
Несмотря на мою поддержку, я позволю себе отличиться: это выполнение произвольного кода , а не JIT - JIT означает «своевременная компиляция », но я не вижу аспекта «компиляции» в этом примере кода.
rwong
4
@rwong: аспект "компиляции" никогда не входил в рамки первоначального вопроса. Способность управляемого кода реализовать преобразование IL -> собственный код довольно очевидна.
Джин Белицкий
70

Да, ты можешь. Собственно, это моя работа :)

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

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

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

Джек П.
источник
4
Разве весь смысл NoExecute не в том, что вы не можете перейти к коду, который создали сами? Вместо того, чтобы перейти к машинному коду с помощью указателя на функцию: разве нельзя перейти к машинному коду с помощью указателя функции?
Ian Boyd
Отличный проект, хотя я думаю, что вы, ребята, получили бы гораздо больше внимания, если бы сделали его бесплатным для некоммерческих приложений. Вы потеряете мелочь на уровне "энтузиастов", но это того стоит, потому что большее количество людей ее использует (я знаю, что определенно буду;)) !
BlueRaja - Дэнни Пфлугхофт
@IanBoyd NoExecute - это еще один способ избежать проблем из-за переполнения буфера и связанных проблем. Это не защита от вашего собственного кода, это то, что помогает уменьшить выполнение незаконного кода.
Luaan
51

Уловкой должен быть 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).

Расмус Фабер
источник
30

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

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

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

Томаш Петричек
источник
1
Хороший хак. Возможно, вы могли бы скопировать некоторые части кода в этот пост, чтобы в дальнейшем избежать проблем с неработающими ссылками. (Или просто напишите небольшое описание в этот пост).
Felix K.
Я получу, AccessViolationExceptionесли попытаюсь запустить ваш пример. Я думаю, это работает, только если DEP отключен.
Расмус Фабер
1
Но если я выделяю память с помощью флага EXECUTE_READWRITE и использую его в поле _methodPtr, он работает нормально. Просматривая код Rotor, кажется, что это в основном то, что делает Marshal.GetDelegateForFunctionPointer (), за исключением того, что он добавляет некоторые дополнительные переходы вокруг кода для настройки стека и обеспечения безопасности.
Расмус Фабер
Думаю ссылка мертвая, увы, отредактировал бы, а переноса оригинала не нашел.
Abel