在[上一章]中,介绍了 arm 的核心寄存器以及相对应的作用,这一章介绍 armv7 架构中的另一个寄存器:状态寄存器。
顾名思义,状态寄存器的作用就是保存处理器的状态信息,程序运行期间可以通过查看状态寄存器来进行相应的处理,或者通过设置状态寄存器来修改处理器当前运行模式。
状态寄存器
在 armv7 中,状态寄存器为 CPSR,即 Current Program Status Register,该状态寄存器中保存了处理器运行时的状态信息:
CPSR 寄存器为 32 位,其中:
- N:bit31,当运算结果为负且运算指令要求更新寄存器时,该位会被置位。
- Z:bit30,当运算结果为0且运算指令要求更新寄存器时,该位会被置位。
- C:bit29,当运算结果产生进位且指令要求更新寄存器时,该位会被置位,具体进位规则见下文。
- V:bit28,当运算结果产生符号位溢出且指令要求更新寄存器时,该位会被置位,具体溢出规则见下文。
- Q:bit27,累积饱和位,置为1表示某些指令中发生溢出或饱和,通常与数字信号处理(DSP)有关。
- IT[1:0],bit[26:25],IT 位,同后续 IT[7:2] 相关。
- J:bit24,指示处理器当前是否处于 Jazelle 状态。
- bit[23:20],保留位。
- GE[3:0]:bit[19:16],大于或者等于标志位,主要被 SIMD 指令使用,SIMD 全称为 single instruction multiple data,armv7 提供一条指令同时处理多个寄存器数据,属于扩展指令。
- IT:bit[15:10],Thumb IT 指令集的 if-then 执行状态位。
- E:bit9,指示当前处理器是运行于大端模式还是小端模式,同时,也可以通过设置该位来切换大小端模式。
- mask bits,bit[8:6],屏蔽位 A、I、F,分别对应异步终止、快中断和中断,当对应的位为1时,相应的功能被屏蔽,当处理器需要屏蔽中断时,通常就是设置该屏蔽位。
- T,bit5,指示处理器当前使用 thumb 指令集还是 arm 指令集,当前位和 J bit决定当前处理器的指令集,是 arm、Thumb、Jazelle 还是 ThumbEE 指令集。J、T的值对应指令集关系为:00-arm指令集,01-Thumb指令集,10-Jazelle指令集,11-ThumbEE指令集。
- M,bit[4:0],模式位,指示处理器当前位于哪种运行模式下,细节见下文。
用户状态寄存器
armv7 架构有多种模式代表不同的系统操作权限,同样的,对于状态寄存器的操作也需要做相应的权限管理,在 User 模式下,状态寄存器为 APSR,即 Application Program Status Register,其实 APSR 是 CPSR 的限制版本,参考下图:
使用 APSR 而不是 CPSR 本质上是因为需要对用户空间做相应限制,所以寄存器的某些 bit 需要对用户空间不可见,比如 mask bits[8:6],如果用户空间可以更新 irq 的屏蔽位,那应用程序很容易就能让整个机器瘫痪。
对于 CPSR 的高 5 位,和 CPSR 是相同的,同时 GE[3:0]也是一致的,而对于其它的位,就有一些不同。
bit[26:24] 被修改成了RAZ/SBZP 位,在 armv7-A 手册中是这样描述的:
software can use MSR instructions that write the top byte of the APSR
without using a read, modify, write sequence. If it does this, it must write zeros to bits[26:24]
手册中说到,用户可以在不遵循 读、修改、写的顺序下使用 msr 指令来更新 APSR 的最高字节,也就是 bit[31:24],在这种情况下,需要将 bits[26:24]设置为0.
但是在实际的测试中,我直接使用 msr 指令写入时,bits[26:24] 不设置为 0,也可以成功对 APSR 寄存器进行更新.
在实际的编程中,很少会出现需要手动修改 APSR 最高字节的情况,一般都是执行指令时自动根据运算结果更新状态寄存器,下一条指令根据上一条指令的结果决定指令的执行。
对于 APSR 的低 16 位,写是无效的,尽管读操作可能可以获取到对应 CPSR 寄存器的结果,这由具体实现决定,但是不要做这种假设,arm 也不建议读 APSR 的 [15:0] 返回 CPSR 的 [15:0] 位。
状态位的更新
在 C 语言中,执行分支指令通常是通过 if 语句判断某个条件是否满足,再根据结果执行不同的分支指令,C 代码总归是要翻译成汇编代码的,从汇编的角度来看,分之代码或者说逻辑判断是如何完成的呢?
首先,从微观层面来说,处理器就只做一件事:处理数据,同时 arm 的精简指令集强制规定了所有的数据处理必须在寄存器中完成,所以处理器所做的事就是:将数据从内存中拷贝到核心寄存器,处理数据、数据写回。
所以,在执行分支代码或者逻辑判断时,通过判断上一条指令的执行结果来实现,那么,我们怎么知道上一条指令的执行结果呢?答案就是将结果保存在状态寄存器中,同时也有一个问题,每一条指令都会产生一个结果,是不是所有的指令都会更新状态寄存器?
对于状态更新,暂且将所有指令分为两种:一种是自动更新状态寄存器的指令,一种是普通指令。
自动更新状态寄存器的指令比如 cmp、tst 等等,分别表示两个操作数的比较、位与,并更新状态寄存器,通常是最高 4 位。
对于普通指令,比如最常见的 mov、add 等指令,默认是不更新状态位的,如果需要这些普通指令也更新状态位,可以在指令后加上后缀 's'。比如 movs r0,#-1,状态寄存器的最高字节就会被更新为 0x80,因为该指令的结果为负值,N 被置位。
carry位的更新
在 CPSR 的高四位中,N 和 Z 位非常好理解,如果操作完成后寄存器中的值为负,则将 N bit 置位,如果寄存器中的值为0,则将 Z bit 置位,如果是正数,则什么也不做。
那么,C 和 V 位就并不是那么好理解了。
C 表示进位位,上过数学课都知道,进位表示运算结果需要使用更高的位来保存,比如 50 + 50 = 100,这就产生了进位,鉴于 armv7 架构中寄存器宽度为 32 位,所以最大值是 0xffffffff,当两个数值相加超过 0xffffffff 时,将产生进位,此时 CPSR 中的 C bit 被置位。
对于减法而言,C bit 变成了借位,当被减数比减数小就需要借位,比如 3-5,但是 C bit 的置位是反过来的,当产生借位时,C bit 为0,否则 C bit 为 1,也就是 3-5 时 C bit 为0,5-3 时 C bit 为 1。
对于移位操作而言,最后 C 为最后移出位的值。比如下面的代码:
mov r0,#6
rors r0,#1
6 循环向右位移一位时,因为被移出位为0,所以 C bit 无变化。
如果循环右移两位,r0 寄存器的值为 0x800000001,所以 N bit 和 C bit 都会被置位。
事实上,对于上文中提到的 C bit 进位方式,"进位"或者"借位"的说法只是表象,C bit 是否被置位,其本质在于当前寄存器执行的操作产生的结果超过32位的最大值时,需要将第33位保存在 cpsr 的 C bit 中,对于加法来说这很好理解,0xffffffff + 1 = 0x100000000,产生的进位保存不了被保存在 C bit 中,留在寄存器中的值为 0x0,所以 Z 和 C bit 都会被置位。
而对于减法而言,这需要一些编码的基础,关于计算机的减法以及计算机补码相关知识可以查看计算机原码反码补码。
总之,长话短说,计算机中的减法就是由加法实现的,比如 1 - 2,相当于 1 + -2,而 -2 的二进制补码为 0xfffffffe,所以执行的结果为 0xffffffff, 结果并没有超过 32 位最大值,所以 1 - 2 并不产生进位,所以对于 1-2 并不会将 C bit 置位。
反过来看 2 - 1,相当于 2 + -1 = 2 + 0xffffffff = 0x100000001,产生了进位,所以 C bit 被置位。这也就是为什么减法和加法是反过来的,需要借位时,比如 1-2 ,C bit 反而是 0,不需要借位时, C bit 是 1.
而对于移位操作,可以理解为被移出的位保存在 C bit 中。
overflow 位的更新
V bit,即 bit28 表示 overflow ,有符号的溢出位。
手册中的计算公式是这样的:
sign_sum = Sint(x)+Sint(y)
result = sign_sum<32-1:0>
overflow = if Sint(result) == Signed_sum then '0' else '1'
看起来是非常抽象的,只能用举例的方式才能说得更明白一点,同时,为了看起来更简洁,我们假设我们操作的是 8 位寄存器而不是 32 位寄存器。
x,y 是两个操作数,假设 x = -1, y = -2。 sign_sum = -3. result = sign_sum<7:0> Sint(result) == sign_sum == -3,因为 0xfd 在有符号数据中就表示 -3.
假设 x = 127,y = 2 sign_sum = 127 + 2 = 129 result = sign_sum<7:0> = 0x81 Sint(result) == -2, 但是 sign_sum = 129,此时不相等,所以 overflow 位被置位。
再假设 x = -128,y = -1 sign_sum = -128 + -1 = -129 result = sign_sum<7:0> = 0x81
总的来说,导致这种符号溢出的结果发生在 正+正=负,或者 负+负=正 这两种情况中。
条件指令
上一条指令的执行结果保存在 CPSR 的状态位中,在代码中如何使用这些状态位呢?我们接着往下看:
armv7 中并没有定义特定的条件指令,而是将每条 arm 指令编码的最高四位作为条件判断,也就是 bit[31:28],这四位的编码对应的执行条件如下表:
表中覆盖的 0b0000~ 0b1110 表示指令的条件执行,而 0b1111 为某些指令特有的,这些指令不支持条件执行。
第二列表示指令后缀,表中最后一列表示当前指令执行时状态寄存器需要满足的条件,比如:
...
cmp r0,r1
beq func
...
第一条指令,cmp 其实是将第一个操作数减去第二个操作数,当两个操作数相等时,结果为 0, CPSR 的 Z(zero)位被置位。
beq 其实不是一条标准指令,b 是一条跳转指令,eq 后缀我们可以在上表中查到:只有当 CPSR 中的 Z 被置位时,b 指令才会被执行。所以这两条指令的意思是:当 r0 和 r1 寄存器中的值相等时,才会跳转到 func 执行,否则这条指令不执行。
同样的,b指令也可以搭配其它的后缀,比如 bne、bcs 等等,同时其它的指令比如 mov 同样可以使用该后缀表示条件执行。
在默认情况下,即指令不带条件执行后缀时,最高四位的值为 1110,也就是默认执行。
为了更清晰地理解条件执行的概念,我们可以看下面的代码:
unsigned int a=1;
unsigned int b=2;
if(a == b){
return 1;
}else{
return 2;
}
return 0;
这份 C 代码就是简单的判断两个变量是否相等,查看对应的未经优化的反汇编代码:
int main(void)
{
unsigned int a=1;
83c2: 2301 movs r3, #1
83c4: 603b str r3, [r7, #0]
unsigned int b=2;
83c6: 2302 movs r3, #2
83c8: 607b str r3, [r7, #4]
if(a == b){
83ca: 683a ldr r2, [r7, #0]
83cc: 687b ldr r3, [r7, #4]
83ce: 429a cmp r2, r3
83d0: d101 bne.n 83d6 <main+0x1a>
return 1;
83d2: 2301 movs r3, #1
83d4: e000 b.n 83d8 <main+0x1c>
}else{
return 2;
83d6: 2302 movs r3, #2
}
return 0;
}
83d8: 4618 mov r0, r3
从汇编代码可以看出,将变量 a、b 分别加载到 r2、r3 中,然后使用 cmp 指令进行比较,如果相等,返回1,不相等则返回 2,其中 bne.n 的后缀 .n 表示指定指令的长度,将会后续的文章中详细介绍。
模式位
armv7架构一共支持 9 种处理器模式,对应 M 位(bits[4:0])的关系为:
状态寄存器的访问以及模式切换
arm 为访问状态寄存器提供了特定的指令,mrs 和 msr,m 表示 move,r 表示 register,即通用寄存器,而 s 对应 special 寄存器,由于 arm 指令中通常将源操作数放在后面,因此,mrs 表示将 special 寄存器中的内容 move 到通用寄存器中,而 msr 相反。
对于 User 模式而言,是没有权限操作 CPSR 的 mode 部分来更新系统模式的,其它模式都处于 PL1 特权级,所以 User 模式下进入到 PL1 特权级需要使用 svc 指令,而对于其他本来就处于 PL1 的模式而言,更改处理器模式可以直接设置 CPSR 中对应的位,处理器就会自动跳到对应的异常向量处,理论上是这样,实际的处理器模式切换通常是这样的:
msr spsr_cxsf, ${target_mode_bits}
...
movs pc, lr
cpsr 的前缀 c 表示 current,而 spsr 的前缀 s 表示 saved,在处理器发生中断或者异常时,自动从一个模式跳转到另一个模式下,而原模式下的 cpsr/apsr 将会被自动保存在目标模式的 spsr 中,通常异常处理程序需要获取原模式下的 cpsr 寄存器信息。
当需要切换模式时,通常会将需要切换的模式位设置先保存到 spsr 中,然后执行 movs 指令,mov 指令的后缀 s 在 arm 中有两种意思,如果目标寄存器为非 pc,它的隐含操作为当前执行的操作结果会更新状态寄存器,如果目标寄存器为 pc,它的隐含操作为将 spsr 中的值赋值给 cpsr,通常使用这个特性执行模式切换.
上面代码中,spsr 寄存器后多出了一个 _cxsf 后缀,实际上这是 4 个单独的后缀:c、x、s、f 分别对应 cpsr 中 4 个 8 位域,其中:
- f:cpsr[31:24],条件标志域,主要包括 N、Z、C 等条件标志码
- s:cpsr[23:16],状态位域
- x:cpsr[15:8],扩展位域
- c:cpsr[7:0],控制位域,只要包括中断控制、模式位控制位
----------------
欢迎关注公众号——“IC硅农场”
--------------------------------------------------------------------------------------------如侵权请联系删除-------------