当你用 C 或 C++ 编写程序时,处理器怎么知道该做什么?
源代码可能描述一次计算、一个循环,或者一个函数。但处理器不会直接阅读这些语句,它执行的是机器指令。
不同的处理器可能支持不同的指令集。这就引出了我们经常听到的名字:x86、Arm 和 RISC-V。
RISC-V 的英文发音是“risk five”。它是一种指令集架构,规定了软件在兼容处理器上运行时可以使用的指令,以及这些指令应当产生的行为。
这些概念究竟是什么意思?它们又怎样与寄存器、内存、编程语言和操作系统联系起来?
下面,我们一步一步来理解。
1. CPU 需要定义明确的指令
CPU 执行指令,每条指令都要求它完成某种具体操作。
一条指令可以把两个数相加,从内存读取数据,把结果写回内存,或者改变接下来执行哪条指令。
这些操作比我们日常描述的任务细小得多。例如,“把这些名字排序”这样的要求,可能需要许多条指令共同完成。
机器指令用一组二进制位表示。位(bit)是一个二进制数字,取值只能是零或一。
处理器内部的电路解释这些二进制位,并执行规定的操作。
要让这个过程正确运行,软件和硬件必须对每条指令的含义达成一致。同一个二进制编码,不能在软件看来代表加法,在处理器看来却代表减法。
这种约定,就是指令集架构的一部分。
2. 什么是指令集架构?
指令集架构(Instruction Set Architecture,简称 ISA)定义了机器级软件与处理器之间的接口。
这里的“接口”,指的是双方共同遵守的规则。
指令集架构规定有哪些指令、它们如何编码,以及执行后应当产生什么行为。它还规定指令可以使用的资源,例如寄存器。
寄存器(Register)是 CPU 内部的小型存储位置,用来保存指令需要处理的数值。
指令集架构描述软件可以期待什么,但不会规定实现这些功能的每一处电路细节。
例如,一条指令要求“把这两个值相加”。架构规定应当得到什么结果,处理器设计者则决定用什么硬件实现这个结果。
RISC-V 就是这样一种架构。它采用开放标准,其规范可以公开获取。RISC-V International:已批准的规范
3. RISC-V 是一种架构,不是某一款具体芯片
一家公司可以设计实现 RISC-V 架构的处理器,另一家公司也可以设计自己的 RISC-V 处理器。
这些处理器的速度、功耗和内部设计可能不同。
**微架构(Microarchitecture)是处理器内部的组织与实现方式。**它包括执行单元如何安排,以及指令如何在硬件内部流动。
执行单元(Execution Unit)是处理器内部负责某类操作的部分,例如负责算术运算的电路。
两款处理器可以遵守相同的架构规则,同时采用不同的微架构。
其中一款可能优先降低功耗,另一款可能使用更多硬件来提高性能。
因此,仅凭“RISC-V”这个名字,无法判断一款处理器究竟有多快。性能还取决于具体实现、整个系统的配置,以及正在执行的程序。
4. “开放”是什么意思?
RISC-V 提供的是一种开放、免指令集使用版税的架构。
个人和组织可以使用这套架构开发处理器,不需要为使用 ISA 本身支付版税。
但是,架构开放,并不意味着采用它的每一款处理器都会公开设计。
公司可以开发专有的 RISC-V 处理器,并对内部实现保密。其他开发者也可以公开处理器设计,允许人们研究或修改。这两种方式都可以采用同一套开放架构。RISC-V International:关于 RISC-V
芯片的设计、测试、制造和后续支持,仍然需要投入资源。
这里需要区分的是:能够获取架构规范,与能够获取某一款处理器的具体硬件设计,是两回事。
5. 从小处开始:RV32I 基础指令集
RISC-V 以基础指令集为核心,再通过扩展增加功能。
扩展(Extension)是额外增加的一组架构功能或指令。
作为第一个例子,我们可以使用 RV32I,也就是 RISC-V 的 32 位基础整数指令集。
其中,“32”表示它的整数寄存器宽度为 32 位;“I”表示基础整数指令集。整数(Integer)是不带小数部分的数值。
RV32I 有 32 个整数寄存器,名称从 x0 到 x31。其中,x0 始终提供数值零。
来看这条汇编指令:
add x5, x6, x7
它的意思是:把 x6 和 x7 中的值相加,并将结果放入 x5。
假设 x6 保存的是 10,x7 保存的是 20,那么执行后,x5 得到的结果就是 30。
这条指令先写目标寄存器,再写两个源寄存器。RISC-V RV32I 规范
6. 从一条指令到硬件活动
“add x5, x6, x7”这样的文字属于汇编语言(Assembly Language),它用便于人阅读的形式表示机器指令。
汇编器(Assembler)是一种工具,负责把汇编指令转换成二进制编码。
CPU 真正执行的是这些二进制编码。
从简化的角度看,处理器先取出指令,对其进行译码,取得源数据,完成加法,再将结果写入目标寄存器。
译码(Decode)就是解释指令编码,确定需要执行什么操作,以及需要使用哪些数据。
加法可以由**算术逻辑单元(Arithmetic Logic Unit,简称 ALU)**完成。它是负责加法、逻辑比较等操作的电路。
现代处理器可能让多条指令的处理过程相互重叠。但这个简化例子已经说明:指令集架构中的一条指令,如何对应到实际的硬件活动。
7. 处理器如何使用内存?
寄存器能保存的数据量有限。程序还需要内存,存放更大的数据集合。
内存地址(Memory Address)是用来标识内存中某个位置的数字。
在基本的 RISC-V 模型中,普通算术指令对寄存器中的数据进行运算。数据在寄存器和内存之间的传输,则由加载和存储指令完成。
加载(Load)把一个值从内存读入寄存器;存储(Store)把寄存器中的值写入内存。RISC-V RV32I 规范
因此,一次计算可能包括:读取数值、执行加法,再把结果写回内存。
不过,程序不需要在每次加法前后都访问内存。如果需要的值已经在寄存器中,处理器就可以直接使用。
把经常使用的值保留在寄存器里,可以减少程序与内存之间的数据传输。
8. C 和 C++ 在哪里发挥作用?
你不需要用汇编语言编写所有 RISC-V 程序。
可以使用 C 或 C++ 这样的高级语言编写程序,再让**编译器(Compiler)**把源代码转换成适合目标架构的代码。
这里的目标(Target),就是生成的程序将来要运行的环境。
例如,编译器可以把 C++ 中的一次计算转换成 RISC-V 处理器能够执行的指令。
但不存在“一行 C++ 固定对应一条机器指令”的规则。一条语句可能需要多条指令,也可能在优化过程中被消除。
优化(Optimization)是指在保留语言规则所要求的程序行为的前提下,对程序进行转换,以改善运行速度、代码体积等方面的表现。
如果编译器能够提前确定一次计算的结果,就可能直接把结果放进生成的代码中,而不再生成运行时计算它的指令。
这也是查看汇编代码的价值:它能帮助你了解编译器究竟生成了什么。
9. 操作系统仍然有自己的工作
能够执行指令,只是运行应用程序的一部分。
程序还可能需要访问文件、分配内存、建立网络连接,或者使用设备。
在运行 Linux 的系统上,操作系统提供这些服务,并管理多个程序共同使用的资源。
系统调用(System Call)是程序通过受控方式,向内核请求服务的机制。内核(Kernel)是操作系统的核心部分,负责管理资源,并维护重要的保护边界。
例如,程序可以先通过 CPU 指令计算出一个结果,再请求系统把结果写入文件。
这些活动相互联系,但涉及不同的职责。
处理器执行指令,操作系统则协调程序对整台计算机资源的使用。
因此,一个编译好的应用程序,仅仅匹配 CPU 架构还不够。它还必须匹配预期的操作系统环境,并具备运行所依赖的软件支持。
10. 总结
RISC-V 为我们提供了一种具体的方法,把软件与处理器的行为联系起来。
指令集架构定义软件可以使用的操作,处理器通过硬件实现这些操作,编译器则把高级语言程序转换成处理器能够执行的指令。
寄存器保存正在使用的数值,算术指令对这些数值进行运算,加载和存储指令则把寄存器与内存连接起来。
在这层硬件接口之上,操作系统管理资源,并向应用程序提供服务。
你不需要记住整套指令集,才能理解这些关系。仅仅一条加法指令,就足以帮助我们看见各个层次如何连接。
有了这个基础,我们就可以回到操作系统,继续研究一个关键问题:计算机如何把普通应用程序代码,与拥有系统管理权限的特权代码区分开来?
