Skip to content

Commit 1cb2cc3

Browse files
committed
Publish Baby's First JIT
1 parent f85240f commit 1cb2cc3

1 file changed

Lines changed: 217 additions & 0 deletions

File tree

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
---
2+
title: Baby's First JIT
3+
---
4+
5+
Let's learn how to write
6+
a basic [just-in-time compiler][jit].
7+
A JIT compiler is a piece of software
8+
which generates machine code at runtime
9+
*just* before executing it.
10+
Many supposedly "interpreted" languages
11+
actually compile code on the fly
12+
with this technique.
13+
14+
[jit]: https://en.wikipedia.org/wiki/Just-in-time_compilation
15+
16+
---
17+
18+
First we'll need to include some header files.
19+
Apart from the usual,
20+
we'll need `sys/mman.h`
21+
for [`mmap`][mmap] and [`mprotect`][mprotect]
22+
and `unistd.h` for [`getpagesize`][getpagesize].
23+
We'll also use [`err.h`][err] and [`sysexits.h`][sysexits]
24+
for error handling and exit codes, respectively.
25+
26+
#include <stdlib.h>
27+
#include <stdio.h>
28+
#include <stdint.h>
29+
#include <sys/mman.h>
30+
#include <unistd.h>
31+
#include <err.h>
32+
#include <sysexits.h>
33+
34+
[mmap]: https://www.freebsd.org/cgi/man.cgi?sektion=2&query=mmap
35+
[mprotect]: https://www.freebsd.org/cgi/man.cgi?sektion=2&query=mprotect
36+
[getpagesize]: https://www.freebsd.org/cgi/man.cgi?sektion=3&query=getpagesize
37+
[err]: https://www.freebsd.org/cgi/man.cgi?sektion=3&query=err
38+
[sysexits]: https://www.freebsd.org/cgi/man.cgi?sektion=3&query=sysexits
39+
40+
This is C by the way.
41+
42+
int main(int argc, char *argv[]) {
43+
return EX_OK;
44+
}
45+
46+
The main idea behind a JIT compiler
47+
is to allocate some memory,
48+
write machine code into it,
49+
then execute it.
50+
To allocate memory which can be executed,
51+
we need to use `mmap`.
52+
However,
53+
modern CPUs won't let us
54+
write and execute the same bit of memory
55+
at the same time,
56+
so we'll start by setting it read-write
57+
and switch it later.
58+
59+
Allocation through `mmap` also works
60+
only at the granularity of [pages][page].
61+
Since we won't be generating a whole lot of code,
62+
we'll just allocate one page worth of memory.
63+
64+
int page = getpagesize();
65+
uint8_t *code = mmap(NULL, page, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE, -1, 0);
66+
if (code == MAP_FAILED) err(EX_OSERR, "mmap");
67+
68+
[page]: https://en.wikipedia.org/wiki/Page_(computer_memory)
69+
70+
The `MAP_ANON` flag tells `mmap`
71+
to just map some plain old memory,
72+
rather than memory-map a file,
73+
which it can also do.
74+
The `-1` would be a file descriptor,
75+
if we were doing that.
76+
77+
The `MAP_PRIVATE` flag means that
78+
the contents of this page
79+
won't be shared between child processes,
80+
i.e. they each get their own copy-on-write data.
81+
This isn't relevant since we won't be calling [`fork`][fork].
82+
83+
[fork]: https://www.freebsd.org/cgi/man.cgi?sektion=2&query=fork
84+
85+
---
86+
87+
Next we'll have to write some machine code
88+
into the memory.
89+
How do we know what to write?
90+
We can ask an [assembler][assembly],
91+
such as [NASM][nasm].
92+
We're going to generate an adder function,
93+
which simply adds some number
94+
to its first argument
95+
and returns it.
96+
97+
bits 64
98+
mov rax, strict dword 0
99+
add rax, rdi
100+
ret
101+
102+
[assembly]: https://en.wikipedia.org/wiki/Assembly_language
103+
[nasm]: http://nasm.us
104+
105+
The `bits 64` directive
106+
tells NASM to generate x86_64 code.
107+
It can also generate 32- and 16-bit code.
108+
109+
The `mov` instruction
110+
sets the `rax` register
111+
to a 32-bit value of zero.
112+
This is the value we'll be replacing
113+
at runtime.
114+
The `strict` modifier
115+
tells NASM not to optimize
116+
the immediate (or literal)
117+
down to just one byte.
118+
119+
The `add` instruction
120+
then adds our value with `rdi`,
121+
which is the register in which
122+
the first argument is passed
123+
according to the [System V ABI][abi].
124+
The ABI also specifies that
125+
the return value of a function
126+
is stored in `rax`.
127+
128+
So with the result of our calculation
129+
in the correct register,
130+
we can use `ret`
131+
to return control to
132+
whichever function called this one.
133+
134+
[abi]: https://software.intel.com/sites/default/files/article/402129/mpx-linux64-abi.pdf
135+
136+
If we assemble this with `nasm foo.asm`,
137+
we can use a hexdump tool
138+
such as `xxd -g1`
139+
to inspect the machine code of `foo`.
140+
141+
48 c7 c0 00 00 00 00 48 01 f8 c3
142+
143+
And that's all it is.
144+
We can clearly see the four zero bytes
145+
making up our 32-bit immediate.
146+
Let's parse a number
147+
from the command line
148+
to replace this with.
149+
150+
if (argc < 2) return EX_USAGE;
151+
int32_t term = (int32_t)strtol(argv[1], NULL, 10);
152+
153+
Now we can write out the code to memory,
154+
keeping in mind that x86 is a [little-endian][endianness] architecture,
155+
which means that the least significant byte
156+
of a number appears first in memory.
157+
158+
code[0] = 0x48;
159+
code[1] = 0xc7;
160+
code[2] = 0xc0;
161+
code[3] = (uint8_t)term;
162+
code[4] = (uint8_t)(term >> 8);
163+
code[5] = (uint8_t)(term >> 16);
164+
code[6] = (uint8_t)(term >> 24);
165+
code[7] = 0x48;
166+
code[8] = 0x01;
167+
code[9] = 0xf8;
168+
code[10] = 0xc3;
169+
170+
[endianness]: https://en.wikipedia.org/wiki/Endianness
171+
172+
---
173+
174+
To call our generated function
175+
from C,
176+
we'll need a function pointer type
177+
to cast the `code` memory to.
178+
179+
typedef int32_t (*fptr)(int32_t);
180+
181+
This declares the `fptr` type
182+
as a pointer to a function
183+
which takes a single integer parameter
184+
and returns an integer.
185+
186+
Currently, though,
187+
trying to execute our generated code
188+
will crash the process.
189+
We first need to set the page's protection
190+
to allow execution and disallow writes.
191+
192+
int error = mprotect(code, page, PROT_READ | PROT_EXEC);
193+
if (error) err(EX_OSERR, "mprotect");
194+
195+
Now we can call the function
196+
with a few numbers
197+
and display the results.
198+
199+
fptr fn = (fptr)code;
200+
printf("%d %d %d\n", fn(1), fn(2), fn(3));
201+
202+
---
203+
204+
Time to compile some code on the fly!
205+
We've made a JIT compiler.
206+
207+
$ ./babyjit 1
208+
2 3 4
209+
$ ./babyjit 2
210+
3 4 5
211+
$ ./babyjit -4
212+
-3 -2 -1
213+
214+
The code is available as a [gist][gist],
215+
unobstructed by my words.
216+
217+
[gist]: https://gist.github.com/programble/2ec38cee7d654e7f1755c91d38882a88

0 commit comments

Comments
 (0)