没脚的雀

JIT入门: Hello, JIT World: The Joy of Simple JITS(翻译)

简介

一篇关于JIT入门的译文,原文传送: http://blog.reverberate.org/2012/12/hello-jit-world-joy-of-simple-jits.html

Hello, JIT World: The Joy of Simple JITS

本文演示了一个简单的JIT编译器能够完成什么样的有趣的事情。JIT,听起来就像是程序中最深层的魔法,似乎只有团队中最核心的成员才会有接触它的想法。它让人联想起类似于 JVM, .NET 这些需要成千上万行代码的大型运行时系统。 也许你从未见过在 JIT 领域中的 “Hello World” 程序,这正是这篇文章尝试改变的。

如果你仔细想想,一个 JIT 其实和一个普通的 printf 函数没什么太大的不同,它们的区别在于: printf 输出了类似于 “Hello, HMM” 这样的信息,而一个 JIT 输出的是一段机器码。 当然了,类似于 JVM 的 JITs 确实是一些复杂的事务,但那是因为它们的任务是在一些复杂的平台上完成一些代码优化任务。 如果我们将事情简化,我们的程序也能够变得十分简单。

完成一个简单的 JIT 最为困难的部分在于,如何输出机器码使得你的目标CPU能够执行这些机器码。举个例子来说, 在x86-64平台中,push rbp 汇编指令被编码为 0x55. 完成这些编码工作需要读许多的CPU手册, 因此我们将跳过这部分的工作,相反使用另外一个非常 nice 的库 DynASM 来处理这些指令编码。DynASM 为你提供了一些巧妙的方式来处理由C编写的JIT 生成的汇编代码。这个库支持了许多的CPU架构(x86, x86-64, PowerPC, MIPS 以及 ARM) 。 同时, DynASM非常的校,完整的运行时包含在一个500-line的头文件中。

在这里,我先阐明一个术语。 我把任何能够在运行时执行生成代码的程序都称为 JIT. 另外一些人可能将 “JIT” 用在更特别的地方。它们更愿意用 JIT 来指代一些混合了解释器和编译器来生成机器码的程序。这些作者会将在运行时生成机器码的技术成为动态编译。 但 JIT 是一个更加普遍更具有标志性的术语,并且常常会被用在与 JIT 的定义截然相反的某些途径上。

Hello, JIT World!

在跳进更大的坑之前,先来看看简单的JIT。 这篇文章的代码都在这个 Github 仓库中 jitdemo . 这些代码只能运行在 Unix 平台上,以及 x86-64 CPU 架构上。

对于第一个例子,我们甚至不会使用 DynASM ,这个程序文件是: jit1.c

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>

int main(int argc, char *argv[]) {
// Machine code for:
// mov eax, 0
// ret
unsigned char code[] = {0xb8, 0x00, 0x00, 0x00, 0x00, 0xc3};

if (argc < 2) {
fprintf(stderr, "Usage: jit1 <integer>\n");
return 1;
}

// Overwrite immediate value "0" in the instruction
// with the user's value. This will make our code:
// mov eax, <user's value>
// ret
int num = atoi(argv[1]);
memcpy(&code[1], &num, 4);

// Allocate writable/executable memory.
// Note: real programs should not map memory both writable
// and executable because it is a security risk.
void *mem = mmap(NULL, sizeof(code), PROT_WRITE | PROT_EXEC,
MAP_ANON | MAP_PRIVATE, -1, 0);
memcpy(mem, code, sizeof(code));

// The function will return the user's value.
int (*func)() = mem;
return func();
}

似乎很难相信代码中第33行, 但是这就是一个 合法正统的 JIT. 这个程序动态生成了返回在运行时指定的整数的函数,并返回该函数的调用值。你可以这样验证它:

1
2
$ ./jit1 42 ; echo $?
42

你应该注意到了,我是用了mmap而非常用的从堆中获取动态内存的malloc方法。这是因为,我们需要一段能够被执行的内存,以便于我们在跳到该地址执行该生成的代码时,不会造成程序的冲突。在大多数系统中,堆栈的内存不具有可执行的属性。通常来说,我们需要避免对一块内存同时设置 可写 以及 可执行的属性(writable & excutable). 在上述的代码中,我没有遵循这个规则,但这是为了是我们的演示代码更加简单。

同样的,我没有在程序中释放动态申请的内存。不过后面我们将会补上。mmap() 函数 有一个 对应的用来释放内存的 munmap()。

你也许会好奇,为什么不能够通过改变从malloc中申请的内存的属性,使它具有可执行的属性来执行我们的代码?通过代码中的方式来申请可执行的内存看起来十分麻烦。事实上,能够用来改变你已经拥有的内存的属性的函数,叫做 mprotect(); 但这个函数只对 page boundaries 有效; malloc() 返回的内存来自一个page的中间的内存,你并不能够完整地拥有整个page。 如果你改变了整个page的属性,将会影响了另外一些使用同一个page的程序;

Hello, DynASM World!

DynASM 作为 LuaJIT 的一部分,但完全独立于 LuaJIT, 能够独立地使用。它包含了两个部分: 一个将 C\assembly 混合的文件(.dasc) 转换成 C 文件的预处理器。以及一个 tiny 运行时用来将必要的 C 代码链接进程序中。

这个设计非常nice,因为像 解析 汇编语言以及生成机器码的功能能够使用 Lua 这种高级语言来编写。

我们第一个使用 DynASM 的例子,将会保持和第一个例子相同的功能,以便于我们能够通过对比两个程序来理解 DynASM 的功能;

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
// DynASM directives.
|.arch x64
|.actionlist actions

// This define affects "|" DynASM lines. "Dst" must
// resolve to a dasm_State** that points to a dasm_State*.
#define Dst &state

int main(int argc, char *argv[]) {
if (argc < 2) {
fprintf(stderr, "Usage: jit1 <integer>\n");
return 1;
}

int num = atoi(argv[1]);
dasm_State *state;
initjit(&state, actions);

// Generate the code. Each line appends to a buffer in
// "state", but the code in this buffer is not fully linked
// yet because labels can be referenced before they are
// defined.
//
// The run-time value of C variable "num" is substituted
// into the immediate value of the instruction.
| mov eax, num
| ret

// Link the code and write it to executable memory.
int (*fptr)() = jitcode(&state);

// Call the JIT-ted function.
int ret = fptr();
assert(num == ret);

// Free the machine code.
free_jitcode(fptr);

return ret;
}

这不是一个完成的程序; 另外一个用来初始化 DynASM 以及 分配和释放可执行内存的辅助函数定义在 dynasm-driver.c 中。 我们会在所有的例子中使用相同的辅助代码,所以在这里我省略它们了;

大佬给口饭吃咧