轉(zhuǎn)載自 #劉坤的技術(shù)博客 : https://blog.cnbluebox.com/blog/2017/07/24/arm64-start/
iOS開發(fā)同學(xué)的arm64匯編入門
在定位某些crash問題的時(shí)候覆获,有時(shí)候遇到一些問題很詭異。有時(shí)候掛在了系統(tǒng)庫里面。這個(gè)時(shí)候定位crash問題往往是比較頭疼的褥符。那么這個(gè)時(shí)候?qū)W會(huì)一些匯編知識(shí)腹暖,利用匯編調(diào)試技巧進(jìn)行調(diào)試可能會(huì)起到意想不到的效果藐握。
學(xué)習(xí)匯編語言不只是幫助定位crash而已穿香,學(xué)習(xí)匯編可以幫助你真正的理解計(jì)算機(jī)野崇。畢竟CPU上跑的就是對(duì)應(yīng)的指令集袜蚕。
0x1 工具
我們面對(duì)的要么是源代碼糟把,要么是二進(jìn)制。因此我們需要一些反匯編的工具來輔助我們進(jìn)行匯編代碼查看牲剃。推薦工具有: – Hopper Disassembler 收費(fèi)應(yīng)用遣疯,看匯編代碼非常方便 – MachOView 開源工具,看Mach-o文件結(jié)構(gòu)非常方便凿傅。
0x2 基本概念
從高級(jí)語言過渡到匯編語言缠犀,重要的是基本概念的轉(zhuǎn)換。匯編里面要學(xué)習(xí)的三個(gè)重要概念聪舒,我認(rèn)為是 寄存器辨液、棧、指令箱残。 arm64架構(gòu)又分為2種執(zhí)行狀態(tài): AArch64 Application Level
和 AArch32 Application Level
, 本文只講AArch64.
0x21 寄存器
如果你還不知道什么是寄存器滔迈,建議先Google一下。 這里不再詳細(xì)說明被辑,寄存器是CPU中的高速存儲(chǔ)單元燎悍,要比內(nèi)存中存取要快的多。
這里說明一下arm64有哪些寄存器:
- R0 – R30
r0 - r30
是31個(gè)通用整形寄存器盼理。每個(gè)寄存器可以存取一個(gè)64位大小的數(shù)谈山。 當(dāng)使用 x0 - x30
訪問時(shí),它就是一個(gè)64位的數(shù)宏怔。當(dāng)使用 w0 - w30
訪問時(shí)奏路,訪問的是這些寄存器的低32位,如圖:
其實(shí)通用寄存器有32個(gè)臊诊,第32個(gè)寄存器x31鸽粉,在指令編碼中,使用來做 zero register
, 即ZR
, XZR/WZR
分別代表64/32位妨猩,zero register
的作用就是0潜叛,寫進(jìn)去代表丟棄結(jié)果,拿出來是0.
其中 r29
又被叫做 fp
(frame pointer). r30
又被叫做 lr
(link register)。其用途會(huì)在下一節(jié)《椡担》中講到销斟。
- SP
SP寄存器其實(shí)就是 x31,在指令編碼中椒舵,使用 SP/WSP
來進(jìn)行對(duì)SP寄存器的訪問蚂踊。
- PC
PC寄存器中存的是當(dāng)前執(zhí)行的指令的地址。在arm64中笔宿,軟件是不能改寫PC寄存器的犁钟。
- V0 – V31
V0 - V31
是向量寄存器,也可以說是浮點(diǎn)型寄存器泼橘。它的特點(diǎn)是每個(gè)寄存器的大小是 128 位的涝动。 分別可以用Bn Hn Sn Dn Qn
的方式來訪問不同的位數(shù)。如圖:
Bn Hn Sn Dn Qn
可以這樣理解記憶, 基于一個(gè)word是32位炬灭,也就是4Byte大写姿凇:
Bn: 一個(gè)Byte的大小
Hn: half word. 就是16位
Sn: single word. 32位
Dn: double word. 64位
Qn: quad word. 128位
- SPRs
SPRs是狀態(tài)寄存器,用于存放程序運(yùn)行中一些狀態(tài)標(biāo)識(shí)重归。不同于編程語言里面的if else.在匯編中就需要根據(jù)狀態(tài)寄存器中的一些狀態(tài)來控制分支的執(zhí)行米愿。狀態(tài)寄存器又分為 The Current Program Status Register (CPSR)
和 The Saved Program Status Registers (SPSRs)
。 一般都是使用CPSR
鼻吮, 當(dāng)發(fā)生異常時(shí)育苟, CPSR
會(huì)存入SPSR
。當(dāng)異匙的荆恢復(fù)违柏,再拷貝回CPSR
。
還有一些系統(tǒng)寄存器拓哺,還有 FPSR
FPCR
是浮點(diǎn)型運(yùn)算時(shí)的狀態(tài)寄存器等勇垛。基本了解上面這些寄存器就可以了士鸥。
0x22 棧
棧就是指令執(zhí)行時(shí)存放臨時(shí)變量的內(nèi)存空間。在學(xué)習(xí)匯編代碼的執(zhí)行過程中谆级,了解棧的結(jié)構(gòu)非常重要烤礁。
先列出一些棧的特性:
- 棧是從高地址到低地址的, 棧低是高地址肥照,棧頂是低地址脚仔。
-
fp
指向當(dāng)前frame的棧底,也就是高地址舆绎。 -
sp
指向棧頂鲤脏,也就是地地址。
下面的圖簡(jiǎn)單的描述了從方法A調(diào)用方法B時(shí) 棧是如何劃分的:
其中3行匯編代碼就是方法B的前三行匯編指令。它們做的事情就是圖中描述的事情 (x29就是fp, x30就是lr):
- 將
fp, lr
保存到sp - 0x10
的地方. 也就是圖中--> fp_B
的位置猎醇。然后將sp設(shè)置為sp-0x10
- 將
fp
設(shè)置為當(dāng)前sp
窥突。也就是--> fp_B
的位置。 這一步就設(shè)置了_funcB
的 fp了 - 將
sp
設(shè)置為sp - 0x30
硫嘶。 也就是將sp
指向了圖中--> sp_B
的位置
注:
lr
是link register
中的值阻问,它存的是方法_funcA
的執(zhí)行的最后一行指令的下一行。它的作用也很好理解:當(dāng)_funcB
執(zhí)行完了之后要返回_funcA
繼續(xù)執(zhí)行沦疾,但是計(jì)算機(jī)要如何知道返回到哪執(zhí)行呢称近? 就是靠lr
記錄了返回的地址,方法才能得以正常返回哮塞。
說道這里刨秆,那么當(dāng) _funcB
執(zhí)行完畢后,是如何把椧涑恢復(fù)到_funcA
的過程的呢衡未? 我們直接分析 _funcB
的最后3條指令:
mov sp, fp; // sp 設(shè)置為fp, 就是圖中 -->fp_B 的位置
ldp fp, lr, [sp], #0x10; // 從sp指向的地址中讀取 2個(gè)64位,分別存入fp,lr邻眷。 然后將sp += 0x10
// 這一步執(zhí)行完之后眠屎,fp就執(zhí)行了圖中 -->fp_A. lr恢復(fù)成 _funcA的返回地址。 sp指向了 -->sp_A.
// 這個(gè)時(shí)候狀態(tài)已經(jīng)完全恢復(fù)到了 _funcA 的環(huán)境
ret; // 返回指令肆饶,這一步直接執(zhí)行l(wèi)r的指令改衩。
上面描述了方法如何調(diào)用的。我們知道在編程語言里面方法都有入?yún)⒀蹦鳎蟹祷刂档暮健T趨R編里面如何體現(xiàn)呢?
- 一般來說 arm64上 x0 – x7 分別會(huì)存放方法的前 8 個(gè)參數(shù)
- 如果參數(shù)個(gè)數(shù)超過了8個(gè)板惑,多余的參數(shù)會(huì)存在棧上橄镜,新方法會(huì)通過棧來讀取。
- 方法的返回值一般都在 x0 上冯乘。
- 如果方法返回值是一個(gè)較大的數(shù)據(jù)結(jié)構(gòu)時(shí)洽胶,結(jié)果會(huì)存在 x8 執(zhí)行的地址上。
0x23 指令
在上一級(jí)的內(nèi)容中我們已經(jīng)看到了一些指令裆馒。 匯編指令除了數(shù)量較多姊氓,其基本原理都是比較簡(jiǎn)單的,單拎出來一條指令就是很simple的操作喷好。 比如mov
就是一個(gè)賦值翔横。ldr
就是一個(gè)取值。
那匯編指令大概可以分為哪幾種呢梗搅?我認(rèn)為了解以下幾種基本指令就可以正常閱讀匯編代碼了禾唁。
0x231 運(yùn)算
- 算術(shù)運(yùn)算
算術(shù)運(yùn)算就是像 ADD
SUB
MUL
… 等加減乘除運(yùn)算效览,也是很好理解的指令
如:
add x0, x1, x2; // 把 x1 + x2 = x0 這樣一個(gè)操作。
sub sp, sp, 0x30; // 把 sp - 30 存入sp.
cmp x11, #4; // 相當(dāng)于 subs xzr, x11, #4\.
// 如果 x11 - 4 == 0, 那么狀態(tài)寄存器NZCV.Z = 1
// 如果 x11 - 4 < 0, 那么 NZCV.N = 1
NZCV
是狀態(tài)寄存器中存的幾個(gè)狀態(tài)值荡短,分別代表運(yùn)算過程中產(chǎn)生的狀態(tài)丐枉,其中:
- N, negative condition flag,一般代表運(yùn)算結(jié)果是負(fù)數(shù)
- Z, zero condition flag, 運(yùn)算結(jié)果為0
- C, carry condition flag, 無符號(hào)運(yùn)算有溢出時(shí)肢预,C=1矛洞。
- V, oVerflow condition flag 有符號(hào)運(yùn)算有溢出時(shí),V=1烫映。
- 邏輯運(yùn)算指令
有 LSL
(邏輯左移) LSR
(邏輯右移) ASR
(算術(shù)右移) ROR
(循環(huán)右移)沼本。
有 AND
(與) ORR
(或) EOR
(異或)
邏輯位移運(yùn)算通常也可以與算術(shù)運(yùn)算一起用,如:
add x14, x4, x27, lsl #1; // 意思是把 (x27 << 1) + x4 = x14;
- 拓展位數(shù)運(yùn)算
有 zero extend
(高位補(bǔ)0) 和 sign extend
(高位填充和符號(hào)位一致锭沟,一般有符號(hào)數(shù)用這個(gè))抽兆。 一般用來補(bǔ)齊位數(shù)。常和算術(shù)運(yùn)算配合一起.
如:
add w20, w30, w20, uxth // 取 w20的低16位族淮,無符號(hào)補(bǔ)齊到32位后再進(jìn)行 w30 + w20的運(yùn)算辫红。
- Mov
0x232 尋址
既然是和內(nèi)存相關(guān)的,那就是兩種祝辣,一種存贴妻,一種取。一般來說
L打頭的基本都是取值指令蝙斜,如 LDR LDP;
S打頭的基本都是存值指令名惩,如 STR STP;
例:
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]; // 把x5的值(64位數(shù)值)存到 sp+24 指向的內(nèi)存地址上
stp x29, x30, [sp, #-16]!; // 把 x29, x30的值存到 sp-16的地址上孕荠,并且把 sp-=16\.
ldp x29, x30, [sp], #16; // 從sp地址取出 16 byte數(shù)據(jù)娩鹉,分別存入x29, x30\. 然后 sp+=16;
其中尋址的格式由分為下面這3種類型:
[x10, #0x10] // signed offset。 意思是從 x10 + 0x10的地址取值
[sp, #-16]! // pre-index稚伍。 意思是從 sp-16地址取值弯予,取值完后在把 sp-16 writeback 回 sp
[sp], #16 // post-index。 意思是從 sp 地址取值个曙,取值完后在把 sp+16 writeback 回 sp
0x233 跳轉(zhuǎn)
跳轉(zhuǎn)氛圍有返回跳轉(zhuǎn)BL
和無返回跳轉(zhuǎn)B
锈嫩。 有返回的意思就是會(huì)存lr
,因此 BL
的L
也可以理解為LR
的意思。
1.存了
LR
也就意味著可以返回到本方法繼續(xù)執(zhí)行垦搬。一般用于不同方法直接的調(diào)用
2.B
相關(guān)的跳轉(zhuǎn)沒有LR
祠挫,一般是本方法內(nèi)的跳轉(zhuǎn),如while
循環(huán)悼沿,if else
等。
跳轉(zhuǎn)相關(guān)的指令還會(huì)有種邏輯運(yùn)算骚灸,就是condition code
糟趾。配合狀態(tài)寄存器中的狀態(tài)標(biāo)示,就是代碼分支if else
實(shí)現(xiàn)的關(guān)鍵。
condition code
有以下這些义郑,表格中還標(biāo)注除了分別是比NZCV的哪個(gè)值:
如:
cmp x2, #0; // x2 - 0 = 0蝶柿。 狀態(tài)寄存器標(biāo)識(shí)zero: PSTATE.NZCV.Z = 1
b.ne 0x1000d48f0; // ne就是個(gè)condition code, 這句的意思是,當(dāng)判斷狀態(tài)寄存器 NZCV.Z != 1才跳轉(zhuǎn)非驮,因此這句不會(huì)跳轉(zhuǎn)
0x1000d4ab0 bl testFuncA; // 跳轉(zhuǎn)方法交汤,這個(gè)時(shí)候 lr 設(shè)置為 0x1000d4ab4
0x1000d4ab4 orr x8, xzr, #0x1f00000000 // testFuncA執(zhí)行完之后跳回lr就周到了這一行
0x4 小結(jié)
本文簡(jiǎn)單介紹了一些arm64的匯編知識(shí),arm64匯編的學(xué)習(xí)對(duì)于理解iOS代碼的執(zhí)行劫笙,計(jì)算機(jī)的運(yùn)行都有著不少的好處芙扎。我們?cè)谌粘V欣脜R編知識(shí)可以定位一些疑難雜癥的crash問題√畲螅可以從匯編原理出手開一個(gè)個(gè)腦洞戒洼,玩一些黑科技。比如包瘦身允华,靜態(tài)掃描等圈浇。
匯編指令的執(zhí)行是簡(jiǎn)單確定的,不會(huì)像我們調(diào)試其他代碼一眼靴寂,有些詭異問題磷蜀,而匯編每條指令的結(jié)果都是確定的,從這一角度來定位問題往往可以定位到根本原因百炬。
在匯編指令執(zhí)行的世界褐隆,你可以對(duì)代碼執(zhí)行有更深刻的理解,原來一行代碼會(huì)被分解成這么多的指令收壕!因此妓灌,如果你在看完本文后對(duì)于學(xué)習(xí)匯編有了興趣,但是有很多細(xì)節(jié)還不太懂蜜宪,建議你自己用hopper
反編譯一些代碼虫埂,自己嘗試一行一行理解每一個(gè)指令的意義,基本看透幾個(gè)方法就可以融匯貫通了圃验。