Nov 27, 2016 • Roger Ferrer Ibáñez • AArch64
到目前为止我们知道如何做一些计算和访问内存。今天我们将学习怎样修改我们程序的控制流。
几乎每个人都希望看到的指令都像我们前面看到的一样,一个接一个执行。这种方式是最基本的,我们称之为隐式序列:一个指令执行完毕之后,另一个指令接着执行。这看起来很明显。
但是,如果我们想去改变这种隐式序列怎么办?比如选择性的执行一段指令,甚至,我们去执行一段依赖某个条件的指令。
instructions.程序生活在内存中。这看起来再明显不过了。但是,事实上不是这样。创建一个特定的数字电路去执行特定的任务是相对比较简单的。他们的程序并没有生活在内存中,相反,他们直接编码在电路中。但是,这些电路有一个地方,如今,我们能用CPU,CPU是非常复杂的电路,这样的电路能够执行像指令编码一样的程序。
如果程序生活在内存中并且程序在他们的有一堆指令的核中,这也意味着指令是可被编址的(每一个都有一个地址,这一个地址对应一个内存地址)。假设,从功能性的角度,一个CPU在任何时间都执行一个单独的指令,那么知道这个指令在哪里就变得十分合理了。指令本身就是一个特殊的二进制码,它本身不是特别有用,但是地址却是特别有用。所以,我们想去把当前在CPU中的指令保存在地址中,然后这就是我们想说的。
一段跟随当前指令的内存叫程序计数器。在AArch64中,程序计数器保存在一个叫PC的寄存器中(PC就是program counter)。它是一个64位的寄存器,保存当前指令的地址。
PC告诉CPU下面哪一条指令将会被执行。CPU就会去内存,请求一个在pc告诉的地址上的4个字节,这4个字节就是指令,回忆一下,aarch64指令就是32位的。然后这个指令就被执行了。当指令执行结束,pc就会增加4个字节。这就是隐式序列如何工作的。
但是我们也可能会想,如果我们不去做pc<-pc+4,而是跳到别的地方。那么CPU执行一些其他的执行,而不是下一条执行?回答是肯定的,除了我们不能再AArch64中直接对pc写入值,但是我们能做分支跳转。
分支是能够改变pc的指令。通过分支他们能够改变我们程序的隐式序列。
有两种类型的分支:无条件分支和有条件分支。无条件分支总是设置pc为一些值,而有条件的分支只有当条件满足的时候才设置pc的值。当一个分支设置了pc的值,我们就说这个分支做好了。被设置pc的地址被称为分支的目标。如果一个分支没有背设置,那么还是隐式序列在起作用。
分支的目标是一个地址,在汇编器中的地址通常以符号label的形式表现。所以一个分支将至少有一个操作数,这个操作数就是分支的目标。假定地址在指令中的被编码,那么这就有一个限制了,这个限制就是我们能够使用的对象(地址)。
无条件分支以使用指令b
的形式表达。分支的目标是一个27位的偏移。也就是偏移能够在正负128MB上寻址。
接下来的程序将会设置错误条件为3,如果有一个指令设置为4。这个分支就会简单地条到第二个mov
下执行。
/* branch */
.text
.globl main
main:
mov w0, #3 // w0 ← 3
b jump // branch to label jump
mov w0, #4 // w0 ← 4
jump:
ret // end function
I will be writing labels in their own line (check main:
and jump:
above) which I think is a bit easier to read. I will also indent instruction after a label (check ret
).我将
如果我们运行这个程序,它将设置错误码为3。
$ ./branch ; echo $?
3
在你问我之前,是的,我们能够把一个程序设置为永远不会结束,只要我们通过跳转到我们自己。这会挂起(使用Ctrl-C去结束它)
/* neverend.s */
.text
.globl main
main:
noprogress:
b noprogress // note: 'main' and 'noprogres'. are two different
// labels to the same address, doing 'b main' would hang
// as well
ret // it will never return!
最初,它看起来无条件的跳转是没什么用的。它们有自己的用途只不过我们忽略了一些细节,使之我们不能欣赏到它的用处。在下一章,我们将看到另外的无条件跳转
条件跳转有点意思因为只有当一些条件成立时才会发生分支跳转。那么有哪些条件呢?
AArch64有4个flag。每个flag以为这一些指令执行的结果。只有少数指令会设置这些flag。对当前的章节而言,我们只考虑cmp
。同样,也有一些指令会使用这些flag:那些指令称为条件指令。
如上所属的4个flag。它们是N
, Z
, C
, V
. 它们被设置为执行某条指令的结果后更新的标志。它们有特殊的意义:
N
(负数)被设置的条件是当指令的结果产生的值是负数。所以正数和零不会设置这个标志位。Z
(零)被设置的条件是当指令执行的结果是0。C
(进位) 被设置的条件是当指令执行的结果不能作为一个无符号整型被表达。V
(溢出)被设置的条件是当指令的执行结果不能被作为一个有符号整型被表达。C
和V
的不同依赖于对结果的解释。如果我们把两个大的正数相加,并且结果不能用一个无符号数表示,那么C
就会被设置。例如,如果我们把231+231,并且我们依然想保持一个32位的整型:2^32则不能用32位表示。这时,C
就会被设置。
个人理解:
两个无符号数 2^31 + 2^31 = 2^32
在32位的范围内无法表达,于是为1,0000,0000,0000,0000,0000,0000,0000,0000
前面的1是第33位,则C被置位
个人理解:逻辑电路可以设计成这样
+---+ +--------------------+
| 1 | |1111111111...1111111|
+---+ +--------------------+
c 32位的无符号数结果
如果我们把两个大的正整数或者负整数,它可能会因为结果太大(绝对值)而导致该位被设置,因为它是被编码成有符号整数。例如,如果我们计算230+230,那么结果是2^31。如果,再一次,我们想保持这个结果在32位中,并且作为一个有符号数(例如,两个补码),它不能简单地被编码,所以此时v
就会被设置,而不是c
个人理解:
如果两个有符号数
两个是大正数 2^30 + 2^30 = 2^31
在32位的范围内:1000,0000,0000,0000,0000,0000,0000,0000
前面的1是第32位,结果本应该为+2^32,但在有符号数种,此时这表示为-0,不是正确的值
结论:本来应该是正数,但是结果是负数或者0
同理,如果是两个负数 -2^32 + (-2^32) = -2^33
在32位的范围内:
-2^32的值表示是1000,0000,0000,0000,0000,0000,0000,0000
-2^33的值表示是1,0000,0000,0000,0000,0000,0000,0000,0000
结论:本来应该是负数,但是结果是正数或者0
结论:
当最高有效位两个都是0,但是结果是1的时候,V被设置
当最高有效位两个都是1,但是结果是0的时候,V被设置
个人理解:逻辑电路可以设计成这样,X一旦发生了变化,则有可能置V(只是有可能,因为不一定置位,因为可能结果是正确的,见下面)
+---+ +--------------------+
| 1 | |X111111111...1111111|
+---+ +--------------------+
v 32位的有符号数结果
你可能会想会不会只有C
被设置,但是V
不会被设置。事实上,确实有很多这样的情况,当两个有符号的整数相加,如果一个是负数,另一个是正数,那么就可能导致C
会被设置,但是V
不会被设置(这种操作在有符号范围内解释不是溢出)。例如,-1+2。结果是1,所以它符合有符号的范畴,因此,V是不会被设置的。但是有符号整数-1被解释位一个无符号整数,只不过它所有的位是1。对一个32位操作,这就是232-1。所以,我们计算-1+2,我们实际上做的是232-1+2,那么就是2^32+1。而这个数不能再32位的范围内作为无符号数被编码,所以,标志C
会被设置。
个人理解:
-1+2=1,其结果依然在32位的有符号范畴内
因为-1是2^32-1,所以-1+2等价于2^32-1+2=2^32+1,此时结果1,0000,0000, ...., 0001
高位的1在33位上,此时产生进位,C被置位
低位的1在 1位上,此时结果正确
是不是无符号数和有符号数相加,要把有符号数当无符号数看?
如果是-2+1呢?
-2的二进制表示是111111...10
+1的二进制表示是000000...01
结果是11111111111
结论:
两个操作数,如果最高位不同,但是产生了进位,则C置位,V不置位
总结论:
如果两个操作数最高位相同,相加,得到到结果使最高位不同,则V置位
如果两个操作数是无符号数,相加,得到的结果产生了进位,则C置位
如果两个操作数是有符号数(可能中间一个操作数是无符号数,也把它当有符号数看),相加,如果产生了进位,则C置位,V不置位
无符号和有符号整数在N位上的编码
既然我们已经看到了4个标志位的意义,我们能够看到一些能够设置它们的指令。注意,flag的结果依赖于一些操作的结果。回忆一下本章的目的,我们只考虑cmp
指令。
cmp
指令的作用是比较两个数。为了达到这个目的,其实现方式是通过第一个操作数减第二个操作数(例如, first - second)并且通过结果更新标志位。计算后的结果被抛弃,所以这个操作的整个目的就是更新标志位。
现在考虑几个不明显的比较的结果,并看看它们式怎样设置标志的。如果我们比较两个有相同值得寄存器,那么相减的结果式零,所以z
标志会被设置。如果两个数不一样,那么结果式非零,所以z
不会被设置。所以z
标志能够用来告诉你两个值是否相等。
如果我们正在比较两个整数,则我们此时必须很小心。让我们先从无符号整型开始。原则上,大的减小的没什么问题。但计算的算法有问题(无法直接做减法),为了避免这个问题,补码+反码 的机制就出现了。在这种情况下,减法就会被处理为普通的加法。如果被减数比减数大,则结果是正数,所以C
会被设置。如果被减数比减数小,则加法不会溢出,因为它依然是个负数,所以C
不会被设置。
x和y均为无符号整数
个人理解:
x-y无法用计算机直接算
此时做法:b先取反,然后加1,变为z
于是 x-y等价于x+z
如果
x==y,则标志位Z会被设置
x>y ,则x-y>0,标志位C会被设置
证明:假设x=2, y=1
x(b)=0000 0010
-y(b)=1111 1111
x(b)-y(b)=0000 0001
此时,产生进位,C置位
x
如果我们考虑两个有符号整数,那么整个事情就变得有点复杂了。如果第一个数比第二个数大,那么就与无符号数的情况相似,但是结果要被解释为一个有符号数。这也意味着N不会被设置。例如,如果我们用一个大的正整数减去-1,那么就像一个大的正整数加上一个1。这也就会导致一个溢出并且结果碰巧会被解释为一个负数,此时N会被设置。原因是如果没有溢出发生,减法看起来像是一个正数。
个人理解:
假设是8位机,0001,1110 + 0000,0010 = 0010,0000
如果溢出发生,则结果必须为负数。
个人理解:
假设是8位机,0111,1110 + 0000,0010 = 1000,0000
此时结果本应为整数,但是在计算机看来,其符号位表示为负数,那么N就置位
所以我们能够说如果第一个数比第二个数大,则N和V有同样的值
个人理解:
如上所述,正是因为有了溢出的发生(V被置位),导致了最高位变化,由正数变成了负数,因此N被置位
结论:
x和y是有符号整数
x>y & x-y => N=V
现在考虑这样的情况,第一个数比第二个小,那么减法的结果总是负数,当然,除了溢出的情况除外。在溢出的情况下,减法的结果将会是正数,例如一个最小值减去1,就如同最小值加上-1。这会导致一个有符号溢出并且结果是正数。所以,一种用于判断是否一个值是否比另一个值小的方法就是判断N和V是否不同。
个人理解:
假设是8位机,最小值x=1000,0000
-1的二进制是1111,1111
x-1(b) = 1000,0000 + 1111,1111 = 1,0111,1111
此时,结果是正数,且产生了溢出,N不等于0,V等于1
假设是8位机,x=-1,即x(b)=1111,1111
y=2, 即y(b)=0000,0010
x
既然现在我们由了一些flag的例子,我们能谈论一下条件码了。当flag由一些特殊值的时候,一个条件码为真。条件码被用于分支指令,用于决定是否它们被调用或者没有被调用。注意,不是所有的条件码都是对一个cmp
操作有用(但是它们对其他的设置flag的指令有用)。我已经用深色标注了大多数常用的条件码。
名称 | 含义 | 条件flag |
---|---|---|
EQ | 值比较相等或者操作的结果是0 | Z被设置 |
NE | 值比较结果不相等或者操作结果是非0 | Z不被设置 |
GE | 大于或等于。有符号整数比较,第一个数比第二个数大于或等于。 | N和V要么同时被设置或者同时不被设置 |
GT | 大于。有符号整数的比较,第一个数比第二个数大。 | Z不被设置。N和V要么同时被设置或者两个同时不被设置 |
LE | 小于等于。有符号数的比较,第一个操作数比第二个操作数小于或等于。 | N被设置,并且V不被操作,或者相反。 |
LT | 小于。有符号整数比较,第一个操作数比第二个操作数小。 | Z不被设置。N被设置并且V不被设置或者相反。 |
CS | 进位设置。例如,一个无符号加法的溢出。跟cmp 没什么关系。 |
C被设置 |
CC | 进位清除。例如,一个无符号加法不溢出。跟cmp 没什么关系。 |
C不被置位 |
MI | 负号。例如,一个加法或者减法的结果是一个负数。跟cmp 没什么关系。 |
N被设置 |
PL | 正数或零。例如,一个加法或者减法的结果是一个非负数。跟cmp 没什么关系。 |
N不被设置 |
VS | 有符号整型数溢出。跟cmp 没关系。 |
V被设置 |
VC | 有符号整数没有溢出。跟cmp 没什么关系。 |
V不被设置 |
HI | 高。两个无符号数的比较,第一个操作数比第二个操作数大。 | C被设置并且Z不被设置 |
HS | 高或者相等。比较两个无符号数,第一个操作数比第二个操作数大或者相等。注意,这与上述的CS等价。 | C被设置。 |
LO | 小于。无符号整数的比较。第一个操作数比第二个数小。注意,这是上述CC的等价。 | C不被设置 |
LS | 小于或等级。无符号整数比较,第一个操作数比第二个操作数小。 | 要么C不被设置,要么Z被设置 |
好了,在上述的开场白之后,我们现在能介绍一下条件分支指令: b.cond
该指令在它的操作数上面有一点特殊,就是条件码,它是以指令名称的形式体现的。这个cond
部分就是条件码,并且必须是上面所述的一部分。
正如一个分株的例子,让我们看看我们怎么实现高级语言中如何改变我们程序的控制流。
Fortran是一个非常老的语言,它发明于1956年。这也意味着有些随机的构造(constructions)(在当前的应用中,我猜在那时还是有道理的)。这些构造中有一个时算术if
语句。其形式如下。
IF expression, label1, label2, label3
在Fortran中,label以数字0到99999之间的值表示,但是我们会用符号名称替代。一个Fortran程序,当碰到算术if语句,评估其表达。这个表达的值然后就会与0进行比较。如果这表达式比0小,那么lable1
就会被调用。如果这个表达式等于0,那么就调用lable2
。否则,就意味着表达式的值比0大,那么lable3
就被会调用。
简单点来说,让我们假设一下,我们表达式的值再w0
中。所以第一件我们必须要做的事情是把它与0进行比较。
arithmetic_if:
// code that evaluates the expression and sets its value in w0
cmp w0, #0 // compares w0 with 0 and updates the flags
让我们假设w0
是一个有符号整型。那么比较的顺序在一开始的时候不是很重要,所以我们能按照上述的描述进行。
b.lt label1 // if w0 < 0 then branch to label1
b.eq label2 // if w0 == 0 then branch to label2
b.gt label3 // if w0 > 1 then branch to label3
label1:
// code for label1
b end_of_arithmetic_if // branch to end_of_arithmetic_if
label2:
// code for label2
b end_of_arithmetic_if // branch to end_of_arithmetic_if
label3:
// code for label3
b end_of_arithmetic_if // branch to end_of_arithmetic_if
end_of_arithmetic_if:
// rest ouf our Fortran program :)
当然,如果我们利用我们程序的布局,我们能够保存一些分支。
arithmetic_if:
// code that evaluates the expression and sets its value in w0
cmp w0, #0 // compares w0 with 0 and updates the flags
b.lt label1 // if w0 < 0 then branch to label1
b.eq label2 // if w0 == 0 then branch to label2
// code for label3
b end_of_arithmetic_if // branch to end_of_arithmetic_if
label1:
// code for label1
b end_of_arithmetic_if // branch to end_of_arithmetic_if
label2:
// code for label2
end_of_arithmetic_if:
// rest ouf our Fortran program :)
这篇文章已经太长了。所以,我们先到此为止。在下一章,我们将把条件分支放到具体的工作中,其方式是通过实现一些高级的构造方法。
今天到此为止。