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的方式為:
使用控制流指令,如條件跳轉、無條件跳轉、異常生成和異常返回指令。
THE END!