news 2026/7/18 2:40:17

从程序流程到机器语言:读《数字设计和计算机体系结构》第 6 章的笔记2

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从程序流程到机器语言:读《数字设计和计算机体系结构》第 6 章的笔记2

对应原书:第 6 章6.3 编程6.4 机器语言

摘要

上一篇笔记认识了寄存器、立即数和内存操作数。本篇只保留第七章 RTL 设计直接需要的程序流程、分支/跳转、word 访存和机器码字段;高级语言控制结构、ABI 和字符串等软件内容不再展开。

我不需要把重点放在写复杂软件上,而要能从一条指令判断:它读哪些寄存器、立即数怎样生成、会不会改写 PC、最终允许哪些体系结构状态发生变化

预备概念:控制流、PC、标签和位字段

程序计数器 PC

程序计数器(Program Counter,PC)是保存当前指令字节地址的 32 位体系结构状态。RV32I 基础指令长 32 bit,即 4 byte,所以正常顺序执行时:

PCnext=PC+4 PC_{next}=PC+4PCnext=PC+4

PC+4是组合逻辑算出的候选值;PC 寄存器只在时钟沿更新。遇到分支或跳转时,控制逻辑会选择另一个候选地址写入 PC。

控制流

控制流(control flow)是指令地址随程序执行而变化的顺序:

  • 顺序执行:下一条指令地址为PC+4
  • 条件分支:比较条件成立时转到目标地址,否则仍为PC+4
  • 无条件跳转:直接转到目标地址;
  • jal:跳转的同时把PC+4写入目的寄存器。

标签、目标地址和偏移量

汇编中的标签(label)是某条指令地址的可读名称。汇编器会将标签转换为数值地址或相对当前 PC 的有符号偏移量。

loop: addi s0, s0, 1 bne s0, s1, loop

标签不是硬件状态;机器码只保存编码后的分支偏移量。

位字段和掩码

位字段(bit field)是一个数据字中具有特定含义的一段连续 bit。掩码(mask)通过01选择要清除、保留或置位的 bit。例如与0xFF做 AND,只保留最低 8 bit。

6.3 编程

6.3.1 程序流程

指令和数据一样存放在内存中。原书示例中,三条连续指令的地址为:

0x538 addi s1, s2, s3 0x53C lw t2, 8(s1) 0x540 sw s3, 3(t6)

每条指令占 4 byte,所以地址依次增加 4:

0x538+4=0x53C,0x53C+4=0x540 0x538+4=0x53C, \qquad 0x53C+4=0x5400x538+4=0x53C,0x53C+4=0x540

从硬件看,取指阶段至少需要:

PC_q ─────────────→ 指令存储器地址 │ └→ 32 位加法器计算 PC_q + 4 │ 分支/跳转目标 ───────────┤ ↓ PC 下一值 MUX → PC_d

下面预设一个单周期控制流情景:指令存储器组合读;当前 PC 稳定时,指令字段、寄存器读数据、立即数、比较结果和下一 PC 都在本周期内组合产生;PC 与寄存器堆只在上升沿更新。该模型只用于理解顺序执行、beqjal的状态变化,不等同于完整处理器实现。

**实际时序情景:**复位释放后的某一周期,当前 PC 为0x0000_1000,指令 RAM 输出0x0100_00EF,即jal x1,+16

时刻组合/时序结果
上升沿后至下一上升沿前取指后识别为jal;J-type 偏移量重排为0x10;跳转目标为0x1010;同时形成返回地址0x1004和对x1的写使能。
下一上升沿PC 更新为0x1010,同时x1更新为0x1004

因此jal不是“先写返回地址、再跳转”的两步操作,而是在同一个上升沿向两个不同体系结构状态提交结果。这个示例只适用于组合读指令存储器;若 FPGA Block RAM 配置为同步读,取指会多出一个寄存器周期,应改用多周期或流水线结构。

需要区分:

  • PC_q是时序逻辑保存的当前状态;
  • PC+4、分支目标和跳转目标是组合逻辑产生的候选值;
  • PC 选择信号错误会改变后续全部指令流,不能当作普通数据错误处理。

6.3.2 逻辑与移位指令

1. 逻辑指令

RISC-V 的andorxor对两个 32 位源寄存器逐位运算,并把结果写入目的寄存器。andiorixori则用一个寄存器和一个经符号扩展的 12 位立即数作为输入。

指令典型用途某一位的结果
and/andi清零、提取字段两个输入都为 1 才为 1
or/ori合并字段、置位任一输入为 1 就为 1
xor/xori翻转、比较差异两个输入不同时为 1

例如提取s7=32'h1234_ABCDs7[15:8]

srli s6, s7, 8 # s6 = 0x0012_34AB andi s6, s6, 0xFF # s6 = 0x0000_00AB

位级计算是:

0x1234_ABCD >> 8 = 0x0012_34AB 0x0012_34AB & 0x0000_00FF = 0x0000_00AB

xori rd, rs1, -1可以实现按位取反,因为 12 位的-1编码为12'hFFF,符号扩展后为32'hFFFF_FFFF

sltsltu都把比较结果写成 32 bit 的01,但前者按有符号数解释,后者按无符号数解释。它们和后面的blt/bltu复用了“比较”的概念,却有不同的体系结构副作用:sltrd,分支只可能改写 PC。

在完整单周期数据通路中,比较器输出也是本周期的组合写回候选值;只有该指令被译码为slt/sltu时,控制器才会在下一个上升沿使能对应rd的写入。不能把比较表达式本身误认为一次独立的“软件赋值”。

2. 移位指令
指令操作空位补什么
sll/slli逻辑左移低位补 0
srl/srli逻辑右移高位补 0
sra/srai算术右移高位复制原符号位

RV32 的立即数移位量只需 5 bit,因为它只能选择0~31

25=32 2^5=3225=32

左移nnn位在不发生有效位截断时相当于乘以2n2^n2n。算术右移适合保持补码符号,逻辑右移则把输入当作无符号位串。移出 32 位边界的 bit 会被直接丢弃,因此不能把移位与数学乘除完全等同。

硬件中,移位类指令通常由桶形移位器完成。若把它并入 ALU,组合路径大致为:

寄存器数据 + 5 位 shamt → 移位器 → 写回 MUX → 寄存器堆

6.3.3 分支

分支通过修改 PC 改变程序流程。条件成立称为分支成功(taken),条件不成立称为分支失败(not taken)。

1. 条件分支
指令条件数值解释
beq rs1, rs2, labelrs1 == rs2位模式相等
bne rs1, rs2, labelrs1 != rs2位模式不等
blt rs1, rs2, labelrs1 < rs2有符号
bge rs1, rs2, labelrs1 >= rs2有符号
bltu rs1, rs2, labelrs1 < rs2无符号
bgeu rs1, rs2, labelrs1 >= rs2无符号

例如:

beq s0, s1, target

对应的 PC 选择关系是:

PCnext={PC+sext(branch_imm),s0=s1PC+4,s0≠s1 PC_{next}= \begin{cases} PC+sext(branch\_imm), & s0=s1 \\ PC+4, & s0\ne s1 \end{cases}PCnext={PC+sext(branch_imm),PC+4,s0=s1s0=s1

bltbltu的输入 bit 可以完全相同,但比较结果可能不同。因此译码器必须把有符号/无符号比较模式传给比较器或 ALU。

2. 跳转

j label无条件跳转到标签处。它是伪指令,实际可展开为:

jal x0, label

因为结果写向x0会被丢弃,所以只改变 PC,不保存返回地址。jaljalr还会在函数调用中保存返回地址,稍后在 6.3.7 讨论。

分支的硬件理解

条件分支至少包含三部分组合逻辑:

rs1、rs2 → 比较器 ─────────────┐ PC、B-type 立即数 → 目标加法器 ├→ PC MUX PC → PC+4 加法器 ──────────────┘

条件分支不应写寄存器,也不应写数据存储器。验证时需要同时检查“PC 是否选对”和“无关写使能是否保持为 0”。

6.3.1的时序模型,beq在组合阶段比较两个源寄存器:条件成立时选择PC+B_imm,否则选择PC+4。该指令没有普通寄存器写使能,因此下一上升沿只更新 PC,不会写寄存器。

6.3.6 数组

数组元素连续存放在内存中。若数组基址为base,索引为i,每个元素大小为element_sizebyte,则:

element_address=base+i×element_size element\_address=base+i\times element\_sizeelement_address=base+i×element_size

1. 32 位整数数组

RV32I 中一个int通常按 32 位 word 处理,占 4 byte:

address(scores[i])=base+(i≪2) address(scores[i])=base+(i\ll2)address(scores[i])=base+(i2)

原书“每个分数加 10”示例的核心地址计算为:

slli t0, s1, 2 # t0 = i * 4 add t0, t0, s0 # t0 = &scores[i] lw t1, 0(t0) addi t1, t1, 10 sw t1, 0(t0)

这里左移 2 位只是把 word 索引转换为字节偏移;本篇只关注 32 bit word 访问。

这五条汇编指令对应五个连续的指令周期,而不是一个组合数据流:先由slli在上升沿写回缩放后的索引,再由add写回元素地址;随后lw读出元素并在上升沿写入t1addi再写回t1+10,最后sw在存储器写入沿提交数据。它们复用同一套寄存器堆、ALU 和存储器接口。若数据 RAM 为同步读,lw的读数据会额外跨越一个时钟边界,不能继续套用这里的单周期假设。

6.3.7 函数调用

第七章直接用到的是jal的硬件语义:

rd ← PC + 4 PC ← PC + J_imm

这要求写回 MUX 能选择PC+4,PC MUX 能选择跳转目标。

6.3.1的时序模型,jal在组合阶段同时生成PC+J_immPC+4;下一个上升沿把前者提交到 PC、把后者提交到 J-type 的rd。上面的jal x1,+16情景中,这两个提交结果分别是0x10100x1004

6.3.8 伪指令

伪指令在进入处理器前已被汇编器展开;译码器只识别真实 opcode,不为jnopret增加专用硬件。

6.4 机器语言

汇编器把助记符、寄存器别名和标签转换成 32 位机器指令。RV32I 实际有 R、I、S、B、U、J 六种格式;原书根据字段相似性把它们归为 R-type、I-type、S/B-type 和 U/J-type 四组。

所有格式都满足两个重要规律:

  1. opcode固定放在instr[6:0]
  2. rdrs1rs2和立即数字段尽量占据相同位置。

这种折中让汇编编码看起来不够直观,但能减少译码与立即数选择硬件。

这正对应原书的设计原则 4:好的设计需要好的折中。RISC-V 没有强迫所有指令只用一种格式,也没有为每条指令设计完全不同的格式。

各类指令共享的字段在取指后由组合连线一次性切出:opcode做主译码,rd/rs1/rs2连接寄存器堆端口,funct3细分具体操作。这些都是组合结果;只有进入流水线设计时,才需要将字段和控制信号锁存到流水级寄存器。

6.4.1 R-type 指令

R-type 用两个源寄存器和一个目的寄存器:

31 25 24 20 19 15 14 12 11 7 6 0 +--------------+--------+--------+------+--------+---------+ | funct7 | rs2 | rs1 |funct3| rd | opcode | +--------------+--------+--------+------+--------+---------+ 7 bit 5 bit 5 bit 3 bit 5 bit 7 bit
  • rs1rs2:寄存器堆两个读地址;
  • rd:写地址;
  • opcodefunct3funct7:共同决定 ALU 操作和控制信号。

6.4.2 I-type 指令

I-type 用一个源寄存器、一个目的寄存器和一个 12 位立即数:

31 20 19 15 14 12 11 7 6 0 +----------------------+--------+------+--------+---------+ | imm[11:0] | rs1 |funct3| rd | opcode | +----------------------+--------+------+--------+---------+ 12 bit 5 bit 3 bit 5 bit 7 bit

它用于addiandiorixori、Load 和jalr等。除立即数移位指令外,imm[11:0]按 12 位补码解释并符号扩展到 32 位。

sllisrlisraiimm[4:0]是 5 位无符号移位量;高 7 位不再是普通数值,其中srai通过instr[30]=1srli区分。译码时不能把整个 12 位字段都送入移位量端口。

lw rd,-36(rs1),I-type 的imm[11:0]=12'hFDC会符号扩展为32'hFFFF_FFDC,再由地址 ALU 计算:

effective_address=rs1+(−36) effective\_address=rs1+(-36)effective_address=rs1+(36)

这里需要掌握的是“12 bit 字段 → 32 bit 有符号操作数”的路径,而非手算整条机器码。

6.4.3 S/B-type 指令

S-type 和 B-type 都没有rd,因为 Store 写内存、Branch 写 PC,不需要普通寄存器写回。二者共同保留rs1rs2funct3opcode,但立即数拼接方式不同。

1. S-type
31 25 24 20 19 15 14 12 11 7 6 0 +--------------+--------+--------+------+-----------+---------+ | imm[11:5] | rs2 | rs1 |funct3| imm[4:0] | opcode | +--------------+--------+--------+------+-----------+---------+

sw rs2, imm(rs1)

  • rs1提供基址;
  • rs2提供写数据;
  • imm经符号扩展后与基址相加;
  • 立即数被拆成高 7 位与低 5 位放入指令。

硬件必须把两段重新拼回:

S_imm = sext({instr[31:25], instr[11:7]})

Store 的控制不变量是RegWrite=0。如果译码器误把instr[11:7]当作rd,就可能产生错误寄存器副作用。

2. B-type

B-type 的 13 位有符号偏移量最低位固定为 0,指令只保存其余 12 位:

B_imm = sext({instr[31], instr[7], instr[30:25], instr[11:8], 1'b0}) branch_target = PC + B_imm

最低位不编码的原因是分支目标至少按 2 byte 对齐,所以偏移量必为偶数。这里编码的是字节偏移量,不是“跨过几条指令”。

6.4.4 U/J-type 指令

1. U-type
31 12 11 7 6 0 +------------------------------------------+--------+---------+ | imm[31:12] | rd | opcode | +------------------------------------------+--------+---------+ 20 bit 5 bit 7 bit

lui rd, imm20把指令中的 20 位立即数放到结果的高 20 位,低 12 位补 0:

U_imm = {instr[31:12], 12'b0}

这与上一篇构造大常数时的lui行为相同。

2. J-type

jal使用 21 位有符号 PC 相对偏移量,最低位同样固定为 0:

J_imm = sext({instr[31], instr[19:12], instr[20], instr[30:21], 1'b0}) jump_target = PC + J_imm rd = PC + 4

RTL 只需保证 J-type 立即数按该位序重排,并同时让 PC 选择jump_target、写回 MUX 选择PC+4;无需手算其整条机器码。

6.4.5 立即数编码

六种格式的立即数生成关系可以集中写成:

I: sext(instr[31:20]) S: sext({instr[31:25], instr[11:7]}) B: sext({instr[31], instr[7], instr[30:25], instr[11:8], 1'b0}) U: {instr[31:12], 12'b0} J: sext({instr[31], instr[19:12], instr[20], instr[30:21], 1'b0})

R-type 不需要立即数。

为什么字段看起来被打乱

RISC-V 尽量让相同含义的立即数 bit 落在相同指令位置,并让instr[31]作为有符号立即数的符号来源。这样可以减少立即数生成器中的 MUX 输入和布线交叉。

硬件可以把它理解为:

instr[31:7] ↓ 按 ImmSrc 选择并重排 ↓ 符号扩展 / 低位补 0 ↓ 32 位 ImmExt

立即数生成器是组合逻辑,不应保存状态。验证时至少覆盖:

  • I/S-type 的最大正数+2047与最小负数-2048
  • B/J-type 的正向与负向偏移;
  • B/J-type 生成结果 bit 0 恒为 0;
  • U-type 低 12 位恒为 0;
  • 不同格式下instr[31]的符号扩展。

真正实现时应把这些拼接式放在同一组合译码区;B/J 型的目标地址计算已在6.3.1的时序情景中说明。I/S/U 型遵循同样原则:字段重排和符号扩展在当前周期完成,产生的值只在驱动 PC 或写回使能的下一个上升沿改变体系结构状态。

6.4.6 寻址方式

寻址方式说明指令从哪里取得操作数或怎样产生新 PC。

寻址方式例子地址/操作数来源硬件路径
寄存器寻址add s0,s1,s2rs1rs2寄存器堆 → ALU
立即数寻址addi s0,s1,4rs1、立即数寄存器堆/立即数 MUX → ALU
基址寻址lw t0,8(s1)rs1+sext(imm)ALU 算有效地址 → 数据存储器
PC 相对寻址beqjalPC+sext(offset)PC/立即数 → 目标加法器

同一个加法运算在不同寻址方式下具有不同输入来源,因此数据通路需要 MUX;不能只看见“都是加法”就忽略控制信号。

6.4.7 解释机器语言代码

译码器的稳定顺序是:

  1. instr[6:0]opcode选择主控制信号和指令格式;
  2. 并行切出rdrs1rs2funct3instr[30]
  3. 由功能字段选择 ALU/比较操作,并由格式选择立即数重排;
  4. 只将明确需要的字段送入寄存器堆、ALU、存储器接口或 PC 选择逻辑。

重点是“字段→控制/数据通路”的连接,反汇编或 ABI 名称转换不是学习的重点。

6.4.8 存储程序

存储程序的核心是:机器指令和普通数据一样,以二进制形式存储在内存中。更换程序只需改变内存中的指令内容,不需要重新连接硬件。

原书示例从地址0x0000_0830开始保存 4 条指令:

0x0000_0830 0x0149_8933 0x0000_0834 0x4073_02B3 0x0000_0838 0xFF23_0913 0x0000_083C 0xFFA9_A383

PC 初始化为0x0000_0830后,处理器重复执行:

按 PC 取指 → 译码 → 读取操作数 → 执行 → 更新体系结构状态 → 选择下一 PC

取指数据流已经在6.3.1首次出现 PC 时给出。这里只需记住:PC 的体系结构含义始终是字节地址pc_q[31:2]仅在连接 32 bit 指令 RAM 时转换为 word 索引,并不改变 PC 本身的单位。

在本章范围内,RV32I 的主要体系结构状态包括:

  • PC;
  • 32 个整数寄存器的可见内容;
  • 内存中的可见数据。

组合逻辑的中间节点、流水线寄存器和分支预测状态不属于这里的软件可见体系结构状态。它们属于具体微体系结构,下一章才会展开。

本篇硬件视角总结

从指令语义到硬件资源

书中概念主要硬件资源或行为
顺序执行PC 寄存器、PC+4加法器
条件分支两端口寄存器读取、比较器、目标加法器、PC MUX
逻辑/移位ALU 逻辑单元、桶形移位器
数组访问索引缩放、地址加法、Load/Store 接口
jalPC+4写回、跳转目标写 PC
指令格式opcode 译码器、寄存器地址字段、立即数生成器
存储程序指令存储器、PC 驱动的取指路径

常见指令的副作用

指令类别rd读内存写内存改变 PC 的方式
R/I-type ALUPC+4
LoadPC+4
StorePC+4
BranchPC+4或分支目标
jal是,写入PC+4跳转目标

这张表适合直接转化为后续控制器的检查清单。尤其要防止 Store、Branch 对寄存器产生非预期写回,也要防止分支失败时仍选择目标 PC。

后续小节内容均偏向于软件,不再进行过于深度的学习,后续直接开始第七章 微体系结构 的学习。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/18 2:40:11

Lubuntu拯救旧电脑:轻量Linux系统优化指南

1. 为什么选择Lubuntu拯救旧电脑十年前的老笔记本开机要三分钟&#xff1f;1GB内存的电脑开个浏览器就卡死&#xff1f;这些困扰我太熟悉了。上周帮邻居大爷抢救他那台2012年的联想G480时&#xff0c;实测Lubuntu 22.04 LTS冷启动仅需23秒&#xff0c;Firefox开5个标签页内存占…

作者头像 李华
网站建设 2026/7/18 2:38:26

虚拟机安装Linux系统全指南:从入门到优化

1. 虚拟机与Linux系统安装概述在开发环境搭建和系统学习中&#xff0c;虚拟机技术已经成为不可或缺的工具。作为一名长期使用虚拟机的开发者&#xff0c;我深刻体会到在VMware Workstation或VirtualBox上安装Linux系统的便利性——它不仅能让你在不影响主机系统的情况下体验各种…

作者头像 李华
网站建设 2026/7/18 2:38:03

Unity移动端视觉优化全攻略:从资源导入到渲染调优

1. 项目概述&#xff1a;为什么视觉优化是Unity开发的“隐藏内容”做Unity开发久了&#xff0c;尤其是做移动端项目&#xff0c;总会遇到一个尴尬的局面&#xff1a;在编辑器里跑得丝滑流畅、画面精美的游戏&#xff0c;一打包到真机上&#xff0c;要么帧率跳水&#xff0c;要么…

作者头像 李华
网站建设 2026/7/18 2:36:22

Python实现高效磁盘重复文件查找与清理工具

1. 项目概述磁盘空间不足是每个电脑用户都会遇到的痛点问题。作为一名长期与存储设备打交道的开发者&#xff0c;我发现重复文件往往是吞噬磁盘空间的"隐形杀手"——它们可能是多次下载的同一份文档、不同命名的相同照片&#xff0c;或是软件安装时产生的冗余备份。手…

作者头像 李华
网站建设 2026/7/18 2:36:20

USB4 M.2 NVMe移动硬盘盒:高速外置存储原理与应用指南

如果你还在用传统的移动硬盘来传输大文件&#xff0c;那可能正在浪费大量宝贵时间。想象一下&#xff1a;一个20GB的4K视频素材&#xff0c;通过USB 3.0需要传输近10分钟&#xff0c;而使用40Gbps的USB4 M.2 NVMe硬盘盒&#xff0c;这个时间可以缩短到不到1分钟。这不仅仅是速度…

作者头像 李华
网站建设 2026/7/18 2:36:11

Python 1.10核心特性解析与现代版本对比

1. Python 1.10学习阶段总结作为一名Python开发者&#xff0c;我想分享下自己在Python 1.10版本学习过程中的心得体会。虽然现在Python已经发展到3.x版本&#xff0c;但回顾1.10这个早期版本的学习经历&#xff0c;对理解Python语言的发展脉络和核心思想很有帮助。Python 1.10发…

作者头像 李华