C03 · 第 3 章 · 指令与处理器

指令集与汇编程序

按寄存器、内存和程序计数器跟踪汇编指令,计算地址偏移,并分析循环和函数调用。

预备知识数字逻辑与时序电路二进制与数值表示

本章目标

  1. 逐条跟踪指令对寄存器、内存和程序计数器的影响。
  2. 计算数组元素的字节地址与装载偏移。
  3. 根据分支条件分析汇编循环。
  4. 说明函数调用中的返回地址与寄存器保存规则。
页面阅读位置0%
章节未开始
本册完成进度0/6 章 · 0%
本页目录

指令集与程序状态

处理器执行程序时,需要知道每条指令使用哪些输入、修改哪些状态,以及执行后从哪里取得下一条指令。指令集体系结构规定软件能够观察到的寄存器、指令和异常等行为。同一指令集可以由不同电路实现,程序依照指令语义运行,具体实现决定延迟、吞吐率与能耗。

本章选用 RISC-V 的 RV32I 基础整数指令作为例子,寄存器宽度为三十二位。我们把寄存器看作固定宽度的位序列,把内存看作按字节寻址的空间,把程序计数器记为 pcpc。寄存器 x0 的读取结果恒为零,对它的写入不改变这个结果,其余通用寄存器用来保存操作数与计算结果。

指令执行状态

简化的程序状态由程序计数器、通用寄存器组和内存组成。一条正常完成的指令把执行前状态转换为执行后状态;遇到非法访问或其他异常时,控制转移还遵循执行环境规定的异常处理规则。

为了追踪程序,我们先列出初始寄存器和内存,再按实际执行顺序更新状态。汇编语句中的操作数位置由指令语法决定,例如 add x5, x6, x7 把后两个寄存器相加并写入第一个寄存器。运算输入先从执行前状态读取,不能因目标寄存器与输入同名就提前覆盖输入。

装载、存储与地址计算

装载指令从内存读取数据到寄存器,存储指令把寄存器中的数据写入内存。普通整数算术指令直接操作寄存器,不会自动读取某个寄存器数值所指向的内存。区分地址与地址处的数据,是阅读汇编程序时最先需要建立的习惯。

以 lw x5, 8(x6) 为例,有效地址等于寄存器 x6 的值加立即数八,指令从该地址读取四个字节组成的字。这里的偏移量以字节计量。若数组元素占四个字节,第 ii 个元素的地址为 a+4ia+4i,其中 aa 为数组起始地址,索引 ii 从零开始。字节序和地址对齐要求还要由所使用的平台确定。

读取两个数组元素

设 x10 保存数组基址 0x1000,数组中三个三十二位整数依次为七、负二和五。执行 lw x5, 0(x10)、lw x6, 8(x10)、add x7, x5, x6 后,各寄存器为何值?第一次读取地址 0x1000,得到七;第二次读取地址 0x1008,偏移八字节对应索引二,得到五。相加后 x7 为十二。若随后执行 sw x7, 4(x10),第二个元素被改为十二,数组成为七、十二、五。我们通过地址差除以元素宽度检查索引,避免把八字节误读成第八个元素。

不同装载宽度具有不同扩展规则。读取一个字节到宽寄存器时,有符号装载复制符号位,无符号装载在高位补零。写出一个字节通常只取寄存器的低八位,剩余位不参与该次写入。访问外部格式或压缩数据结构时,需要逐条核对读取宽度、扩展方式和地址范围。

条件分支与循环

顺序执行时,基础 RV32I 的下一条指令地址通常增加四字节;条件分支则比较寄存器,并在条件成立时选择分支目标。汇编器把标签转换为相对位置,程序员仍需要明确循环条件检查发生在循环体之前还是之后。带压缩指令扩展的程序可能含有其他指令长度,本章的四字节步长仅用于所选基础指令序列。

循环累加的状态跟踪

计算一到四的整数和,设 x5 为累加值、x6 为当前加数、x7 为循环上界之后的值,初始化为零、一、五。循环执行下列基础指令,标签只表示指令地址。

loop:
  add  x5, x5, x6
  addi x6, x6, 1
  blt  x6, x7, loop

四次循环后的状态依次为 (x5,x6)=(1,2),(3,3),(6,4),(10,5)(x5,x6)=(1,2),(3,3),(6,4),(10,5)。前三次比较均成立,第四次比较五与五为假,控制转入后续指令。累加结果为十,与 4×5/2=104\times5/2=10 一致。该程序从一开始,并在循环体后判断,因此只适用于这里给定的正上界;若允许空区间,需在进入循环前增加判断,避免额外执行一次。

有符号比较和无符号比较使用不同解释。最高位为一的寄存器,按补码可能是负数,按无符号数则位于较大范围。在比较数组索引、长度和差值时,应确保两边采用相同类型约定。分支目标也需要满足指令对齐要求,错误目标可能触发异常,无法简单当成另一条有效语句继续执行。

函数调用与寄存器约定

调用指令保存返回地址并跳转到函数入口,返回指令使用保存的地址恢复执行。函数调用还需要约定参数放置位置、返回值位置、哪些寄存器由调用方保存、哪些由被调用方恢复。这些规则属于调用约定,补充了指令集本身对数据流的规定。

栈通常用于保存返回地址、局部变量和需要跨调用保留的寄存器。调用方先准备参数,被调用方根据需要调整栈指针,使用栈空间后按约定恢复。递归调用的每次激活都要保存独立状态,否则后一次调用会覆盖前一次的返回位置。栈空间有限,深递归还需要考虑容量和错误处理。

阅读编译器生成的汇编时,我们可以选定一个简单输入,把高级语言变量与寄存器或栈位置对应起来,再逐条检查。优化可能消除变量、合并运算或改变循环组织,因此源代码的一行未必对应一条指令。判断程序是否保持语义,应比较允许输入上的结果与副作用,同时考虑语言对溢出和别名的约束。

编码、汇编与链接

机器指令用固定位置的位字段表示操作码、寄存器编号、立即数和其他选择信息。汇编器把指令文本转换为机器码,链接器处理跨文件符号并确定地址,装载过程把程序和数据放入执行环境。各阶段承担不同工作,汇编文本中的符号名称不一定直接出现在最终执行路径中。

伪指令是汇编器提供的简写,可能展开为一条或多条实际指令。估计动态执行指令数时,需要查看展开后的机器指令,并统计循环执行次数。调试时还应区分十六进制机器码、寄存器值和内存内容,它们虽然都显示为数字,却分别描述指令编码与程序数据。

汇编程序练习

练习

数组基址为 0x2000,每个元素占八字节,求索引五对应的地址,并说明它是第几个元素。

查看解答

索引五的偏移为五乘八,即四十字节。有效地址为十六进制2028。下标从零计数,因此这是数组的第六个元素。

练习

初始 x5=3,x6=4,执行 add x5,x5,x6 与 addi x5,x5,-1,逐步写出 x5 的值。

查看解答

第一条指令执行后,x5从三变为七;第二条再从最新的七减一,得到六。每条指令先读取当前状态,再生成执行后的状态。

练习

修改本章累加程序,使其计算一到三的和,给出初始化上界、循环次数和最终寄存器状态。

查看解答

上界之后的值应设置为四。循环体执行三次,累加状态依次为一、三、六,最终当前加数为四,比较失败后退出。

练习

一个三十二位寄存器全为一,另一个保存整数一。分别按有符号和无符号语义比较大小。

查看解答

全一位序列按三十二位补码表示负一,因此有符号比较小于一;按无符号解释为四十二亿九千四百九十六万七千二百九十五,大于一。

指令集参考资料

9 Designing an Instruction Set: Computation Structures

Chris Terman · 2017 · Massachusetts Institute of Technology

打开官方来源

RV32I 规范用于核对本章寄存器、装载存储和分支指令的语义。MIT 6.004 的指令集设计讲义使用课程中的 Beta 处理器,可作为不同指令设计的对照阅读;其中的指令名称与编码应按各自体系核对。