Skip to content

数字设计和计算机体系结构

一本书的内容必然全面庞杂,更何况还是国外这种循序渐进引人入胜的计算机经典教材。

人脑不是硬盘+大模型,该笔记摘抄并总结了这本书中我认为的重点,包括整本书对知识讲解的引导逻辑、重要概念、重点知识和自己的理解,供后面忘记的时候复习。

二进制

控制复杂性的艺术

  • 抽象
  • 约束
  • 三 Y 原则

这里就像设计模式一样,属于思想层面上很有用的东西,且必须要与实践结合。

数制

重点:计算机如何通过补码实现减法

数字抽象

数电是模电更上层的抽象。

为了将连续的物理量转换为离散的二进制信号,定义了逻辑电平。即定义了电压的高范围、低范围和禁止区域,来对应连续物理量是0、1还是不可知。

为了避免输入落到禁止区域,行业又规定了静态约束要求,通过约束换来了模电到数电抽象层次的提高。

组合逻辑设计

输出仅仅依赖于当前输入。

引言

在数字电路中,电路是一个可以处理离散值变量的网络。

一个电路可以看作一个黑盒子,其中包括:

  • 一个或多个离散值输入端
  • 一个或多个离散值输出端
  • 描述输入和输出关系的功能规范(本章重点介绍。表示输入输出逻辑,个人感觉逻辑门就是一种功能规范,逻辑门的组合也是。)
  • 描述当输入改变时输出响应延迟的时序规范(输入到输出的延迟)

组合电路的功能规范通常描述为真值表或布尔表达式,组合电路的后续的章节:

  • 如何通过真值表得到布尔表达式(离散学过)
  • 如何使用布尔代数和卡诺图来化简表达式(前者离散学过,后者记规则)
  • 如何通过逻辑门来实现这些表达式(看看就会了)
  • 如何分析这些电路的速度(跳过)

基础知识

此处要在心中熟记以下布尔表达式相关的基本概念,以及熟悉七种逻辑门,能够将二者关联起来。

离散数学

基本概念

析取范式:与或式

合取范式:或与式

主析取范式:由最小项组成

  • 最小项:所有变量的与项

主合取范式:有蕴涵项(最大项)组成

  • 蕴涵项:所有变量的或项

推理

  • 根据真值表得到布尔表达式。
  • 化简布尔表达式

    • 方法
      • 布尔代数(离散中学过的知识)
      • 卡诺图
        • 一种更方便化简布尔表达式的方法,对处理最多 4 个变量的问题非常好。
        • 不知道原理,使用方法倒是挺简单的,遵循规则即可。(规则忘记了去查)
    • 为什么要学?
      • 布尔代数和卡诺图是两种逻辑化简方法,最终的目标都是找出开销最低的特定逻辑函数实现方法。
      • 后面会学习专门用于化简电路的计算机程序。上面两种人工方法适合分析小问题。

其他

优先级:编程语言中运算的优先级似乎就是按照布尔表达式的优先级来的,NOT > AND > OR。

德摩根律在数字电路中有着形象的推气泡的规律:一个与非门等效于一个带逆变器输入的或门。

逻辑门

TODO

多级组合逻辑

减少硬件

电路最上层的设计一般是两级逻辑,这其中通常会涉及多输入的与门和或门。

  • 原来两级逻辑就是主析取范式和主合取范式的作用!
  • 核心思想:两层逻辑结构是实现布尔函数最简单和标准化的形式,关键是方便逻辑化简,进而优化工艺。

多输入的与门可以通过串联的与门来实现,其他逻辑门同理。这就是多级组合逻辑的核心思想之一。

此处似乎要灌输电路的设计策略,使其工艺更好。

推气泡

与门和或门更方便读取布尔表达式。为了化简与非门和或非门(CMOS电路喜欢用,不过那里是选学,我没学),可以使用推气泡发,将输出端的气泡推到输入端,在多级电路中相互消除,化简电路。

X 和 Z

非法值 X

  • 发生场景:两电路合并,两电路分别是01

浮空值 Z

  • 发生场景:输入端忘记连接电压
  • 注意:不要将浮空值等同于逻辑0

组合逻辑模块

前面学习了:全加器、优先级电路、7段译码显示器(译码器的一种特殊应用)。

这里又学习了:复用器、译码器。

具体看教材。

时序

跳了,感觉应该没什么用吧,组合逻辑电路在速度和成本上各有侧重的设计策略,这些不是软件工程师该考虑的事。

时序逻辑设计

输出依赖于当前的输入和过去的输入。

基本的时序元件:寄存器,可以记住过去的输入。

基于寄存器和组合逻辑构成的有限状态机提供了一种强有力的系统化方法来构造复杂系统。

引言

  • 锁存器和触发器:存储 1 位状态的简单时序逻辑电路。
  • 同步逻辑设计:由组合逻辑和一组表示电路状态的触发器组成。(时序逻辑电路的分析很复杂,为了简化设计,此处之涉及同步时序逻辑电路。)
  • 有限状态机:设计时序电路的一种简单方法。
  • 又是时序。

锁存器和触发器

如何一步步从基本的逻辑门构造出计算机数据存储的基本单元:寄存器

最简单的双稳态元件:交叉耦合反向器。

  • 无法输入,可以存储 1 位信息。

SR 锁存器:

  • 最基础的存储单元。

D 锁存器:

  • 将 S R 整合到一起,用一个输入 D 来控制状态,简化输入,解决 SR 锁存器的"非法状态"问题。
  • 触发方式:电平触发。
  • 一个时钟信号。
  • 通过时钟信号的电平(处于高电平或低电平)控制数据存储的时间节点,而不是输入一有更新就存储。

D 触发器:

  • 触发方式:边沿触发。
  • 一个时钟信号。
  • 通过时钟信号的边沿(上升沿或下降沿到来)控制数据存储的时间节点,而不是输入一有更新就存储。

寄存器:

  • 一个时钟信号,多个 D 触发器。从而可以存储多位数据。
  • 寄存器是计算机中数据存储的基本单元。

带使能端的触发器:

  • 在 D 触发器基础上增加了一个使能信号(Enable),决定数据是否可以被写入到触发器中。
  • 使能信号 = 1:触发器正常工作;
  • 使能信号 = 0:写入无效,触发器存储内容不变。
  • 注意:使能信号与 D 触发器本身的时钟周期是两码事。
  • D 触发器的时钟信号就像相机的快门,触发后记录状态。
  • 使能信号相当与在时钟信号的基础上加了个开关,开关关闭时,快门(时钟信号)也不会工作。

带复位功能的触发器:

  • 在 D 触发器基础上增加了一个复位信号(Reset),可以将触发器的存储值强制恢复到一个固定状态。
  • 复位信号 = 1:输出 Q 强制变为 0 (无论输入 D 是什么);
  • 复位信号 = 0:触发器正常工作。

同步逻辑设计

讲解了同步时序电路。

重点是时序同步电路的规则。(看书或 STFW)

有限状态机

独热编码,不同与二进制编码,一个位表示一个状态,电路更加简单。

Moore 型状态机:输出与输入无关,只有当前状态决定(我处在什么状态,我就输出什么)

Meanly 型状态机:输出看当前状态和输入(我处在什么状态,还要看输入是什么,才能决定输出)

状态机的分解:将复杂状态机分解为多个相互作用的更简单状态机。


本人的目标是成为软件工程师,出于看懂 PA 的目的学习数电,目前来看前三章已经基本满足了我的需求。后续内容粗略过了一下,如果未来需要,再去细看。

硬件描述语言

HDL,硬件的编程语言。

使用代码描述数字系统,更快更方便。

4.10 总结 第三段 好好看看。

数字模块

这章研究组合逻辑和时序逻辑模块,如:

  • 算数电路:加法器、减法器、比较器和移位器
  • 时序电路:计数器(使用加法器和寄存器来递归当前计数)和移位寄存器
  • 存储器阵列
  • 逻辑阵列

体系结构

引言

什么是体系结构?

体系结构是指计算机系统设计的逻辑框架和组织方法。它定义了硬件、软件之间的交互方式,以及计算机执行指令的基本模型。

RISC-V、ARM 和 x86 就可以说是三种常用的计算机体系结构

  • 这些名词可以指指令集架构(ISA),也可以指从指令集(蓝图)到工艺(为体系结构)到实现(硬件实现)的一整套体系结构设计。

重点:

  • 指令集架构(ISA):软件与硬件间的接口,定义了计算机能够执行的基本指令。
    • 相当与一栋建筑的设计图纸,规定楼房的功能和布局。软件根据蓝图来“建造”(运行)程序。
  • 微体系结构:定义处理器内部的具体实现方式,例如流水线、缓存、分支预测等。
    • 相当与如何搭建这栋楼的具体施工方式、工艺。
    • 不同的微体系结构可以使用相同的指令集,就像不同的使用工艺可以用于同一个蓝图,例如 AMD 和 Intel 的芯片。
  • 硬件实现:硬件电路的具体实现,比如门电路、寄存器设计等。
    • 相当与建筑工人基于蓝图(ISA)和施工工艺(微架构),真正造出了楼(处理器芯片)。

目录

从底层硬件到高层程序设计的完整视角

目录翻译

体系结构

  • 引言
  • 汇编语言
    • 指令
    • 操作数:寄存器、存储器和常数
  • 编程
    • 程序流程
    • 逻辑、移位和乘法指令(算数/逻辑指令)
    • 分支
    • 条件语句
    • 循环
    • 数组
    • 函数调用
    • 伪指令
  • 机器语言
    • R 类型指令
    • I 类型指令
    • S/B 类型指令
    • U/J 类型指令
    • 立即数编码
    • 寻址模式
    • 解释机器语言代码
    • 存储程序
  • 编译、汇编和装入
    • 内存映射
    • 汇编器指令
    • 编译
    • 汇编
    • 链接
    • 装入
  • 杂项*
    • 字节序
    • 异常
    • 有符号和无符号指令
    • 浮点指令
    • 压缩指令
  • RISC-V 架构的演变*
    • RISC-V 基础指令集和扩展
    • RISC-V 和 MIPS 架构的对比
    • RISC-V 和 ARM 架构的对比
  • 另一个视角:x86 架构*
    • x86 寄存器
    • x86 操作数
    • 状态标志
    • x86 指令集
    • x86 指令编码
    • x86 的其他特性
    • 小结
  • 总结

目录大致解释

介绍体系结构的基本概念

讲解汇编语言

  • 基本指令

  • 操作数!什么是操作数?我现在还不知道。

    操作数是指令操作的输入数据

讲解如何用汇编语言编程

  • 简单介绍
  • 介绍算术和逻辑操作(相当与 C 中的位操作(与或非、移位))
  • 分支:汇编实现条件跳转(if else)
  • 条件语句:汇编实现条件判断
  • 循环
  • 数组:通过汇编操作内存中的数组数据
  • 函数调用:如何用汇编实现函数的定义与调用,包括传参与返回值
  • 伪指令:辅助功能指令,用于简化汇编编程

机器语言

  • 指令类型
    • R:寄存器间的操作,如加法乘法
    • I:包含立即数,用于算术操作、加载数据
    • S:存储操作
    • B:条件跳转
    • U/J:用于加载高位地址和无条件跳转
  • 详解立即数相关知识
  • 介绍不同类型的内存地址访问方式,例如直接寻址、寄存器偏移寻址等。
  • 教授如何手动分析和理解机器语言的二进制表示。
  • 讨论计算机存储程序的基本原理以及其对计算能力的影响。

编译、汇编和装入:描述从高级语言到机器语言的全过程。

  • 内存映射:讲解程序运行时内存的分布,例如代码段、数据段、堆栈段。
  • 汇编器指令:伪指令等辅助功能如何帮助汇编器生成机器代码。
  • 编译:高级语言(如 C、Java)被翻译成中间语言的过程。
  • 汇编:中间语言被翻译成机器代码的过程。
  • 链接:把多个目标文件合并为一个可执行程序。
  • 装入:把程序装入内存并准备执行的步骤。

RISC-V设计原则

  1. regularity supports simplicity;
    • 设计:规则性和简洁性(设计规则简介统一,易于实现与理解)
  2. make the common case fast;
    • 优化常见的情况(会带来最大的收益,即使某些非常规操作的性能有所牺牲)
  3. smaller is faster;
    • 更快更小(精简设计)
  4. good design demands good compromises.
    • 优秀的设计需要合理的权衡(一个好的设计会在性能、成本、功耗和复杂性之间找到最佳平衡,而不是极端优化某一方面)

RISC-V寄存器

32 个

编号 别名 功能描述
x0 zero 恒等于 0 的寄存器,所有写入操作都会被忽略。
x1 ra 返回地址寄存器,用于存储函数调用的返回地址。
x2 sp 栈指针寄存器,指向当前堆栈的顶部。
x3 gp 全局指针寄存器,用于全局变量的地址指针。
x4 tp 线程指针寄存器,用于线程本地存储(TLS)。
x5–x7 t0–t2 临时寄存器(Caller-Saved),用于临时数据,不需要在函数调用中保存。
x8 s0 / fp 保存寄存器 0,或用作帧指针(Frame Pointer)。
x9 s1 保存寄存器 1,保存调用者需要的数据(Callee-Saved)。
x10–x11 a0–a1 函数调用参数寄存器,也用于存储函数返回值(第 1 和第 2 个返回值)。
x12–x17 a2–a7 函数调用参数寄存器(Caller-Saved)。
x18–x27 s2–s11 保存寄存器(Callee-Saved),保存调用者需要的数据。
x28–x31 t3–t6 临时寄存器(Caller-Saved),用于临时数据,不需要在函数调用中保存。

汇编语言

汇编语言是指令集的符号化表示,方便程序员阅读。

每条汇编语言指令制定了要执行的操作(激励事件)和要操作的操作数(状态)。

指令

操作数

  • 操作数可以存储在寄存器或内存中,也可以是存储在指令本身中的常量(立即数)
    • 常量&寄存器:速度快,容量小
    • 内存:速度慢,容量大
  • 寄存器:操作数只有常量是不够的,所以大部分体系结构都会指定少量寄存器来保存常用的操作数。RISC-V 有 32 个操作数。
  • 寄存器集:32 个寄存器(名称与用法)。
  • 立即数:“立即数是 12 位二进制的补码数,因此它们是直接拓展到 32 位的。”
    • 立即数在 RV32I 中占用指令的 12 位。
    • 补码:表示有符号整数的一种方式,能统一处理正数与负数。
      • 正数的补码与原码相同
      • 负数的补码是原码的取反+1
      • 12 位补码表示范围为:-2^11 ~ 2^11-1(-2048 ~ 2047)
    • 符号扩展:将一个较短的有符号数扩展到较长位宽。补码拓展,正数高位补 0,负数高位补 1。
      • 此处将 12 位立即数扩展为 32 位,使其能够与寄存器中的 32 位数据计算。
  • 内存:
    • RISC-V 中,指令只能在寄存器上操作,存储在内存中的数据必须在处理之前移动到寄存器中。
    • 存储器&数据字
      • 存储器相当于由数据字组成的数组。
      • 所以 RISC-V 的存储器是“按字节寻址“的,即每个存储器中的每个字节都有一个唯一的地址,存储器可以通过该地址访问或操作单个字节。
      • 在 RISC-V 中,数据字(RV32)是 32 位。
      • 处理器运算的最小粒度是数据字。
      • 存储器操作的基本粒度是数据字,能够操作的最小粒度为字节。

编程

程序流

(一个程序的执行,就是一段指令流的依次执行)

指令也存储在内存中,每条指令 32 位(4 字节)长,也是根据地址访问,因此连续的指令地址增加 4。

指令-逻辑

and s3, s1, s2 ; s3 = s1 and s2
or  s4, s1, s2 ; s4 = s1 or  s2
xor s5, s1, s2 ; s5 = s1 xor s2

指令-移位

; 逻辑左移
slli t0, s5, 7  ; t0 = s5 << 7
; 逻辑右移(通常应用于无符号数)
srli s1, s5, 17 ; s1 = (unsigned int)s5 >> 17
; 算数右移(通常用于有符号数,会保留符号位)
srai t2, s5, 3  ; t2 = s5 >> 3

指令-乘法

; 低位乘法,常用
mul t2, s3, s5
; 高位乘法,用于大整数运算
mulh t1, s3, s5

指令-分支-条件分支

主要是 6 个指令

beq : Branch if Equal

beq t0, t1, label
if (t0 == t1) {
    goto label;
}

bne : Branch if Not Equal

bne t0, t1, label
if (t0 != t1) {
    goto label;
}

blt : Branch if Less Than

blt t0, t1, label
if (t0 < t1) {
    goto label;
}

bge : Branch if Greater Than or Equal

bge t0, t1, label
if (t0 >= t1) {
    goto label;
}

blut : Branch if Less Than, Unsigned

bltu t0, t1, label
if ((unsigned int)t0 < (unsigned int)t1) {
    goto label;
}

bgut : Branch if Greater Than or Equal, Unsigned

bgeu t0, t1, label
if ((unsigned int)t0 >= (unsigned int)t1) {
    goto label;
}

指令-分支-跳跃

j : Jump,无条件跳转

j label
goto label;

jal : Jump and Link, 跳转并链接

功能:通常用于函数调用。

jal ra, func
  • 跳转到 func,同时将当前指令的下一条地址存入寄存器 ra
  • 当子程序完成后,可以通过返回地址(ra)跳回原来的位置。
// 代码
func(); // 函数调用
// 代码

jr : Jump Register, 跳转寄存器

功能:根据寄存器中保存的地址跳转。

jr ra
  • 跳转到 ra 中存储的地址,通常用于从子程序返回。
return; // 返回到调用点

使用-条件语句-if

if 语句

; s0 = apples, s1 = oranges
; s2 = f, s3 = g, s4 = h
    bne s0, s1, L1 ; skip if (apples != oranges)
    add s2, s3, s4 ; f = g + h
L1:
    sub s0, s1, s4 ; apples = oranges − h
if (apples = = oranges)
    f = g + h;
apples = oranges  h;

使用-条件语句-if/else

if/else 语句

# s0 = apples, s1 = oranges
# s2 = f, s3 = g, s4 = h
    bne s0, s1, L1 # skip if (apples != oranges)
    add s2, s3, s4 # f = g + h
    j   L2
L1:
    sub s0, s1, s4 # apples = oranges − hL2:
if (apples = = oranges)
    f = g + h;
else
    apples = oranges  h;

使用-条件语句-switch/case

switch/case 语句

switch (button) {
    case 1:
        amt = 20;
        break;
    case 2:
        amt = 50;
        break;
    case 3:
        amt = 100;
        break;
    default:
        amt = 0;
}
// 同样的if/else语句
if (button = = 1)
    amt = 20;
else if (button = = 2)
    amt = 50;
else if (button = = 3)
    amt = 100;
else
    amt = 0;
# s0 = button, s1 = amt
case1:
    addi t0, zero, 1     # t0 = 1
    bne  s0, t0, case2   # button = = 1?
    addi s1, zero, 20    # if yes, amt = 20
    j    done            # break out of case
case2:
    addi t0, zero, 2     # t0 = 2
    bne  s0, t0, case3   # button = = 2?
    addi s1, zero, 50    # if yes, amt = 50
    j    done            # break out of case
case3:
    addi t0, zero, 3     # t0 = 3
    bne  s0, t0, default # button = = 3?
    addi s1, zero, 100   # if yes, amt = 100
    j    done            # break out of case
default:
    add  s1, zero, zero  # amt=0
done:

使用-循环-while

while 循环

// determines the power
// of x such that 2x = 128
int pow = 1;
int x = 0;
while (pow != 128) {
    pow = pow * 2;
    x = x + 1;
}
# s0 = pow, s1 = x
    addi s0, zero, 1    # pow = 1
    add  s1, zero, zero # x = 0

    addi t0, zero, 128  # t0 = 128
while:
    beq  s0, t0, done   # pow = 128?
    slli s0, s0, 1      # pow = pow * 2
    addi s1, s1, 1      # x = x + 1
    j    while          # repeat loop
done:

使用-循环-for

for 循环

// add the numbers from 0 to 9
int sum = 0;
int i;
for (i = 0; i < 10; i = i + 1) {
    sum = sum + i;
}
# s0 = i, s1 = sum
    addi s1, zero, 0 # sum = 0
    addi s0, zero, 0 # i = 0
    addi t0, zero, 10 # t0 = 10
for:
    bge  s0, t0, done # i >= 10?
    add  s1, s1, s0 # sum = sum + i
    addi s0, s0, 1 # i = i + 1
    j    for # repeat loop
done:

指令-数据字

lw

lw : Load Word,从内存加载数据到寄存器。

具体来说,它将一个 32 位的数据字从内存地址加载到指定的寄存器中。

格式:

lw rd, offset(rs1)
  • rd:目标寄存器,用于保存从内存加载的数据。
  • rs1:基地址寄存器,存储内存地址的基地址。
  • offset:偏移量,是一个立即数,用于指定相对于基地址的内存位置(以字节为单位)。

示例:

lw t0, 8(s1)
  • s1 + 8 这个内存地址处加载一个 32 位的数据,存入寄存器 t0
  • 如果 s1 = 0x1000,那么 t0 将存储内存地址 0x1008 处的数据。

适用场景:

  • 读取数组元素。
  • 获取变量值。
  • 加载指针所指向的数据。

sw

sw : Store Word,将寄存器中的数据存储到内存。

具体来说,它将寄存器中的 32 位数据存储到内存中的指定地址。

格式:

sw rs2, offset(rs1)
  • rs2:源寄存器,包含要存储到内存的数据。
  • rs1:基地址寄存器,存储内存地址的基地址。
  • offset:偏移量,是一个立即数,用于指定相对于基地址的内存位置(以字节为单位)。

示例:

sw t0, 8(s1)
  • 将寄存器 t0 中的数据存储到内存地址 s1 + 8 的位置。
  • 如果 s1 = 0x1000,那么 t0 的值将存储到内存地址 0x1008

适用场景:

  • 修改数组元素。
  • 保存变量值。
  • 更新指针所指向的内容。

使用-数组

示例

int i;
int scores[200];
for (i = 0; i < 200; i = i + 1)
    scores[i] = scores[i] + 10;
# s0 = scores base address, s1 = i
    addi s1, zero, 0 # i = 0
    addi t2, zero, 200 # t2 = 200
for:
    bge  s1, t2, done # if i >= 200 then done
    slli t0, s1, 2 # t0 = i * 4
    add  t0, t0, s0 # address of scores[i]
    lw   t1, 0(t0) # t1 = scores[i]
    addi t1, t1, 10 # t1 = scores[i] + 10
    sw   t1, 0(t0) # scores[i] = t1
    addi s1, s1, 1 # i = i + 1
    j    for # repeat
done:

指令-字节&字符

ASCII 码,每个字符占用一个字节,C 语言使用 char 类型表示字符。

lb

lb : Load Byte,加载字节

从内存中加载一个字节(8 位)到寄存器。

加载的字节会进行符号扩展到 32 位。

  • 符号拓展:字节的高位为 0 补 0,高位为 1 补 1。

格式:

lb rd, offset(rs1)
  • rd:目标寄存器,用于保存从内存加载的数据。
  • rs1:基地址寄存器,存储内存基地址。
  • offset:偏移量(立即数),相对于基地址的内存地址。

示例:

lb t0, 3(s1)
  • 从地址 s1 + 3 处加载一个字节(8 位)。
  • 该字节会扩展为 32 位,并存入寄存器 t0

lbu

lbu : Load Byte Unsigned,加载无符号字节

从内存中加载一个字节(8 位)到寄存器。

加载的字节会进行零扩展到 32 位:

  • 零拓展:无论字节的最高位是 0 或 1,扩展时高位都补 0。

格式:

lbu rd, offset(rs1)
  • rd:目标寄存器,用于保存从内存加载的数据。
  • rs1:基地址寄存器,存储内存基地址。
  • offset:偏移量(立即数),相对于基地址的内存地址。

示例:

lbu t0, 3(s1)
  • 从地址 s1 + 3 处加载一个字节(8 位)。
  • 该字节会扩展为 32 位,并存入寄存器 t0,但始终执行零扩展。

sb

sb : Store Byte,存储字节

  • 将寄存器的最低 8 位(字节)存储到内存。
  • 不影响寄存器的其他位,也不会扩展高位。

格式:

sb rs2, offset(rs1)
  • rs2:源寄存器,包含要存储的数据。
  • rs1:基地址寄存器,存储内存基地址。
  • offset:偏移量(立即数),相对于基地址的内存地址。

示例:

sb t0, 3(s1)
  • 将寄存器 t0 的最低 8 位存储到地址 s1 + 3

字符串

字符串的本质就是字符数组,一个字符对应一个字节,空字符表示字符串的结束。

使用-字节&字符

示例

// high-level code
// chararray[10] was declared and initialized earlier
// 将字符数组元素从小写转换为大写
int i;
for (i = 0; i < 10; i = i + 1)
    chararray[i] = chararray[i]  32;
# RISC-V assembly code
# s0 = base address of chararray (initialized earlier), s1 = i
    addi s1, zero, 0 # i = 0
    addi t3, zero, 10 # t3 = 10
for:
    bge  s1, t3, done # i >= 10 ?
    add  t4, s0, s1 # t4 = address of chararray[i]
    lb   t5, 0(t4) # t5 = chararray[i]
    addi t5, t5, −32 # t5 = chararray[i] − 32
    sb   t5, 0(t4) # chararray[i] = t5
    addi s1, s1, 1 # i = i + 1
    j    for # repeat loop
done:

使用-函数调用

当一个函数调用另一个函数时,调用函数(调用方)和被调用函数(被调用方)必须就在何处放置参数和返回值达成一致。

  • 调用方调用函数:在寄存器 a0~a7 中放置至多 8 个参数。
  • 被调用者在完成之前将返回值放在寄存器a0中。
  • 被调用方不能干扰调用方的行为,即不能操作调用者所需的任何寄存器或内存。(这句话与上一句并不冲突,遵循调用约定,调用约定中调用方允许被调用方操作返回值寄存器 a0。)
  • jal 指令通过被调用方地址和返回地址寄存器 ra 完成指令流的连续执行。

函数调用和返回

int main() {
    simple();
    //...
}
// function
void simple() {
    return;
}
main:
    jal simple
    ;...
simple:
    jr  ra

输入参数和返回值

根据 RISC-V 约定,函数使用 a0~a1 作为输入参数,a0 作为返回值。

int main() {
    int y;
    //...
    y = diffofsums(2, 3, 4, 5);
    //...
}
int diffofsums(int f, int g, int h, int i) {
    int result;
    result = (f + g)  (h + i);
    return result;
}
# s7 = y
main:
    ;...
    addi a0, zero, 2 # argument 0 = 2
    addi a1, zero, 3 # argument 1 = 3
    addi a2, zero, 4 # argument 2 = 4
    addi a3, zero, 5 # argument 3 = 5
    jal  diffofsums # call function
    add  s7, a0, zero # y = returned value
    ;...
# s3 = result
diffofsums:
    add t0, a0, a1 # t0 = f+g
    add t1, a2, a3 # t1 = h+i
    sub s3, t0, t1 # result = (f+g)−(h+i)
    add a0, s3, zero # put return value in a0
    jr  ra # return to caller

堆栈

堆栈是用作临时存储空间的内存。存储在内存中。

当调用参数超过 8 个的函数时,额外的输入参数会被存放在堆栈上。

寄存器 2 sp 是堆栈指针。

  • 始终指向堆栈顶部(即最近压入堆栈的数据)
  • 栈帧从高地址向低地址增长,sp 的值随着数据压入而减小,弹出而增大

在上一个实例中,函数违反了 RISC-V 约定,修改了 t0、t1 和 s3,即修改了调用者的寄存器。

解决:堆栈

  • 在堆栈上腾出空间来存储一个或多个寄存器的值。
  • 将寄存器的值存储在堆栈上。
  • 使用寄存器执行函数。
  • 从堆栈中恢复寄存器的原始值。
  • 释放堆栈上的空间。

改进:

int diffofsums(int f, int g, int h, int i) {
    int result;
    result = (f + g)  (h + i);
    return result;
}
# s3 = result
diffofsums:
    addi sp, sp, −12 # make space on stack to
                     # store three registers
    sw s3, 8(sp) # save s3 on stack
    sw t0, 4(sp) # save t0 on stack
    sw t1, 0(sp) # save t1 on stack
    add t0, a0, a1 # t0 = f + g
    add t1, a2, a3 # t1 = h + i
    sub s3, t0, t1 # result = (f + g) − (h + i)
    add a0, s3, zero # put return value in a0
    lw s3, 8(sp) # restore s3 from stack
    lw t0, 4(sp) # restore t0 from stack
    lw t1, 0(sp) # restore t1 from stack
    addi sp, sp, 12 # deallocate stack space
    jr ra # return to caller

保存寄存器

寄存器应当根据调用函数后续是否会继续使用来选择性保存,提高性能。

RISC-V 将寄存器分为保存类和非保存类。

  • 保存类寄存器

    • 保存类寄存器必须在被调用函数的开始和结束处包含相同的值。
    • 保存寄存器:s0~s11
    • 返回地址:sp
    • 堆栈指针:ra
  • 非保存类寄存器

    • 临时寄存器:t0~t6
    • 参数寄存器:a0~a7

继续改进:只在堆栈上保留 s3,t0 和 t1 是非保存寄存器。

# s3 = result
diffofsums:
    addi sp, sp, −4 # make space on stack to store one register
    sw   s3, 0(sp) # save s3 on stack
    add  t0, a0, a1 # t0 = f + g
    add  t1, a2, a3 # t1 = h + i
    sub  s3, t0, t1 # result = (f + g) − (h + i)
    add  a0, s3, zero # put return value in a0
    lw   s3, 0(sp) # restore s3 from stack
    addi sp, sp, 4 # deallocate stack space
    jr   ra # return to caller

非叶函数调用(没细看)

不调用其他函数的函数被称为叶函数。调用其他函数的函数被称为非叶函数。非叶函数较为复杂。

递归函数调用(没细看)

递归函数是调用自己的非叶函数。递归函数即使调用者又是非调用者,必须同时保存保存类寄存器和非保存类寄存器。

附加参数和局部变量*

函数超过 8 个的额外参数在堆栈上存储。

函数的局部变量存储在 s0~s11 上。超出部分存储在堆栈上。

伪指令

用来简化指令的编写。

机器语言

RISC-V 定义了四种主要的指令格式

  • R,rigister,寄存器类型
  • I,immediate,立即数类型
  • S/B,store/branch,存储/分支类型
  • U/J,upper immediate/jump,上位立即数/跳跃类型

R类型指令

R 型指令用于三操作数寄存器运算,例如加法、减法、逻辑操作等。

31       25   20    15       12     7        0
| funct7 | rs2 | rs1 | funct3 | rd  | opcode |
字段名 位数 描述
opcode 7 操作码,指明操作类型(如算术、逻辑等)。
rd 5 目标寄存器,存放运算结果的寄存器。
funct3 3 子操作码,进一步指定指令的功能(如加法/减法)。
rs1 5 源操作数 1 的寄存器编号。
rs2 5 源操作数 2 的寄存器编号。
funct7 7 子操作码,用于扩展指令功能(如区分加法和减法)。

示例: add rd, rs1, rs2 // rd = rs1 + rs2

funct7 | rs2  | rs1  | funct3 | rd   | opcode
0000000| 01000| 00101| 000    | 01101| 0110011

I类型指令

I 型指令用于包含立即数的运算,如加载、算术运算或逻辑运算。

31          20    15      12    7        0
| imm[11:0] | rs1 | funct3 | rd | opcode |
字段名 位数 描述
opcode 7 操作码,指明操作类型。
rd 5 目标寄存器,存放操作结果的寄存器。
funct3 3 子操作码,进一步指定指令功能。
rs1 5 源操作数寄存器编号。
imm 12 12 位立即数(可以是符号扩展的值)。

示例: addi rd, rs1, imm // rd = rs1 + imm

imm[11:0]   | rs1  | funct3 | rd   | opcode
000000000101| 00101| 000    | 01101| 0010011

S类型指令

S 型指令用于将寄存器的值存储到内存中。

31        25   20     15     12       7        0
|imm[11:5]| rs2 | rs1 |funct3|imm[4:0]| opcode |
字段名 位数 描述
opcode 7 操作码,指明操作类型(如存储)。
imm 12 立即数,分为高位 imm[11:5] 和低位 imm[4:0]
funct3 3 子操作码,指定操作的类型。
rs1 5 基址寄存器,用于存储操作的内存地址基址。
rs2 5 源操作数寄存器,存储到内存的值。

示例: sw rs2, imm(rs1)

功能: 将 rs2 中的值存储到 rs1 + imm 指向的内存地址中。

二进制格式:

imm[11:5]|rs2  |rs1  |funct3| imm[4:0] | opcode
0100000  |01000|00101|010   | 00010    | 0100011

B类型指令

B 型指令用于条件分支跳转。

31          25  20  15     12           7      0
|imm[12|10:5]|rs2|rs1|funct3|imm[4:1|11]|opcode|
字段名 位数 描述
opcode 7 操作码,指明分支操作。
imm 13 13 位偏移量,用于计算跳转目标地址。
funct3 3 子操作码,指定分支的类型(如等于、不等等)。
rs1 5 源操作数 1 的寄存器编号。
rs2 5 源操作数 2 的寄存器编号。

示例: beq rs1, rs2, imm

功能: 如果 rs1rs2 相等,则跳转到 PC + imm

二进制格式:

imm[12|10:5]| rs2 | rs1 |funct3|imm[4:1|11]|opcode
000000      |01000|00101| 000  | 00010     |1100011

U类型指令

U 型指令用于加载高 20 位立即数。

31           11   7        0
| imm[31:12] | rd | opcode |
字段名 位数 描述
opcode 7 操作码,指明操作类型(如加载高位立即数)。
rd 5 目标寄存器,存放结果的寄存器。
imm 20 高 20 位立即数。

示例: lui rd, imm

功能: 将立即数 imm 的高 20 位加载到 rd 的高位,低 12 位填充为 0。

二进制格式:

imm[31:12]          |  rd   | opcode
00000000000010100101| 01101 | 0110111

J类型指令

J 型指令用于跳转指令。

31                     11    7        0
| imm[20|10:1|11|19:12] | rd | opcode |
字段名 位数 描述
opcode 7 操作码,指明跳转操作。
rd 5 跳转链接寄存器,存储返回地址(通常为 ra)。
imm 21 跳转偏移量,用于计算目标地址。

示例: jal rd, imm

功能: 跳转到 PC + imm,并将返回地址存储到 rd 中。

二进制格式:

imm[20|10:1|11|19:12]| rd    | opcode
00000000000100001000 | 00001 | 1101111

直接编码

讲解不同指令中立即数是如何设计的,没看。

寻址模式

RISC-V 的四种寻址模式

仅寄存器寻址 和 立即寻址 主要用于寄存器操作;基寻址 和 PC 相对寻址 则涉及到内存访问和控制流跳转。

  • 仅寄存器寻址

    • 操作数完全来自寄存器。
    • 不涉及内存访问,运算只在寄存器间进行。
    • 通常用于算术和逻辑运算指令。所有 R 型指令都是仅寄存器寻址。
  • 立即寻址

    • 操作数来自寄存器和立即数。
    • 不需要从内存中加载操作数,立即数直接参与运算。
    • 常用于对寄存器值进行固定偏移量的运算。常见于 I 型指令。
  • 基寻址

    • 利用基址寄存器加偏移量来访问内存地址,偏移量可以是立即数或寄存器中的值。
    • 主要用于内存访问操作(加载和存储)。
    • 常见于 I 型指令(加载指令)和 S 型指令(存储指令)。
    • 应用场景
      • 从内存加载数据到寄存器(lwlblh)。
      • 将寄存器数据存储到内存中(swsbsh)。
      • 栈操作(栈基址寄存器通常是 sp)。
  • PC 相对寻址

    • 地址通过当前程序计数器(PC)的值加上一个偏移量来计算。

    • 通常用于控制流指令,例如跳转(jal)和分支跳转(beqbne 等)。

    • 偏移量通常是指令中的立即数,并会被符号扩展为目标地址。

    • 应用场景

      • 函数跳转:

        • 使用 jal 指令实现跳转并保存返回地址。
        • 用于调用函数和递归。
      • 条件跳转:

        • 使用 beqbne 实现基于条件的跳转。
        • 用于循环控制、条件分支等。

解释机器语言代码

讲解如何将机器语言翻译为汇编语言,没看。

存储程序的威力

指令存储在内存中。

程序的运行仅仅是向内存中写入指令,CPU 从存储器中检索/取出指令并执行,从而进行一系列的内存访问和指令执行。

存储程序指的就是存储在内存中的一系列指令。

编译、汇编和链接*

gcc

  • -o:output,指定输出文件名称

预处理:预处理器 C 代码(main.c)翻译成宏和头文件展开的 C 代码(main.i)。

gcc -E main.c -o main.i

编译:编译器将预处理后的高级代码(main.i)翻译成汇编代码(main.s)。

gcc -S main.i -o main.s

汇编:汇编器将汇编代码(main.s)翻译成目标机器码(二进制文件)(main.o)(不可执行)。

gcc -c main.s -o main.o

链接:将目标文件(main.o)与所需的库文件(如标准库 libc)链接起来,生成最终的可执行文件(如 main)。

gcc main.o -o main