JIT(Just-int-time) 编译器是任何程序在被转换成机器码的运行过程中产生的。JIT 代码和其他代码(比如,fmt.Println)的区别在于 JIT 代码是在运行过程中生成的。
用 Golang 编写的程序是静态类型且提前编译。生成任意代码似乎是不可能的,更不用说执行所述代码了。但是,可以将指令发送到正在运行的进程。这是使用 Type Magic 完成的 - 将任何类型转换为任何其他类型的能力。
x64 指令集上的 JIT 编译器
href="https://software.intel.com/en-us/articles/introduction-to-x64-assembly">x64 指令集以下代码必须在 x64 处理器上运行。
生成 x64 代码打印 "Hello World"
为了打印 “Hello World”,系统调用应该指示处理器打印数据。打印数据的系统调用是 。
此系统调用的第一个参数是要写入的位置,表示为文件描述符。将输出打印到控制台是通过写入标准文件描述符 stdout 来实现的。stdout 的文件描述符编号为 1.
第二个参数是必须写入的数据的位置。有关这方面的更多信息将在下一节中提供。
第三个操作数是 count - 即要写入的字节数。在 “Hello World!” 的情况下,要写入的字节数为 12。为了进行系统调用,需要将三个操作数保存在特定的寄存器中。这里有一个表格显示了保存操作数的寄存器。
Syscall #Param 1Param 2Param 3Param 4Param 5Param 6raxrdirsirdxr10r8r9
将所有这些放在一起,这里是一系列代表初始化一些寄存器的指令的字节。
- 第一条指令将 rax 设置为 1 - 表示写入系统调用。
- 第二条指令将 rdi 设置为 1 - 表示 stdout 的文件描述符
- 第三条指令将 rdx 设置为 12 以表示要打印的字节数。
- 数据的位缺失,实际上调用 write 就是如此
为了指定包含 “Hello World!” 的数据的位置,数据需要先拥有一个位置 - 即它需要存储在内存中的某个位置。
表示 “Hello World!” 的字节序列是 48 65 6c 6c 6f 20 57 6f 72 6c 64 21。这应该存储在处理器不会尝试执行的位置。否则,该程序将引发段错误(segmentation fault)。
在这种情况下,数据可以存储在可执行指令的末尾 - 即在返回指令之后。在返回指令之后存储数据是安全的,因为处理器在遇到返回时“跳”到不同的地址,并且不会顺序执行。
由于直到返回指令被布置时才知道过去的返回地址,所以可以使用它的临时占位符,并且一旦数据的地址已知就用正确的地址替换。这是连接器所遵循的确切程序。链接过程只需填写这些地址以指向正确的数据或函数。
在上面的代码中,加载 “Hello World!” 地址的 lea 指令指向自己(指向距离 rip 0 字节的位置)。这是因为数据尚未存储,数据地址未知。
系统调用本身由字节序列 0F 05 表示。
现在可以存储数据,因为返回指令已经布置。
在整个程序中,现在我们可以更新指令来指向数据。以下是更新的代码:
上面的代码可以表示为 Golang 中任何基本类型的片段。
[]uint16与上面列出的字节相比,上述字节略有偏差。这是因为当它与切片条目的开始对齐时,它更清晰(更易于读取和调试)来表示数据 “Hello World!”。
因此,我使用填充指令 cc 指令(无操作)将数据部分的开始推送到 slice 中的下一个条目。我还更新了 lea 指向 4 个字节的位置以反映这一变化。
注意:您可以在此`找到各种系统调用的系统调用号码。
转换切片函数
[]uint16Golang 函数值只是一个指向 C 函数指针的指针(注意两级指针)。从切片到函数的转换首先是提取一个指向保存可执行代码的数据结构的指针。这存储在 unsafePrintFunc 中。指向 unsafePrintFunc 的指针可以被转换为所需的函数类型。
此方法仅适用于没有参数或返回值的函数。需要为调用具有参数或返回值的函数创建堆栈帧。函数定义应始终以指令开始,以动态分配堆栈帧以支持可变参数函数。有关不同函数类型的更多信息,请参阅。
如果您希望我写关于在 Golang 中生成更复杂的函数的信息,请在下面评论。
使函数可执行
上述函数不会实际运行。这是因为 Golang 将所有数据结构存储在二进制文件的数据部分。本节中的数据设置了标志,阻止其执行。
printFunction slice 中的数据需要存储在一段可执行的内存中。这可以通过删除 printFunction slice 上的 No-Execute 标志或将其复制到可执行的内存位置来实现。
在下面的代码中,数据已被复制到一个新分配的可执行内存(使用 mmap)。这种方法比较好,因为只在整个页面上设置不执行标志 - 很容易使数据部分的其他部分无法执行。
标志 syscall.PROT_EXEC 确保新分配的内存地址是可执行的。将此数据结构转换为函数将使其运行平稳。
以下是完整的代码,尝试在x64机器上运行。
结论
尝试以上源代码。敬请期待 Golang 的深入探索!
本文由 原创编译, 荣誉推出
更多Go资讯,欢迎关注微信公众号:Go语言中文网