有可能用完全由托管的.NET语言编写JIT编译器(到本机代码)吗?

84

我在考虑写一个JIT编译器,想知道是否有可能完全用托管代码来实现。特别是,一旦你把汇编生成到一个字节数组中,如何跳转到它以开始执行?


我不相信这是可能的 - 虽然在托管语言中有时可以在不安全的上下文中工作,但我不相信您可以从指针合成委托 - 否则您怎么跳转到生成的代码呢? - Damien_The_Unbeliever
@Damien:不安全的代码难道不能让你写入函数指针吗? - H H
2
有了像“如何动态转移控制到非托管代码”的标题,您可能被关闭的风险会降低。这看起来更加直接。生成代码并不是问题。 - H H
8
最简单的方法是将字节数组写入文件并让操作系统运行它。毕竟,你需要的是一个编译器,而不是一个解释器(虽然也可以用解释器实现,但更加复杂)。 - Vlad
3
一旦你已经 JIT 编译好了代码,你可以使用 Win32 API 来分配一些未托管的内存(标记为可执行),将编译好的代码复制到那段内存空间中,然后使用 IL 中的 calli 操作码来调用编译好的代码。 - Jack P.
显示剩余3条评论
4个回答

71

这是一个完整的概念证明,这里有一个 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

愉快地执行yield操作

Value before: FFFFFFFC
Value after: 7FFFFFFE

尽管我点了赞,但我不同意:这是任意代码执行,而不是JIT - JIT的意思是“即时编译”,但我从这个代码示例中看不到“编译”方面。 - rwong
4
@rwong提到的“编译”方面并不是原问题所关注的范围。托管代码实现IL -> 本地代码转换的能力是相当明显的。 - Gene Belitski

71

是的,你可以。实际上这是我的工作 :)

我完全使用F#编写了GPU.NET(除了我们的单元测试)--它实际上会在运行时反汇编和JITs IL,就像.NET CLR一样。我们为您想要使用的任何底层加速设备发出本机代码;目前我们仅支持Nvidia GPU,但我已经设计了我们的系统以便在最少的工作量下进行重新定向,因此很可能我们将来会支持其他平台。

至于性能,我要感谢F#--当以优化模式编译(包括尾递归)时,我们的JIT编译器本身可能与CLR内部的编译器(用C++编写)大致同速。

对于执行,我们有一个好处,即可以将控制权传递给硬件驱动程序来运行JIT编译的代码;但是,在CPU上做到这点不会更难,因为.NET支持指向非托管/本机代码的函数指针(尽管您会失去.NET通常提供的任何安全性/安全保障)。


4
NoExecute 的整个意义不就在于不能跳转到自己创建的代码吗?通过函数指针跳转到本地代码是不可能的,而不是可以跳转到本地代码。 - Ian Boyd
很棒的项目,不过我认为如果你们将其免费提供给非盈利应用程序,就能获得更多曝光。虽然你们会失去“爱好者”层次的小额收入,但由于更多人使用它而获得的曝光将是值得的(我肯定会使用它;))! - BlueRaja - Danny Pflughoeft
@IanBoyd NoExecute主要是另一种避免缓冲区溢出和相关问题引起麻烦的方式。它不是保护你自己代码的一种方式,而是帮助减轻非法代码执行的影响。 - Luaan

51

这个技巧应该使用带有EXECUTE_READWRITE标志的VirtualAlloc函数(需要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字段,可以使用反射设置。以下是一些示例代码: 当然,这是一个肮脏的黑客技巧,在.NET运行时更改时可能随时停止工作。
我认为,原则上,完全托管的安全代码不能允许实现JIT,因为这会破坏运行时依赖的任何安全假设。(除非生成的汇编代码带有可机器检查的证明,表明它不违反这些假设…)

1
不错的技巧。也许你可以复制代码的某些部分到这篇文章中,以避免后来出现链接失效的问题。(或者只需在此文章中写一个小描述)。 - Felix K.
如果我尝试运行你的示例,我会收到一个“AccessViolationException”的错误。我猜想只有在禁用DEP时它才能正常工作。 - Rasmus Faber
1
但是,如果我使用EXECUTE_READWRITE标志分配内存,并将其用于_methodPtr字段,则可以正常工作。通过查看Rotor代码,似乎基本上就是Marshal.GetDelegateForFunctionPointer()所做的,只是它在设置堆栈和处理安全性的代码周围添加了一些额外的thunk。 - Rasmus Faber
我认为链接已经失效了,唉,我想编辑它,但是我找不到原始链接的位置。 - Abel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接