Чи можна написати компілятор JIT (до власного коду) повністю керованою мовою .NET


84

Я граюся з ідеєю написання компілятора JIT, і мені просто цікаво, чи можливо навіть теоретично написати все це в керованому коді. Зокрема, як тільки ви сформували асемблер у байтовий масив, як ви переходите до нього, щоб розпочати виконання?


Я не вірю, що існує - хоча ви можете часом працювати в небезпечному контексті на керованих мовах, я не вірю, що ви можете синтезувати делегат з покажчика - і як інакше ви переходите до сформованого коду?
Damien_The_Unbeliever

@Damien: чи не небезпечний код не дозволить вам писати на покажчик функції?
Хенк Холтерман,

2
З заголовком на кшталт "як динамічно передати управління в некерований код" у вас може бути менший ризик бути закритим. Це також виглядає суттєвіше. Проблема не в генерації коду.
Хенк Холтерман,

8
Найпростішою ідеєю було б записати байтовий масив у файл і дати ОС запустити його. Зрештою, вам потрібен компілятор , а не інтерпретатор (що було б також можливо, але складніше).
Влад

3
Після того, як JIT зібрав потрібний код, ви можете використовувати API Win32 для виділення неконтрольованої пам’яті (позначеної як виконуваний), скопіювати скомпільований код у цей простір пам’яті, а потім використати 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 -> перетворення власного коду є якось очевидною.
Gene Belitski

70

Так, ти можеш. Насправді це моя робота :)

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

Що стосується продуктивності, я маю подякувати F # - при компіляції в оптимізованому режимі (із зворотними викликами) наш компілятор JIT, мабуть, приблизно такий же швидкий, як і компілятор у CLR (що написано на C ++, IIRC).

Для виконання ми маємо перевагу можливості передавати управління драйверам обладнання для запуску нестабільного коду; однак це не буде складніше зробити на центральному процесорі, оскільки .NET підтримує покажчики функцій на некерований / власний код (хоча ви втратите будь-яку безпеку / захист, яку зазвичай надає .NET).


4
Чи не суть NoExecute у тому, що ви не можете перейти до коду, який ви створили самі? Замість можливості переходу до власного коду через покажчик функції: чи не можливо перейти до власного коду через покажчик функції?
Ian Boyd

Чудовий проект, хоча, я думаю, ви, хлопці, отримали б набагато більший вплив, якби ви зробили його безкоштовним для некомерційних програм. Ви втратите зміни на рівні "ентузіастів", але це було би того варте того, щоб збільшити вплив більшої кількості людей, які його використовують (я знаю, що точно б;)) !
BlueRaja - Danny Pflughoeft

@IanBoyd NoExecute - це, в основному, ще один спосіб уникнути проблем із перевищенням буфера та супутніми проблемами. Це не захист від вашого власного коду, це щось, що допомагає пом'якшити незаконне виконання коду.
Луан

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
Хороший хак. Можливо, ви можете скопіювати деякі частини коду в цю публікацію, щоб уникнути пізніших проблем із непрацюючими посиланнями. (Або просто напишіть невеликий опис у цій публікації).
Фелікс К.

Я отримую, AccessViolationExceptionякщо спробую навести ваш приклад. Я думаю, це працює, лише якщо DEP відключено.
Расмус Фабер,

1
Але якщо я виділяю пам'ять із позначкою EXECUTE_READWRITE і використовую це в полі _methodPtr, це працює нормально. Переглядаючи код ротора, здається, це в основному те, що робить Marshal.GetDelegateForFunctionPointer (), за винятком того, що він додає додаткові хитрощі навколо коду для налаштування стека та управління безпекою.
Расмус Фабер,

Я думаю, що посилання мертве, на жаль, я б його відредагував, але не зміг знайти перенесення оригіналу.
Абель
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.