Rodrigo Kumpera Weblog

Meus achados sobre tecnologia

Escrevendo um JIT em menos de 40 linhas de C#

August 30th, 2010 · 3 Comments

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

Tags: Programming

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