是否可以完全用托管的 .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
字段,可以使用反射进行设置。下面是一些示例代码:
当然,这是一个肮脏的 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 编译器(本地代码)的主要内容,如果未能解决你的问题,请参考以下文章