前言
本文综合三篇前作—— native x86-64 汇编语言设计 (完整的语法设计)、 x86 汇编的 opaque 指针与 offset-of 寻址 (opaque 指针的修订)与 本次设计讨论——并以 [meta-lisp.ts] 中的现有实现为准,重新整理出一份清晰的设计文档。
与前几篇日志相比,实现已经走得更远,本次更新如实反映:
- 去掉了
claim——define-data不再标注类型,类型由值自身「自描述」推断。 - 指针 opaque 已落地——
pointer-t无 pointee 类型,(pointer (struct ...))的目标 struct 必须具名。 offset-of收编为寻址位移——只出现在reg-deref的位移处,不是独立算子。- Exp 语法统一使用裸关键词——
(struct)/(pointer)/(array)作为数据构造器;AddressExp直接由裸符号表达(foo即取foo的地址)。 exp-t与value-t分离——实现中求值前的表达式与求值后的值是两个 ADT,文档如实反映。data-operand桥接 exp 与 operand——函数体中可以直接写字面量(如"hello"、42、(pointer ...)),由预编码阶段解析为具体 operand。(array-t <T> <N>)定长数组类型——literal 写作(array <elem> ...)。define-struct字段类型用hash-t——字段声明不是exp-t,而是直接的类型映射。
贯穿全文的原则来自 LLVM 的 Opaque Pointers:
值只携带它内在的属性;「如何解释」的语义放到操作上。
- 地址的内在属性 = 它是个 8 字节地址,指向什么类型无真实语义 → 指针 opaque。
- struct 的「形状/布局」是内存访问真正需要的 → 保留为偏移计算的依据。
- 类型不附着在指针值上,而是落在「声明」(
define-struct)与「操作」(offset-of)上。
下文用 meta-lisp 的 ADT(define-enum / define-struct)描述关键数据类型的抽象语法。
类型
类型是一个 tagged union——可以是具名类型或定长数组类型:
原始类型由内建注册,每个名字对应一个固定字节大小:
类型 大小 pointer-t8 bytes string-t8 bytes int8-t1 byte int16-t2 bytes int32-t4 bytes int64-t8 bytes uint8-t1 byte uint16-t2 bytes uint32-t4 bytes uint64-t8 bytes pointer-t是 opaque 指针——无 pointee 类型,纯 8 字节地址。string-t语义上等于pointer-t(C 意义的const char *),是 8 字节指针。(array-t <T> <N>)是定长数组——N个连续<T>元素,总大小 =sizeof(T) * N。写作(array-t uint8-t 256)。struct 类型由
define-struct引入(见「顶层定义」)。一个类型的字节大小这样计算:原始类型查注册表;struct 类型按字段顺序递归累加每个字段的大小;array 类型 =
sizeof(element) * length(__attribute__((packed))布局,无 padding)。
表达式
表达式(exp-t)是求值前的 AST——用户在源码中书写的形式,带有位置信息:
address-exp:符号引用——裸符号foo即取foo的地址。求值时先查 env,再查 mod 定义:是数据/代码/空间定义则返回address-value;是类型定义则返回type-value。在汇编语言中,标签本身就是地址。int-exp:整数字面量,如42、-1。string-exp:字符串字面量,如"hello"。struct-exp:struct 字面量,写作(struct <name> (<field> <exp>) ...)。name可省略(当上下文已知类型时)。pointer-exp:写作(pointer <exp>)——编译期创建匿名 data slot 存放<exp>,求值为指向它的指针。array-exp:写作(array <exp> ...)——数组字面量。
文本语法:
;; 符号 — 裸符号即取该符号的地址
origin
;; 整数字面量
42
;; 字符串字面量
"hello"
;; struct 字面量(必须带类型名)
(struct point-t
(x 0)
(y 0))
;; 省略类型名(当字段类型已知)
(struct (x 0) (y 0))
;; pointer 表达式——匿名 slot + 指针
(pointer (struct node-t (value 1) (next 0)))
;; array 字面量
(array 1 2 3 4 5)数据值
数据值(value-t)是表达式求值后的结果——布局阶段使用,不带源码位置:
int-value:整数值。string-value:字符串值。address-value:符号地址值。struct-value:struct 值。字段是hash-t(有序插入),与exp-t中struct-field-t的列表顺序一一对应。pointer-value:包裹另一个 value,在布局时创建匿名 slot。type-value:类型定义求值的结果。address-exp("int64-t")→type-value(named-type "int64-t")。array-value:数组字面量求值的结果。
求值过程是结构递归的:
evaluate(mod, env, exp-t) → value-t
int-exp → int-value
string-exp → string-value
struct-exp → struct-value (递归求值每个 field.exp)
pointer-exp → pointer-value (递归求值 target)
array-exp → array-value (递归求值每个 element)
address-exp → env 或 mod 中查找 → type-value 或 address-value自描述与 opaque 的强制:因为没有 claim,define-data 的类型完全由值自身推断:
(struct <name> ...)→ 该 struct 类型;struct 字面量必须带类型名。(pointer ...)/ 裸符号 →pointer-t。- 字符串 →
string-t。 - 裸整数 / 裸 array → 错误(不自描述,必须包在 struct 中)。
由于指针 opaque,(pointer (struct ...)) 的目标 struct 必须具名——目标的类型只能由字面量自带的类型名提供。
文本语法:
;; 字符串常量
(define-data greeting "hello")
;; struct 常量(必须带类型名)
(define-data origin
(struct point-t
(x 0)
(y 0)))
;; 指针常量:匿名 slot + 指针
(define-data head
(pointer
(struct node-t
(value 1)
(next 0))))
;; 地址常量:存 origin 的指针
(define-data ptr origin)位移
reg-deref 的位移(displacement)是一个编译期常量,有两种形态:
int-displacement:直接给出字节数,如-8、8。offset-of-displacement:写作(offset-of <struct-type> <field> ...), 沿struct-type的字段路径逐级累加偏移,求值为字节数。
offset-of 把「struct 类型 + 字段名 → 偏移」分离成独立的编译期计算,正是 GEP 的精神: 类型是偏移操作的参数,地址是 opaque 的 base。汇编器有一个 pass 在编码前把所有 offset-of-displacement 求值替换为 int-displacement。
offset-of 不穿指针——它只在单个 struct 内逐字段累加;一旦字段是 pointer-t,路径即终止。 跨指针的字段访问要靠「offset-of + deref + offset-of」链式组合(见「示例」)。
操作数
reg-operand:物理寄存器,写作(reg rax)、(reg r15)等。imm-operand:立即数,写作(imm 42)。label-operand:代码标号的引用,按机器语义专用于 rel32 域——用于jmp/call/j的跳转目标,写作(label <name>)。标号只由block定义;(label ...)不能作为指令。address-operand:符号地址作为 64-bit 立即数(movabs+ 重定位),写作(address <name>)。不带字段路径——字段偏移一律交给offset-of。deref-operand:写作(deref (address <name>)),读取该符号地址处的内容,编码为[rip + disp32]。内部包裹一个address-operand。reg-deref-operand:寄存器相对寻址,支持完整 SIB。base/index存寄存器名,disp是上一节的位移:(reg-deref (reg rbp))→[rbp](reg-deref (reg rbp) -8)→[rbp - 8](reg-deref (reg rbp) (offset-of point-t y))→[rbp + offset](reg-deref (reg rbp) (reg rax) 8)→[rbp + rax*8](reg-deref (reg rbp) (reg rax) 8 -16)→[rbp + rax*8 - 16]
cc-operand:条件码,写作(cc e)/(cc ne)/(cc l)/(cc le)/(cc g)/(cc ge)/(cc b)/(cc be)/(cc a)/(cc ae),仅用于j。var-operand:寄存器分配前的虚拟变量,写作(var <name>)。带var的汇编语言是中间表示;寄存器分配 pass 会把它替换为reg或reg-deref。external-label-operand:外部符号引用(如 libc 函数、syscall 入口),写作(external-label <name>),由外部重定位解析。data-operand:包装一个exp-t,允许在函数体操作数位置直接写数据表达式。没有显式语法——当操作数解析不匹配任何已知形式时,fallback 到parseExp并包装为data-operand。预编码阶段(
ResolveDataOperandspass)将data-operand解析为具体 operand:exp.kind求值结果 解析为 编码 int-expint-valueimm-operand立即数 string-expstring-value匿名 data slot + deref-operand[rip + disp32]pointer-exppointer-value匿名 data slot + deref-operand[rip + disp32]address-expaddress-valueaddress-operandmovabs var-exp查到定义 address-operandmovabs struct-exp/array-exp— 报错(bare struct/array 不支持) — 当前 只在
.exe格式下支持(flat 格式没有独立 data section,无法放置匿名数据)。
同一符号在两个域的对比(如函数 factorial):
(call (label factorial)) ;; 控制流,rel32
(mov (reg rax) (address factorial)) ;; 取其地址作立即数,movabs
(mov (reg rax) (deref (address k))) ;; 读 k 处内容,[rip + disp32]
;; data-operand 的 fallback 语法:
(mov (reg rax) "hello") ;; 匿名字符串,等价于 deref
(mov (reg rax) 42) ;; 等价于 (imm 42)
(mov (reg rax) (pointer (struct point-t (x 0) (y 0)))) ;; 匿名 struct + deref实现注记:
reg-deref的base/index在实现里是寄存器名字符串,文本语法仍写(reg ...)。
指令
所有指令统一为「操作码 + 操作数」,按 NASM 方向(dest, src):
按 op 区分操作:
| 类别 | op | 说明 |
|---|---|---|
| 数据移动 | mov | (mov dest src) |
| 算术 | add sub imul cmp test | (add dest src) 等 |
| 栈 | push pop | (push (reg rbp)) |
| 控制流 | call ret jmp j | j 的操作数为 [cc, label] |
| 位运算 | and or xor shl shr | (xor (reg rax) (reg rax)) |
| 地址计算 | lea | (lea dest (reg-deref ...)) |
| 系统 | syscall | 无操作数 |
call既可直接调用(call (label helper)),也可间接调用(call (reg-deref (reg rax)))。j是 x86 的单目标条件跳转:条件成立跳到目标,不成立则 fall-through——因此它不是完整的 terminator,不必配一个jmp伪装成双目标分支。(label ...)不能作为指令——要新增跳转目标,就新建一个block。
基本块
name:block 名称,即一个本地标号(跳转目标)。- 一个函数的第一个 block 是 entry block。
- block 之间忠实 x86 地 fall-through:一个 block 执行到末尾若没有
ret/jmp,控制流顺序流入紧邻的下一个 block。
顶层定义
code-definition:定义一个可执行函数,写作(define-code <name> (block ...) ...)。data-definition:定义一个具名数据块,写作(define-data <name> <value>);类型由<value>自描述推断(无claim)。<value>是一个exp-t。metadata-definition:为target这个 label 的-8slot 填充元数据,写作(define-metadata <name> <value>)。<value>典型为(pointer (struct <name> ...))——汇编器创建匿名 struct slot,并在-8处填入指向它的指针。元数据 struct 同样必须具名。struct-definition:声明结构体类型与字段布局,写作(define-struct <name>-t (<field> <type>) ...)。字段是有序的——(hash-t symbol-t type-t)依赖 JSRecord的插入序表达布局顺序,决定各字段偏移与总大小。类型名必须以-t结尾。space-definition:分配未初始化内存(BSS 语义),写作(define-space <name> <size>)。
实现注记:
- 实现把顶层语句(解析层)与定义(模块登记层)分成两层,一个 submit pass 把前者转为后者; 其中
define-metadata的name在模块里以target索引。- 原始类型在实现里也是一种定义(内建注册,名字 + 大小),不属于用户可写的语法,这里不展开。
模块
模块(mod)是所有定义的容器,加上元数据表与匿名数据表:
definitions:所有define-code/define-data/define-struct/define-space的定义。metadata-definitions:按target索引的元数据表(define-metadata)。anonymous-data:data-operand解析时产生的匿名data-definition(含生成的名字如©data.0)。
过程
汇编管道:
源码
→ parseStmt → Stmt[] (解析层)
→ SubmitPass → Mod (提交层)
→ CheckPass → 对 Mod 中所有 data-definition / metadata-definition 做类型检查
→ ResolveDisplacements → offset-of → int-displacement
→ ResolveDataOperands → data-operand → imm / address / deref (+ 注册匿名 data)
→ assembleExe → collectCodeLayout → emitCodeSection → emitDataSection → bytes- SubmitPass:把
Stmt转换为Definition并注册到Mod。 - CheckPass:遍历所有数据定义,用
inferDataType推断类型,然后用check验证exp与类型匹配。 - ResolveDisplacements:把所有
offset-of-displacement替换为int-displacement。 - ResolveDataOperands:遍历所有指令,把
data-operand求值并替换为具体 operand。仅.exe模式运行(flat 模式遇到data-operand报错)。 - assembleExe:代码布局 + 数据布局 + 重定位 → 最终二进制。
约定
-8 slot 与元数据
每个有 define-metadata 的 label 自动在前面对齐到 8 字节并保留一个 -8 slot: label - 8 处存放一个 8 字节指针,指向与该 label 关联的元数据 struct 实例。
┌──────────────────┐
│ metadata ptr │ ← label_addr - 8
label_addr → ├──────────────────┤
│ push rbp │
│ ... │
└──────────────────┘运行时 O(1) 访问:
function_metadata_t* meta = *(function_metadata_t**)((char*)func_ptr - 8);
uint16_t arity = meta->arity;序列化与重定位
- 所有 struct 按
__attribute__((packed))布局——字段间无 padding,每个字段按其类型大小写入(小端)。 - 指针 / 字符串字段先写占位地址,再登记重定位项。Loader mmap 后回填。
- 重定位分四类:
- rel32 标号:
jmp/call/j的相对位移。 - 内部指针:data 内部
(pointer ...)的占位 → 匿名 slot 偏移。 - 数据地址:
AddressExp(裸符号) → 目标 label 地址。 - 外部符号:
(external-label ...)→ 外部地址。
- rel32 标号:
示例
条件分支
10 > 3,走 is-greater 分支返回 1(取自 conditional.x86.asm):
(define-code test-conditional
(block entry
(mov (reg rax) (imm 10))
(mov (reg rcx) (imm 3))
(cmp (reg rax) (reg rcx))
(j (cc g) (label is-greater))
(mov (reg rax) (imm 0))
(ret))
(block is-greater
(mov (reg rax) (imm 1))
(ret)))j (cc g) 成立则跳到 is-greater,不成立则 fall-through 到 (mov (reg rax) (imm 0))。
跨指针链与 offset-of
chain 是 node-a → node-b → node-c 的指针链,读出末端 value = 77 (取自 pointer-chain.x86.asm):
(define-struct node-c-t
(value int64-t))
(define-struct node-b-t
(next pointer-t)
(value int64-t))
(define-struct node-a-t
(next pointer-t)
(value int64-t))
(define-data chain
(struct node-a-t
(next (pointer
(struct node-b-t
(next (pointer
(struct node-c-t
(value 77))))
(value 0))))
(value 0)))
(define-code read-chain
(block entry
(mov (reg rax) (address chain))
(mov (reg rax) (reg-deref (reg rax) (offset-of node-a-t next)))
(mov (reg rax) (reg-deref (reg rax)))
(mov (reg rax) (reg-deref (reg rax)))
(ret)))每个 offset-of 只在单一 struct 内计算偏移;跨指针靠「offset-of 取出指针 → deref 解引用 → 再 offset-of」链式组合,没有任何手算的魔法数字。
元数据与 -8 slot
元数据 struct 有 arity(int64)+ name(string-t),函数经 -8 slot 读出名字首字节 't' = 116 (取自 meta-with-pointer.x86.asm):
(define-struct func-meta-t
(arity int64-t)
(name string-t))
(define-metadata my-func
(pointer
(struct func-meta-t
(arity 3)
(name "triple"))))
(define-code my-func
(block entry
(mov (reg rax) (address my-func))
(mov (reg rax) (reg-deref (reg rax) -8)) ;; 读 -8 slot → 元数据指针
(mov (reg rax) (reg-deref (reg rax) 8)) ;; meta.name(string-t 指针字段)
(mov (reg rax) (reg-deref (reg rax))) ;; 解引用字符串首字节
(and (reg rax) (imm 255))
(ret)))data-operand:函数体中的字面量
(define-code string-in-body
(block entry
;; data-operand fallback:字符串 → 匿名字符串 + deref
(mov (reg rax) "hello")
;; data-operand fallback:整数 → imm
(mov (reg rcx) 42)
(ret)))等价于:
(define-data %anon-data-0 "hello")
(define-code string-in-body
(block entry
(mov (reg rax) (deref (address %anon-data-0)))
(mov (reg rcx) (imm 42))
(ret)))留待后续
- 寄存器分配:
var的替换 pass——把虚拟变量分配到reg或栈槽(reg-deref)。 - GC 元数据:safepoint、liveness 等在元数据 struct 中的编码。
string-t的形态:opaque 世界里它等于pointer-t+ 字符串字面量便利;保留为糖,还是拆成(pointer "...")。- 原始类型符号性:按「整数符号性」类比,布局层只需大小,
int8-t与uint8-t的区分可考虑简化。 size-of:与offset-of对称的编译期大小算子,目前尚未引入。- label + offset 一步 rip-relative 寻址的 peephole:将
(mov r (address X))紧邻(reg-deref r (offset-of …))合成一条[rip + (X + const)]。 data-operand的 flat 模式支持:当前仅在.exe格式下支持,flat 格式需要不同策略(代码段后追加匿名数据)。(array ...)的完整实现:当前类型系统已支持,layout emit 未实现。