O desafio é escrever um programa capaz de gerar o código nativo para uma função “int foo () { return 10; }” e executá-lo. Parece impossível, mas não é. E com um pouco de esforço não deve consumir muito tempo também. Para isso vamos precisar saber algumas coisas antes.
Primeiro, é necessário saber um pouco de assembly e entender como funciona a calling convention de um pc 32bit. Feito isso o próximo passo é baixar os manuais da plataforma, sejam os da Intel ou da AMD. Estou acostumado com os da Intel, inclusive tenho eles em árvore morta.
Depois disso, precisamos aprender a usar a ferramenta mais importante para quem quer se aventurar nessa área, o disassembler. No meu caso, usei o objdump da gnu. Instalei via macports no OSX é o gobjdmp por algum motivo.
São dois comandos essenciais que vamos usar, o primeiro é o “gobjdump -d” que disassembla um binário padrão da plataforma, vamos usá-lo para roubar o assembly necessário para nossa função a ser compilada. O segundo é “gobjdump -D -b binary -m i386″ que desassembla um blog binário não estruturado, essa usamos para fazer dump em arquivos do nosso código gerado e verificar seu o conteúdo.
Feito isso, compilamos o código C da nossa função e o assembly dela é algo como:
1 2 3 4 5 6 7 | <_foo>: push %ebp mov %esp,%ebp sub $0x8,%esp mov $0xa,%eax leave ret |
Curto e simples, entender oque cada instrução faz deixo como exercício ao leitor porém. Para gerar essa seqüência em runtime precisamos saber como encodificar cada instrução e um manual da arquitetura é a melhor forma para tal. Por sorte não é necessário saber de nenhuma das formas avançadas de ModRM.
Mas primeiro precisamos alocar um bloco de memória no qual podemos escrever e executar. No caso de sistemas unix usamos a função mmap que permite mapear um bloco anônimo de memória [1] no processo corrente. Com C# no OSX isso é tão trivial quanto o seguinte[2]:
1 2 3 4 5 6 | [DllImport ("libc", EntryPoint="mmap")] public static extern IntPtr mmap (IntPtr addr, IntPtr len, int prot, int flags, uint off_t); int prot = 0x1 | 0x2 | 0x4; //PROT_READ | PROT_WRITE | PROT_EXEC int flag = 0x1000 | 0x0002; //MAP_ANON | MAP_PRIVATE var res = mmap (IntPtr.Zero, (IntPtr)4096, prot , flags, 0); |
Para escrever nesse bloco de memória recém alocado usamos UnmanagedMemoryStream que é uma bela mão na roda para esses casos. Por fim, para invocarmos o código usamos outra facilidade do framework, o método Marshal.GetDelegateForFunctionPointer. Quando fiz isso em Java a alguns anos esses 3 passos foram muito mais difíceis.
Enfim, chega de enrolação, o código completo para nosso super JIT é o seguinte:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | using System; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.IO; public delegate int NoArgsReturnInt (); class Test { [DllImport ("libc", EntryPoint="mmap")] public static extern IntPtr mmap (IntPtr addr, IntPtr len, int prot, int flags, uint off_t); public static unsafe void Main () { int prot = 0x1 | 0x2 | 0x4; //PROT_READ | PROT_WRITE | PROT_EXEC int flag = 0x1000 | 0x0002; //MAP_ANON | MAP_PRIVATE var res = mmap (IntPtr.Zero, (IntPtr)4096, prot , flags, 0); var mem = new UnmanagedMemoryStream ((byte*)res, 4096, 4096, FileAccess.ReadWrite); //push ebp mem.WriteByte (0x50 + 0x05); //mov esp, ebp mem.WriteByte (0x89); mem.WriteByte (0xE5); //mov 10, eax mem.WriteByte (0xB8 + 0x00); mem.WriteByte (0x0A); mem.WriteByte (0x00); mem.WriteByte (0x00); mem.WriteByte (0x00); //leave mem.WriteByte (0xC9); //ret mem.WriteByte (0xC3); NoArgsReturnInt dele = (NoArgsReturnInt)Marshal.GetDelegateForFunctionPointer (res, typeof (NoArgsReturnInt)); Console.WriteLine (dele ()); } } |
Incrivelmente simples e curto. Tudo bem que não dá nem para chamar de um brinquedo, porém seu propósito é explorar os princípios básicos por traz de um JIT compiler. Um próximo passo para ele seria gerar 1 função que recebe 2 ints e os soma. Ou tornar o código
menos tosco e mais OO.
[1] mmap permite que bloco de memória seja mapeado para um arquivo
[2] Os valores de prot e flag são específicos ao OSX, então talvez não funcionem em outros derivados unix. Esses valores costumam ficar em /usr/include/sys/mman.h
3 responses so far ↓
1 Marcos Vasconcelos // Sep 2, 2010 at 5:00 pm
Nada que eu não faça com ASM ou BCEL no Java.
2 Anderson // Nov 3, 2010 at 8:42 am
Olá!
Parabéns!
Gostei do blog, acabei de assinar seu feed; continuarei lendo seus futuros posts.
E qual não foi minha surpresa quando vi seu segundo post no feed, de data 01/12/2008, sobre uma bandida (Dilma Rousseff). Concordo em gênero e número com o que havia sido dito! Mas agora há um agravante: esta bandida foi eleita a ‘presidenta’ de nosso país! Apesar de todo meu humor negro e minha ironia, tenho dúvidas se isto é para rir ou chorar…
3 Andrius Bentkus // Feb 2, 2011 at 9:35 am
Nice article, this shows that it is possible to write a JIT engine almost entirely using C# (with a few interop calls) and Reflection.
Leave a Comment