程序与程序计数器
复习
- 第二十六、二十八章:了解了寄存器和时序逻辑电路
- 第三十五章:了解了指令是什么、有哪些类型
TL;DR
- 程序就是一串按顺序排列的指令
- 程序计数器(PC)记住“下一条指令的地址“:顺序执行靠它自增,跳转靠改它
- PC 用“寄存器 + 加法器 + 多路选择器“就能搭出来
- 取回指令后,由指令译码器把机器码翻译成控制信号
正文
从一条指令到一个程序
第三十五章里,我们看的是“一条“指令。但现实任务几乎没有一条指令就能干完的——就像做菜不可能只有一个动作。
把一串指令按先后顺序排好,让计算机依次执行,这就是程序(Program)。
谁来记住下一条
问题来了:计算机执行完一条指令,怎么知道下一条在哪里?
靠一个专门的寄存器——程序计数器(PC,Program Counter)。它保存着“下一条要执行的指令“在内存中的地址。每执行完一条,PC 就自动加上这条指令的长度,指向下一条。
所以,“顺序执行“的硬件实现其实特别简单:取指时按 PC 的地址去取,取完让 PC 加上一个固定值。
拐弯:跳转
如果程序永远只能顺着往下走,那它连 if 和循环都写不了。好在,只要修改 PC 的值,就能让程序“跳“到别处。
CMP R1, #0 ; 比较 R1 和 0,设置标志位
BEQ ZERO ; 若相等,把 PC 改成 ZERO 的地址
ADD R2, R1, #1 ; 否则继续往下
JMP NEXT
ZERO:
MOV R2, #0 ; R1 为 0 时执行这里
NEXT:
BEQ(Branch if Equal)会根据标志位决定要不要改 PC:条件成立就跳,不成立就照常往下。JMP 则是无条件改 PC。
一个完整的程序
来写一个小程序:计算 1 到 N 的和。
MOV R1, #5 ; N = 5
MOV R2, #0 ; sum = 0
MOV R3, #1 ; i = 1
LOOP:
CMP R3, R1 ; 比较 i 和 N
BGT END ; 若 i > N,跳出循环
ADD R2, R2, R3 ; sum = sum + i
ADD R3, R3, #1 ; i = i + 1
JMP LOOP ; 回到循环开头
END:
; 此时 R2 里就是 1+2+3+4+5 = 15
注意 JMP LOOP——它把 PC 改回循环开头的地址,于是这几条指令被反复执行,直到条件不满足。循环,本质上就是“往回跳“。
无论程序多复杂,拆开来都逃不出三种结构:顺序、选择(if)、循环(while、for)。它们全都靠“条件跳转“实现,这正是结构化编程的基石。
把 PC 造出来
PC 听起来玄乎,其实用我们手里的零件就能搭:一个寄存器存当前地址,一个加法器专门做“加 4“(32 位系统里一条指令占 4 字节),一个多路选择器决定下一个地址从哪来。
+--------+ +----------+
| 加法器 | <---- | 常数 +4 |
+--------+ +----------+
|
v
+--------+ +----------+
| PC | <---- | 多路选择器 |
+--------+ +----------+
^
|
跳转目标地址
- 正常执行:多路选择器选“PC + 4“,PC 就指向下一条
- 遇到跳转:多路选择器改选“跳转目标地址“,PC 被直接改写
就这么点东西,却撑起了整个程序的“顺序“。
取指令与译码
有了 PC,取指令的流程就很自然了:
- 把 PC 的值送到存储器当地址
- 存储器把那个地址里的内容吐出来,这就是一条指令
- 指令送进指令寄存器(IR)
- PC 加上 4,为取下一条做准备
可取回来的指令,对 CPU 来说只是一串 0 和 1,比如 0001 0010 0011。它怎么知道这是“加法“还是“跳转“?
这就要靠指令译码器(Decoder):它把操作码翻译成一组具体的控制信号——让 ALU 做什么运算、读哪几个寄存器、结果写回哪里、PC 要不要跳转。
还记得第十八章造 ALU 时,用一个译码器把操作码翻译成“选中哪一种运算“吗?指令译码是同样的套路,只不过被翻译的东西,从“一个运算“变成了“一整套动作“。
于是,一条指令的执行被拆成清晰的几步:取指 → 译码 → 执行 → 写回。这也是后面流水线的基础。
思考题
上面算 1 到 N 和的程序,如果还想顺便算出平均值(和 ÷ N),应该怎么改?注意 N 在循环里已经被改动过了。
另外想一想:如果 PC 只会无脑加 4,它怎么处理长度不固定的指令?
小结
知识点
- 程序是指令的有序序列
- 程序计数器(PC):顺序执行与跳转
- 顺序、选择、循环三种基本结构
- PC 的硬件构成
- 取指令与指令译码器
参考资料
- Wikipedia(zh):程序计数器:程序计数器的基本概念
- Wikipedia(zh):指令译码器:指令译码的基本原理
- Wikipedia(zh):结构化编程:结构化编程的原理
思考题答案(仅供参考)
- 循环结束后 R3 已经变成 N+1,要先把它减回 N,再做除法:
SUB R3, R3, #1 ; R3 = N
DIV R4, R2, R3 ; R4 = sum / N,即平均值
这也提醒我们:使用寄存器时,要留意它是否被前面的指令改动过。
- 变长指令的 PC 不能无脑加 4,而要加“当前这条指令的实际长度“。译码阶段顺便算出指令长度,再让 PC 加上它。定长指令之所以受欢迎,就是因为 PC 更新特别简单、流水线也好设计。
协议
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
封面图
设计师 | 南国微雪