Date: 2018.9.21
1、参考
https://blog.csdn.net/SoaringLee_fighting/article/details/82155608
https://blog.csdn.net/SoaringLee_fighting/article/details/81743505
https://blog.csdn.net/SoaringLee_fighting/article/details/81906495
https://blog.csdn.net/SoaringLee_fighting/article/details/82530435
2、前言
最近三个月的时间,都在进行解码库的arm架构汇编优化,包括arm32位汇编优化和arm64位汇编优化。在arm32位入门之后,只要掌握了两种架构的寄存器和指令集差异之后,就可以很快上手编写arm64位汇编代码了。下面就arm32位和arm64位架构、寄存器和指令差异进行分析总结。
3、架构差异
ARM是RISC(精简指令集)处理器,不同于x86指令集(CISC,复杂指令集)。
Arm32位是ARMV7架构,32位的,对应处理器为Cortex-A15等; iphone5以前均是32位的;
需要注意:ARMV7-A和ARMV7-R系列支持neon指令集,ARMv7-M系列不支持neon指令集。
ARM64位采用ARMv8架构,64位操作长度,对应处理器有Cortex-A53、Cortex-A57、Cortex-A73、iphones的A7和A8等,苹果手机从iphone 5s开始使用64位的处理器。
4、寄存器差异
4.1 ARM通用寄存器
ARM32位通用寄存器和ARM64位通用寄存器差异详见:ARM寄存器及其说明
4.2 NEON寄存器
ARM32位neon寄存器和ARM64位neon寄存器差异:
32位下 NEON寄存器:
包括:
- 32个S寄存器,S0~S31,(单字,32bit)
- 32个D寄存器,D0~D31,(双字,64bit)
-
16个Q寄存器,Q0~Q15,(四字,128bit)
使用注意:
1、NEON寄存器将每个寄存器均视为一个向量,该向量又包含1,2,4,8或16个大小和类型均相同的元素。也可以将各个元素当做标量访问。
NEON的这三种寄存器是重叠的,物理地址是一样的。
2、NEON寄存器在使用时,如果用到d8~d15寄存器,需要先入栈保存vpush {d8-d15},使用完之后要出栈vpop {d8-d15}。
64位下NEON寄存器:
包括:
- 32个B寄存器(B0~B31),8bit
- 32个H寄存器(H0~H31),半字 16bit
- 32个S寄存器(S0~S31) ,单字 32bit
- 32个D寄存器(D0~D31),双字 64bit
- 32个Q寄存器(V0~V31),四字 128bit
不同位数下寄存器之间的关系如下图所示:
其中S0是D0的低半部分,D0是V0的低半部分 。
注意:
64位下NEON寄存器与32位下NEON寄存器之间的关系不同!
neon寄存器 v0~v31使用说明:
v0~v7:用于参数传递和返回值,子程序不需要保存;
v8~v15:子程序调用时必须入栈保存(低64位);
v16~v31:子程序使用时不需要保存。
具体可参考:
http://infocenter.arm.com/help/topic/com.arm.doc.ihi0055b/IHI0055B_aapcs64.pdf 5.1.2 SIMD and Floating-Point Registers
5、A64指令集特有的指令及其用法(指令差异)
AARCH64是全新32位固定长度指令集,支持64位操作数的新指令,大多数指令可以具有32位或64位参数。
- 32位neon指令都是以V开头,而64位neon指令没有V;
- 32位寄存器需要保存的是r4~r11,q4~q7,而64位寄存器需要保存的是x19~x29 , v8~v15;
- 64位下NEON寄存器与32位下NEON寄存器之间的关系不同,32位下d寄存器和q寄存器是重叠映射的,而64下的d寄存器和v寄存器是一一对应的,具体详见4.1;
- 向64位或者更低位的矢量寄存器中写数据时,会将高位置零;
- 在AArch64中,没有通用寄存器的SIMD和饱和算法指令。只有neon寄存器才有SIMD或饱和算法指令;
- ARM64下没有swap指令和条件执行指令。
- 关于指令中立即数取值范围的说明:
不同指令中的#<imm>或者#<const>具有不同的取值范围,这个取决于所使用的指令,比如:
mov <wd>, #<imm> //该指令中立即数范围为-65536~65535。
cmp <wn>, #<imm> //该指令中#<imm>为无符号立即数,取值范围为0~4095(12 bit)。
特别说明:大部分ARM指令中的立即数不能是负数,需要注意不同指令的取值范围。
- 更多ARM32和ARM64位对应关系可以参考文档: ARM.Reference_Manual中5.7.23小节 。
1. shl和ushr指令
shl <V>.<d>, <V>.<n>, #<shift>
ushr <V>.<d>, <V>.<n>, #<shift>
ushr d2, d2, #8
使用注意事项:这两条指令只能操作64位数据,即只能对D寄存器进行处理。
ushr最多只能进行64位数据的右移,并且右移时会影响V2寄存器的高64位数据(清零),因此高64位数据需要在右移前保存,否则相关数据会被修改。
2. INS指令
用法与MOV指令基本一样,可以实现neon标量与neon标量之间的传送,以及ARM寄存器与neon标量之间的传送。
INS <Vd>.<Ts>[index1], <Vn>.<Ts>[index2]
INS <Vd>.<Ts>[index1], Rn
3. SUQADD、USQADD指令
既有标量用法,也有矢量用法。
SUQADD <V><d>, <V><d> // signed saturating accumulate of unsigned value
SUQADD <Vd>.<T>, <Vn>.<T>
USQADD <V><d>, <V><d> // unsigned saturating accumulate of signed value
USQADD <Vd>.<T>, <Vn>.<T>
4. RBIT、REV指令
RBIT <Wd>, <Wn> //reverse bits
REV <Wd>, <Wn> //reverse bytes
5. ADDV,SADDLV,SMAXV,SMINV (Vector Reduce(across lanes))
后缀带V指令:
ADDV <V><d>, <Vn><T> // Integer sum element to scalar(vector)
SADDLV <V><d>, <Vn><T> // Signed Interger sum elements to long scalar(vector)
SMAXV <V><d>, <Vn><T> // Signed Interger maximum elements to scalar(vector)
SMINV <V><d>, <Vn><T> // Signed Interger minimum elements to scalar(vector)
eg.:
addv B0, v1.8B // 将v1寄存器中的低64位中8个8位数据相加求和后,赋给v0的最低8位。
addv H2, v1.8B // 将v1寄存器中的低64位中8个字节依次相加求和,并将结果赋给v1第2个16位标量中。
用法说明:
vector reduce instrunctions perform arithmetic operations horizontally, that is across all lanes of the input vector. they deliver a single scalar result. 对矢量中每个元素进行水平方向的算术运算,并产生一个标量结果。
更多详细解释可以参考:https://static.docs.arm.com/ddi0487/a/DDI0487A_j_armv8_arm.pdf
6. sxtw使用注意事项
负数在使用时必须进行符号扩展!
比如:
sxtw x4, w4
7. w寄存器到v寄存器
直接使用dup指令
dup v0.8B, w2
8.常用指令对应关系(arm32---->arm64)
vmovl------>uxtl/sxtl
vqmovn----->sqxtn
vqmovun----->sqxtun
vqrshrun---->sqrshrun
vceq------->cmeq
vcge------->cmge
vadd------>add
vsub------>sub
vaddl----->saddl,uaddl
vaddw----->saddw,uaddw,sw2addw2,uadd
vmull----->smull,smull2,umull,umull2
vmax,vmin----->smax,umax,smin,umin
vmlal--------> smlal,smlal2,umlal,umlal2
vrshl--------> urshl,srshl
vtrn---------> trn1,trn2
vstm/vstr----> stp/str
vld1.32 {d0[]}, [r0], r2-----> ld1r {v0.S}[0], [x0], x2
addgt,addle,subgt,suble----->csel,csetm,cset,csinc,csinv
9. 指令结尾带“2”的寄存器高64位操作:
smull2 v0.4S, v1.4H, v2.4H //将v1和v2高64位中每一个16位相乘,并将结果放在v0的每个32位中。
sqxtn2 v5.8H, v4.4S //将v4中的每个32位元素,饱和缩进到v5高64位的每个16位中。
sxtl2 v16.4S, v17.4H //将v17高64位中的每个16位元素,扩展到v16的每个32位元素中。
10. ADDP,SMAXP,SMINP,UMAXP,UMINP
后缀带P指令:
addp v1.8B, v2.8B, v3.8B // add pairwise
用法说明:
the SIMD pairwise arthmetic instructions perform operations on pairs of adjacent element and deliver a vector result. 对矢量中的相邻元素两两进行算术运算,并产生一个矢量结果。
11. 替代arm32下条件执行指令的arm64位指令:
arm32位:
bgt,addgt,suble等
arm64位:
b.gt,csinc, csel, cset, csetm, csnev
说明: 在ARM64下没有条件执行指令。
6、AArch32与AArch64的区别
6.1 入栈和出栈:
arm64位(aarch64架构):
(1)arm寄存器入栈和出栈:
入栈:
sub sp, sp, #0x10
stp x8, x9, [sp] // 寄存器成对入栈
出栈:
ldp x8, x9, [sp]
add sp, sp, #0x10 //寄存器成对出栈
原则:1、堆栈入栈和出栈后,SP指针应该保持不变(栈平衡)。2、LIFO。 3、特别注意是,从SP位置存取数据都是从低地址开始的。
(2)neon寄存器入栈和出栈:
ARM64位三种入栈出栈方法:
方法一:
stp d8,d9, [sp, #-64]!
stp d10,d11,[sp, #16]!
stp d12,d13,[sp, #16]!
stp d14,d15,[sp, #16]!
ldp d14,d15,[sp], #-16
ldp d12,d13,[sp], #-16
ldp d10,d11,[sp], #-16
ldp d8,d9,[sp], #64 //恢复sp位置
方法二:
stp d8,d9, [sp, #-16]!
stp d10,d11,[sp, #-16]!
stp d12,d13,[sp, #-16]!
stp d14,d15,[sp, #-16]!
ldp d14,d15,[sp], #16
ldp d12,d13,[sp], #16
ldp d10,d11,[sp], #16
ldp d8,d9,[sp], #16 //恢复sp位置
方法三:
.macro push_v_regs
stp d8, d9, [sp, #-16]!
stp d10, d11, [sp, #-16]!
stp d12, d13, [sp, #-16]!
stp d14, d15, [sp, #-16]!
.endm
.macro pop_v_regs
ldp d14, d15, [sp], #16
ldp d12, d13, [sp], #16
ldp d10, d11, [sp], #16
ldp d8, d9, [sp], #16
.endm
arm32位:
push {r4-r11, lr}
vpush {d8-d15}
vpop {d8-d15}
pop {r4-r11, pc}
6.2 4x4矩阵转置:
arm64位(aarch64架构):
.macro transpose4x4B r0,r1,r2,r3,t4,t5,t6,t7
trn1 \t4\().8B, \r0\().8B, \r1\().8B
trn2 \t5\().8B, \r0\().8B, \r1\().8B
trn1 \t6\().8B, \r2\().8B, \r3\().8B
trn2 \t7\().8B, \r2\().8B, \r3\().8B
trn1 \r0\().8B, \t4\().8B, \t6\().8B
trn1 \r1\().8B, \t5\().8B, \t7\().8B
trn2 \r2\().8B, \t4\().8B, \t6\().8B
trn3 \r3\().8B, \t5\().8B, \t7\().8B
.endm
arm32位:
vtrn.16 q0, q1
vtrn.8 d0, d1
vtrn.8 d2, d3
6.3 Difference between AArch64 and AArch32
来源参考:https://www.nxp.com/docs/en/application-note/AN12212.pdf
6.4 加载数据和存储数据差异
ARM32位存取数据的格式:
vld1加载和vst1存储:
vld1.8 {d0,d1} , [r1], r2 // 将r1地址里面的连续的128bits数据依次赋给d0和d1,然后r1+r2。这里的.8表示以8bit为单位。
vld1.16 {d0[],d1[]}, [r0:16] //这里d0和d1中的数据相同,将地址r0中取4个16位数据加载到d0和d1中。
vst1.8 {d0,d1}, [r2], r3 //将d0和d1中32个bytes数据存储到r2地址处。
ARM64位存取数据的格式:
ld1 {v20.8H, v21.8H}, [x1] // 从x1指向的存储单元位置一次性加载128*2位数据到v20和v21中
ld1 {v1.8B}, [x1], x2 // 从x1指向的存储单元位置加载64位数据到v1的低64位中,然后x1=x1+x2
ld1 {v18.S}[0], [x0], x1 // 将x0地址里面的数据取32位加载到v18的最低32位,然后x0=x0+x1
ld1r {v30.8H}, [x1] // 从x1地址中以16位为单位取128位加载到v30中。
st1 {v30.8H}, [x1], #16 // 将 寄存器v30中128位数据存储到x1地址处,然后x1=x1+16
st1 {v0.S}[0], [x0], x2 // 将 寄存器v0的低32位数据存储到x0地址处吗,然后x0=x0+x2
6.5 补充知识
- 关于调用规则和SP
ARM32位的调用规则遵循ATPCS(The ARM-THUMB Procedure Call Standarfd)调用规则,从2007年开始ARM公司正式推出AAPCS(Procedure Call Standard for the ARM Architecture)标准,是ATPCS的改进版,目前两者都是可用的标准。
AAPCS调用规则规定,栈任何时候必须4字节对齐,在调用入口需8字节对齐。
ARM64位下,SP在公共接口或访问内存时均必须保证16字节对齐,这是硬件要求的。 - 指令编码长度
不管是aarch32还是aarch64,指令在arm指令集模式下,指令固定编码长度为32bit。 - 获取标签地址的方法
arm32位下可以使用adr指令、adrl(伪指令)和ldr指令获取标签地址,需要注意使用adr、adrl指令的标签要在同一代码节中,ldr可以在不同节中。
arm64位下,可以使用adr和adrp采用相对寻址的方式来实现,64位下不支持伪指令adrl。 - EL0没有权限进行AArch64和AArch32状态切换。因此A64指令无法切换到A32和T16指令。因此ARM64位指令无法在ARM32位设备上运行。
疑问: Android 64位指令可以在32位设备上运行? - 关于PC
ARM32位下:PC=当前指令地址+8,具体跟arm处理器多级流水线机制相关,不可用作通用寄存器。
ARM64位下:PC不能直接访问,但可以通过伪指令间接使用。需要注意的是:与32位不同的是,64位下当通过指令读取PC相对地址时,其值即为当前指令的地址。
(1). 64位可读取PC值的情况有:
计算相对地址,如adr,adrp,文字池加载以及直接分支;
子程序返回地址,比如bl,blr
(2). 可修改pc的方式为:
使用控制流指令,如条件跳转、无条件跳转、异常生成和异常返回指令。
<font color=red>** THE END! ** </font>