是否有可能完全用托管的.NET语言编写JIT编译器(至本机代码)


84

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


我不相信有-尽管您有时可以在不安全的上下文中使用托管语言进行工作,但我不相信您可以通过指针合成委托-以及如何跳到生成的代码?
Damien_The_Unbeliever 2012年

@Damien:不安全的代码不会让您写入函数指针吗?
Henk Holterman'3

2
使用“如何将控制权动态转移到非托管代码”这样的标题,可能会降低被关闭的风险。它看起来也更重要。生成代码不是问题。
Henk Holterman'3

8
最简单的想法是将字节数组写到文件中,然后让操作系统运行它。毕竟,您需要一个编译器,而不是一个解释器(也可以,但是更复杂)。
弗拉德(Vlad)2012年

3
JIT编译完所需的代码后,可以使用Win32 API分配一些非托管内存(标记为可执行文件),将编译后的代码复制到该内存空间中,然后使用ILcalli操作码来调用编译后的代码。
Jack P.

Answers:


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 2015年

4
@rwong:“编译”方面从来不在原始问题范围之内。实现IL->本机代码转换的托管代码能力是显而易见的。
Gene Belitski 2015年

70

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

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

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

对于执行,我们的好处是能够将控制权传递给硬件驱动程序以运行固定代码;但是,由于.NET支持指向非托管/本机代码的功能指针,因此在CPU上执行此操作将变得更加轻松(尽管您将失去.NET通常提供的任何安全性/安全性)。


4
难道不是您不能跳转到自己创建的代码的全部目的吗?而不是可以通过函数指针跳转到本地代码:不是不可能通过函数指针跳转到本地代码吗?
伊恩·博伊德

很棒的项目,尽管我认为如果您将其免费提供给非营利性应用程序,他们将获得更多的机会。您将失去“热情”阶层的挫败感,但由于越来越多的人使用它而引起的更多接触,这是非常值得的(我知道我肯定会;))
BlueRaja-Danny Pflughoeft 2012年

@IanBoyd NoExecute通常是避免缓冲区溢出和相关问题引起麻烦的另一种方法。这不是对您自己的代码的保护,而是有助于减轻非法代码执行的某种手段。
a安

51

技巧应该是带有-flag的VirtualAllocEXECUTE_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);
 }

完整示例(现在可用于X86和X64)。


30

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

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

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


1
不错的技巧。也许您可以将代码的某些部分复制到这篇文章中,以避免以后出现链接断开的问题。(或者只是在这篇文章中写一个简短的描述)。
Felix K.

AccessViolationException如果尝试运行您的示例,我会得到一个提示。我猜只有在DEP禁用的情况下才有效。
Rasmus Faber

1
但是,如果我使用EXECUTE_READWRITE标志分配内存并在_methodPtr字段中使用它,则可以正常工作。查看Rotor代码,这似乎基本上就是Marshal.GetDelegateForFunctionPointer()所做的,除了它在代码周围添加了一些额外的功能来设置堆栈和处理安全性。
Rasmus Faber 2012年

我认为该链接已失效,a,我会对其进行编辑,但找不到原始链接的重定位。
亚伯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.