Appearance
Python 编译时生成的文件 jit_stencils.h,在这个文件中前面两个数组的定义其实很好理解,我们也可以很容易的知道它们是怎么生成。copy-and-patch 最难的过程不是 copy,而是 patch,因为我们不知道如何打补丁,具体在哪个位置对机器码和数据块修改打补丁。
看到生成的文件,你应该会好奇这些函数是 patch_64,patch_32r,patch_x86_64_32rx 等等是做什么用的,然后他们的参数为什么这么奇怪,有些是 data 加一个偏移量,有些是 code 加一个十六进制的偏移量,第二个参数有些是 Python 的变量,另一部是 data 的指针位置加一个偏移量。
JIT emit_* 函数最终生成形态
如下代码便是操作码 BINARY_OP 生成的 jit 函数。
c
void
emit__BINARY_OP(
unsigned char *code, unsigned char *data, _PyExecutorObject *executor,
const _PyUOpInstruction *instruction, jit_state *state)
{
const unsigned char code_body[221] = {};
const unsigned char data_body[88] = {};
memcpy(data, data_body, sizeof(data_body));
patch_64(data + 0x20, instruction->oparg);
patch_64(data + 0x28, (uintptr_t)&_PyEval_BinaryOps);
patch_64(data + 0x30, (uintptr_t)&_Py_NegativeRefcount);
patch_64(data + 0x38, (uintptr_t)&_Py_DECREF_DecRefTotal);
patch_64(data + 0x40, (uintptr_t)&_Py_Dealloc);
patch_64(data + 0x48, (uintptr_t)code + sizeof(code_body));
patch_64(data + 0x50, state->instruction_starts[instruction->error_target]);
memcpy(code, code_body, sizeof(code_body));
patch_32r(code + 0x11, (uintptr_t)data + 0x1c);
patch_x86_64_32rx(code + 0x18, (uintptr_t)data + 0x24);
patch_32r(code + 0x48, (uintptr_t)data + -0x4);
patch_x86_64_32rx(code + 0x56, (uintptr_t)data + 0x2c);
patch_x86_64_32rx(code + 0x64, (uintptr_t)data + 0x34);
patch_x86_64_32rx(code + 0x81, (uintptr_t)data + 0x3c);
patch_32r(code + 0x90, (uintptr_t)data + -0x4);
patch_x86_64_32rx(code + 0x9e, (uintptr_t)data + 0x2c);
patch_x86_64_32rx(code + 0xab, (uintptr_t)data + 0x34);
patch_x86_64_32rx(code + 0xc4, (uintptr_t)data + 0x44);
patch_x86_64_32rx(code + 0xcd, (uintptr_t)data + 0x3c);
patch_x86_64_32rx(code + 0xd9, (uintptr_t)data + 0x4c);
}
看代码找规律,代码分为两个部分一部分是 data,一部分是 code,分别由 memcpy 领头。紧随其后的是不同的 patch 参数,有两个参数,data 部分的第一个参数是 data 加上偏移量,第二个参数是具体的变量,没有偏移量。而 code 部分第一个参数是 data 加上偏移量,第二个参数都是 (uintptr_t)data
加上一个十六进制的数值。要分析就需要从代码中找出这些规律来,以便我们更好的推进分析。
memcpy 的作用很好理解,就是内存拷贝。memcpy(data, data_body, sizeof(data_body))
是将 data_body 的内存拷贝到 data 中;同理 memcpy(code, code_body, sizeof(code_body))
将 code_body 的内存拷贝到 code。
随便找个函数,在目录 Tools/jit
下进行搜索,可以找到这个补丁函数的映射关系。x86_64-unknown-linux-gnu
对应的补丁函数较少,本着学习研究的目的,由简入繁,决定先找个比较轻松的 target(x86_64-unknown-linux-gnu)的研究。
Tools/jit 下各个文件作用
- Tools/jit/_llvm.py。构造并执行 llvm 相关命令
- Tools/jit/_schema.py。Section 的定义,如:ELFSection
- Tools/jit/_stencils.py。将 JSON 文件的内容转换为我们需要的数据结构
- Tools/jit/_targets.py。不同平台的可执行程序二进制文件的数据结构,如 _ELF,_MachO 等
- Tools/jit/_writer.py。dump 数据,按照固定的模版,将活动的数据嵌入模版中,并写入到文件中
如下是 Python 生成 emit_*
函数相关的代码。我们可以不用了解每个 Python 类和函数的具体实现。但是需要简单知道它们的作用。
object JSON 文件解析,以及生成 Hode 结构
python
# Map relocation types to our JIT's patch functions. "r" suffixes indicate that
# the patch function is relative. "x" suffixes indicate that they are "relaxing"
# (see comments in jit.c for more info):
_PATCH_FUNCS = {
# aarch64-apple-darwin:
"ARM64_RELOC_BRANCH26": "patch_aarch64_26r",
...
# x86_64-unknown-linux-gnu:
"R_X86_64_64": "patch_64",
"R_X86_64_GOTPCREL": "patch_32r",
"R_X86_64_GOTPCRELX": "patch_x86_64_32rx",
"R_X86_64_PC32": "patch_32r",
"R_X86_64_REX_GOTPCRELX": "patch_x86_64_32rx",
...
}
接下来我们反向去推断 patch 的生成过程。找到 Tools/jit/_writer.py 的 def _dump_stencil(opname: str, group: _stencils.StencilGroup) -> typing.Iterator[str]:
函数,打印输出 hole.as_c(part)
和 hode。
c
patch_x86_64_32rx(code + 0x13, (uintptr_t)data + 0x4);
Hole(
offset=19,
kind='R_X86_64_GOTPCRELX',
value=<HoleValue.DATA: 3>, symbol=None, addend=18446744073709551620,
need_state=False,
func='patch_x86_64_32rx'
)
offset=19 是偏移量,换算成 16 进制就是 0x13,0x4 是什么呢?查看代码知道这个值是通过调用 _signed(self.addend)
得到的,也就是:18446744073709551620 % (1 << 64) = 0x4。
python
def _signed(value: int) -> int:
value %= 1 << 64
if value & (1 << 63):
value -= 1 << 64
return value
如何找到 addend 和 kind = 'R_X86_64_GOTPCRELX'
?在上一篇文章中,我们通过 llvm-readobj
打印出了一个 JSON,从 JSON 中我们就可以找到。在 Linux 下二进制可执行文件的结构为 ELF,所有我们只需要关注 _targets.py 文件的 class _ELF():
,它便是将 JSON 转化为我们需要的 Hold 结构的关键逻辑所在。
json
{
"Relocation": {
"Offset": 335,
"Type": {
"Name": "R_X86_64_PC32",
"Value": 2
},
"Symbol": {
"Name": ".L__PRETTY_FUNCTION__._PyFrame_GetStackPointer",
"Value": 9
},
"Addend": 18446744073709552000
}
}
value 映射关系,每个 HodeValues 有一个对应的 C 表达式,作为 patch 的第二个参数或者参数的一部分。
python
# Translate HoleValues to C expressions:
_HOLE_EXPRS = {
HoleValue.CODE: "(uintptr_t)code",
HoleValue.CONTINUE: "(uintptr_t)code + sizeof(code_body)",
HoleValue.DATA: "(uintptr_t)data",
HoleValue.EXECUTOR: "(uintptr_t)executor",
# These should all have been turned into DATA values by process_relocations:
# HoleValue.GOT: "",
HoleValue.OPARG: "instruction->oparg",
HoleValue.OPERAND0: "instruction->operand0",
HoleValue.OPERAND0_HI: "(instruction->operand0 >> 32)",
HoleValue.OPERAND0_LO: "(instruction->operand0 & UINT32_MAX)",
HoleValue.OPERAND1: "instruction->operand1",
HoleValue.OPERAND1_HI: "(instruction->operand1 >> 32)",
HoleValue.OPERAND1_LO: "(instruction->operand1 & UINT32_MAX)",
HoleValue.TARGET: "instruction->target",
HoleValue.JUMP_TARGET: "state->instruction_starts[instruction->jump_target]",
HoleValue.ERROR_TARGET: "state->instruction_starts[instruction->error_target]",
HoleValue.ZERO: "",
}
最终通过 _writer.py 生成 jit_stencils.h
在 _targets.py 和 _stencils.py 处理之后,拿到我们需要的 Hole 数组后,_writer.py 的处理逻辑会遍历这个数组,按照模版数据填充的方式将这些 patch 写入到 jit_stencils.h 中。这样整个字节码转化为 JIT 机器代码的工作就结束了。
思考
patch 之后是 Python 源码的继续编译。我们接下来的疑问是:这些 patch 代码是如果工作的?这个 patch 在 Python runtime 如何被运行?带着这些问题我们将继续探索。