Appearance
ADD、SUB、MUL 与 CPSR 状态标志

上一课,我们用 MOV 和 SWI 写出了第一个能跑的程序,并用退出码 42 证明它真的活过一次。但要说句实话——MOV 只是个搬运工,它只会把数据从 A 挪到 B,一个数都算不出来。一台只会搬运、不会算术的 CPU,充其量是个昂贵的快递柜。
这一课,我们让 CPU 真正动起来:加(ADD)、减(SUB)、乘(MUL)。算完之后,还会顺手认识一位一直在场、却始终被我们无视的角色——CPSR。它像裁判手里的记分牌,时刻记着上一次运算留下的痕迹。别小看这块记分牌,后面所有的「如果……就跳转」,靠的全是它。
本系列基于 ARMv7(ARM 的 32 位版本)。本课的寄存器与标志位变化,建议在 CPUlator(https://cpulator.01xz.net/ ,选 ARM + ARMv7)里单步看最直观——它会把你设置的标志位高亮成粗体。
1. 一句话记住 ARM 的算术指令
好消息是,ARM 的算术指令长得几乎一模一样——它们共享同一副骨架,都是三操作数结构。它做的事永远是这一句:把第一操作数和第二操作数做运算,结果存进目标寄存器。
无论是加、减、乘,还是后面要学的逻辑、移位,换汤不换药。变的只是前面的助记符,和第二操作数允许填什么。
asm
助记符 目标寄存器, 第一操作数, 第二操作数
Rd , Rn , <操作数2>注: 这正是上一课 MOV 的延伸:MOV 是把第二操作数抄进 Rd;本课的 ADD/SUB/MUL 是把两个操作数算一下再放进 Rd。

图 1 · 算术指令的三操作数结构:Rn 与 <操作数2> 运算后存入 Rd
2. ADD:加法,两种填法
ADD 是这课的主角,先拿它开刀。它有两种用法,区别只在第二操作数填什么。
2.1 立即数加法
第二操作数填一个常量(立即数,前缀 #):
asm
mov r0, #4 @ 先给 R0 放个值 4
add r1, r0, #3 @ R1 = R0 + 3 = 7这条 add 读出 R0(值为 4),加上立即数 #3,把结果 7 写进目标寄存器 R1。翻译成你熟悉的高级语言,就是一句 r1 = r0 + 3;
注: # 表示立即数(常量),默认十进制;#0x.. 则是十六进制。和上一课 MOV 的写法一脉相承。
2.2 寄存器加法
第二操作数也可以填另一个寄存器——把两个寄存器的值相加:
asm
mov r0, #4 @ R0 = 4
mov r2, #2 @ R2 = 2
add r1, r0, r2 @ R1 = R0 + R2 = 6结果 6 落进 R1。至于把哪个寄存器相加——无所谓,R0+R2 也好、R3+R5 也好,CPU 一视同仁。其实上一课的 MOV 也支持寄存器源(mov r1, r0 把 R0 复制到 R1),原理同此。

图 2 · ADD 加法实例:R0 与操作数2 相加后存入 R1
3. SUB:减法,与加法镜像
学会了加法,减法就是白送的。SUB(subtract)的写法和 ADD 完全对称,做的事是 Rd = Rn − <操作数2>。
asm
mov r0, #4 @ R0 = 4
mov r2, #2 @ R2 = 2
sub r1, r0, r2 @ R1 = R0 - R2 = 2它同样有立即数和寄存器两种填法。所以 ADD 会了,SUB 基本就会了——把 + 换成 −,仅此而已。
注: 助记符大小写不限:ADD/add、SUB/sub 等价。本系列统一用小写,少按一个 Shift。

图 3 · SUB 减法实例:与加法同构,只是把 + 换成 −
4. MUL:乘法,且只能用寄存器
MUL(multiply)把两个值相乘,写法依旧眼熟:
asm
mov r0, #4 @ R0 = 4
mov r2, #2 @ R2 = 2
mul r1, r0, r2 @ R1 = R0 * R2 = 8结果 8 落进 R1。
这里有个容易让你 debug 到怀疑人生的坑:MUL 没有立即数版本,第二操作数必须是寄存器。想乘以一个常量?对不起,先 MOV 进某个寄存器,再 MUL。这是 MUL 和 ADD/SUB 唯一的别扭之处,记住它,能少摔一个跟头。
注意: 千万不要写成 mul r1, r0, #4——MUL 没有立即数版本,第二操作数必须是寄存器。想乘常量,先 mov 进寄存器。
注: 在 CPUlator 里 Compile & Load 后单步,你会眼睁睁看到:执行 mul r1, r0, r2 那一步,R1 从 0 变成 0x00000008。

图 4 · MUL 乘法实例:仅支持寄存器,没有立即数版本
5. CPSR:那块记录运算痕迹的看板
算术做完了,结果乖乖躺在 Rd 里。但运算过程中,还悄悄留下了别的东西——状态标志。它们集中存在一个一直被我们无视的特殊寄存器里:CPSR(Current Program Status Register,当前程序状态寄存器)。
CPSR 是 32 位的,其中最高几位是标志位——可以理解成裁判记分牌上的几盏灯,每一盏记录一种运算后果:N(Negative,结果为负)、Z(Zero,结果为零)、C(Carry,进位/借位)、V(oVerflow,有符号溢出)。
- 第 31 位 N:运算结果为负时置 1
- 第 30 位 Z:运算结果为零时置 1
- 第 29 位 C:加法产生进位 / 减法无借位时置 1
- 第 28 位 V:有符号运算溢出时置 1
注: C(进位)和 V(溢出)牵扯到无符号/有符号的细节,后续会单独细讲。本课先盯住 N 和 Z。

图 5 · CPSR 高 4 位标志:N Z C V 分别占用第 31、30、29、28 位
为什么要盯着这块记分牌?因为后面写「如果上次结果为零就跳转」「如果是负数就走另一条路」这类条件分支时,CPU 靠的就是 CPSR 里这些灯的明灭来做判断。记分牌不亮,CPU 就是个只会闷头算、不会做决定的书呆子。
6. 一个关键字母:S 后缀
谜底揭晓:默认情况下,add / sub / mul 跑完,并不会去更新 CPSR 的标志位——它们只管闷头算、只管存结果,压根不碰记分牌。
想让 CPU 顺手翻一下记分牌?就在助记符后面加一个 S:
asm
add r1, r0, r2 @ 只算结果,CPSR 不动
adds r1, r0, r2 @ 算结果,并按结果更新 CPSR 标志S = Set flags(设置标志位)。ADD、SUB、MUL 都能戴这顶帽子,变成 ADDS/SUBS/MULS。一个字母,拨动的却是「要不要把这次运算的痕迹写进 CPSR」这道开关。

图 6 · add 与 adds 的区别:加不加 S,决定了 CPSR 标志位是否被更新
7. 实战:让 Z 标志亮起来
光说不练假把式。来看一个完整例子,我们的目标是亲手点亮 Z(零)标志那盏灯——方法无他,造一个结果为零的减法:
asm
mov r0, #3 @ R0 = 3
mov r1, #3 @ R1 = 3
subs r2, r0, r1 @ R2 = R0 - R1 = 0,并更新 CPSRsubs 把 R0 − R1 = 0 存进 R2,同时顺手翻了一下记分牌。因为结果是 0,Z 标志(第 30 位)应声点亮。
例. 这里的 s 真是一字千金:写成 sub 就不会动 CPSR,Z 标志永远不亮;非 subs 不可。
7.1 在二进制里看清楚
CPSR 是 32 位的,CPUlator 默认用十六进制显示。把那串十六进制值翻译回二进制,你会看到:第 30 位(Z)从 0 翻成了 1。
为什么是 32 位?因为 ARMv7 的寄存器就是 32 位 = 8 个十六进制位 = 8 × 4 个二进制位。每个十六进制数字管 4 个二进制位,8 × 4 = 32,严丝合缝。
注: CPUlator 的贴心之处:点亮的标志会变成粗体黑色,没点亮的则是灰色。一眼就能看出 Z(以及本例里同时被点亮的 C)真的亮了。
小结
这一课,我们让 CPU 从快递柜升级成了会算术的计算器,并第一次窥见了那块一直在场的记分牌:
- 三操作数骨架:助记符 Rd, Rn, <操作数2>,运算结果进 Rd,加减乘共用一副骨架
- ADD / SUB:各有立即数、寄存器两种填法,加法减法镜像对称,会一个就会一双
- MUL:只有寄存器版本,没有立即数——这是它和加减法唯一的别扭,记住能少摔跟头
- CPSR:用 N / Z / C / V 四盏灯记录运算后果,是后续所有条件分支的裁判
- S 后缀:adds/subs 才会翻记分牌,少了这个字母,四盏灯纹丝不动
加减乘有了,记分牌也会看了。下一课,我们就要让 CPU 拿着这些亮起的灯,去做决定了。下一课见。