第五章 统一汇编语言指令
本章是对统一汇编语言的一般性介绍。它并不提供每个指令的详细解释。
指令大致可以分为以下几个类别:
数据操作(如算术逻辑单元操作中的ADD)。
存储操作(对内存的加载和存储)。
分支(用于循环、跳转、条件代码和其他程序流程控制)。
数字信号处理(对打包数据的操作、饱和数学运算和其他特殊指令,针对编解码器)。
杂项(协处理器、调试、模式更改等)。
我们将依次简要介绍这些类别。在此之前,让我们先探讨不同指令类别共有的功能。
5.1 指令集基础
ARMv7-R架构增加了对硬件除法的支持,即UDIV和SDIV指令,这在其他架构配置文件中是不支持的。在Cortex-R核心上,写入PC的数据处理指令的行为也有所不同。以前,数据处理指令不能导致状态改变,除非是从异常返回。在ARMv7-R中,任何写入PC的数据处理指令都可以根据地址的位[0]改变状态。然而,指令集的所有部分仍有一些共同的特征。
5.1.1 常量和立即值
ARM或Thumb汇编语言指令的长度仅为16或32位。这带来了一些问题。这意味着你无法在操作码中编码任意的32位值。
在ARM指令集中,由于操作码位用于指定条件码、指令本身以及要使用的寄存器,因此只有12位可用于指定一个立即值。我们必须在如何使用这12位上发挥一些创意。不是允许指定大小为-2048到+2047的常量,而是将这12位分为一个8位常量和4位旋转值。旋转值使得8位常量值可以以2的倍数(即0, 2, 4, 6, 8…)右旋0到30位。
因此,你可以有如0x23或0xFF这样的立即值。你可以生成其他有用的立即值,例如,外设或内存块的地址。例如,0x23000000可以通过将其表示为0x23 ROR 8来生成。但是许多其他常量,如0x3FF,无法在单条指令中生成。对于这些值,你必须使用多条指令构造它们,或者从内存中加载。程序员通常不会为此担心,除非汇编器报错抱怨无效常量。相反,你可以使用汇编语言伪指令来生成所需的常量。
在Thumb中,指令中编码的常量可以是以下几种形式之一:
通过在32位字内旋转8位值产生的常量
形式为0x00XY00XY的常量
形式为0xXY00XY00的常量
形式为0xXYXYXYXY的常量
其中XY是一个十六进制数,范围从0x00到0xFF。
MOVW指令(移动宽),将一个16位常量移入一个寄存器,同时将目标寄存器的顶部16位清零。MOVT(移动顶部)将一个16位常量移入给定寄存器的顶部,而不改变底部的16位。这允许MOV32伪指令构造任何32位常量。汇编器在这里提供了一些帮助。前缀:upper16:
和:lower16:
允许你从32位常量中提取相应的半部分:
MOVW R0, #:lower16:label
MOVT R0, #:upper16:label
尽管这需要两条指令,但它不需要额外的空间来存储常量,也没有从内存中读取数据项的要求。
你还可以使用诸如LDR Rn, =<constant>
或LDR Rn, =label
的伪指令。(这是缺少MOVW和MOVT的老处理器的唯一选项。)然后汇编器将使用最佳序列在指定寄存器中生成常量(MOV、MVN或从文字池中的LDR)。文字池是代码部分内保存的常量数据区域,通常位于函数末尾和另一个函数开始之前。如果需要手动控制文字池的放置,可以使用汇编器指令——对于armasm使用LTORG,使用GNU工具时使用.ltorg
。加载的寄存器可以是程序计数器,这将导致分支。
这可以用于绝对寻址或对当前节以外的引用;显然,这将导致位置相关的代码。常量的值可以由汇编器或链接器确定。
ARM工具还提供了相关的伪指令ADR Rn, =label
。这使用基于PC的ADD或SUB,将标签的地址放入指定的寄存器,只使用一条指令。如果地址太远而无法以此方式生成,则使用ADRL伪指令。这需要两条指令,提供了更好的范围。这可以用于生成位置无关代码的地址,但仅限于同一代码节内。
5.1.2 条件执行
ARM指令集的一个特点是几乎所有的指令都可以条件执行。在大多数其他架构中,只有分支或跳转可以条件执行。这在避免小型if/then/else结构中的条件分支或用于复合比较时非常有用。
例如,考虑一段代码,用于找出寄存器R0和R1中的两个值中较小的一个,并将结果放入R2。这将在下面示例中展示。后缀LT表示该指令应该只在最近的标志设置指令返回小于时执行;GE表示大于或等于。
现在让我们看看使用条件MOV指令而不是分支编写的相同代码,如下示例所示。
后者代码体积更小,并且在旧的ARM处理器上执行速度更快。然而,在某些处理器上,这段代码实际上可能会更慢,因为指令间的依赖关系可能会导致比分支更长的停顿,而分支预测可以减少,甚至可能消除分支的成本。
作为提醒,这种编程风格依赖于这样一个事实:某些指令可以可选地设置状态标志。如果上图示例中的MOVGE指令自动设置了标志,程序可能无法正确工作。加载和存储指令从不设置标志。然而,对于数据处理操作,您有一个选择。默认情况下,在执行此类指令时标志会被保留。如果指令带有S后缀(例如,使用MOVS而不是MOV),该指令将设置标志。对于显式的比较指令,不需要或不允许使用S后缀。标志也可以通过使用专用的PSR操作指令(MSR)手动设置。一些指令根据ALU的进位设置进位标志(C),而其他指令则基于桶式移位器的进位(在单个时钟周期内将数据字按指定的位数移位)来设置。
Thumb代码有一种略有不同的机制用于条件执行。分支可以条件执行。通过使用比较和零时分支(CBZ)以及比较和非零时分支(CBNZ)指令,也可以条件地执行指令。这些指令将寄存器的值与零进行比较并根据结果进行分支。
Thumb-2技术还引入了If-Then(IT)指令,为多达四个连续指令提供了条件执行。这些条件可能全部相同,或者其中一些可能是其他条件的反义。IT块内的指令也必须指定要应用的条件码。
IT是一个16位指令,它使得几乎所有的Thumb指令都可以根据ALU标志的值和条件码后缀条件地执行。该指令的语法是IT{x{y{z}}}
,其中x、y和z指定IT块中可选指令的条件开关,可以是Then(T)或Else(E),例如ITTET
。
ITT EQ
SUBEQ r1, r1, #1
ADDEQ r0, r0, #60
通常,IT指令是由汇编器自动生成的,而不是手动编码的。通常改变条件码标志的16位指令,在IT块内不会这样做,除非是CMP、CMN和TST,这些指令的唯一作用是设置标志。IT块内可以使用哪些指令有一些限制。IT块内可能会发生异常,当前的if-then状态存储在CPSR中,并在异常入口复制到SPSR,以便异常返回时,IT块的执行可以正确地恢复。
某些指令总是设置标志并且没有其他效果。这些指令包括CMP、CMN、TST和TEQ,它们与SUBS、ADDS、ANDS和EORS类似,但ALU计算的结果仅用于更新标志,而不放置在寄存器中。
下表中列出了可以附加到大多数指令的15个条件码。
5.1.3 状态标志和条件码
在第三章的《程序状态寄存器》中提到,ARM处理器有一个当前程序状态寄存器(CPSR),其中包含四个状态标志,(Z)零,(N)负,(C)进位和(V)溢出。下表指示了这些位在标志设置操作中的值。
C标志在无符号操作的结果溢出32位结果寄存器时被设置。例如,这个位可能被用来实现使用32位操作的64位(或更长的)算术。
V标志的工作方式与C标志相同,但适用于有符号操作。0x7FFFFFFF是32位可以表示的最大有符号正整数。例如,如果你将2加到这个值上,会产生0x80000001,这是一个大的负数。V位被设置以指示从位[30]到位[31]的溢出或下溢。
5.2 数据处理操作
这些操作本质上是处理器的基本算术和逻辑操作。乘法可以被视为这些操作的一种特殊情况 - 它们通常具有略有不同的格式和规则,并在处理器的专用单元中执行。
ARM处理器只能在寄存器上执行数据处理,而永远不会直接在内存上执行。数据处理指令(在大多数情况下)使用一个目标寄存器和两个源操作数。基本格式可以认为是操作码,后面可选地跟着一个条件码,再后面可选地跟着S(设置标志),如下所示:Operation{cond}{s} Rd, Rn, Operand2
下表总结了数据处理汇编语言指令,给出了它们的助记符操作码、操作数以及它们功能的简要描述。
对于大多数程序员来说,这些指令的目的和功能应该是显而易见的,但有些指令需要额外的解释。
在算术运算中,请注意MOV和MVN移动操作只需要一个操作数(这被视为操作数2,以实现最大的灵活性,我们将会看到)。RSB执行反向减法——也就是说,它从第二个操作数中减去第一个操作数。需要这个指令是因为第一个操作数是固定的——它只能是一个寄存器值。所以,要编写R0 = 100 - R1
,你必须使用RSB R0,R1,#100
,因为SUB R0,#100,R1
是一个非法指令。ADC、RSC和SBC执行带有进位的加法和减法。这让你可以对大于32位的值进行算术运算。
逻辑操作与相应的C运算符基本相同。注意使用ORR而不是OR(这是因为原始的ARM指令集对所有数据处理操作使用了三个字母的缩写)。BIC指令执行一个寄存器与操作数2的反值的AND操作。例如,如果你想要清除寄存器R0的位[11],你可以使用指令BIC R0, R0, #0x800
。
第二个操作数0x800只有位[11]设置为1,其他所有位都是0。BIC指令对这个操作数取反,将除位[11]之外的所有位设置为逻辑1。将这个值与R0中的值进行AND操作,会清除位[11],然后将这个结果写回R0。
比较和测试指令修改CPSR,没有其他效果。
5.2.1 操作数2和桶形移位器
所有数据处理操作的第一个操作数必须始终是寄存器。第二个操作数更加灵活,可以是立即值(#x)、寄存器(Rm),或者是通过立即值或寄存器“Rm, shift #x”或“Rm, shift Rs”移位后的寄存器。共有五种移位操作:逻辑左移(LSL)、逻辑右移(LSR)、算术右移(ASR)、右旋(ROR)和扩展右旋(RRX)。
右移会在寄存器的顶部创建空位。在这种情况下,必须区分逻辑移位(在最高有效位插入0)和算术移位(用寄存器的第[31]位,即符号位,填充空位)。因此,ASR操作可能用于有符号值,而LSR用于无符号值。左移不需要这样的区分,总是向最低有效位插入0。
与许多汇编语言不同,ARM汇编语言不需要显式的移位指令。相反,可以使用MOV指令进行移位和旋转。例如,R0 = R1 >> 2 可以通过 MOV R0, R1, LSR #2
来实现。同样,通常将移位与ADD、SUB或其他指令结合使用。例如,要将R0乘以5,可以这样写:
ADD R0, R0, R0, LSL #2
左移n位实际上相当于乘以2的n次方,所以这实际上使得 R0 = R0 + (4 × R0)。右移提供了相应的除法操作,尽管ASR对负值的四舍五入与C语言中的除法不同。
除了乘法和除法,移位操作数的另一个常见用途是数组索引查找。考虑以下情况,R1指向一个int(32位)值数组的基元素,R2是指向该数组中第n个元素的索引。您可以使用单个加载指令来获取数组值,该指令使用计算 R1 + (R2 × 4) 来获取适当的地址。下面的示例提供了ARM指令中使用不同操作数2类型的示例。
5.2.2 乘法操作
乘法操作很容易理解。一个关键的局限性是没有范围去乘以一个立即值。乘法操作只对寄存器中的值进行。乘以一个常数可能需要首先将这个常数加载到寄存器中。ARM处理器的后续版本增加了更多的乘法指令,为8位、16位和32位数据提供了多种可能性。我们将在第五章后面讨论DSP指令时,考虑这些整数SIMD指令。
下表总结了乘法汇编语言指令,给出了它们的助记符操作码、操作数以及它们功能的简要描述。
5.2.3 额外的乘法指令
我们在数据处理指令中看到,我们有能力将一个32位寄存器与另一个寄存器相乘,以产生32位的结果或者64位的有符号或无符号结果。在所有情况下,都有将32位或64位值累加到结果中的选项。已经添加了额外的乘法指令。有符号最高字乘法指令,包括SMMUL、SMMLA和SMMLS。这些指令执行32×32位的乘法,结果为乘积的最高32位,最低32位被丢弃。结果可以通过应用R后缀来四舍五入,否则它会被截断。UMAAL(无符号乘累加长)指令执行32×32位的乘法,并加入两个32位寄存器的内容。
5.2.4 硬件除法操作
SDIV和UDIV硬件除法指令在所有ARMv7-R架构配置文件实现中可用,但在某些ARMv7-A实现中才可用。SDIV执行有符号32位整数除法。UDIV执行无符号32位整数除法。
5.3 内存指令
ARM处理器仅在寄存器上执行算术逻辑单元(ALU)操作。支持的唯一内存操作是加载(从内存读取数据到寄存器)或存储(从寄存器写入数据到内存)。LDR和STR可以条件执行,与其他指令的方式相同。
您可以通过在指令后附加B(字节)、H(半字)或D(双字,64位)来指定加载或存储传输的大小,例如LDRB。对于仅加载操作,可以使用额外的S来指示有符号字节或有符号半字(SB为有符号字节,SH为有符号半字)。
这种方法很有用,因为如果您将8位或16位的数据加载到32位寄存器中,您必须决定如何处理寄存器的最高有效位。对于无符号数,您执行零扩展,即您将寄存器的最高16位或24位写入零。但对于有符号数,有必要将符号位(对于字节是位[7],对于半字是位[15])复制到寄存器的顶部16位(或24位)。
5.3.1 寻址模式
对于加载和存储,可以使用多种寻址模式。括号中的数字指的是下文的示例:
寄存器寻址 - 地址在寄存器中(1)。
预索引寻址 - 在内存访问之前,将偏移量加到基寄存器上。这种模式的基本形式是 LDR Rd, [Rn, Op2]。偏移量可以是正数或负数,可以是立即值或带有可选移位的另一个寄存器(2),(3)。
预索引并回写 - 通过在指令后添加感叹号(!)来指示。内存访问发生后,通过添加偏移量来更新基寄存器(4)。
后索引并回写 - 在这里,偏移量在方括号后面写入。仅使用基寄存器中的地址进行内存访问,内存访问后将偏移量加到基寄存器上(5)。
5.3.2 多重传输
加载和存储多重指令允许连续的单词从内存中被读取或写入。这些指令对于堆栈操作和内存复制非常有用。只能以这种方式传输单词值,并且必须使用字对齐的地址。
操作数是一个基寄存器,以及一对花括号内的寄存器列表。可选的感叹号(!)表示基寄存器的回写。寄存器列表用逗号分隔,使用连字符来表示范围。寄存器加载或存储的顺序与在此列表中指定的顺序无关。相反,操作以固定方式进行,按照递增的寄存器顺序,编号最低的寄存器总是映射到最低的地址。
例如:
LDMIA R10!, { R0-R3, R12 }
这条指令从寄存器(R10)指向的地址读取五个寄存器,并且因为指定了回写,所以在最后将R10增加20(5 × 4字节)。
指令还必须指定如何从基寄存器Rd继续操作。有四种可能性:IA/IB(Increment After/Before,增加后/前)和DA/DB(Decrement After/Before,减少后/前)。这些也可以使用别名(FD, FA, ED和EA)来指定,这些别名从堆栈的角度工作,并指定堆栈指针是否指向堆栈的满或空顶部,以及堆栈在内存中是上升还是下降。
按照惯例,在基于ARM处理器的系统中,只使用全降(FD)选项用于堆栈。这意味着堆栈指针指向堆栈内存中最后一个填充的位置,并且每次将新的数据推入堆栈时,堆栈指针都会递减。
例如:
STMFD sp!, {r0-r5} ; Push onto a Full Descending Stack
LDMFD sp!, {r0-r5} ; Pop from a Full Descending Stack
下图显示了将两个寄存器推入堆栈的过程。在执行STMFD(PUSH)指令之前,堆栈指针指向堆栈中最后一个被占用的字。指令完成后,堆栈指针已经减少了8(两个字),两个寄存器的内容已经被写入内存,编号最低的寄存器被写入最低的内存地址。
5.4 分支
指令集提供了多种不同类型的分支指令。对于简单的相对分支(那些从当前地址偏移的),使用B指令。调用子程序时,需要将返回地址存储在链接寄存器中,使用BL指令。
如果您想要更改指令集(从ARM到Thumb或从Thumb到ARM),使用BX或BLX。
您也可以将PC指定为正常数据处理操作(如ADD或SUB)结果的目标寄存器,但这通常是不推荐的,并且在Thumb中不支持。可以使用加载(LDR)指令(以PC为目标)、加载多个(LDM)或堆栈弹出(POP)指令(列表中的寄存器包含PC)来实现另一种类型的分支指令。
Thumb有比较和分支指令。这将CMP指令和有条件分支融合在一起,但不改变CPSR条件码标志。
有两个操作码,CBZ(如果Rn为零则比较并跳转到标签)和CBNZ(如果Rn不为零则比较并跳转到标签)。这些指令只能向前分支4到130字节。Thumb还有TBB(表分支字节)和TBH(表分支半字)指令。这些指令从偏移量表(字节或半字大小)中读取一个值,并执行一个前向的PC相关分支,其分支值是字节或半字表中返回值的两倍。这些指令需要在一个寄存器中指定表的基址,在另一个寄存器中指定索引。
了解处理器在分支时的行为对于编写高度优化的代码非常有用。硬件性能监视计数器可以生成关于正确或错误预测的分支数量的信息。
当移动或修改已经在该系统上执行过的代码的地址时,可能需要(并且总是谨慎的)使用CP15指令来清除分支历史逻辑中的陈旧条目,该指令使所有条目无效。
5.4.1 直接和间接分支
分支可以分为两类,直接分支和间接分支。直接分支是PC相关的,并且分支到当前地址的偏移。直接分支的范围是有限的,例如,对于ARM的B或BL指令,范围为+/-32MB。因为分支目的地是PC相关的,所以可以在流水线的早期阶段精确确定。
间接分支执行绝对分支,因此可以分支到地址空间中的任何位置。然而,因为目的地是在寄存器中指定或从内存加载的,处理器无法轻易预测目的地。
5.5 分支预测
许多分支指令是有条件的。对于条件分支,是否应该执行分支直到指令执行时才能确定。处理器对分支是否会被执行做出预测,并基于预测进行取指。处理器还必须能够检测到当它预测错误时,并从正确的位置重新取指。
分支预测逻辑在Cortex-R系列处理器中实现高吞吐量是一个重要因素。如果没有分支预测,您必须等到条件分支执行后,才能确定从哪里取下一个指令。
分支预测逻辑预测以下内容:
在给定地址是否有一个分支指令。
分支的类型:
无条件或条件。
立即或加载。
正常分支、函数调用或函数返回。
目标地址和分支的状态,是ARM还是Thumb。
条件分支的方向,是执行还是不执行。
有两种分支预测方法:
静态分支预测。
动态分支预测。
5.5.1 静态分支预测
静态分支预测方法很简单,因为它不需要关于分支的先验信息。预测发生在解码阶段。在解码阶段之前,不能做出取指决策。第一次取条件分支指令时,关于下一条指令地址的预测几乎没有信息可以依据。它推测性地选择向后分支而不是向前分支。
向后分支的目标地址低于其自身的地址。因此,它可以查看单个操作码位来确定分支方向。由于循环的普遍存在,这种技术可以给出合理的预测精度,在循环中,向后指向的分支通常会被执行。
5.5.2 动态分支预测
由于流水线长度较长,复杂的分支预测方案,如动态预测,可以提供更好的预测精度。动态预测硬件可以利用关于条件分支之前执行时是否被取的历史信息来减少平均分支惩罚。它可以推测性地取执行代码的选定分支。
在Cortex-R7处理器中,分支目标地址缓存(BTAC)是一个缓存,它保存关于先前分支指令执行的信息。动态分支预测避免了不必要的指令缓存查找和内存访问。对于之前见过的分支,预测质量更高。默认情况下,处理器使用动态分支预测。如果没有历史信息,那么它使用静态分支预测。
5.5.3 返回栈预测
对于大多数分支指令,目标地址是固定的,并在指令中编码。然而,有一类分支,其目标地址不能仅通过查看指令来确定。例如,如果您执行一个修改PC的数据处理操作(例如, MOV, ADD or SUB
),您必须等待ALU评估结果后才能知道分支目标。同样,如果您使用LDR、LDM or POP
指令从内存加载PC,您在加载完成之前无法知道目标地址。
这类分支称为间接分支,通常在硬件中无法预测。一个常见的间接分支情况是函数返回,尽管这可以通过在预取硬件中使用后进先出(LIFO)栈,即返回栈进行优化。
Cortex-R4和Cortex-R5处理器的返回栈由一个四入口的LIFO缓冲区组成。Cortex-R7处理器有一个八入口的FIFO缓冲区。当执行函数调用指令时,生成的LR被推入LIFO缓冲区。当检测到函数返回时,流水线预测目的地是LIFO缓冲区的顶部条目。
识别的函数调用:
BL immediate
BLX immediate
BLX Rm
识别的函数返回:
POP {..,pc}
LDMIB Rn{!}, {..,pc}
LDMDA Rn{!}, {..,pc}
LDMDB Rn{!}, {..,pc}
LDR pc, [sp], #4
BX Rm
BX LR
并非所有可能的返回序列都被预测。例如,返回栈不识别可能在遗留代码中存在的MOV pc, lr
。
当执行函数调用(BL or BLX
)指令时,您应该将以下指令的地址输入到这个栈中。当您遇到被识别为函数返回指令的指令时,您可以推测性地从栈中弹出一个条目,并从那个地址开始取指令。当实际的返回指令执行时,硬件将指令生成的地址与栈预测的地址进行比较。如果存在不匹配,流水线将被清空,您将从正确的位置重新开始。
返回栈的大小是固定的。如果特定的代码序列包含大量的嵌套函数调用,一个八入口的返回栈只能预测前八个函数返回。
5.6 整数SIMD指令
这一节描述了在ARMv6架构中添加的SIMD(单指令,多数据)操作。SIMD是Michael J. Flynn在1966年根据计算机架构中可用的指令和数据流数量定义的四种计算机架构分类之一。
这些指令提供了将8位和16位数量打包、提取和从32位寄存器中解包的能力,并能够对这样的打包数据执行多个算术操作,如加、减、比较或乘,仅使用一个指令。
注意:
SIMD指令是ARM和Thumb指令集的一部分。
5.6.1 整数寄存器SIMD指令
SIMD操作利用CPSR中的GE(大于或等于)标志。这些标志与正常的条件标志不同。每个字中的四个字节位置都有一个对应的标志。普通数据处理操作产生一个值并设置N、Z、C和V标志(如第三章的第三个图所示)。SIMD操作产生多达四个输出,并根据指令的结果设置或清除GE标志,以指示溢出。MSR和MRS指令可以直接用于写入或读取这些标志。
SIMD指令的一般形式是,每个寄存器中的子字量并行操作(例如,可以执行四个字节上的四个加法操作),并根据指令的结果设置或清除GE标志。可以使用适当的前缀指定不同的加法和减法类型。例如,QADD16在寄存器中的半字上执行饱和加法。SADD/UADD8和SSUB/USUB8分别设置GE位,而SADD/UADD16和SSUB/USUB16根据上半字结果共同设置GE位[3:2],根据下半字结果共同设置GE位[1:0]。
此外,还提供了ASX和SAX类指令,它们反转一个操作数的半字,并并行添加/减或减/加对。与之前描述的ADD和Subtract指令类似,这些指令存在无符号(UASX/USAX)、有符号(SASX/SSAX)和饱和(QASX/QSAX)版本。
上图中的SADD16指令显示了一个单一指令如何执行两个独立的加法操作。寄存器R3和R0的顶部半字被加在一起,结果存入寄存器R1的顶部半字,而寄存器R3和R0的底部半字也被加在一起,结果存入寄存器R1的底部半字。CPSR中的GE[3:2]位根据上半字结果设置,GE[1:0]位根据下半字结果设置。在每种情况下,进位标志在指定的对位中复制。这两个操作是完全独立的。特别是,从位15(较低加法的顶部)到位16(较高加法的底部)没有溢出。
5.6.2 整数寄存器SIMD乘法
与SIMD的其他操作类似,这些操作也是并行进行的,在寄存器中的子字量上进行。指令还可以包括一个累加选项,可以选择加法或减法。这些指令包括SMUAD(SIMD乘法和加法,不累加)、SMUSD(SIMD乘法和减法,不累加)、SMLAD(乘法和加法累加)和SMLSD(乘法和减法累加)。
在D之前添加L(long)表示64位累加。
使用X(交换)后缀表示在计算前将Rm中的半字交换。
如果累加溢出,则设置Q标志。
下图中的SMUSD指令执行两个有符号的16位乘法(顶部乘以顶部,底部乘以底部),然后减去这两个结果。这种操作在执行复数操作(具有实部和虚部)时非常有用,这是滤波算法中的常见任务。
5.6.3 绝对差值之和
计算绝对差值之和是常见视频编解码器运动向量估计组件中的关键操作,并在像素数据的数组上执行。下图中的USADA8 Rd, Rn, Rm, Ra指令展示了这一点。它计算寄存器Rn和Rm中字内字节之间的绝对差值之和,将存储在Ra中的值加到结果中,并将结果存入Rd。
5.6.4 数据打包与解包
打包数据在许多视频和音频编解码器中很常见(视频数据通常表示为8位像素数据的打包数组,音频数据可以使用打包的16位样本),在网络协议中也是如此。在ARMv6架构添加额外指令之前,这种数据必须使用LDRH和LDRB指令加载,或者作为字加载然后使用移位和位清除操作解包;这两种方法都相对低效。打包指令(PKHBT, PKHTB)能够从寄存器的任何位置提取16位或8位值,并将其打包到另一个寄存器中。解包指令(UXTH, UXTB,以及包括有符号、带加法的许多变体)可以从寄存器内的任何位位置提取8位或16位值。
这使得内存中的打包数据序列可以高效地使用字或双字加载,解包到单独的寄存器值中,进行操作,然后重新打包到寄存器中,以便高效地写入内存。
在上图所示的一个简单示例中,R0包含两个独立的16位值,分别表示为A和B。您可以使用UXTH指令将这两个半字解包到寄存器中以便进行额外的处理,然后可以使用PKHBT指令将来自两个寄存器的半字数据打包到一个寄存器中。在这种情况下,可以替换每个解包指令为MOV指令以及LSL或LSR指令,但是在这种情况下,您使用的是旨在操作寄存器部分的单一指令。
5.6.5 字节选择
SEL指令使我们能够根据CPSR中的GE[3:0]位的值,从第一个或第二个操作数中的相应字节选择结果中的每个字节。打包数据算术操作在执行加法或减法操作后设置这些位,而SEL可以在这些操作之后使用,以提取数据的一部分——例如,找出每个位置的两个字节中的较小者。
5.7 饱和算术
饱和算术在音频和视频编解码器中经常使用。计算结果高于(或低于)可以表示的最大正数(或负数)时不会溢出。相反,结果被设置为最大的正数或负数(饱和)。ARM指令集包括一些指令,使得实现此类算法变得简单。
5.7.1 饱和数学指令
ARM的饱和算术指令可以对字节、字或半字大小的值进行操作。例如,QADD8和QSUB8指令中的8表示它们操作的是字节大小的值。操作的结果会被饱和到可能的最大正数或负数。如果结果本应溢出并且已被饱和,溢出标志(CPSR Q位)会被设置。这个标志被称为粘性的。一旦被设置,它将保持设置状态,直到通过写入CPSR明确清除。
指令集提供了具有这种行为特殊指令,即QSUB和QADD。此外,还提供了QDSUB和QDADD以支持Q15或Q31固定点算术。这些指令在执行指定的加法或减法之前,会将其第二个操作数加倍并饱和。
计数前导零(CLZ)指令返回设置的最高位之前0位的数量。这对于归一化和某些除法算法可能很有用。要将值饱和到特定的位位置(有效地饱和到2的幂),可以使用USAT或SSAT(无符号或有符号)饱和操作。USAT16和SSAT16允许将寄存器内打包的两个半字值饱和。
5.8 杂项指令
剩余的指令涵盖了协处理器、超级调用、PSR修改、字节反转、缓存预加载、位操作以及一些其他指令。
5.8.1 协处理器指令
协处理器指令占据了ARM指令集的一部分。可以实现最多16个协处理器,编号从0到15(CP0, CP1 … CP15)。这些协处理器可以是内部的(内置在处理器中)或者通过专用接口外部连接的。在旧处理器中使用外部协处理器并不常见,在Cortex-R系列中完全不支持。
协处理器15是一个内置协处理器,它提供了对许多处理器特性的控制,包括缓存和MPU。
协处理器14是一个内置协处理器,它控制处理器的硬件调试功能,如断点单元(在第17章 Debug中描述)。
协处理器10和11提供了对系统中浮点硬件的访问(在第6章 Floating-Point中描述)。
如果执行了协处理器指令,但系统中没有相应的协处理器,将发生未定义指令异常。
协处理器指令有五个类别:
CDP – 发起协处理器数据处理操作。
MRC – 从协处理器寄存器移动到ARM寄存器。
MCR – 从ARM寄存器移动到协处理器寄存器。
LDC – 从内存加载协处理器寄存器。
STC – 从协处理器寄存器存储到内存。
这些指令的多个寄存器和其他变体也是可用的:
MRRC – 将值从协处理器传输到一对ARM寄存器。
MCCR – 将一对ARM寄存器传输到协处理器。
LDCL – 从协处理器寄存器读取多个内存字。
STCL – 将多个内存字写入协处理器寄存器。
这些变体和其他变体在附录A指令概要中有更详细的描述。
5.8.2 SVC
SVC(超级调用)指令执行时,会导致超级调用异常。这将在第11章 Exceptions and Interrupts中描述。该指令包括一个24位(ARM)或8位(Thumb)的数值,可以被SVC处理程序代码检查。通过SVC机制,操作系统可以指定一组特权操作,用户模式下的应用程序可以请求这些操作。这个指令最初被称为SWI(软件中断)。
5.8.3 PSR修改
有几条指令允许写入或读取PSR:
MRS指令将CPSR或SPSR的值传输到通用寄存器。MSR指令将通用寄存器的值传输到CPSR或SPSR。可以更新整个状态寄存器,或其部分。在用户模式下,所有位都可以读取,但只有条件标志(_f)允许被修改。
在特权模式下,可以使用更改处理器状态(CPS)指令直接修改CPSR中的模式以及中断启用或禁用(I和F)位。请参见第三章的第三个图。
SETEND修改CPSR中的一个单独位,即E(Endian)位。这可以在具有混合端序数据的系统中使用,以临时在小端和大端数据访问之间切换。
5.8.4 位操作
有几条指令允许对寄存器中的值进行位操作:
位字段插入(BFI)指令允许从一个寄存器的底部(通过提供宽度值和LSB位置指定)的一系列相邻位被放置到目标寄存器的任何位置。
位字段清除(BFC)指令允许清除寄存器内的相邻位。
SBFX和UBFX指令(有符号和无符号位字段提取)将一个寄存器中的相邻位复制到第二个寄存器的最低有效位,并将值符号扩展或零扩展到32位。
RBIT指令反转寄存器内所有位的顺序。
5.8.5 缓存预加载
提供了两条指令,PLD(数据缓存预加载)和PLI(指令缓存预加载)。这两条指令都是对内存系统的提示,表明可能会很快访问指定的地址。PLD指令中指定的非法地址不会导致数据中止异常。
5.8.6 字节反转
用于反转字节顺序的指令在处理相反端序的数据或其他数据重新排序操作时可能很有用。
REV指令反转字中的字节顺序。
REV16反转寄存器中每个半字节的字节顺序。
REVSH反转最低两个字节,并将结果符号扩展到32位。
下图说明了REV指令的操作,展示了寄存器内的四个字节如何在字中反转其顺序。
5.8.7 其他指令
还有一些其他可用的指令:
断点指令(BKPT)将导致预取中止(参见第11章的异常类型)或使处理器进入调试状态(取决于处理器是否配置为监视器模式或暂停模式调试)。这个指令被调试器使用。请参见第17章的调试事件。
等待中断(WFI)将处理器置于待机模式,这在第16章电源管理中描述。处理器停止执行,直到被中断或调试事件唤醒。如果在中断禁用的情况下执行WFI,中断仍然可以唤醒处理器,但不会产生中断异常。处理器继续执行WFI之后的指令。在较旧的ARM处理器中,WFI作为CP15操作实现。
无操作(NOP)指令不执行任何操作。它可能执行时需要时间,也可能不需要,因此不应使用NOP指令在代码中插入定时延迟。它旨在用作填充。
等待事件(WFE)指令以类似于WFI的方式将核心置于待机模式。核心将睡眠,直到被另一个执行REV指令的核心生成的事件唤醒。中断或调试事件也会导致核心唤醒。
SEV(发送事件)指令用于生成可能唤醒集群中其他核心的唤醒事件。