在定位某些crash問題的時候讨永,有時候遇到一些問題很詭異侣姆。有時候掛在了系統(tǒng)庫里面。這個時候定位crash問題往往是比較頭疼的儒将。那么這個時候?qū)W會一些匯編知識师崎,利用匯編調(diào)試技巧進(jìn)行調(diào)試可能會起到意想不到的效果。
學(xué)習(xí)匯編語言不只是幫助定位crash而已椅棺,學(xué)習(xí)匯編可以幫助你真正的理解計算機(jī)犁罩。畢竟CPU上跑的就是對應(yīng)的指令集。
0x1 工具
我們面對的要么是源代碼两疚,要么是二進(jìn)制床估。因此我們需要一些反匯編的工具來輔助我們進(jìn)行匯編代碼查看。推薦工具有: –?Hopper Disassembler?收費(fèi)應(yīng)用诱渤,看匯編代碼非常方便 –?MachOView?開源工具丐巫,看Mach-o文件結(jié)構(gòu)非常方便。
0x2 基本概念
從高級語言過渡到匯編語言,重要的是基本概念的轉(zhuǎn)換递胧。匯編里面要學(xué)習(xí)的三個重要概念碑韵,我認(rèn)為是 寄存器、棧缎脾、指令祝闻。 arm64架構(gòu)又分為2種執(zhí)行狀態(tài):?AArch64 Application Level?和?AArch32 Application Level, 本文只講AArch64.
0x21 寄存器
如果你還不知道什么是寄存器,建議先Google一下遗菠。 這里不再詳細(xì)說明联喘,寄存器是CPU中的高速存儲單元,要比內(nèi)存中存取要快的多辙纬。
這里說明一下arm64有哪些寄存器:
R0 – R30
r0 - r30?是31個通用整形寄存器豁遭。每個寄存器可以存取一個64位大小的數(shù)。 當(dāng)使用?x0 - x30訪問時贺拣,它就是一個64位的數(shù)蓖谢。當(dāng)使用?w0 - w30訪問時,訪問的是這些寄存器的低32位譬涡,如圖:
其實通用寄存器有32個蜈抓,第32個寄存器x31,在指令編碼中昂儒,使用來做?zero register, 即ZR,?XZR/WZR分別代表64/32位,zero register的作用就是0委可,寫進(jìn)去代表丟棄結(jié)果渊跋,拿出來是0.
其中?r29?又被叫做?fp?(frame pointer).??r30?又被叫做?lr?(link register)。其用途會在下一節(jié)《椬徘悖》中講到拾酝。
SP
SP寄存器其實就是 x31,在指令編碼中卡者,使用?SP/WSP來進(jìn)行對SP寄存器的訪問蒿囤。
PC
PC寄存器中存的是當(dāng)前執(zhí)行的指令的地址。在arm64中崇决,軟件是不能改寫PC寄存器的材诽。
V0 – V31
V0 - V31?是向量寄存器,也可以說是浮點(diǎn)型寄存器恒傻。它的特點(diǎn)是每個寄存器的大小是 128 位的脸侥。 分別可以用Bn Hn Sn Dn Qn的方式來訪問不同的位數(shù)。如圖:
Bn Hn Sn Dn Qn可以這樣理解記憶, 基于一個word是32位盈厘,也就是4Byte大姓稣怼:
Bn: 一個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)識。不同于編程語言里面的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ā)生異常時跳仿,?CPSR會存入SPSR诡渴。當(dāng)異常恢復(fù)塔嬉,再拷貝回CPSR玩徊。
還有一些系統(tǒng)寄存器,還有?FPSR?FPCR是浮點(diǎn)型運(yùn)算時的狀態(tài)寄存器等谨究《鞲ぃ基本了解上面這些寄存器就可以了。
0x22 棧
棧就是指令執(zhí)行時存放臨時變量的內(nèi)存空間胶哲。在學(xué)習(xí)匯編代碼的執(zhí)行過程中畔塔,了解棧的結(jié)構(gòu)非常重要。
先列出一些棧的特性:
棧是從高地址到低地址的鸯屿, 棧低是高地址澈吨,棧頂是低地址。
fp指向當(dāng)前frame的棧底寄摆,也就是高地址谅辣。
sp指向棧頂,也就是地地址婶恼。
下面的圖簡單的描述了從方法A調(diào)用方法B時 棧是如何劃分的:
其中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ī)要如何知道返回到哪執(zhí)行呢蕉饼? 就是靠lr記錄了返回的地址虐杯,方法才能得以正常返回。
說道這里昧港,那么當(dāng)?_funcB執(zhí)行完畢后厦幅,是如何把棧恢復(fù)到_funcA的過程的呢慨飘? 我們直接分析?_funcB的最后3條指令:
12345movsp,fp;//? sp 設(shè)置為fp, 就是圖中 -->fp_B 的位置ldpfp,lr,[sp],#0x10;//? 從sp指向的地址中讀取 2個64位确憨,分別存入fp,lr译荞。 然后將sp += 0x10// 這一步執(zhí)行完之后,fp就執(zhí)行了圖中 -->fp_A. lr恢復(fù)成 _funcA的返回地址休弃。 sp指向了 -->sp_A. // 這個時候狀態(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 分別會存放方法的前 8 個參數(shù)
如果參數(shù)個數(shù)超過了8個糯俗,多余的參數(shù)會存在棧上,新方法會通過棧來讀取睦擂。
方法的返回值一般都在 x0 上得湘。
如果方法返回值是一個較大的數(shù)據(jù)結(jié)構(gòu)時,結(jié)果會存在 x8 執(zhí)行的地址上顿仇。
0x23 指令
在上一級的內(nèi)容中我們已經(jīng)看到了一些指令淘正。 匯編指令除了數(shù)量較多,其基本原理都是比較簡單的臼闻,單拎出來一條指令就是很simple的操作鸿吆。 比如mov就是一個賦值。ldr就是一個取值述呐。
那匯編指令大概可以分為哪幾種呢惩淳?我認(rèn)為了解以下幾種基本指令就可以正常閱讀匯編代碼了。
0x231?運(yùn)算
算術(shù)運(yùn)算
算術(shù)運(yùn)算就是像?ADD?SUB?MUL?… 等加減乘除運(yùn)算乓搬,也是很好理解的指令?
如:
12345addx0,x1,x2;// 把 x1 + x2 = x0 這樣一個操作思犁。subsp,sp,0x30;// 把 sp - 30 存入sp.cmpx11,#4;// 相當(dāng)于 subs xzr, x11, #4.? // 如果 x11 - 4 == 0, 那么狀態(tài)寄存器NZCV.Z = 1// 如果 x11 - 4 < 0, 那么 NZCV.N = 1
NZCV是狀態(tài)寄存器中存的幾個狀態(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, 無符號運(yùn)算有溢出時褐着,C=1坷澡。?
* V, oVerflow condition flag 有符號運(yùn)算有溢出時,V=1含蓉。?
邏輯運(yùn)算指令
有?LSL(邏輯左移)?LSR(邏輯右移)?ASR(算術(shù)右移)?ROR(循環(huán)右移)频敛。?
有?AND(與)??ORR(或)?EOR(異或)
邏輯位移運(yùn)算通常也可以與算術(shù)運(yùn)算一起用,如:
1addx14,x4,x27,lsl#1;// 意思是把? (x27 << 1) + x4 = x14;
拓展位數(shù)運(yùn)算
有?zero extend(高位補(bǔ)0) 和?sign extend(高位填充和符號位一致馅扣,一般有符號數(shù)用這個)斟赚。 一般用來補(bǔ)齊位數(shù)。常和算術(shù)運(yùn)算配合一起.
如:
1addw20,w30,w20,uxth// 取 w20的低16位差油,無符號補(bǔ)齊到32位后再進(jìn)行? w30 + w20的運(yùn)算拗军。
Mov
0x232 尋址
既然是和內(nèi)存相關(guān)的任洞,那就是兩種,一種存发侵,一種取交掏。一般來說?
L打頭的基本都是取值指令,如 LDR LDP;?
S打頭的基本都是存值指令刃鳄,如 STR STP;?
例:
12345ldrx0,[x1];// 從`x1`指向的地址里面取出一個 64 位大小的數(shù)存入 `x0`ldpx1,x2,[x10,#0x10];// 從 x10 + 0x10 指向的地址里面取出 2個 64位的數(shù)盅弛,分別存入x1, x2strx5,[sp,#24];// 把x5的值(64位數(shù)值)存到 sp+24 指向的內(nèi)存地址上stpx29,x30,[sp,#-16]!;// 把 x29, x30的值存到 sp-16的地址上,并且把 sp-=16. ldpx29,x30,[sp],#16;// 從sp地址取出 16 byte數(shù)據(jù)叔锐,分別存入x29, x30. 然后 sp+=16;
其中尋址的格式由分為下面這3種類型:
123[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齿梁。 有返回的意思就是會存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)的指令還會有種邏輯運(yùn)算稿辙,就是condition code。配合狀態(tài)寄存器中的狀態(tài)標(biāo)示气忠,就是代碼分支if else實現(xiàn)的關(guān)鍵邻储。
condition code有以下這些,表格中還標(biāo)注除了分別是比NZCV的哪個值:?
如:
12345cmpx2,#0;// x2 - 0 = 0旧噪。? 狀態(tài)寄存器標(biāo)識zero: PSTATE.NZCV.Z = 1b.ne0x1000d48f0;// ne就是個condition code, 這句的意思是吨娜,當(dāng)判斷狀態(tài)寄存器 NZCV.Z != 1才跳轉(zhuǎn),因此這句不會跳轉(zhuǎn)0x1000d4ab0bltestFuncA;// 跳轉(zhuǎn)方法淘钟,這個時候 lr 設(shè)置為 0x1000d4ab40x1000d4ab4orrx8,xzr,#0x1f00000000// testFuncA執(zhí)行完之后跳回lr就周到了這一行
0x4 小結(jié)
本文簡單介紹了一些arm64的匯編知識宦赠,arm64匯編的學(xué)習(xí)對于理解iOS代碼的執(zhí)行,計算機(jī)的運(yùn)行都有著不少的好處米母。我們在日常中利用匯編知識可以定位一些疑難雜癥的crash問題勾扭。可以從匯編原理出手開一個個腦洞铁瞒,玩一些黑科技妙色。比如包瘦身,靜態(tài)掃描等慧耍。
匯編指令的執(zhí)行是簡單確定的身辨,不會像我們調(diào)試其他代碼一眼丐谋,有些詭異問題,而匯編每條指令的結(jié)果都是確定的栅表,從這一角度來定位問題往往可以定位到根本原因笋鄙。
在匯編指令執(zhí)行的世界,你可以對代碼執(zhí)行有更深刻的理解怪瓶,原來一行代碼會被分解成這么多的指令萧落!因此,如果你在看完本文后對于學(xué)習(xí)匯編有了興趣洗贰,但是有很多細(xì)節(jié)還不太懂找岖,建議你自己用hopper反編譯一些代碼,自己嘗試一行一行理解每一個指令的意義敛滋,基本看透幾個方法就可以融匯貫通了许布。
https://blog.cnbluebox.com/blog/2017/07/24/arm64-start/