【arm】arm架構64位入門基礎:架構分析、暫存器、呼叫規則、指令集、程式除錯以及參考手冊
Date: 2018.8.21
1、參考
2、ARM64位架構分析
ARM64位採用ARMv8架構,64位操作長度,對應處理器有Cortex-A53、Cortex-A57、Cortex-A73、iphones的A7和A8等。
AARCH64是全新32位固定長度指令集,支援64位運算元的新指令,大多數指令可以具有32位或64位引數。
ARM64位架構有兩種主要的執行狀態:
- AArch64 ——64 位執行狀態,包括該狀態的異常模型、記憶體模型、程式設計師模型和指令集支援
- AArch32 ——32 位執行狀態,包括該狀態的異常模型、記憶體模型、程式設計師模型和指令集支援
這些執行狀態支援三個主要指令集:
- A32(或 ARM):32 位固定長度指令集,通過不同架構變體增強部分 32 位架構執行環境現在稱為 AArch32。
- T32 (Thumb) 是以 16 位固定長度指令集的形式引入的,隨後在引入 Thumb-2 技術時增強為 16 位和 32 位混合長度指令集。部分 32 位架構執行環境現在稱為 AArch32。
- A64:提供與 ARM 和 Thumb 指令集類似功能的 32 位固定長度指令集。隨 ARMv8-A 一起引入,它是一種 全新的AArch64 指令集。
ARM ISA 不斷改進,以滿足前沿應用程式開發人員日益增長的要求,同時保留了必要的向後相容性,以保護軟體開發投資。在 ARMv8-A 中,對 A32 和 T32 進行了一些增補,以保持與 A64 指令集一致。
3、ARM64位暫存器
主要包括64位下的ARM暫存器和NEON暫存器。
ARM架構64位暫存器:
31個通用暫存器X0~X30,以及SP(x31)和PC,共33個。其中W0~W31分別是X0~X31的低32位,如下圖所示:
64位下通用暫存器關係圖
ARM64位引數呼叫規則遵循AAPCS64,規定堆疊為滿遞減堆疊。
暫存器呼叫規則如下:
- X0~X7:用於傳遞子程式引數和結果,使用時不需要儲存,多餘引數採用堆疊傳遞,64位返回結果採用X0表示,128位返回結果採用X1:X0表示。
- X8:用於儲存子程式返回地址, 儘量不要使用 。
- X9~X15:臨時暫存器,使用時不需要儲存。
- X16~X17:子程式內部呼叫暫存器,使用時不需要儲存,儘量不要使用
- X18:平臺暫存器,它的使用與平臺相關,儘量不要使用。
- X19~X28:臨時暫存器,使用時必須儲存。
- X29:幀指標暫存器,用於連線棧幀,使用時需要儲存。
- X30:連結暫存器LR
- X31:堆疊指標暫存器SP或零暫存器ZXR
注意:
子程式呼叫時必須要儲存的暫存器:X19~X29和SP(X31)。
不需要儲存的暫存器:X0~X7,X9~X15
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
4、ARM64位指令集A64以及參考手冊
5、ARM64位程式除錯
方法一: 直接列印資料
ARM64位下列印資料的方法:
(1). 列印V暫存器:
mov w0, v0.s[0]
mov w1, v0.s[1]
mov w2, v0.s[2]
mov w3, v0.s[3]
bl _print
(2). 列印V暫存器的低64位:
mov w0, v2.s[0]
mov w1, v2.s[1]
bl _print
(3). 列印w暫存器
mov w0, w12
mov w1, w3
bl _print
其中print函式的定義如下:
void print(int a, int b, int c, int d)
{
printf("%08x %08x %08x %08x\n",a,b,c,d);
}
(4).將V暫存器列印到記憶體的方法
.macro printf_m in1=x0, in2=x1
st1 {\in2\().2D}, [\in1\()]
mov x0, \in1
bl cprintf
.endm
cprintf定義如下:
void cprint(unsigned char *src8)
{
signed char* srcs8 = (signed char*)src8;
short* srcs16 = (short*)src8;
unsigned short* srcu16 = (unsigned short*)src8;
int* srcs32 = (int*)src8;
printf("u8:\n");
for(int i=0; i < 16; i++)
{
printf("%d", src8[i]);
}
printf("s8:\n");
for(int i=0; i < 16; i++)
{
printf("%d", srcs8[i]);
}
printf("u16:\n");
for(int i=0; i < 8; i++)
{
printf("%d", srcu16[i]);
}
printf("s16:\n");
for(int i=0; i < 8; i++)
{
printf("%d", srcs16[i]);
}
printf("s32:\n");
for(int i=0; i < 4; i++)
{
printf("%d", srcs32[i]);
}
}
方法二: GDB除錯
詳細除錯方法可以參考:GDB除錯方法
對於neon暫存器入棧:
.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
Registers v8-v15 must be preserved by a callee across subroutine calls; the remaining registers (v0-v7, v16-v31) do not need to be preserved (or should be preserved by the caller). Additionally, only the bottom 64-bits of each value stored in v8-v15 need to be preserved; it is the responsibility of the caller to preserve larger values.
在採用gdb除錯程式時遇到以下兩個問題:
問題一:在對v暫存器(v8~v15)入棧後,採用gdb除錯會出現下面的問題:
/build/gdb-qLNsm9/gdb-7.11.1/gdb/aarch64-tdep.c:334: internal-error: aarch64_analyze_prologue: Assertion
inst.operands[0].type == AACH64_OPAND_Rt
failed.
解決方案:
通過分析可知,在對neon暫存器(v8~v15)進行入棧後採用gdb除錯會出現報錯,無法實現在存在對neon暫存器入棧的彙編程式碼進行gdb除錯。這是當前gdb版本7.11.1存在的堆疊問題,是屬於gdb本身存在的bug,可以通過升級gdb版本實現除錯。
另外可以採用st1,ld1對SP存取資料的方式進行臨時替換,當然該方案僅用於除錯,通過測試可知,採用該方式替代stp,ldp入棧出棧後子程式可以得到正確的結果,並且不會影響呼叫者中的值。對於這點尚存在疑問?
關於採用st1,ld1方式入棧出棧的說明:
單獨採用st1,ld1方式進行入棧出棧,測試可知不會影響呼叫者中的值。
採用st1,ld1方式進行入棧出棧,中間存在大量彙編程式碼,進行測試可知:可能會影響呼叫者的值。列印輸出時間資訊為0,不能正常顯示呼叫者的值,但是子程式可以得到正確的值。
.macro push_v_regs_d
sub sp, sp, #128
st1 {v8.8H, v9.8H}, [sp], #32
st1 {v10.8H, v11.8H}, [sp], #32
st1 {v12.8H, v13.8H}, [sp], #32
st1 {v14.8H, v15.8H}, [sp]
.endm
.macro pop_v_regs_d
ld1 {v14.8H, v15.8H}, [sp]
sub sp, sp, #32
ld1 {v12.8H, v13.8H}, [sp]
sub sp, sp, #32
ld1 {v10.8H, v11.8H}, [sp]
sub sp, sp, #32
ld1 {v8.8H, v9.8H}, [sp]
add sp, sp, #128
.endm
關於SP入棧、出棧更多可參考:
問題二:程式出現 segmention fault後,採用gdb除錯
可能原因分析:
1、段錯誤一般是由於堆疊被破環,在存取資料時引起SIGSEGV crash,通常是由於記憶體讀寫越界導致。關於SIGSEGV的解釋可以詳見SIGSEGV與SIGBUS的區別分析。
2、堆疊可能是正確的,但是在存取資料時訪問的地址不對(即指標所對應的地址是無效地址,沒有實體記憶體對應該地址),造成訪問越界引起crash,比如含有指標地址的函式宣告與函式實現不一致會引起段錯誤。(2018.9.25 除錯svac2dec庫總結經驗)
6、IOS64和ARM64的引數傳遞差異和編譯差異
(1)ARM64引數入棧都要保證8位元組對齊,跟資料型別無關,而IOS64的引數入棧跟資料型別有關;
(2)ARM64引數傳遞是成對傳遞的,比如(x0,x1),(x2,x3)等,而IOS64的引數傳遞並不應遵守這一準則;
(3)ARM和IOS編譯的差別:
ARM在Linux下編譯gcc早期版本函式名前需要新增下劃線,目前最新版本的gcc(4.4.7)不需要新增,這與gcc編譯版本相關;
IOS平臺下編譯都需要新增下劃線:“_”。
7、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
8、ARM64位下程式註釋
在ARM32位下,單行註釋採用@或者//,多行註釋可以採用/**/;
在ARM64位下,單行註釋採用//,多行註釋採用/* */;
因此為了程式註釋的統一,建議在ARM32位和ARM64位程式中註釋都採用//的格式。
THE END!