SICI · 天机门 · 第七章

指令契约

软件与硬件的契约 · 筑基一层

你以为你写的是指令。你写的是愿望。机器收到的,是四条它才看得懂的命令。

门楣刻字:「软件说一句话,硬件走四步」
序章

契约之门

一行代码,机器要几步?

筑基殿前的石阶尽头,矗立着一扇巨门。门面密密麻麻刻满了 0 和 1,像某种无法阅读的经文。

流水之河,指令流水线

门下的裂缝里渗出暗紫色的光,光里有声音——像很多人在低声争论,又像一个声音在笑。

诺依曼老祖 诺依曼老祖

「你从炼气期过来,会画电路、会写 HDL。现在,写一行你想让机器做的事。然后猜猜——它要几步才能做完?」

语义之渊 语义之渊

「写下吧。猜对了,门开。猜错了……我记一笔。」

详解 · ISA 到底是什么(K07-01)

定义:指令集架构(ISA, Instruction Set Architecture)是软件与硬件之间的抽象接口——它规定了处理器"对外可见"的一切:指令集合、寄存器数量与语义、内存寻址模型、数据类型与异常行为。

为什么叫"契约":软件只依赖 ISA 承诺的行为,不关心内部实现。同一套 ISA 可以有慢速单周期、高速流水线、乱序超标量等完全不同的微架构实现——就像同一份合同可以由不同施工队执行。

例子:一段编译好的 RV32I 机器码,在 5 级流水的教学核上、在 SiFive 的乱序核上、在 FPGA 软核上跑,结果完全一致。契约不变,实现自由。

翻译之赌 · 一行 C 代码要几条指令?

a = b + c;

这条语句,机器要几条指令完成?👆 点击下方选项下注

第一幕 · 崩塌

翻译殿 · 一句抵四令

亲手把 C 代码拖过汇编层、拖到机器码层

穿过门缝是一座悬空石殿,殿中央浮着三层透明石板:最上层刻着 C 代码,中层空白,底层空白。

诺依曼老祖

「上层是你写的。中层是汇编——人能读、机器不能。底层是机器码——机器能读、人不能读。把上面一层拖到下面,看看会发生什么。」

详解 · 为什么一行变四行(K07-01 / K07-07)

load-store 架构:RISC-V 规定——只有 load/store 指令可以访问内存,所有运算指令的源和目的都必须是寄存器。所以 a = b + c 被拆成:取 b(lw)→ 取 c(lw)→ 相加(add)→ 存 a(sw),四步缺一不可。

翻译链条的分工:编译器把 C 翻译成汇编(语义映射 + 寄存器分配 + 优化),汇编器把汇编逐条翻译成机器码(纯粹的格式转换,几乎一一对应)。汇编是"给人看的机器码",机器码是"给机器看的汇编"。

为什么中间隔着汇编:直接看 32 位二进制无法调试。汇编用助记符(add/lw)代替 opcode、用寄存器名代替编号,是人类介入机器世界的最后一级可读台阶。

三层翻译 · 拖拽代码穿过每一层

C 层
a = b + c;
汇编层
机器码层
👆 拖动 C 层代码到汇编层

编译透镜 · 换一句 C 代码,看编译器怎么翻

选一个语句,透镜把它逐行翻成 RISC-V。

第一幕 · 思辨

哲学殿 · 两条道路

CISC 用复杂换少,RISC 用简单换快

翻译殿后是一座天平殿。殿中央悬着一架巨秤,左盘刻"CISC",右盘刻"RISC"。

两座玉碑分立两侧:左侧泛着复杂的暗金光纹——x86;右侧泛着干净的天蓝光纹——RISC-V。

详解 · CISC 与 RISC 的路线之争(K07-02 / K07-12)

历史背景:1970-80 年代内存昂贵、编译器弱小,设计者把越来越多功能塞进单条指令(如 x86 的字符串搬运 rep movs)——这是 CISC。80 年代流水线技术兴起后人们发现:简单规整的指令更容易流水化,每条一个节拍,CPI 逼近 1——这是 RISC。

核心量化指标:CPU 时间 = 指令数 × CPI × 时钟周期。CISC 减少"指令数"但抬高 CPI 与译码复杂度;RISC 增加指令数但压低 CPI 并简化硬件。没有绝对胜负,只有权衡。

现代融合:奔腾 Pro 之后的 x86 在硬件内部把 CISC 指令译码成 RISC 风格的微操作(μop)再执行——外表 CISC、内核 RISC。RISC-V 则通过 M/A/F/D/C 等扩展按需增加能力。两条路线在工程上早已互相渗透。

特征归位 · 拖卡片到正确的玉碑

CISC
RISC
第一幕 · 格式

编码台 · 定与变

定长一步解码 vs 变长逐字节扫描

天平殿后是一座编码台。台上并排刻着两种指令模板:左侧 RISC-V 的 32 格定长条,右侧 x86 的可伸缩变长条。

详解 · 定长与变长的本质分歧(K07-03)

定长(RISC-V,32 位):每条指令占 4 字节,opcode/寄存器字段永远在同一位置。译码器可以并行预取多条指令、一步切出所有字段——这是现代超标量处理器"每周期译码 4-8 条"的前提。

变长(x86,1-15 字节):指令边界取决于内容,必须逐字节扫描才能知道下一条从哪开始。译码成为 x86 流水线的天然瓶颈,现代 x86 靠 μop 缓存(decoded uop cache)缓解。

代码密度补偿:变长的优势是省内存(x86 常见指令平均约 3-4 字节)。RISC-V 用 C 扩展(16 位压缩指令,把高频指令如 addi/lw 压成 2 字节)补回密度——RV32GC 已成为嵌入式事实标配。

双模板填充 · 定长 vs 变长

RISC-V 定长 32 位

解码器一步定位所有字段——固定位置,无需扫描

x86 变长 1-15 字节

解码器必须逐字节扫描才能确定边界

第二幕 · 全局

RV32I · 四十符文

四十条指令,三十二个寄存器,这就是全部

编码台后是一座环形符文殿,殿壁上排列着四十枚发光符文——每一枚是一条 RV32I 基本指令。殿中央浮着 32 个寄存器槽位。

诺依曼老祖

「四十条指令,三十二个寄存器。这就是全部。剩下的,是组合。」

详解 · RV32I 的最小完备集(K07-04)

40 条指令的分工:整数运算 21 条(add/sub/and/or/xor/slt 及立即数版)、分支 6 条(beq/bne/blt/bge/bltu/bgeu)、访存 10 条(lw/lh/lb/sw/sh/sb 及无符号版)、跳转 2 条(jal/jalr),外加 fence/ecall/ebreak 等系统指令。这就是能跑通用计算的"最小完备集"。

为什么 32 个寄存器:寄存器编号占 5 位(2⁵=32),三种指令格式各留出 rs1/rs2/rd 三个字段。寄存器多→编译器调度余地大,但寄存器堆面积、功耗、上下文切换成本都上升。32 是几代 ISA 收敛出的甜点。

ABI 别名:x0 硬连线恒为 0(zero),x1 存返回地址(ra),x2 是栈指针(sp),x5-x7/x28-x31 是临时寄存器(t0-t6),x8-x9/x18-x27 是保存寄存器(s0-s11),x10-x17 传参和返回值(a0-a7)。别名不是硬件特性,是软件约定——这正是"契约"二字的含义。

指令分类 · 拖符文到功能槽

第二幕 · 崩塌

位域炼狱 · 六种契约

位不排队。位跳格子。

符文殿深处是一座六面体炼狱。六面墙上各刻着一种指令类型的位域模板:R、I、S、B、U、J。

语义之渊

「从左到右填嘛。位嘛,排着队等着你。」

详解 · 六种格式为什么这样切(K07-05 · 推导级)

字段位置恒定原则:六种格式中,opcode 恒在 [6:0]、rd 恒在 [11:7]、funct3 恒在 [14:12]、rs1 恒在 [19:15]、rs2 恒在 [24:20]。译码器不需要先判断指令类型再决定从哪读寄存器号——寄存器堆的读端口可以"盲读",与类型译码并行进行。

立即数为何乱序:I/S/B/U/J 型的立即数字段故意保持低位对齐、高位符号位恒在 bit[31]。B 型把 imm[11] 放到 bit[7]、J 型把 imm 拆成四段——换来的是符号扩展器只需一套数据通路。位的排列服从硬件效率,不服从人类直觉。

六型速记:R=寄存器运算(add)、I=立即数/访存读(addi/lw/jalr)、S=存(sw)、B=分支(beq)、U=大立即数(lui/auipc)、J=跳转(jal)。记住"哪里变了、哪里没变",比背表格有用。

六面体位域编码器

add x5, x6, x7
当前:0x00000000 目标:0x007302B3
第二幕 · 手感

寻址回廊 · 四条暗道

指令怎么告诉硬件数据在哪

位域炼狱后是一条环形回廊,廊壁上开着四条暗道,各刻一种寻址符文。

诺依曼老祖

「数据在哪?在指令里、在寄存器里、在内存里、还是在代码的某个位置?四条路,四种走法。」

详解 · 四种寻址方式的本质(K07-06)

立即数寻址(addi x5, x6, 10):操作数直接编码在指令里,取指即得,零访存开销——代价是立即数位宽受限(I 型仅 12 位,范围 -2048~2047)。

寄存器寻址(add x5, x6, x7):操作数在寄存器堆,最快,是 RISC 运算指令的唯一形态。

基址偏移寻址(lw x5, 8(x6)):有效地址 = 寄存器 + 12 位偏移。数组访问 arr[i]、结构体成员 s->field、栈上局部变量,全部编译成这一种模式。

PC 相对寻址(beq/jal,及 auipc):目标地址 = 当前 PC + 偏移。它让代码可以被加载到内存任意位置而无需修改——这是位置无关代码(PIC)的根基,动态链接库(.so/.dll)全靠它。

寻址匹配 · 为指令选对暗道

第二幕 · 执行

执行场 · 三类符咒

三类指令,三种改变

寻址回廊尽头是一座环形执行场。场中央浮着一个迷你 CPU 架构图:PC、指令存储器、寄存器堆、ALU、数据存储器,用光路连接。

诺依曼老祖

「看了这么多,该亲手跑一次了。三类指令,三种操作。拖进去,看它怎么动。」

详解 · 三类指令 = 三种状态改变(K07-07 / K07-08 / K07-09)

数据传输(lw/sw):在寄存器堆与数据存储器之间搬数。不改变数据的值,只改变它的位置。

算术逻辑(add/sub/and/or/slt...):从寄存器读两个源,经 ALU 算出结果写回寄存器。改变数据的值。

控制流(beq/jal/jalr):比较两个寄存器后决定是否修改 PC。改变程序的执行顺序。

为什么是"完备"的:搬数 + 计算 + 分支,三者组合即可表达任意算法——这是图灵完备在 ISA 层面的具体形态。ch08 你会看到,整个 CPU 数据通路就是为这三类状态改变铺设的电路。

迷你 CPU 数据通路模拟器

PC0x004
IMEM—
REG
ALU—
DMEM—
拖入指令执行
第三幕 · 顿悟

调用之链 · 栈上契约

调用不是跳转——跳过去之前先记住从哪跳的

执行场后是一座栈殿。殿中央浮着一块垂直内存区——栈,从高地址向下生长。殿侧有一台两遍汇编器。

你物品栏里那枚 S5 裂了纹的 B 型指令玉简微微震动。

诺依曼老祖

「调用不是跳转。跳过去之前,你得先记住从哪跳的。跳过去之后,你得给别人留出地方放东西。」

详解 · 调用约定与两遍汇编(K07-10 / K07-11)

调用约定(calling convention):jal 把返回地址(PC+4)写入 ra 并跳转——但 ra 只有一个,函数里再调用别的函数就会把它冲掉。所以被调用者必须先分配栈帧(sp -= 16 等)、把 ra 和 s0 等"callee-saved"寄存器压栈,返回前再恢复。谁保存什么寄存器(a/t 系由调用者保存,s 系由被调用者保存),全是约定而非硬件强制。

栈帧的纪律:RISC-V ABI 要求 sp 在函数调用边界保持 16 字节对齐。栈帧里依次放着返回地址、保存的寄存器、局部变量和溢出参数——递归之所以可行,正因为每次调用都有自己独立的一层帧。

两遍汇编的必要性:当 beq 向前引用一个还没扫描到的标号时,汇编器还不知道它的地址。第一遍只收集"标号→地址"符号表,第二遍才回填所有跳转偏移生成机器码。链接器干的也是这件事,只不过跨越多个目标文件。

栈帧组装 · 亲手操作函数调用

两遍汇编器

第三幕 · 高潮

Boss 战 · 语义之渊三问

翻译账清算 · 排除刻痕收网

殿深处的紫色裂隙撕裂开来,语义之渊凝聚成形——一张由无数 0 和 1 构成的面孔,眼眶里淌着编译器的中间代码。

语义之渊

「你走完了翻译链路。但你真的懂了吗?三问。答对,我散。答错,你留下来——当我的翻译器。」

渊账累计 0 笔 · 三问全对后清零

收束

契约之外

ISA 会扩展、需要权衡、最终链接成可执行文件

深渊消散后,筑基殿归于清明。晨光从殿顶照下来,契约石板上的刻痕在光中微微发亮。

诺依曼老祖

「契约签完了。但 ISA 不是一个死的东西——它会长。看看这三扇门。」

详解 · 扩展、权衡与链接(K07-13 / K07-14 / K07-15)

模块化扩展:RV32I 是强制核心,M(乘除)、A(原子操作)、F/D(单/双精度浮点)、C(16 位压缩)全部可选。一个 ISA 字符串如 "RV32GC" = RV32IMAFDC_Zicsr,就是处理器的能力清单——编译器照单生成代码。

工程权衡:每加一个扩展,性能上限提高,但硅面积、功耗、验证成本同步上升;每砍一刀兼容性,生态碎片化就深一分。x86 选择 40 年向后兼容、背着历史包袱换取生态;RISC-V 选择精简+模块化,赌的是"场景定制"时代。

链接与 ELF:编译器产出 .o 目标文件,链接器做三件事——符号解析(把跨文件的函数/变量引用对上号)、段合并(所有 .text 合一、所有 .data 合一)、重定位(确定最终虚拟地址并回填)。产物是 ELF 可执行文件:程序头表 + 段 + 符号表,是 ISA 契约最终的物理载体。

ISA 三扇门

扩展之门
权衡之门
链接之门
交互实验

指令编码与立即数范围

选择指令格式并拖动立即数位宽,观察可编码数值范围。