É possível escrever um compilador JIT (para código nativo) inteiramente em uma linguagem .NET gerenciada

84

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?

JD
fonte
Não acredito que haja - embora você possa trabalhar em um contexto inseguro às vezes em linguagens gerenciadas, não acredito que você possa sintetizar um delegado a partir de um ponteiro - e de que outra forma você pularia para o código gerado?
Damien_The_Unbeliever
@Damien: um código inseguro não deixaria você gravar em um ponteiro de função?
Henk Holterman
2
Com um título como "como transferir dinamicamente o controle para código não gerenciado", você corre menos risco de ser fechado. Parece mais objetivo também. Gerar o código não é o problema.
Henk Holterman
8
A ideia mais simples seria escrever a matriz de bytes em um arquivo e deixar que o sistema operacional o execute. Afinal, você precisa de um compilador , não de um interpretador (o que também seria possível, mas é mais complicado).
Vlad,
3
Depois de compilar o código desejado pelo JIT, você pode usar APIs do Win32 para alocar alguma memória não gerenciada (marcada como executável), copiar o código compilado nesse espaço de memória e usar o calliopcode IL para chamar o código compilado.
Jack P.

Respostas:

71

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
Gene Belitski
fonte
Apesar do meu voto positivo, discordo: esta é a execução de código arbitrário , não JIT - JIT significa " compilação just in time ", mas não consigo ver o aspecto de "compilação" neste exemplo de código.
rwong
4
@rwong: O aspecto de "compilação" nunca esteve no escopo das questões originais. A capacidade do código gerenciado de implementar IL -> transformação de código nativo é meio aparente.
Gene Belitski
70

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).

Jack P.
fonte
4
O objetivo do NoExecute não é que você não pode pular para o código que você mesmo criou? Ao invés de ser possível salto para código nativo através de um ponteiro de função: não é não possível saltar para código nativo através de um ponteiro de função?
Ian Boyd,
Projeto incrível, embora eu ache que vocês teriam muito mais exposição se o tornassem gratuito para aplicativos sem fins lucrativos. Você perderia a vantagem do nível "entusiasta", mas valeria a pena pelo aumento da exposição de mais pessoas que o usam (eu sei que definitivamente o faria;)) !
BlueRaja - Danny Pflughoeft
@IanBoyd NoExecute é mais uma maneira de evitar problemas de estouro de buffer e problemas relacionados. Não é uma proteção de seu próprio código, é algo para ajudar a mitigar a execução ilegal de código.
Luaan de
51

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).

Rasmus Faber
fonte
30

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 _methodPtrcampo, 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 ...)

Tomas Petricek
fonte
1
Belo hack. Talvez você possa copiar algumas partes do código neste post para evitar problemas posteriores com links quebrados. (Ou apenas escreva uma pequena descrição neste post).
Felix K.
Eu recebo um AccessViolationExceptionse tentar seguir seu exemplo. Acho que só funciona se a DEP estiver desativada.
Rasmus Faber
1
Mas se eu alocar memória com o sinalizador EXECUTE_READWRITE e usar isso no campo _methodPtr, funcionará bem. Olhando através do código do Rotor, parece ser basicamente o que Marshal.GetDelegateForFunctionPointer () faz, exceto que adiciona alguns thunks extras em torno do código para configurar a pilha e lidar com a segurança.
Rasmus Faber
Acho que o link está morto, infelizmente, gostaria de editá-lo, mas não consegui encontrar uma realocação do original.
Abel