是否可以完全用托管的 .NET 语言编写 JIT 编译器(本地代码)

Posted

技术标签:

【中文标题】是否可以完全用托管的 .NET 语言编写 JIT 编译器(本地代码)【英文标题】:Is it possible to write a JIT compiler (to native code) entirely in a managed .NET language 【发布时间】:2012-03-22 08:56:42 【问题描述】:

我在玩弄编写 JIT 编译器的想法,只是想知道理论上是否可以用托管代码编写整个东西。特别是,一旦您将汇编程序生成为字节数组,您如何跳入其中开始执行?

【问题讨论】:

我不相信有 - 虽然您有时可以在不安全的上下文中使用托管语言工作,但我不相信您可以从指针合成委托- 还有什么方法可以跳转到生成的代码? @Damien:不安全的代码不会让你写入函数指针吗? 标题为“如何动态地将控制权转移到非托管代码”,您可能被关闭的风险较低。它看起来也更重要。生成代码不是问题。 最简单的想法是将字节数组写入文件并让操作系统运行它。毕竟,您需要一个编译器,而不是一个解释器(这也是可能的,但更复杂)。 一旦你 JIT 编译了你想要的代码,你可以使用 Win32 API 分配一些非托管内存(标记为可执行),将编译后的代码复制到该内存空间,然后使用 IL @987654321 @opcode 调用编译后的代码。 【参考方案1】:

使用不安全代码,您可以“破解”委托并使其指向您生成并存储在数组中的任意汇编代码。这个想法是委托有一个_methodPtr 字段,可以使用反射进行设置。下面是一些示例代码:

Inline x86 ASM in C#

当然,这是一个肮脏的 hack,当 .NET 运行时发生变化时,它可能随时停止工作。

我猜,原则上,完全托管的安全代码不允许实现 JIT,因为这会破坏运行时所依赖的任何安全假设。 (除非生成的汇编代码带有机器可检查的证明,证明它不违反假设......)

【讨论】:

不错的技巧。也许您可以将部分代码复制到这篇文章中,以避免以后出现链接断开的问题。 (或者只是在这篇文章中写一个小描述)。 如果我尝试运行您的示例,我会收到 AccessViolationException。我猜它只有在禁用 DEP 时才有效。 但是如果我使用 EXECUTE_READWRITE 标志分配内存并在 _methodPtr 字段中使用它,它就可以正常工作。查看转子代码,它似乎基本上就是 Marshal.GetDelegateForFunctionPointer() 所做的,除了它在代码周围添加了一些额外的 thunk 以设置堆栈和处理安全性。 我认为链接已经失效,唉,我会编辑它,但我找不到原始的重定位。【参考方案2】:

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

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

至于性能,我要感谢 F#——当在优化模式下编译时(带有尾调用),我们的 JIT 编译器本身可能与 CLR 中的编译器(用 C++、IIRC 编写)一样快。

对于执行,我们的好处是能够将控制权传递给硬件驱动程序以运行 jit 代码;然而,这在 CPU 上并不难做到,因为 .NET 支持指向非托管/本机代码的函数指针(尽管您会失去 .NET 通常提供的任何安全性/安全性)。

【讨论】:

NoExecute 的全部意义不在于您不能跳转到您自己创建的代码吗?而不是可以通过函数指针跳转到本机代码:不是不可能可以通过函数指针跳转到本机代码吗? 很棒的项目,不过我认为如果您将其免费提供给非营利性应用程序,你们会获得更多的曝光率。你会失去“发烧友”级别的小钱,但这是值得的,因为更多的人使用它会增加曝光率(我知道我肯定会;)) @IanBoyd NoExecute 主要是另一种避免缓冲区溢出和相关问题的麻烦的方法。这不是保护您自己的代码,而是帮助减少非法代码执行。【参考方案3】:

技巧应该是VirtualAlloc 与EXECUTE_READWRITE-标志(需要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);
 

Full example(现在适用于 X86 和 X64)。

【讨论】:

【参考方案4】:

对于完整的概念证明,这里是将 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

愉快地执行让步

Value before: FFFFFFFC
Value after: 7FFFFFFE

【讨论】:

尽管我赞成,但我不敢苟同:这是任意代码执行,而不是 JIT - JIT 的意思是“及时编译”,但是从这个代码示例中我看不到“编译”方面。 @rwong:“编译”方面从未在原始问题关注的范围内。实现 IL -> 本机代码 转换的托管代码能力有点明显。

以上是关于是否可以完全用托管的 .NET 语言编写 JIT 编译器(本地代码)的主要内容,如果未能解决你的问题,请参考以下文章

Windbg如何设置应用程序的断点

.NET学习:中间语言(IL)=托管代码?

『GCTT 出品』使用 Go 语言写一个即时编译器(JIT)

JIT 编译后如何(以及在​​何处)加载本机代码?

托管C++C++/CLICLR

CLR 语言优化。语言编译器 VS JIT 编译器