Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

程序与程序计数器

复习

  1. 第二十六、二十八章:了解了寄存器和时序逻辑电路
  2. 第三十五章:了解了指令是什么、有哪些类型

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)、循环(whilefor)。它们全都靠“条件跳转“实现,这正是结构化编程的基石。

把 PC 造出来

  PC 听起来玄乎,其实用我们手里的零件就能搭:一个寄存器存当前地址,一个加法器专门做“加 4“(32 位系统里一条指令占 4 字节),一个多路选择器决定下一个地址从哪来。

   +--------+       +----------+
   | 加法器  | <---- | 常数 +4   |
   +--------+       +----------+
        |
        v
   +--------+       +----------+
   |   PC   | <---- | 多路选择器 |
   +--------+       +----------+
                        ^
                        |
                   跳转目标地址
  • 正常执行:多路选择器选“PC + 4“,PC 就指向下一条
  • 遇到跳转:多路选择器改选“跳转目标地址“,PC 被直接改写

  就这么点东西,却撑起了整个程序的“顺序“。

取指令与译码

  有了 PC,取指令的流程就很自然了:

  1. 把 PC 的值送到存储器当地址
  2. 存储器把那个地址里的内容吐出来,这就是一条指令
  3. 指令送进指令寄存器(IR)
  4. PC 加上 4,为取下一条做准备

  可取回来的指令,对 CPU 来说只是一串 0 和 1,比如 0001 0010 0011。它怎么知道这是“加法“还是“跳转“?

  这就要靠指令译码器(Decoder):它把操作码翻译成一组具体的控制信号——让 ALU 做什么运算、读哪几个寄存器、结果写回哪里、PC 要不要跳转。

  还记得第十八章造 ALU 时,用一个译码器把操作码翻译成“选中哪一种运算“吗?指令译码是同样的套路,只不过被翻译的东西,从“一个运算“变成了“一整套动作“。

  于是,一条指令的执行被拆成清晰的几步:取指 → 译码 → 执行 → 写回。这也是后面流水线的基础。

思考题

  上面算 1 到 N 和的程序,如果还想顺便算出平均值(和 ÷ N),应该怎么改?注意 N 在循环里已经被改动过了。

  另外想一想:如果 PC 只会无脑加 4,它怎么处理长度不固定的指令?

小结

知识点

  • 程序是指令的有序序列
  • 程序计数器(PC):顺序执行与跳转
  • 顺序、选择、循环三种基本结构
  • PC 的硬件构成
  • 取指令与指令译码器

参考资料

  1. Wikipedia(zh):程序计数器:程序计数器的基本概念
  2. Wikipedia(zh):指令译码器:指令译码的基本原理
  3. Wikipedia(zh):结构化编程:结构化编程的原理

思考题答案(仅供参考)

  1. 循环结束后 R3 已经变成 N+1,要先把它减回 N,再做除法:
    SUB R3, R3, #1   ; R3 = N
    DIV R4, R2, R3   ; R4 = sum / N,即平均值

  这也提醒我们:使用寄存器时,要留意它是否被前面的指令改动过。

  1. 变长指令的 PC 不能无脑加 4,而要加“当前这条指令的实际长度“。译码阶段顺便算出指令长度,再让 PC 加上它。定长指令之所以受欢迎,就是因为 PC 更新特别简单、流水线也好设计。

协议

  本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

封面图

设计师 | 南国微雪