É 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?

c#  .net  f#  jit 

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

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


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


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


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
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.