本文首發在我的個人部落格: blog.shenyuanluo.com,喜歡的朋友歡迎訂閱。
arm64 彙編準備
暫存器
通用暫存器
31
個R0 ~ R30
,每個暫存器可以存取一個 64 位大小的數。 當使用 x0 - x30
訪問時,是一個 64位的數;當使用 w0 - w30
訪問時,是一個 32 位的數,訪問的是暫存器的 低 32
位,如圖:
向量暫存器
(也可以說是 浮點型暫存器)每個暫存器的大小是 128
位的。 分別可以用Bn、Hn、Sn、Dn、Qn
的方式來訪問不同的位數;如圖:
**注:**word 是
32
位,也就是 4 Byte大小。
- Bn: 一個 Byte的大小,即
8
位 - Hn: half word,即
16
位 - Sn: single word,即
32
位 - Dn: double word,即
64
位 - Qn: quad word,即
128
位
特殊暫存器
- sp: (Stack Pointer),棧頂暫存器,用於儲存棧頂地址;
- fp(x29): (Frame Pointer)為棧基址寄存,用於儲存棧底地址;
- lr(x30): (Link Register) ,儲存呼叫跳轉指令
bl
指令的下一條指令的記憶體地址; - zr(x31): (Zero Register),
xzr/wzr
分別代表 64/32 位,其作用就是 0,寫進去代表丟棄結果,讀出來是0
; - pc: 儲存將要執行的指令的地址(有作業系統決定其值,不能改寫)。
狀態暫存器 CPSR
CPSR (Current Program Status Register)和其他暫存器不一樣,其他暫存器是用來存放資料的,都是整個暫存器具有一個含義;而 CPSR 暫存器是按位起作用的,即,每一位都有專門的含義,記錄特定的資訊;如下圖
注: CPSR 暫存器是 32 位的。
-
CPSR
的 低8位(包括I
、F
、T
和M[4:0]
)稱為控制位,程式無法修改,除非 CPU 執行於 特權模式 下,程式才能修改控制位。 -
N
、Z
、C
、V
均為條件碼標誌位;其內容可被算術或邏輯運算的結果所改變,並且可以決定某條指令是否被執行。-
N(Negative)標誌: CPSR 的第
31
位是 N,符號標誌位;記錄相關指令執行後其結果是否為負數,如果為負數,則N = 1
;如果是非負數,則N = 0
。 -
Z(Zero)標誌:
CPSR
的第30
位是 Z,0標誌位;記錄相關指令執行後,其結果是否為0,如果結果為0,則Z = 1
;如果結果不為0,則Z = 0
。 -
C(Carry)標誌: CPSR 的第
29
位是C,進位標誌位;- 加法運算:當運算結果產生了 進位 時(無符號數溢位),
C = 1
,否則C = 0
; - 減法運算(包括
CMP
): 當運算時產生了 借位 時(無符號數溢位),C = 0
,否則C = 1
。
- 加法運算:當運算結果產生了 進位 時(無符號數溢位),
-
V(Overflow)標誌: CPSR 的第
28
位是 V,溢位標誌位;在進行有符號數運算的時候,如果超過了機器所能標識的範圍,稱為溢位。
-
條件碼列表
操作碼 | 條件碼助記符 | 標誌 | 含義 |
---|---|---|---|
0000 | EQ | Z=1 | 相等 |
0001 | NE(Not Equal) | Z=0 | 不相等 |
0010 | CS/HS(Carry Set/High or Same) | C=1 | 無符號數大於或等於 |
0011 | CC/LO(Carry Clear/LOwer) | C=0 | 無符號數小於 |
0100 | MI(MInus) | N=1 | 負數 |
0101 | PL(PLus) | N=0 | 正數或零 |
0110 | VS(oVerflow set) | V=1 | 溢位 |
0111 | VC(oVerflow clear) | V=0 | 沒有溢位 |
1000 | HI(High) | C=1,Z=0 | 無符號數大於 |
1001 | LS(Lower or Same) | C=0,Z=1 | 無符號數小於或等於 |
1010 | GE(Greater or Equal) | N=V | 有符號數大於或等於 |
1011 | LT(Less Than) | N!=V | 有符號數小於 |
1100 | GT(Greater Than) | Z=0,N=V | 有符號數大於 |
1101 | LE(Less or Equal) | Z=1,N!=V | 有符號數小於或等於 |
1110 | AL | 任何 | 無條件執行(預設) |
1111 | NV | 任何 | 從不執行 |
指令讀取
在 arm64
架構中,每個指令讀取都是 64 位,即 8
位元組 空間。
arm64 約定(一般來說)
x0 ~ x7
分別會存放方法的前 8 個引數;如果引數個數超過了8個,多餘的引數會存在棧上,新方法會通過棧來讀取。- 方法的返回值一般都在 x0 上;如果方法返回值是一個較大的資料結構時,結果會存在 x8 執行的地址上。
常見彙編指令
-
mov: 將某一暫存器的值複製到另一暫存器(只能用於暫存器與暫存器或者暫存器與常量之間傳值,不能用於記憶體地址),如:
mov x1, x0 ; 將暫存器 x0 的值複製到暫存器 x1 中 複製程式碼
-
add: 將某一暫存器的值和另一暫存器的值 相加 並將結果儲存在另一暫存器中,如:
add x0, x0, #1 ; 將暫存器 x0 的值和常量 1 相加後儲存在暫存器 x0 中 add x0, x1, x2 ; 將暫存器 x1 和 x2 的值相加後儲存到暫存器 x0 中 add x0, x1, [x2] ; 將暫存器 x1 的值加上暫存器 x2 的值作為地址,再取該記憶體地址的內容放入暫存器 x0 中 複製程式碼
-
sub: 將某一暫存器的值和另一暫存器的值 相減 並將結果儲存在另一暫存器中,如:
sub x0, x1, x2 ; 將暫存器 x1 和 x2 的值相減後儲存到暫存器 x0 中 複製程式碼
-
mul: 將某一暫存器的值和另一個暫存器的值 相乘 並將結果儲存在另一暫存器中,如:
mul x0, x1, x2 ; 將暫存器 x1 和 x2 的值相乘後結果儲存到暫存器 x0 中 複製程式碼
-
sdiv:(有符號數,對應 udiv: 無符號數)將某一暫存器的值和另一個暫存器的值 相除 並將結果儲存在另一暫存器中,如:
sdiv x0, x1, x2 ; 將暫存器 x1 和 x2 的值相除後結果儲存到暫存器 x0 中 複製程式碼
-
and: 將某一暫存器的值和另一暫存器的值 按位與 並將結果儲存到另一暫存器中,如:
and x0, x0, #0xf ; 將暫存器 x0 的值和常量 0xf 按位與後儲存到暫存器 x0 中 複製程式碼
-
orr: 將某一暫存器的值和另一暫存器的值 按位或 並將結果儲存到另一暫存器中,如:
orr x0, x0, #9 ; 將暫存器 x0 的值和常量 9 按位或後儲存到暫存器 x0 中 複製程式碼
-
eor: 將某一暫存器的值和另一暫存器的值 按位異或 並將結果儲存到另一暫存器中,如:
eor x0, x0, #0xf ; 將暫存器 x0 的值和常量 0xf 按位異或後儲存到暫存器 x0 中 複製程式碼
-
str: (store register) 將暫存器中的值寫入到記憶體中,如:
str w9, [sp, #0x8] ; 將暫存器 w9 中的值儲存到棧記憶體 [sp + 0x8] 處 複製程式碼
-
strb: (store register byte) 將暫存器中的值寫入到記憶體中(只儲存一個位元組),如:
strb w8, [sp, #7] ; 將暫存器 w8 中的低 1 位元組的值儲存到棧記憶體 [sp + 7] 處 複製程式碼
-
ldr: (load register) 將記憶體中的值讀取到暫存器中,如:
ldr x0, [x1] ; 將暫存器 x1 的值作為地址,取該記憶體地址的值放入暫存器 x0 中 ldr w8, [sp, #0x8] ; 將棧記憶體 [sp + 0x8] 處的值讀取到 w8 暫存器中 ldr x0, [x1, #4]! ; 將暫存器 x1 的值加上 4 作為記憶體地址, 取該記憶體地址的值放入暫存器 x0 中, 然後將暫存器 x1 的值加上 4 放入暫存器 x1 中 ldr x0, [x1], #4 ; 將暫存器 x1 的值作為記憶體地址,取內該存地址的值放入暫存器 x0 中, 再將暫存器 x1 的值加上 4 放入暫存器 x1 中 ldr x0, [x1, x2] ; 將暫存器 x1 和暫存器 x2 的值相加作為地址,取該記憶體地址的值放入暫存器 x0 中 複製程式碼
-
ldrsb: (load register byte) 將記憶體中的值(只讀取一個位元組)讀取到暫存器中,如:
ldrsb w8, [sp, #7] ; 將棧記憶體 [sp + 7] 出的 低 1 位元組的值讀取到暫存器 w8 中 複製程式碼
-
stur: 同
str
將暫存器中的值寫入到記憶體中(一般用於負
地址運算中),如:stur w10, [x29, #-0x4] ; 將暫存器 w10 中的值儲存到棧記憶體 [x29 - 0x04] 處 複製程式碼
-
ldur: 同
ldr
將記憶體中的值讀取到暫存器中(一般用於負
地址運算中),如:ldur w8, [x29, #-0x4] ; 將棧記憶體 [x29 - 0x04] 處的值讀取到 w8 暫存器中 複製程式碼
-
stp: 入棧指令(
str
的變種指令,可以同時操作兩個暫存器),如:stp x29, x30, [sp, #0x10] ; 將 x29, x30 的值存入 sp 偏移 16 個位元組的位置 複製程式碼
-
ldp: 出棧指令(
ldr
的變種指令,可以同時操作兩個暫存器),如:ldp x29, x30, [sp, #0x10] ; 將 sp 偏移 16 個位元組的值取出來,存入暫存器 x29 和暫存器 x30 複製程式碼
-
scvtf: (Signed Convert To Float)帶符號 定點數 轉換為 浮點數,如:
scvtf d1, w0 ; 將暫存器 w0 的值(頂點數,轉化成 浮點數) 儲存到 向量暫存器/浮點暫存器 d1 中 複製程式碼
-
fcvtzs: (Float Convert To Zero Signed)浮點數 轉化為 定點數 (舍入為0),如:
fcvtzs w0, s0 ; 將向量暫存器 s0 的值(浮點數,轉換成 定點數)儲存到暫存器 w0 中 複製程式碼
-
cbz: 和 0 比較(Compare),如果結果為零(Zero)就轉移(只能跳到後面的指令);
-
cbnz: 和非 0 比較(Compare),如果結果非零(Non Zero)就轉移(只能跳到後面的指令);
-
cmp: 比較指令,相當於
subs
,影響程式狀態暫存器 CPSR ; -
cset: 比較指令,滿足條件,則並置
1
,否則置0
,如:cmp w8, #2 ; 將暫存器 w8 的值和常量 2 進行比較 cset w8, gt ; 如果是大於(grater than),則將暫存器 w8 的值設定為 1,否則設定為 0 複製程式碼
-
brk: 可以理解為跳轉指令特殊的一種
-
LSL: 邏輯左移
-
LSR: 邏輯右移
-
ASR: 算術右移
-
ROR: 迴圈右移
-
adrp: 用來定位資料段中的資料用, 因為 aslr 會導致程式碼及資料的地址隨機化, 用 adrp 來根據 pc 做輔助定位
-
b: (branch)跳轉到某地址(無返回), 不會改變 lr (x30) 暫存器的值;一般是本方法內的跳轉,如
while
迴圈,if else
等 ,如:b LBB0_1 ; 直接跳轉到標籤 ‘LLB0_1’ 處開始執行 複製程式碼
-
bl: 跳轉到某地址(有返回),先將下一指令地址(即函式返回地址)儲存到暫存器 lr (x30)中,再進行跳轉 ;一般用於不同方法直接的呼叫 ,如:
bl 0x100cfa754 ; 先將下一指令地址(‘0x100cfa754’ 函式呼叫後的返回地址)儲存到暫存器 ‘lr’ 中,然後再呼叫 ‘0x100cfa754’ 函式 複製程式碼
-
blr: 跳轉到
某暫存器
(的值)指向的地址(有返回),先將下一指令地址(即函式返回地址)儲存到暫存器 lr (x30)中,再進行跳轉 ;如:blr x20 ; 先將下一指令地址(‘x20’指向的函式呼叫後的返回地址)儲存到暫存器 ‘lr’ 中,然後再呼叫 ‘x20’ 指向的函式 複製程式碼
-
br: 跳轉到某暫存器(的值)指向的地址(無返回), 不會改變 lr (x30) 暫存器的值。
-
brk: 可以理解為跳轉指令特殊的一種。
-
ret: 子程式(函式呼叫)返回指令,返回地址已預設儲存在暫存器 lr (x30) 中
函式呼叫
每個函式呼叫,都會有 入棧 和 出棧 操作。
例子: PushAndPop.c
原始碼
#include <stdio.h>
void TestPushAndPop()
{
printf("Push an Pop !");
}
複製程式碼
彙編程式碼
-
通過 Xcode "
Product——>Perform Action——>Assemble PushAndPop.c
" 檢視其對應的彙編程式碼: -
也可以通過
clang
編譯成彙編程式碼:// 注意,以下程式碼將預設生成pc版的彙編指令 clang -S PushAndPop.c // arm64彙編需要如下命令,指定架構和系統標頭檔案所在的目錄,請務必將isysroot的sdk版本修改為對應的 xcode 中存在的版本! clang -S -arch arm64 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS11.1.sdk PushAndPop.c 複製程式碼
去除一大堆 不相干東西 得到對應彙編程式碼,如下:
sub sp, sp, #32 ; 更新棧頂暫存器的值,(可以看出:申請 32 位元組佔空間作為新用)
stp x29, x30, [sp, #16] ; 儲存呼叫該函式前的棧頂暫存器的值和該函式結束返回後下一將執行指令地址值
add x29, sp, #16 ; 更新棧底暫存器的值,(可以看出:還剩餘 16 位元組空間給該函式用)
adrp x0, l_.str@PAGE ; 獲取 ‘l_.str’ 標籤所在的頁的地址
add x0, x0, l_.str@PAGEOFF ; 獲取 ‘l_.str’ 標籤對應頁地址的偏移
bl _printf ; 呼叫 ‘printf’ 函式進行列印
stur w0, [x29, #-4] ; 將 w0 暫存器的值('bl' 函式呼叫的返回值)儲存到 [x29 - 4] 的記憶體地址中
ldp x29, x30, [sp, #16] ; 恢復呼叫該函式之前棧底暫存器的值
add sp, sp, #32 ; 恢復呼叫該函式之前棧頂暫存器的值
ret ; 返回
複製程式碼
對與上面的彙編程式碼,分配了
32
自己空間,其中16
位元組是用作 入棧操作,剩下的16
位元組是用於儲存臨時變數的。疑問: 例子函式命名是沒有臨時變數,為什麼還會需要申請佔空間?
解釋: 雖然該函式沒有臨時變數,但是呼叫
printf
函式後,編譯器自動會加上 該函式返回值 的處理,由於 arm64 規定了整數型返回值放在x0
暫存器裡,因此會隱藏有一個區域性變數int return_value;
的宣告在,該臨時變數佔用4
位元組空間;又因為 arm64 下對於使用sp
作為地址基址定址的時候,必須要16byte-alignment
(對齊),所以申請了16
位元組空間作為臨時變數使用。具體參見 這裡。
-
其 入棧操作 彙編程式碼流程解析如下:
-
其 出棧操作 彙編程式碼流程解析如下:
注意: 對棧的
分配/釋放
操作只會對棧指標做加減法, 而不會對棧記憶體中的內容做任何修改(也不會把釋放的棧空間設定為 0)。