Estou brincando com a ideia de escrever um compilador JIT e apenas me perguntando se é mesmo teoricamente possível escrever a coisa toda em código gerenciado. Em particular, depois de gerar o assembler em uma matriz de bytes, como você pula para dentro dele para iniciar a execução?
84
calli
opcode IL para chamar o código compilado.Respostas:
E para a prova de conceito completa aqui está uma tradução totalmente capaz da abordagem de Rasmus para JIT em 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
que felizmente executa rendendo
Value before: FFFFFFFC Value after: 7FFFFFFE
fonte
Sim você pode. Na verdade, é meu trabalho :)
Eu escrevi GPU.NET inteiramente em F # (modulo nossos testes de unidade) - ele realmente desmonta e JITs IL em tempo de execução, assim como o .NET CLR faz. Emitimos código nativo para qualquer dispositivo de aceleração subjacente que você deseja usar; atualmente, só oferecemos suporte a GPUs da Nvidia, mas projetei nosso sistema para ser redirecionado com um mínimo de trabalho, então é provável que tenhamos suporte para outras plataformas no futuro.
Quanto ao desempenho, tenho que agradecer ao F # - quando compilado no modo otimizado (com chamadas de cauda), nosso compilador JIT em si é provavelmente tão rápido quanto o compilador dentro do CLR (que é escrito em C ++, IIRC).
Para a execução, temos a vantagem de poder passar o controle aos drivers de hardware para executar o código montado; no entanto, isso não seria mais difícil de fazer na CPU, pois o .NET suporta ponteiros de função para código nativo / não gerenciado (embora você perderia qualquer proteção / segurança normalmente fornecida pelo .NET).
fonte
O truque deve ser VirtualAlloc com o
EXECUTE_READWRITE
-flag (precisa de P / Invoke) e Marshal.GetDelegateForFunctionPointer .Aqui está uma versão modificada do exemplo de rotação de inteiro (observe que nenhum código inseguro é necessário aqui):
[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); }
Exemplo completo (agora funciona com X86 e X64).
fonte
Usando código inseguro, você pode "hackear" um delegado e fazê-lo apontar para um código de montagem arbitrário que você gerou e armazenou em uma matriz. A ideia é que o delegado tenha um
_methodPtr
campo, que pode ser definido usando o Reflection. Aqui está um exemplo de código:Este é, obviamente, um hack sujo que pode parar de funcionar a qualquer momento quando o tempo de execução do .NET for alterado.
Acho que, em princípio, o código seguro totalmente gerenciado não pode implementar o JIT, porque isso quebraria todas as suposições de segurança nas quais o tempo de execução depende. (A menos que o código de montagem gerado veio com uma prova verificável por máquina de que não viola as suposições ...)
fonte
AccessViolationException
se tentar seguir seu exemplo. Acho que só funciona se a DEP estiver desativada.