Thinking Just-in-Time, Part 0: World's Simplest JIT
This post is the first in a series on JIT compilation.
A First Attempt
void* jit_buffer_alloc(size_t size) {
void* buffer = malloc(size);
if (NULL == buffer) fatal_error("malloc()");
return buffer;
}
void emit_code(void* ptr, byte_t* code, size_t length) {
memcpy(ptr, code, length);
}
void* buffer = jit_buffer_alloc(BUFFER_SIZE);
/**
* int return_one() { return 1; }
*/
byte_t code[] = {
0xB8, 0x01, 0x00, 0x00, 0x00, // mov eax, 0x1
0xC3 // ret
};
emit_code(buffer, code, sizeof(code));
// Tell the compiler how to call our function
return_one_f fn = buffer;
// Call our function
int one = fn();
$ ./driver0
[1] 59813 segmentation fault (core dumped) ./driver0
$ gdb driver0
...
(gdb) run
Starting program: driver0
Program received signal SIGSEGV, Segmentation fault.
0x00005555555592a0 in ?? ()
(gdb) bt
#0 0x00005555555592a0 in ?? ()
#1 0x00005555555552b8 in main () at driver0.c:54
The offending line in the source code, line 54, which (it should come as no surprise) is the line on which we invoke our JITed code: int one = fn();.
GDB gives us a couple other pieces of useful information: the address of the call site for our JITed function in the body of main(): 0x00005555555552b8, and the address of our JITed function itself: 0x00005555555592a0.
We can use the pmap tool to get some details of the virtual address space for the process in which driver0 executes.
$ pmap 60538
60538: driver0
0000555555554000 4K r---- driver0
0000555555555000 4K r-x-- driver0
0000555555556000 4K r---- driver0
0000555555557000 4K r---- driver0
0000555555558000 4K rw--- driver0
...
total 2368K
The main() function resides in the region that begins at address 0000555555555000. This region has permissions r-x--. In contrast, our JITed function is located in the region that begins at address 0000555555558000 which has permissions rw---. Importantly, our JITed code lies in a region that does not have execute (x) permissions, so when we try to execute code in this region we get a segmentation fault.
A Working JIT
- Allocate a page-aligned, writable region of memory
- Write code to this memory region
- Change the protections of this memory region, removing write permissions and adding execute permissions
- Run code in the now-executable region
void* jit_buffer_alloc(size_t size) {
void* ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (MAP_FAILED == ptr) fatal_error("mmap()");
return ptr;
}
void jit_buffer_free(void* ptr, size_t size) {
if (munmap(ptr, size) == -1) {
fatal_error("munmap()");
}
}
void jit_buffer_finish(void* ptr, size_t size) {
if (mprotect(ptr, size, PROT_READ | PROT_EXEC) == -1) {
fatal_error("mprotect()");
}
}
Now the following works:
/**
* int return_one() { return 1; }
*/
byte_t code[] = {
0xB8, 0x01, 0x00, 0x00, 0x00, // mov eax, 0x1
0xC3 // ret
};
void* buffer = jit_buffer_alloc(BUFFER_SIZE);
emit_code(buffer, code, sizeof(code));
jit_buffer_finish(buffer, BUFFER_SIZE);
int one = ((return_one_f)buffer)();
Accepting Arguments
/**
* int add(int x, int y) {
* return x + y;
* }
*/
byte_t code[] = {
0x48, 0x89, 0xF8, // mov rax, rdi
0x48, 0x01, 0xF0, // add rax, rsi
0xC3 // ret
};
int three = ((add_f)buffer)(1, 2);