《ARMv8-A编程指南》阅读笔记-03

第五章 ARMv8指令级简介

ARMv8架构最重大的变化之一就是引入了64位的指令集,对已有的32位指令级的起到补充作用。新加入的特性包括:64位宽整数的寄存器访问和操作,以及使用64位长度的指针对存储空间的访问。新的指令级称为A64,并在AArch64运行状态下使用。ARMv8也支持原有的ARM指令级,改称作A32,与新加入的A64相对应。同样支持Thumb(T32)指令集。A32和T32均在AArch32运行状态下运行,并且后向兼容ARMv7。
尽管ARMv8-A具有对32位ARM指令的后向兼容性,但A64指令级实际上完全区别于原有的指令级架构,并且有不同的编码方式。A64增加了一些新的功能但是也去除了一些可能影响速度和能效的功能。ARMv8架构同样也对32位指令集做了加强(A32和T32),然而对于这些改进的功能并不能做到与旧的ARMv7架构兼容。值得注意的是,A64指令级中的操作码长度仍然是32位不是64位。

5.1 ARMv8指令级

新的A64指令集与已有的A32指令集类似。指令都是32位长度,并且使用类似的语法。
在ARMv8架构中,指令集使用通用的命名约定,即使用A32、A64、T32来尽可能保证命名风格的一致性。

指令集介绍
A32在AArch32运行状态下,A32指令级基本可以与ARMv7兼容,但也有少许不同。改进的A32指令集也引入了一些新的指令来与A64的新特性保持一致。
T32Thumb指令级在ARM7TDMI处理器中首次被引入,最初仅包含16位长度的指令。16位的Thumb指令所带来的好处是:在有限的性能损失下大大减少了代码体积。到了ARMv7处理器,包括那些Cortex-A系列处理器均支持Thumb-2技术。Thumb-2对Thumb指令进行拓展,是一个包括16位指令和32位指令的混合指令集。Thumb-2的性能可以达到接近原生的ARM指令,同时也能减少代码的体积。正时因为它同时具有性能和体积上的优势,逐渐被普遍应用在32位代码的编译和汇编过程中。
A64A64提供了与A32和T32类似的功能,A64指令级有如下几个方面的提升
A64所带来的性能提升
1. 一致的编码方案: A32指令集中,后期加入的指令导致了编码方案的一些不确定性,例如,支持半字时,LDR和STR指令的编码方式就与主流的字节和字的变换指令略有不同。结果就是寻址方式也会有轻微不同。
2. 更宽数值范围的常量:A64指令集为常量提供了更宽的选择范围来迎合特定的指令类型。
(1)算数指令通常接受12位的立即常数。
(2)逻辑指令通常接受一个32位或64位的常数,并在编码时对常数的取值有一定约束。
(3)MOV指令接受一个16位的立即数,并可以被移位到任意16位边界。
(4)地址生成指令生成的地址会与4K的页大小对齐。
位操作指令中还有一些更复杂的常数约束规则。位操作指令可以处理源、目的操作数中的任何连续位。
A64提供了灵活的常数操作,但是如何对它们进行编码,甚至是判定特定的常数是否可以在特定的上下文环境中的被合法的编码都意义重大。
3. 数据类型实现更简单:A64指令级下可以自如的处理64位有符号或无符号数据类型,相比于A32更加简洁有效。这十分有利于一些包含64位长整型(long / unsigned long)的编程语言,例如C和Java。
4. 允许更大范围的相对寻址:A64指令级普遍支持更大的相对地址,包括PC寄存器的相对分支(程序跳转)以及相对寻址。
增加的分支范围可以让程序的交叉跳转处理起来更容易。动态生成的代码普遍被放置在堆中,在实际操作中,它们可能被放置在堆中的任意位置,增加了程序跳转的分支范围以后,实时系统可以更容易地找到分散在堆中地代码,省去了安排代码位置的工作。
对数据缓冲池(literal pool)(被嵌入到代码中的字符常量会被单独存放到数据缓冲池中)的操作是ARM指令级很早就有的功能,A64也不例外。因为有了更大的与PC寄存器相关的加载偏置范围,我们不再需要在一段很长的代码中手动处理多个数据缓冲池的存储位置。
5. 指针:在AArch64运行状态下,指针变量是64位的,这样我们就可以使用更大范围的虚拟内存空间,给寻址映射增加了很大的灵活性。但是,我们需要为此付出一点代价。64位指针变量本身使用的存储空间大小是原来32位指针的两倍。多出来4个字节的内存空间可能听起来没什么,但积少成多。另外更大的内存寻址空间会降低cache的命中率,从而对性能造成负面影响。一些编程语言可以使用被压缩的指针,例如Java,避免了长指针变量造成的性能衰退。
6.使用条件指令代替IT块:IT块是T32指令集下的有用功能,使用高效序列来避免未执行指令附近的前向短分支的使用。然而,有些时候,硬件不能有效的处理。A64指令集中去掉了IT块,使用条件指令代替。例如CSEL(条件选择)或CINC条件增量。这些条件指令更加简洁,易于处理并且不会出现特例。
7.移位和循环过程更加直白容易理解:A32指令集和T32指令集中的移位和循环过程并不总是简单按照高级编程语言中所表达的那样。ARMv7提供了一个桶型移位器,可以参与到数据处理指令的运行过程中。然而,指定移位的类型和移位数量需要一个固定位数的操作码,这种操作码也会在其他地方被用到。
8.代码生成:当生成代码时,无论是动态的还是静态的,对于普通的算数运算函数,A32指令集和T32指令集经常需要不同的指令或指令序列。这是为了处理不同的数据类型。对于不同数据类型的操作在A64指令集中具有更好的一致性,所以对不同数据类型的简单操作更容易生成通用的指令序列。
例如,在T32指令集中,相同的指令可以用不同的编码,这取决于使用什么寄存器(低寄存器或高寄存器)。
A64指令集让编码过程更加常规和理性化,最终A64汇编器可以比T32需要更少的代码量来实现相同的功能。
9.指令的长度是固定的:所有的A64指令的长度是相同的,这不同于T32这种变长指令集。固定的指令长度可以让生成的代码序列能被更好的管理和追踪,尤其是对动态代码生成器而言。
10.三操作数指令的完善:总的来说,在A32中为数据处理操作保留了一个真正的三操作数指令。另一方面,在T32中,在二操作数指令的格式中只有一个有效数的位置,这就降低了T32的灵活性,降低了生成代码的灵活性。A64则引入了三操作数语法,增强了指令集的规范性和同质性,这对于编译器而言,是非常有益的。

5.1.1 32位和64位的A64指令

大多数A64指令集中的指令有两种形式,分别使用32位和64位通用寄存器。

  1. 如果寄存器以字母“X”开头,那么它存储一个64位的值。
  2. 如果寄存器以字母“W”开头,它存储一个32位的值。

当使用32位的指令形式时,需要遵循以下事实:

  1. 右移和循环操作在第31位注入而不是第63位。
  2. 被指令置位的条件标志位仅低32位有效。
  3. 对W寄存器的写操作将会将寄存器的高32位清0。

即使A64指令集中所包含32位和64位的指令有以上的区别,32位指令的运行结果与64位指令的运算结果的低32位几乎是一致的。例如,一个32位指令 O R R ORR ORR,也可以通过一个64位的 O R R ORR ORR指令并忽略结果的高32位来实现。但事实上A64指令集中同时包含 O R R ORR ORR指令的32位和64位形式。
C,C++LP64以及LLP64被视为AArch64中最常用的数据模型。它们都常用的int,short以及char,均为32位及以下的数据类型。我们可以在程序的实现方式上利用数据类型这一语义信息。例如:为了避免额外的能源消耗以及减少计算、转发以及存储的循环次数。那些短数据类型的高32位将被人为忽略(识别到这类较短的数据类型后切换到使用32位的A64指令以提高效率)。具体什么时候有必要使用这种优化方式取决于开发者认为是否需要提高某段代码的能源、计算效率,并不强制。
所以,新的A64指令集提供了清晰的有符号和零扩展(zero-extend)指令。另外,A64指令可以对ADD、SUB、CMN或CMP指令的末尾源寄存器以及Load、Store指令的索引寄存器进行拓展和移位操作。这样就可以提供对64位指针序列、32位索引序列进行高效计算的实现方式。

5.1.2 寻址

当处理器可以在一个寄存器中存储一个64位的值,就可以更容易地在程序中访问更大的内存空间。在一个32位处理器核心上执行的单线程可以访问的内存空间上限为4GB。可寻址范围中的大端预留给了系统内核,库代码,硬件以及其他必要组件。内存空间的缺乏意味着程序需要在执行期间映射一部分数据到内存空间之外。而64位的指针长度所带来的更大可寻址内存范围则从根本上解决了这个问题,当然,这也可以让一些技术,例如内存映射文件,变得更加具有吸引力,更易用。文件的内容被映射到线程的内存映射表中,即使硬件上的内存并不足以装下整个文件(有了更大的可寻址空间,或者说是虚拟内存空间,让一些大文件的处理更加简单高效)。
其他的一些寻址方面的提升有如下几个方面:

寻址方面的改进所带来的提升
1. 互斥访问(Exclusive Access):对一个字节、半字或是字甚至双字内存空间的存取。对于一对双字内存空间的互斥访问会允许对一对指针的更新,例如循环列表插入。所有的互斥访问必须对齐,并且互斥对的访问必须以两个字符大小为单位进行对齐(即,与访问的内存空间大小对齐),具体来说,一对64位的值需要按照128位对齐。
2. PC寄存器的相对寻址空间被扩展:PC寄存器的常量加载有不超过±1MB范围的相对寻址空间。相对于A32指令集PC存器相关的加载过程,A64减少了数据缓冲池的数量,并且增加了函数之间数据缓冲池的共享程度。反过来,也减少了对I-cache和TLB(Translation Lookaside Buffer 转译后备缓冲器)的污染。
大多数条件分支都有一个±1MB的范围,这对于单一函数中所需要处理的主要条件分支来说通常是足够的。非条件分支,包括分支和链接,有±128MB的寻址空间,这对于大多数可执行模块的加载,对象的共享所需要的静态代码段的扩展是足够的,并且不需要插入链接的修饰
— 注意 —
插入链接的修饰是指被链接器自动插入的一小段代码,例如,当连接器检测到分支目标超出了预定寻址范围(±128MB),这段修饰就成为了原始分支的中间目标,而修饰本身成为了指向目标地址的另一段分支(二段跳)。
连接器可以重用一段为先前的调用所生成的修饰,对于其他的指向同一个函数的调用,如果该函数同时在两个不同调用的寻址范围内,这一段修饰会偶然成为一个影响性能的可优化项。
如果一个循环需要通过修饰调用多个函数,会造成流水线被频繁强制清空,从而对性能造成负面影响。在内存中将相关的代码放到一起可以避免这种情况的发生。
— —
PC寄存器相关的存取和寻址被生成在一段4GB的内存范围内,这样可以只用两个指令实现内联,也就是说不需要从数据缓冲池中加载偏移量
3. 对未对齐地址的支持:除了互斥访问和有序访问,所有访问普通存储空间的加载和存储过程都支持使用未对齐的地址。这简化了A64指令集的移植代码。
4. 批量传输:A64指令集中去除了 L D M , S T M , P U S H LDM,STM,PUSH LDM,STM,PUSH P O P POP POP指令。块传输可以通过使用 L D P LDP LDP S T P STP STP指令来实现。这两个指令可以从连续的存储空间中读取、存储一对相互独立的寄存器值。
L D N P LDNP LDNP S T N P STNP STNP指令用于提供流或非临时数据的指示,让数据不必要长期保存在cache中。
P R F M PRFM PRFM,预取内存指令,能够将预先载入的内容绑定到特定的cache等级
5. Load/Store:
所有的Load/Store指令现在支持一致的寻址模式。这让存取过程变得更加容易。可以一视同仁的对char,short,int,long long数据类型进行统一处理。
浮点和NEON寄存器现在与核心寄存器支持相同的寻址模式,这样可以更容易地交替使用两个寄存器组。
6. 对齐检查:当在AArch64运行模式下,额外地对齐检查操作将会在 ①取指令,② 使用栈指针加载或存储数据,③ 使能对PC和SP寄存器对齐失败检查 以上三个过程中进行。
对齐检查共有以下几种类型
1. 当在AArch64运行模式下,试图在PC寄存器未对齐时进行取指令操作时,程序计数器(PC寄存器)的对齐检查会生成一个指令预取异常。
如果PC寄存器没有对齐,则寄存器的[1:0]就不会是00(因为字对齐时PC的值必然是4的倍数)。
如果PC没有对齐,会在该异常等级下的异常表征寄存器(exception syndrome register)中有所体现。
当PC对齐失败造成的异常在AArch64运行模式下被受理时,该异常等级下的链接寄存器将以未对齐的形式保存PC的值,这就像各个异常等级下的错误地址寄存器, F A R _ E L n FAR\_ELn FAR_ELn一样。
PC对齐检查只在AArch64运行模式下进行,在AArch32运行模式下是作为数据终止异常(Data Avort Exception)来处理的。
2. 在AArch64运行模式下,当试图使用未对齐的栈指针(SP寄存器)作为基地址进行数据存取操作时,对齐检查将会产生一个数据内存访问异常(Data Memory Access)
用作数值运算基地址的栈指针如果低四位[4:0]不全是0则未对齐,只要栈指针寄存器的值用作基地址,则必然是16字节对齐的。
栈指针的对齐检查仅可以在AArch64运行模式下进行,并且各个异常等级下的使能是相互独立的。
— EL0和EL1受SCTRL_EL1中的两位分别控制
— EL2受SCTLR_EL2中的一位控制
— EL3受SCTLR_EL3中的一位控制

5.1.3 寄存器

A64指令集的64位寄存器组可以在大多数应用中减小寄存器的是使用压力。
A64 函数调用标准(Procedure Call Standard,PCS)支持传递多达8个参数到寄存器中(X0-X7)。作为对比,A32和T32指令集仅支持一次传递4个参数到寄存器中,多出的部分参数将被传递到栈中。
A64的函数调用标准(PCS)也定义了一个专用的帧指针(frame pointer),提高了可靠展开堆栈的可能性,降低了调试和调用图分析的难度。
使用64位寄存器的一个后果是会使编程语言中不同类型的变量长度发生变化。目前有很多被广泛接受的标准模型,它们的整型,长整型和指针的数据长度定义不尽相同。
在这里插入图片描述
64位Linux所使用的标准是LP64,A64 函数调用标准支持该标准,其他的变量长度定义是其他操作系统所使用的。

  1. 0寄存器:0寄存器( W Z R / X Z R WZR/XZR WZR/XZR)被用在一些编码技巧中。例如,没有单纯的乘法编码,只有乘加。因此指令MUL W0, W1, W2实际上在执行时等效于MADD W0, W1, W2, WZR 。这里使用0寄存器来填充加数的空位。也并不是所有的指令都可以使用0寄存器。0寄存器与栈指针(SP寄存器)共享了相同的编码过程,这意味着有一小部分指令在执行时 W Z R / X Z R WZR/XZR WZR/XZR是不能被访问的,其实也就是WSP/SP寄存器被使用的时候。
    示例:使用0寄存器向存储空间中写入一个0
    在这里插入图片描述

    可以看到,A64指令集下不需要使用一个额外的寄存器来存储立即数0,直接从 W Z R WZR WZR中读取即可。
    0寄存器所带来的另一个方便的副产品是有许多有立即数域的NOP指令,例如,ADR XZR,#<imm>,这个指令实际上并没有做任何事,但也不会造成任何负面影响。这对即时编译器来说很有用,代码可以随时通过这种改为空指令的方式进行修补。
  2. 栈指针(SP):栈指针(SP)不能被大多数的指令引用。一些形式的算数指令可以读或写当前的栈指针(SP)。这种操作通常用来在函数开始或结束的位置调整栈指针的位置。例如 ADD SP, SP, #256 // SP = SP + 256
  3. 程序计数器(PC):程序运行时程序计数器的内容不能像通用寄存器那样以数字形式直接引用。因此也不能直接用作① 算数指令的源或是目的寄存器,同样也不能作为② 基地址,索引或是 ③ 加载、存储指令的转换寄存器。
    那些仅有的可以读取PC寄存器值并按照预期正确解析PC寄存器内容的指令有: ① 用于计算出一个相对于PC寄存器(当前指令运行位置)的地址(例如ADR、ADRP指令,常量加载和直接分支操作),② 还有那些将返回地址存储到链接寄存器(BL,BLR)中的分支和链接指令。修改PC寄存器中内容仅有的几种途径有 ①使用分支(程序转跳),异常产生和异常返回指令
    当PC被指令读取,用于计算相对于PC的地址时,读取的PC值就是那条指令的地址。不同于A32指令集或是T32指令集,并没有4或8字节的隐含偏移 (多级流水线的原因)。
  4. FP和NEON寄存器组:NEON寄存器组最大的更新是现在NEON寄存器组有了32个16字节的寄存器,而不是之前的16个寄存器。浮点和NEON寄存器组中不同的寄存器尺寸之间有更简单的映射表,让这些寄存器更加易用。新的映射表让编译和优化器的建模和分析变得更容易。
  5. 寄存器的变址寻址:A64指令集相对于A32而言提供了额外的寻址模型,允许一个64位的索引寄存器与64位的基地址寄存器相加,并且可以索引寄存器在可访问的存储范围内倍乘。另外,它提供了在索引寄存器内对32位值的标记或0扩展,也可以对索引寄存器中的内容进行倍乘。(这个模型实际上就是: b a s e + i n d e x × s c a l e base + index × scale base+index×scale

5.2 C/C++的内联汇编

在这一部分中我们将简短地介绍如何在C/C++代码段中内联汇编。
汇编的关键字可以组合GCC语法的汇编代码,整合到函数中,例如:
在这里插入图片描述
内联汇编的通用格式是:
在这里插入图片描述
上图中:
code:是指汇编代码,在上面的例子中就是“ADD %[result],%[input_i],%[input_j]”。
output_oprand:输出操作数列表是可选的,列表中的输出操作数用逗号分隔。每一个操作数包含一个在方括号内的符号名,一个字符串常量,以及在一对圆括号中的C语言表达式。在上面的例子中,该列表仅包含一个输出操作数,形如:[result] “=r” (res)。
ubput_oprand_list:输入操作数列表是一个可选项,列表中的元素之间使用逗号分隔。输入操作数的语法与输出操作数相同,在该例中,输入操作数位 [input_i] “r” (i) 以及 [input_j]“r”(j)。
clobber_list: 参考:什么是clobber。clobbered registers,字面意思是被重创的寄存器,实际上内联汇编在使用特定寄存器时会覆盖寄存器中原有的内容,这就是colbber的含义,该列表的意义就在于告诉编译器,列表中寄存器的内容可能会在不知情的情况下被覆盖,在这个例子中,cobber list被省略了。
当在C/C++和汇编代码之间进行函数调用时,必须遵循AAPCS64规则。

5.3 在不同指令集之间切换

我们不可能在同一个应用中使用两种运行状态(AArch64和AArch32)的代码。在ARMv8中,A64和A32或T32指令集之间没有任何的交互配合。这与A32和T32之间的关系不同。为ARMv8使用A64指令集编写的代码不能在ARMv7 Cortex-A系列处理器上运行。但是,为ARMv7-A处理器编写的代码可以在ARMv8处理器的AArch32运行状态下运行。这种关系可以用下图来概括:
在这里插入图片描述

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值