Bawię się pomysłem napisania kompilatora JIT i zastanawiam się tylko, czy teoretycznie jest możliwe napisanie całości w kodzie zarządzanym. W szczególności, po wygenerowaniu asemblera w tablicy bajtów, jak wskoczyć do niej, aby rozpocząć wykonywanie?
84
calli
kodu operacyjnego IL, aby wywołać skompilowany kod.Odpowiedzi:
A pełny dowód słuszności koncepcji jest tutaj w pełni zdolnym tłumaczeniem podejścia Rasmusa do JIT na 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
to szczęśliwie wykonuje ustępstwa
Value before: FFFFFFFC Value after: 7FFFFFFE
źródło
Tak, możesz. Właściwie to moja praca :)
Napisałem GPU.NET całkowicie w F # (modulo nasze testy jednostkowe) - faktycznie deasembluje i JITs IL w czasie wykonywania, tak jak robi .NET CLR. Emitujemy kod natywny dla dowolnego podstawowego urządzenia akceleracyjnego, którego chcesz użyć; obecnie obsługujemy tylko procesory graficzne Nvidia, ale zaprojektowałem nasz system tak, aby można go było ponownie zaadresować przy minimalnym nakładzie pracy, więc prawdopodobnie w przyszłości będziemy obsługiwać inne platformy.
Jeśli chodzi o wydajność, to muszę podziękować za F # - po skompilowaniu w trybie zoptymalizowanym (z wywołaniami końcowymi) nasz kompilator JIT jest prawdopodobnie tak szybki, jak kompilator w środowisku CLR (który jest napisany w C ++, IIRC).
W przypadku wykonywania korzystamy z możliwości przekazania kontroli do sterowników sprzętu, aby uruchomić jitted kod; nie byłoby to jednak trudniejsze do zrobienia na procesorze, ponieważ .NET obsługuje wskaźniki funkcji do niezarządzanego / natywnego kodu (chociaż straciłbyś wszelkie bezpieczeństwo / ochronę zwykle zapewniane przez .NET).
źródło
Sztuką powinien być VirtualAlloc z
EXECUTE_READWRITE
FLAG (potrzebuje P / Invoke) i Marshal.GetDelegateForFunctionPointer .Oto zmodyfikowana wersja przykładu rotacji liczb całkowitych (pamiętaj, że nie jest potrzebny żaden niebezpieczny kod):
[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); }
Pełny przykład (teraz działa z X86 i X64).
źródło
Używając niebezpiecznego kodu, możesz „zhakować” delegata i wskazać dowolny kod asemblera, który został wygenerowany i przechowywany w tablicy. Chodzi o to, że delegat ma
_methodPtr
pole, które można ustawić za pomocą Odbicia. Oto przykładowy kod:Jest to oczywiście brudny hack, który może przestać działać w dowolnym momencie, gdy zmieni się środowisko wykonawcze .NET.
Wydaje mi się, że w zasadzie w pełni zarządzany bezpieczny kod nie może być dopuszczony do implementacji JIT, ponieważ złamałoby to wszelkie założenia dotyczące bezpieczeństwa, na których opiera się środowisko wykonawcze. (Chyba że wygenerowany kod asemblera został dostarczony z możliwym do sprawdzenia maszynowym dowodem, że nie narusza założeń ...)
źródło
AccessViolationException
jeśli spróbuję dać ci przykład. Myślę, że działa tylko wtedy, gdy DEP jest wyłączone.