這里主要介紹 iOS 平臺上 ARM64 架構(gòu)的寄存器和指令以及內(nèi)存堆棧。
堆棧
在 iOS 中,棧空間是向下生長的,也就是從高地址向低地址生長,棧頂在低地址,棧底在高地址,比如:
棧頂?shù)刂罚?x00
棧底地址:0x1C
寄存器
ARM64 主要有34個(gè)寄存器,其中包括 31 個(gè)通用寄存器(x0-x30)以及 SP,PC,CPSR,可以通過在調(diào)試時(shí)輸入 register read 命令查看
寄存器是64位的,可以使用W0-W30來訪問前32位空間
x0-x7
主要用來存放函數(shù)傳遞的參數(shù),以及臨時(shí)變量,超過8個(gè)參數(shù)的存放在棧上,其中 x0 也用來存放函數(shù)的返回值
FP(x29)
通常存放棧幀地址(棧底指針),指向當(dāng)前函數(shù)棧的底部
LR(x30)
通常稱 x30 為程序鏈接寄存器,因?yàn)檫@個(gè)寄存器會記錄著當(dāng)前方法的調(diào)用方地址,也就是當(dāng)前方法返回后程序繼續(xù)執(zhí)行的下一條指令地址
SP
棧頂指針,指向當(dāng)前函數(shù)棧的頂部
PC
程序計(jì)數(shù)器,指向即將要執(zhí)行的下一條指令地址,軟件無法修改
CPSR
狀態(tài)寄存器,有些指令會修改 CPSR 的值,比如 CPM,有些指令會根據(jù) CPSR 的值來做跳轉(zhuǎn)操作。
CPSR 的每一位值都有特殊的意義,其中最常見的是 NZCV 標(biāo)志位
NZCV是狀態(tài)寄存器的條件標(biāo)志位,分別代表運(yùn)算過程中產(chǎn)生的狀態(tài),其中:
N, negative condition flag,一般代表運(yùn)算結(jié)果是負(fù)數(shù)
Z, zero condition flag, 指令結(jié)果為0時(shí)Z=1,否則Z=0;
C, carry condition flag, 無符號運(yùn)算有溢出時(shí),C=1。
V, oVerflow condition flag 有符號運(yùn)算有溢出時(shí),V=1。
常用指令
常用指令可大致分為運(yùn)算指令,尋址指令,跳轉(zhuǎn)指令等
運(yùn)算指令
mov x1,x0 ;傳送:x1 = x0
add x0,x1,x2 ;加法:x0 = x1 + x2
sub x0,x1,x2 ;減法:x0 = x1 - x2
mul x0,x1,x2. ;乘法:x0 = x1 * x2
and x0,x0,#0xF ;位與:x0 = x0 & 0xF
orr x0,x0,#9 ;位或:x0 = x0 | 9
eor x0,x0,#0xF ;異或:x0 = x0 ^ 0xF;
lsl x0,#1 ;邏輯左移:x0 = x0 << 1
lsr x0,#1 ;邏輯右移:x0 = x0 >> 1,左邊統(tǒng)一補(bǔ)0
asr x0,#1 ;算術(shù)右移:x0 = x0 >> 1,左邊補(bǔ)符號位
尋址指令
分為兩種,存和取
L 打頭的基本都是取值指令,如 LDR(Load Register)、LDP(Load Pair)
S 打頭的基本都是存值指令,如 STR(Store Register)、STP(Store Pair)
ldr x0,[x1] ;從 x1 指向的地址里面取出一個(gè)64位大小的數(shù)存入x0
ldp x1,x2,[x10, #0x10] ;從 x10+0x10 指向的地址里面取出2個(gè)64位的數(shù),分別存入x1、x2
str x5,[sp, #24] ;往內(nèi)存中寫數(shù)據(jù)(偏移值為正), 把 x5 的值(64位的數(shù)值)存到 sp+24 指向的地址內(nèi)存上
stur w0,[x29, #0x8] ;往內(nèi)存中寫數(shù)據(jù)(偏移值為負(fù)),將 w0 的值存儲到 x29 - 0x8 這個(gè)地址里
stp x29,x30,[sp, #-16]! ;把 x29、x30 的值存到 sp-16 的地址上,并且把sp-=16 Note:后面有個(gè)感嘆號的,然后沒有stup這個(gè)指令哈
ldp x29,x30,[sp],#16 ;從 sp 地址取出16 byte數(shù)據(jù),分別存入x29、x30,然后 sp+=16
跳轉(zhuǎn)指令
bl/b bl 是有返回的跳轉(zhuǎn);b 是無返回的跳轉(zhuǎn),BL的L也可以理解為Lr
跳轉(zhuǎn)指令可以配合條件狀態(tài)(根據(jù) CPSR 相應(yīng)位置的值)
B ;跳轉(zhuǎn)指令,可帶條件跳轉(zhuǎn)與cmp配合使用
BL ;帶返回的跳轉(zhuǎn)指令, 返回地址保存到LR(X30)
BLR ; 帶返回的跳轉(zhuǎn)指令,跳轉(zhuǎn)到指令后邊跟隨寄存器中保存的地址(例:blr x8 ;跳轉(zhuǎn)到x8保存的地址中去執(zhí)行)
RET ;子程序返回指令,返回地址默認(rèn)保存在LR(X30)
比較指令
cmp x1,x0 ;將x1和x0(可以是立即數(shù))的值相減,并根據(jù)結(jié)果設(shè)置 CPSR 標(biāo)志位
CBZ ;比較(Compare),如果結(jié)果為零(Zero)就轉(zhuǎn)移(只能跳到后面的指令)
CBNZ ;比較,如果結(jié)果非零(Non Zero)就轉(zhuǎn)移(只能跳到后面的指令)
入門Demo
以一個(gè)簡單的 C 程序?yàn)槔?,如下代碼:
int sum(int a, int b)
{
int c = a + b + 1;
return c;
}
int main()
{
int a = 10;
int b = 20;
int rs = sum(a,b);
if (rs > 30) {
return 0;
}
else {
return 1;
}
}
有2個(gè)函數(shù),main 函數(shù)和 sum 函數(shù),通過 clang 命令成匯編代碼
clang -arch arm64 -S testAsm.c
最后生成的匯編代碼如下:
.section __TEXT,__text,regular,pure_instructions
.build_version macos, 10, 15 sdk_version 10, 15, 4
.globl _sum ; -- Begin function sum
.p2align 2
_sum: ; @sum
.cfi_startproc
; %bb.0:
sub sp, sp, #16 ; =16
.cfi_def_cfa_offset 16
str w0, [sp, #12]
str w1, [sp, #8]
ldr w8, [sp, #12]
ldr w9, [sp, #8]
add w8, w8, w9
add w8, w8, #1 ; =1
str w8, [sp, #4]
ldr w0, [sp, #4]
add sp, sp, #16 ; =16
ret
.cfi_endproc
; -- End function
.globl _main ; -- Begin function main
.p2align 2
_main: ; @main
.cfi_startproc
; %bb.0:
sub sp, sp, #32 ; =32
stp x29, x30, [sp, #16] ; 16-byte Folded Spill
add x29, sp, #16 ; =16
.cfi_def_cfa w29, 16
.cfi_offset w30, -8
.cfi_offset w29, -16
stur wzr, [x29, #-4]
mov w8, #10
str w8, [sp, #8]
mov w8, #20
str w8, [sp, #4]
ldr w0, [sp, #8]
ldr w1, [sp, #4]
bl _sum
str w0, [sp]
ldr w8, [sp]
cmp w8, #30 ; =30
b.le LBB1_2
; %bb.1:
stur wzr, [x29, #-4]
b LBB1_3
LBB1_2:
mov w8, #1
stur w8, [x29, #-4]
LBB1_3:
ldur w0, [x29, #-4]
ldp x29, x30, [sp, #16] ; 16-byte Folded Reload
add sp, sp, #32 ; =32
ret
.cfi_endproc
; -- End function
.subsections_via_symbols
先看 sum 函數(shù)的代碼(簡單起見,這里去除一些不(看)重(不)要(懂)的代碼)
_sum: ; sum 函數(shù)入口
sub sp, sp, #16 ; sp = sp-16,棧頂指針向下移16字節(jié),相當(dāng)于分配了16個(gè)字節(jié)的??臻g(iOS中,棧是向下(低地址)生長的)
str w0, [sp, #12] ; 將 w0(x0的低32位) 存放到 s p12的位置(參數(shù)a)
str w1, [sp, #8] ; 將 w1(x1的低32位) 存放到 sp+8 的位置(參數(shù)b)
ldr w8, [sp, #12] ; 將 sp+12 位置的值讀到 w8 上,也就是 w8=a
ldr w9, [sp, #8] ; 將 sp+8 位置的值讀到 w9 上,也就是 w9=b
add w8, w8, w9 ; 兩數(shù)相加 w8 = w8 + w9
add w8, w8, #1 ; w8 = w8 + 1
str w8, [sp, #4] ; 將 w8 保存到 sp+4 的位置
ldr w0, [sp, #4] ; 將 sp+4 的位置讀到 w0 上(前面說過,x0也用來存放函數(shù)返回值)
add sp, sp, #16 ; 將棧頂指針向上移16字節(jié)(恢復(fù)棧指針,相當(dāng)于釋放堆棧)
ret ; 函數(shù)返回
總體上邏輯比較簡單,基本上沒什么難懂的地方
這里有個(gè)不明白的地方就是
str w8, [sp, #4] ldr w0, [sp, #4]為什么不直接
mov w0, w8有知道的大佬還請指教一下,為了寄存器內(nèi)容可以恢復(fù)么?
再來看 main 函數(shù)的,同樣去除一些不重要的代碼
_main: ; main函數(shù)入口
sub sp, sp, #32 ; sp = sp - 32,將棧頂指針向下移動 32 字節(jié)
stp x29, x30, [sp, #16] ; 將 x29,x30 存儲到 sp+16 的位置(x29,x30 參考前面介紹,這里相當(dāng)于做個(gè)備份,因?yàn)楹竺嬉薷倪@2個(gè)寄存器)
add x29, sp, #16 ; x29 = sp + 16(相當(dāng)于移動棧底指針,堆棧大小 16 字節(jié))
stur wzr, [x29, #-4] ; 將 wzr(也叫零寄存器,這是一個(gè)特殊的寄存器,值為0)存儲到 x29 - 4 的位置,相當(dāng)于把這快內(nèi)存清0
mov w8, #10 ; 將 w8 置為 10
str w8, [sp, #8] ; 將 w8 存儲到 sp+8 的位置
mov w8, #20 ; 將 w8 置為 20
str w8, [sp, #4] ; 將 w8 存儲到 sp+4 的位置
ldr w0, [sp, #8] ; 將 sp+8 讀到 w0(w0 = 10)
ldr w1, [sp, #4] ; 將 sp+4 讀到 w1 (w1 = 20,前面說過x0-x7是存放參數(shù)的)
bl _sum ; 跳轉(zhuǎn)到 sum 函數(shù)
str w0, [sp] ; 存儲函數(shù)返回值(w0)到 sp
ldr w8, [sp] ; 讀取 sp 的值到 w8
cmp w8, #30 ; 將 w8 的內(nèi)容和30比較,同時(shí)設(shè)置 CPSR 寄存器
b.le LBB1_2 ; 根據(jù) CPSR 結(jié)果來決定是跳轉(zhuǎn) LBB1_2(其中條件le的意思是小于等于的意思,具體可以參考我下面的鏈接)
stur wzr, [x29, #-4] ; 將 x29-4 位置內(nèi)存清0
b LBB1_3 ; 跳轉(zhuǎn)到 LBB1_3
LBB1_2:
mov w8, #1 ; 將 w8 置 1
stur w8, [x29, #-4] ; 將 w8 保存到 x29-4 的位置
LBB1_3:
ldur w0, [x29, #-4] ; 讀取 x29-4 位置的值到 w0 作為函數(shù)返回值
ldp x29, x30, [sp, #16] ; 將 sp+16 位置的值讀取到 x29,x30(恢復(fù)x29,x30)
add sp, sp, #32 ; sp = sp + 32,恢復(fù)棧頂指針,釋放堆棧
ret ; 函數(shù)返回
main 函數(shù)和 sum 函數(shù)的分析基本就到這里,基本邏輯還是比較簡單的,這里有個(gè)最大的區(qū)別就是 main 函數(shù)有 x29,x30 的備份&恢復(fù)操作,原因是 sum 函數(shù)是葉子函數(shù),而 main 函數(shù)是非葉子函數(shù),非葉子函數(shù)有對其他函數(shù)的調(diào)用,需要開辟新的堆??臻g,以及其他函數(shù)返回后需要繼續(xù)執(zhí)行下一條指令,這需要修改 x29,x30的值,所以要先備份起來,而葉子函數(shù)就不需要。