Skip to content

x86-64 汇编语言设计

· xieyuheng, deepseek

前言 ​

本文综合三篇前作—— native x86-64 汇编语言设计 (完整的语法设计)、 x86 汇编的 opaque 指针与 offset-of 寻址 (opaque 指针的修订)与 本次设计讨论——并以 [meta-lisp.js] 中的现有实现为准,重新整理出一份清晰的设计文档。

与前几篇日志相比,实现已经走得更远,本次更新如实反映:

  • 去掉了 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——可以是具名类型或定长数组类型:

meta-lisp
(define-enum type-t
  (named-type (name symbol-t))
  (array-type (element type-t) (length int-t)))
  • 原始类型由内建注册,每个名字对应一个固定字节大小:

    类型大小
    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——用户在源码中书写的形式,带有位置信息:

meta-lisp
(define-struct struct-field-t
  (name symbol-t)
  (exp exp-t))

(define-enum exp-t
  (address-exp (name symbol-t))
  (int-exp (value int-t))
  (string-exp (content string-t))
  (struct-exp (name (maybe-t symbol-t)) (fields (list-t struct-field-t)))
  (pointer-exp (target exp-t))
  (array-exp (elements (list-t exp-t))))
  • 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> ...)——数组字面量。

文本语法:

lisp
;; 符号 — 裸符号即取该符号的地址
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)是表达式求值后的结果——布局阶段使用,不带源码位置:

meta-lisp
(define-enum value-t
  (int-value (value int-t))
  (string-value (content string-t))
  (address-value (name symbol-t))
  (struct-value (name (maybe-t symbol-t)) (fields (hash-t symbol-t value-t)))
  (pointer-value (target value-t))
  (type-value (type type-t))
  (array-value (elements (list-t 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 必须具名——目标的类型只能由字面量自带的类型名提供。

文本语法:

lisp
;; 字符串常量
(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)是一个编译期常量,有两种形态:

meta-lisp
(define-enum displacement-t
  (int-displacement (value int-t))
  (offset-of-displacement (struct-type symbol-t) (fields (list-t symbol-t))))
  • 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」链式组合(见「示例」)。

操作数 ​

meta-lisp
(define-enum operand-t
  (reg-operand (name symbol-t))
  (imm-operand (value int-t))
  (label-operand (name symbol-t))
  (address-operand (name symbol-t))
  (deref-operand (address address-operand))
  (reg-deref-operand
    (base symbol-t)
    (index (maybe-t symbol-t))
    (scale (maybe-t int-t))
    (disp (maybe-t displacement-t)))
  (cc-operand (code symbol-t))
  (var-operand (name symbol-t))
  (external-label-operand (name symbol-t))
  (data-operand (exp exp-t)))
  • 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。

    预编码阶段(ResolveDataOperands pass)将 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):

lisp
(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):

meta-lisp
(define-struct instr-t
  (op symbol-t)
  (operands (list-t operand-t)))

按 op 区分操作:

类别op说明
数据移动mov(mov dest src)
算术add sub imul cmp test(add dest src) 等
栈push pop(push (reg rbp))
控制流call ret jmp jj 的操作数为 [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。

基本块 ​

meta-lisp
(define-struct block-t
  (name symbol-t)
  (instrs (list-t instr-t)))
  • name:block 名称,即一个本地标号(跳转目标)。
  • 一个函数的第一个 block 是 entry block。
  • block 之间忠实 x86 地 fall-through:一个 block 执行到末尾若没有 ret / jmp,控制流顺序流入紧邻的下一个 block。

顶层定义 ​

meta-lisp
(define-enum definition-t
  (code-definition (name symbol-t) (blocks (list-t block-t)))
  (data-definition (name symbol-t) (value exp-t))
  (metadata-definition (target symbol-t) (value exp-t))
  (struct-definition (name symbol-t) (fields (hash-t symbol-t type-t)))
  (space-definition (name symbol-t) (size int-t)))
  • code-definition:定义一个可执行函数,写作 (define-code <name> (block ...) ...)。
  • data-definition:定义一个具名数据块,写作 (define-data <name> <value>);类型由 <value> 自描述推断(无 claim)。<value> 是一个 exp-t。
  • metadata-definition:为 target 这个 label 的 -8 slot 填充元数据,写作 (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) 依赖 JS Record 的插入序表达布局顺序,决定各字段偏移与总大小。类型名必须以 -t 结尾。
  • space-definition:分配未初始化内存(BSS 语义),写作 (define-space <name> <size>)。

实现注记:

  • 实现把顶层语句(解析层)与定义(模块登记层)分成两层,一个 submit pass 把前者转为后者; 其中 define-metadata 的 name 在模块里以 target 索引。
  • 原始类型在实现里也是一种定义(内建注册,名字 + 大小),不属于用户可写的语法,这里不展开。

模块 ​

模块(mod)是所有定义的容器,加上元数据表与匿名数据表:

meta-lisp
(define-struct mod-t
  (definitions (hash-t symbol-t definition-t))
  (metadata-definitions (hash-t symbol-t metadata-definition))
  (anonymous-data (list-t data-definition)))
  • 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) 访问:

c
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 ...) → 外部地址。

示例 ​

条件分支 ​

10 > 3,走 is-greater 分支返回 1(取自 conditional.x86.asm):

lisp
(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):

lisp
(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):

lisp
(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:函数体中的字面量 ​

lisp
(define-code string-in-body
  (block entry
    ;; data-operand fallback:字符串 → 匿名字符串 + deref
    (mov (reg rax) "hello")
    ;; data-operand fallback:整数 → imm
    (mov (reg rcx) 42)
    (ret)))

等价于:

lisp
(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 未实现。