MC-Emitter(getBinaryCodeForInstr)
本页中的每个偏移、值和地址都从
libtpu-0.0.40-cp314wheel 中的libtpu.so按字节精确读取(BuildID md589edbbe81c5b328a958fe628a9f2207d)。其他版本会有所不同。
摘要
TPUMCCodeEmitter::getBinaryCodeForInstr(0x13c74da0,0x29c6 字节)是 LLVM-MC 函数,它把一条 LLO 机器指令(MCInst)降低成其打包位模式。它是标准的 TableGen 生成宽指令 emitter:读取 opcode,对其做边界检查,把每个 opcode 的基础位从 InstBits 表复制到一个 239 位 APInt 记录,然后运行按 opcode 分派的 switch,其函数体是一串操作数编码器,用 APInt::insertBits(value, pos, width) 把记录中的空洞覆盖掉。该结构符合通用 LLVM MCCodeEmitter 模板;TPU 的特殊之处在于宽记录(239 位、4 个 word)、默认基础位数组和 BarnaCore 基础位数组之间的双表 HwMode 选择,以及 emitter 在多个字段间复用的逐操作数暂存 APInt。
对重实现者而言,C++ 骨架不是契约。契约是以下内容的组合:
- opcode 边界和索引算术(
opcode − 499、5667 条记录、32 字节 stride)。 InstBits(默认,全零)和InstBits_BarnaCorePxcHwMode(有填充)之间的 HwMode 选择。- 22 个函数体的
switch,以及哪些 opcode 类路由到零基础默认路径、哪些路由到真实的insertBits序列。 - 逐操作数编码器:
encodePredicateOperand、getMachineOpValue和getSpecialOpEncoding描述符查询,以及每次写入的(pos, width)。 - 伪指令防护和越界/default 分支上都会触发的
reportUnsupportedInst陷入。
| 函数 | llvm::(anon)::TPUMCCodeEmitter::getBinaryCodeForInstr @ 0x13c74da0(0x29c6 B) |
| 签名 | (MCInst&, SmallVectorImpl<MCFixup>&, APInt& record, APInt& scratch, MCSubtargetInfo&) const |
| 记录 | APInt,239 位,4 个 word — 见 记录格式 |
| 基础表 | InstBits @ 0x3366d90 / InstBits_BarnaCorePxcHwMode @ 0x33931f0(由 HwMode 选择) |
| 分派 | 自相对跳转表 @ 0xaed7dac(int32,由原始 opcode 索引,加到表基址上);22 个不同 case 函数体 |
| 默认分支 | 0x13c74e9d — 零基础,无 insertBits,返回(4457 个 opcode) |
| 操作数编码器 | encodePredicateOperand @ 0x13c77c40,getMachineOpValue @ 0x13c777e0 |
| 陷入 | MCCodeEmitter::reportUnsupportedInst @ 0x1a31c420,通过两个函数内 stub 到达(0x13c77750 ud1,0x13c7775d int3) |
| 置信度 | CONFIRMED(字节锚定),除非行内另有说明 |
发射流水线
发射路径具有固定的五步形态,全部可在反编译函数体中看到。该函数接收 MCInst(a2)、fixup 列表(SmallVectorImpl<MCFixup>&)、两个 APInt& 输出(a3 = 记录,此处别名为 v260;a4 = 逐操作数 scratch)以及 MCSubtargetInfo(a5)。
// TPUMCCodeEmitter::getBinaryCodeForInstr @ 0x13c74da0 (annotated pseudocode)
uint64_t emit(MCInst *inst, MCFixup_vec *fixups,
APInt *record /*a3/v260*/, APInt *scratch /*a4*/,
MCSubtargetInfo *sti) {
uint32_t opc = inst->opcode; // step 1: read opcode
if (opc <= 0x1F2) // opcode <= 498
reportUnsupportedInst(); // -> pseudo/target-indep: trap
// step 2: ensure the scratch APInt is 239 bits wide (zext if not)
if (scratch->BitWidth != 239)
scratch->zext(239);
uint32_t index = 4 * opc - 1996; // step 3: (opcode-499)*4 words
if (index >= 0x588D) // 5667-record bound
trap(); // ud1
// step 4: copy the per-opcode base row into the record (HwMode-selected table)
*record = APInt(239, &InstBits[index*8], /*NumWords=*/4);
// step 5: per-opcode operand fill
switch (opc) {
// ... 21 real BarnaCore encoding cases (insertBits sequences) ...
default: // 4457 opcodes: TC + V5+
return record_normalize(record); // zero base, no insertBits
}
}
```text
第 1 步和第 3 步是两处边界检查。opcode `≤ 498` 是 MC 伪指令/目标无关形式(`PHI`、`INLINEASM`、预展开的 `BR`/`BRcond`/`BRret` 伪分支),它们永远不会到达真实编码,并会在伪指令防护处陷入(`cmp $0x1f2; jbe`)。opcode `≥ 499 + 5667 = 6166` 超出表范围,会在 `ud1` 上陷入。两者之间的所有内容都以 `(opcode − 499) × 32` 字节索引基础位表;`InstBits` 大小为 `0x2c460` 字节 = 5667 行、32 字节 stride。
第 2 步是对 scratch `APInt` 的宽度防护:如果调用方传入的 scratch 还不是 239 位,emitter 会对它执行 `zext`(释放此前更宽用法留下的堆存储)。从这点之后,两个 `APInt` 都是 239 位宽。
第 4 步从基础行构建记录。第 5 步是按 opcode 的 `switch`;反编译器把它展开为每个活跃 opcode 一个 `case` 标签(`499..5666`,5168 个标签),共享 22 个函数体。压倒性多数(TensorCore 和 V5+ opcode)落到零基础默认函数体,该函数体什么也不做,因为其基础为零且这些指令由 proto-bundle 路径编码,而不是在这里编码。
---
## HwMode 表选择
emitter 在两个基础位表之间选择。默认 `InstBits`(`0x3366d90`)在序言中读取;BarnaCore 变体 `InstBits_BarnaCorePxcHwMode`(`0x33931f0`,紧随其后)在有内容的 BarnaCore case 函数体内部读取。反编译器把选择呈现为两个不同的 GOT 相对基址计算,它们送入同一个 `APInt(239, base, 4)` 构造:
```c
// prologue (default path): InstBits (GOT - 65189758 -> 0x3366d90)
APInt(&record, 239, &GLOBAL_OFFSET_TABLE_ + index4 - 65189758, 4);
// inside a BarnaCore case body: InstBits_BarnaCorePxcHwMode (GOT - 65167090 -> 0x33931f0)
APInt(&record, 239, &GLOBAL_OFFSET_TABLE_ + index4 - 65167090, 4);这两个偏移以 8 字节(_QWORD)GOT 单位计,差值为 65189758 − 65167090 = 22668 qword = 0x2c460 字节,正好是每个数组的大小,确认二者背靠背排列(InstBits 0x3366d90 + 0x2c460 = InstBits_BarnaCorePxcHwMode 0x33931f0)。从 BarnaCore 表重新读取基础的 case 函数体,会先通过 (*(vtable + 0x28))(MCSubtargetInfo, 3) 查询 subtarget 的 HwMode:特性索引 3,即 BarnaCorePxcHwMode 查询。当该模式激活时,BarnaCore 基础会提供真实的 opcode 判别位;否则使用默认零基础。由于默认表全零,实际效果是二元的:要么该指令是 BarnaCore-Pxc op(真实基础位、真实 insertBits 序列),要么它是 TensorCore / V5+ op(零基础,无 insertBits)。
| HwMode | 基础表 | 磁盘上 | 此处编码的 opcode |
|---|---|---|---|
| default | InstBits @ 0x3366d90 | 全零,无 RELA | 无(返回全零记录) |
BarnaCorePxcHwMode | InstBits_BarnaCorePxcHwMode @ 0x33931f0 | 704 个有填充的行(范围 2855..3991) | Pufferfish BarnaCore lane + 原生 op |
注意 — 全零默认表不是加载时占位符。没有
.rela.dyn重定位以任一InstBits范围为目标,因此零就是实际编码。若重实现期望链接器在加载时填充这些基础位,就会把每条 TensorCore 和 V5+ 指令编码为全零且无法察觉,因为 proto-bundle 路径会在下游提供真实字节。
按 Opcode 分派
switch 实现为 0xaed7dac 处的自相对跳转表。分派序列按字节精确为:lea -0x8d9d0e8(%rip),%rcx(表基址 0xaed7dac)、movslq (%rcx,%rbx,4),%rdx(加载 table[raw_opcode] 处的带符号 int32)、add %rcx,%rdx(相对表基址重定位)、jmp *%rdx。索引是原始 opcode(%rbx),不是 opcode − 499,条目是相对表基址的偏移;默认值 0x08d9d0f1 解析为 0xaed7dac + 0x08d9d0f1 = 0x13c74e9d,即零基础默认分支。前 499 个条目(opcode 0..498)是死填充,越过伪指令防护后永远不可达;另有一个 cmp $0x1622; ja(opcode > 5666)限制索引上界。反编译器把活跃范围呈现为 5168 个 case 标签,折叠到22 个不同 case 函数体,这一点已通过读取全部 5667 个表条目按字节确认。下面的直方图是结构契约:哪些 opcode 类带有真实 insertBits 序列,哪些原样返回零基础。
| Case 函数体 | #opcode | 作用 |
|---|---|---|
0x13c74e9d | 4457 | DEFAULT — 零基础,无 insertBits,返回(全部 TensorCore + V5+) |
0x13c74eb9 | 384 | BarnaCore _V1/_V2 vector-ALU lane |
0x13c74f47 | 195 | BarnaCore _V0 vector-ALU lane(predicate + operand insertBits) |
0x13c754d1 | 34 | BarnaCore compose/sub _V1/_V2 |
0x13c75559 | 18 | BarnaCore VIMM*/VMOV* _V1/_V2/_VM |
0x13c75723 … 0x13c75d77 | 17+12+10+9+6+6+5 | 七个更小的 BarnaCore vector 类 |
0x13c765a0 / 0x13c76628 / 0x13c766b0 / 0x13c76738 | 2 each | BarnaCore store-slot 类(bcVST*) |
0x13c767c0 | 2 | bcVLDi/bcVLDr load slot(26 个 insertBits) |
0x13c770f8 | 1 | bcLOOP_START loop slot |
0x13c77180 / 0x13c77208 / 0x13c77290 / 0x13c77318 | 1 each | BarnaCore singleton(ops 3789/3787/3786/3588) |
default(reportUnsupportedInst) | — | 越界 opcode 在跳转表前被陷入(> 5666) |
这种不对称正是关键。5168 个活跃 opcode 中有 4457 个,也就是 TensorCore 和 V5+ 侧的每个具体 branch/call/halt、每个 load/store、每个 compute op,都会路由到零基础默认路径。它们的字节不是在这里发射的;而是由 proto-bundle EmitX 填充器和逐 slot 的 <Slot>Encoder::Encode BitCopy 调用产生。只有 711 个 opcode(全部位于 BarnaCore 范围 2855..3991)在此函数中带有真实 insertBits 序列。若重实现把默认分支当成错误路径,就会把 4457 个合法 opcode 误诊为错误;若重实现试图通过此函数编码 V5+ 指令,则会发射全零 bundle。
怪异点 — sequencer branch/call opcode 会到达 switch,但编码为零。
BRabs(505)、BRind(507)、BRrel(508)、BRrelrot(509)、CALLabs(514)、CALLrel(515) 和HALT(571) 是真实 MC opcode,会索引跳转表,但它们路由到零基础默认路径。它们的 20 位偏移、destination/x 寄存器和 predication 由 proto-bundleEmitBranchOp/EmitCallOp/EmitImmediate/EmitPredicationToSlot路径写入,而不是由getBinaryCodeForInstr写入。MC emitter 对 V5+ branch 的位完全没有贡献。见 V5+ EmitX 位位置。
操作数编码器
在真实 case 函数体内部,操作数填充是一串 insertBits 写入,由两个 helper 和逐操作数“暂存再写入”惯用法提供输入。
encodePredicateOperand(0x13c77c40)写入 slot 相对的 7 位 predicate 字段。它接收一个逐 slot 索引参数(第三个参数;17 个调用点中出现的 1/2/3/4 值选择正在编码哪个 slot 的 predicate),并在 bit 0 写入一个 4 位寄存器索引,在 bit 4 写入 negate 位,在 bits 5:6 写入 2 位 mode:
// encodePredicateOperand @ 0x13c77c40 (decompiled, exact)
// a1 = MCInst&, a2 = the per-operand array base, a3 = slot index (1/2/3/4), a4 = dst APInt
__int64 encodePredicateOperand(MCInst *inst, MCInst_ops *ops, uint32_t slot, APInt *dst) {
uint64_t flags = ops[slot].flags; // *(a2 + 16*slot + 24): operand flags word
uint32_t reg = ops[slot].preg; // *(a2 + 16*slot + 8): predicate-register operand
insertBits(dst, *(uint16_t *)(inst + 2*reg), /*pos=*/0, /*width=*/4); // reg index (P0..P14)
if (flags & 1)
dst->word0 |= 0x10; // bit 4: negate / inversion
return insertBits(dst, (flags >> 5) & 3, /*pos=*/5, /*width=*/2);// bits 5:6: mode
}
```text
`getMachineOpValue`(`0x13c777e0`)是通用操作数降低。它定位操作数在 `MCInst` 中的索引,调用 `llvm::getSpecialOpEncoding(MCInstrDesc&, opno)`(`0x13c63a80`,已确认)从逐 opcode 描述符(`TPUDescs`)读出该操作数的编码类,并产生寄存器编码(`uint16` 寄存器索引查表 — 具体表地址 UNVERIFIED)、立即数、表达式值或 `MCFixup`。结果暂存在 scratch `APInt` 中,case 函数体再读回相关低位,并把它们写入类固定位置。
因此,case 函数体中的通用逐操作数模式是:清零 scratch,把操作数降低进去,提取所需位,把它们写入 record。store-slot 尾部展示了从单个已降低操作数进行三次这样的写入(bit 207 的 16 位 displacement、bit 141 的 2 位 qualifier、bit 35 的 6 位字段):
```c
// excerpt: bcVST store-slot operand fill (getBinaryCodeForInstr, decompiled)
*scratch = 0;
getMachineOpValue(self, inst, &inst->op[2], scratch);
insertBits(record, extractBitsAsZExtValue(scratch, /*w=*/0x10, /*lo=*/8), /*pos=*/0xCF, 0x10); // imm16 @ bit 207
insertBits(record, extractBitsAsZExtValue(scratch, /*w=*/2, /*lo=*/0), /*pos=*/0x8D, 2); // qualifier @ bit 141
insertBits(record, extractBitsAsZExtValue(scratch, /*w=*/6, /*lo=*/2), /*pos=*/0x23, 6); // base-reg @ bit 35(pos, width) 常量按编码类固定:bit 35(base register)、bit 88(load destination)、bit 207(immediate displacement),以及 BarnaCore slot 的其他位置。值则由 getSpecialOpEncoding 通过描述符驱动。这种拆分是编码器的核心:位置来自类,值来自描述符和寄存器编码表。参见 InstBits DB,了解这些 helper 读取的描述符、名称和寄存器编码表。
返回和陷入路径
正常退出时,emitter 会规范化 record APInt(当值宽于一个 inline word 时为 assignSlowCase)并返回它。有两条路径会陷入而不是返回:
- 伪指令防护(
opcode ≤ 498,cmp $0x1f2; jbe 0x13c77750)会在任何表访问前调用reportUnsupportedInst;这些 opcode 会在 MC 发射前被展开,不应到达 emitter。该 stub 以ud1结束。 - 越界 /
switchdefault分支也会调用reportUnsupportedInst。当 opcode 超过表边界时会到达这里:对 word indexv8 = 4·opcode − 1996的索引检查cmp $0x588d; jae 0x13c77758(即 opcode ≥ 6166,因为0x588d/4 ≈ 5667.25行)和分派上限cmp $0x1622; ja 0x13c7775d(原始 opcode > 5666)都会在读取跳转表之前或替代读取跳转表时路由到这里。常见的 V5+ default 是0x13c74e9d处独立的零基础函数体,而不是这个陷入。
两个 stub 都调用共享的 MCCodeEmitter::reportUnsupportedInst(0x1a31c420),随后是 ud1(0x13c77758)或 int3(0x13c7775d)。重实现必须区分两个看起来像“default”的路径:0x13c74e9d 函数体是 4457 个 opcode 合法的零基础返回;陷入 stub 才是 499..5666 之外 opcode 的真实错误路径。混淆两者会把每条 TensorCore 和 V5+ 指令变成 unsupported-instruction 崩溃。
交叉引用
- 记录格式 — 此 emitter 填充的 239 位
APInt,以及基础位 /insertBits空洞模型。 - InstBits DB — 操作数编码器读取的
InstBits/InstBits_BarnaCorePxcHwMode基础位表、TPUDescs、TPUInstrNameData和TPURegEncodingTable。 - IsaEmitter 注册表 — proto-bundle
EmitX/<Slot>Encoder::Encode路径,它会编码此 emitter 为其返回全零的每个 opcode。 - V5+ EmitX 位位置 —
getBinaryCodeForInstr不会编码的 TensorCore 和 V5+ slot 的绝对 bundle 位位置。