iOS彙編語音有不少鍾。常見的有8086彙編、arm彙編、x86彙編等等。xcode
iOS的架構從最初的armv6發展到後來的armv7和armv7s,最後發展到如今的arm64,不論是armv6仍是後來的armv7,以及arm64都是arm處理器的指令集。armv7和armv7s是真機32位處理器使用的架構,而arm64是真機64位處理器使用的架構。bash
iPhone 5C是最後一款arm32位版本的iPhone,在iPhone5s以後,全部的iPhone設備都採用arm64架構。arm64彙編在真機上使用,以下:markdown
TestFont`-[ViewController test]: 0x10286e574 <+0>: sub sp, sp, #0x20 ; =0x20 0x10286e578 <+4>: mov w8, #0x14 0x10286e57c <+8>: mov w9, #0xa 0x10286e580 <+12>: str x0, [sp, #0x18] 0x10286e584 <+16>: str x1, [sp, #0x10] -> 0x10286e588 <+20>: str w9, [sp, #0xc] 0x10286e58c <+24>: str w8, [sp, #0x8] 0x10286e590 <+28>: add sp, sp, #0x20 ; =0x20 0x10286e594 <+32>: ret 複製代碼
x86彙編是模擬器使用的彙編語言,它的指令和arm64彙編的語法不一樣,以下數據結構
TestFont`-[ViewController test]: 0x10b089520 <+0>: pushq %rbp 0x10b089521 <+1>: movq %rsp, %rbp 0x10b089524 <+4>: movq %rdi, -0x8(%rbp) 0x10b089528 <+8>: movq %rsi, -0x10(%rbp) -> 0x10b08952c <+12>: movl $0xa, -0x14(%rbp) 0x10b089533 <+19>: movl $0x14, -0x18(%rbp) 0x10b08953a <+26>: popq %rbp 0x10b08953b <+27>: retq 複製代碼
在日常開發中,在調試程序的時候,若是程序crash,一般會定位到具體的崩潰代碼。可是有時候也會遇到一些比較詭異的crash,好比說崩潰在了系統庫中,這個時候定位到具體的crash緣由會很是困難。若是利用匯編調試技巧來進行調試,可能會讓咱們事半功倍。架構
在逆向別人App過程當中,咱們能夠經過LLDB對內存地址進行斷點操做,可是當執行到斷點時,LLDB展示給咱們的是彙編代碼,而不是OC代碼,因此想要逆向而且動態調試別人的App,就須要學習彙編的知識。iphone
想要學習arm64彙編,須要從如下三個方面入手,寄存器、指令和堆棧。函數
arm64中有34個寄存器,以下學習
也會有人將x0 ~ x30叫作通用寄存器,可是在實際使用中x29和x30並無對應的低32位的寄存器w2九、w30,並且x29和x30寄存器有着特殊的用途,因此在此我只講x0 ~ x28記爲通用寄存器測試
pc (Program Counter)寄存器,它記錄着當前CPU正在執行的指令的地址,經過register read pc查看寄存器中存儲的值spa
(lldb) register read pc pc = 0x000000010286e588 TestFont`-[ViewController test] + 20 at ViewController.m:28 (lldb) 複製代碼
lr (Link Register)寄存器,也就是以前所說的x30寄存器,它存儲着函數的返回地址
arm體系中包含一個當前程序狀態寄存器cpsr (Current Program Status Register)和五個備份的程序狀態寄存器spsr (Saved Program Status Registe),備份的程序狀態寄存器用來進行異常處理。
ARM指令以下:
助記符 | ARM指令及功能描述 |
---|---|
ADC | 帶進位加法指令 |
ADD | 加法指令 |
AND | 邏輯與指令 |
B | 跳轉指令 |
BIC | 位清除指令 |
BL | 帶返回的跳轉指令 |
BLX | 帶返回和狀態切換的跳轉指令 |
BX | 帶狀態切換的跳轉指令 |
CDP | 協處理器數據操做指令 |
CMN | 比較反值指令 |
CMP | 比較指令 |
EOR | 異或指令 |
LDC | 存儲器帶協處理器的數據傳輸指令 |
LDM | 加載多個寄存器指令 |
LDR | 存儲器到寄存器的數據傳輸指令 |
MCR | 從ARM寄存器到協處理器寄存器的數據傳輸指令 |
MLA | 乘加運算指令 |
MOV | 數據傳送指令 |
MRC | 從協處理器寄存器到ARM寄存器的數據傳輸指令 |
MRS | 傳送CPSR或SPSR的內容到通用寄存器指令 |
MSR | 傳送通用寄存器到CPSR或SPSR指令 |
MUL | 32位乘法指令 |
MLA | 32位乘加指令 |
MVN | 數據反傳送指令 |
ORR | 邏輯或指令 |
RSB | 逆向減法指令 |
RSC | 帶借位的逆向減法指令 |
SBC | 帶借位減法指令 |
STC | 協處理器寄存器寫入存儲器指令 |
STM | 批量內存字寫入指令 |
STR | 寄存器到寄存器的數據傳輸指令 |
SUB | 減法指令 |
SWI | 軟件中斷指令 |
SWP | 交換指令 |
TEQ | 相等測試指令 |
TST | 位測試指令 |
mov指令能夠將另外一個寄存器、被移位的寄存器或者將一個當即數加載到目的寄存器
; 此處.text表示此代碼放在text段中 .text ; .global表示將後面跟隨的方法給暴露出去,否則外部沒法調用,方法名以_開頭 .global _test ; 此處爲_test方法 _test: ; mov指令,將當即數4加載到x0寄存器中 mov x0, #0x4 mov x1, x0 ; 彙編指令中,ret表示函數的終止 ret 複製代碼
#ifndef test_h #define test_h void test(void); #endif /* test_h */ 複製代碼
(lldb) register read x0 x0 = 0x000000010320c980 (lldb) si (lldb) register read x0 x0 = 0x0000000000000004 (lldb) register read x1 x1 = 0x00000001e60f3bc7 "viewDidLoad" (lldb) si (lldb) register read x1 x1 = 0x0000000000000004 複製代碼
經過對彙編指令增長斷點,一步一步調試能夠看出,在執行完mov指令後,x0和x1寄存器的值都被修改了
ret指令表示函數的返回,並且它還有一個很是重要的做用,就是將lr(x30)寄存器的值賦值給pc寄存器
(lldb) register read lr lr = 0x00000001021965a4 TestFont`-[ViewController viewDidLoad] + 68 at ViewController.m:23 (lldb) register read pc pc = 0x00000001021965a4 TestFont`-[ViewController viewDidLoad] + 68 at ViewController.m:23 (lldb) 複製代碼
此時,lr寄存器和pc寄存器的值都是test()函數起始地址
(lldb) register read lr lr = 0x00000001021965a8 TestFont`-[ViewController viewDidLoad] + 72 at ViewController.m:24 (lldb) register read pc pc = 0x0000000102196abc TestFont`test 複製代碼
(lldb) register read lr lr = 0x00000001021965a8 TestFont`-[ViewController viewDidLoad] + 72 at ViewController.m:24 (lldb) register read pc pc = 0x00000001021965a8 TestFont`-[ViewController viewDidLoad] + 72 at ViewController.m:24 (lldb) 複製代碼
add指令是將兩個操做數相加,並將結果存放到目標寄存器中。具體說明以下
在arm64彙編中,相應的就是操做x0~x28,執行以下彙編代碼
.text .global _test _test: mov x0, #0x4 mov x1, #0x3 add x0, x1, x0 ret 複製代碼
執行完test()函數,經過register read查詢x0的值,最後能夠看到x0存放的值爲7,以下
(lldb) register read x0 x0 = 0x0000000000000004 (lldb) si (lldb) register read x1 x1 = 0x0000000000000003 (lldb) si (lldb) register read x0 x0 = 0x0000000000000007 複製代碼
sub指令是將操做數1減去操做數2,再減去cpsr中的C條件標誌位的反碼,並將結果存放到目標寄存器中
cmp指令是把一個寄存器的內容和另外一個寄存器的內容或者當即數作比較,同時會更新CPSR寄存器中條件標誌位的值
.text .global _test _test: mov x0, #0x4 mov x1, #0x3 cmp x0, x1 ret 複製代碼
(lldb) register read cpsr cpsr = 0x60000000 (lldb) si (lldb) si (lldb) si (lldb) register read cpsr cpsr = 0x20000000 (lldb) 複製代碼
能夠發現,在執行cmp操做以後,cpsr寄存器的值變成了0x20000000,轉換成16進制後,獲得32位標誌位以下
能夠發現第31位,也就是N位的值爲0,同時第30位,也就是Z位的值也爲0,這就表示,x0和x1寄存器相比較以後的值爲非零非負,而使用x0 - x1獲得的結果是1,符合非零非負的條件。
_test: mov x0, #0x4 mov x1, #0x3 cmp x1, x0 ret 複製代碼
(lldb) register read cpsr cpsr = 0x60000000 (lldb) s (lldb) register read cpsr cpsr = 0x80000000 (lldb) 複製代碼
這個時候,cpsr寄存器的值變成了0x80000000,轉換成16進制後,以下
能夠看出,第31位N位的值變成了1,第30位Z位的值爲0,這表示,x0和x1寄存器相比較以後的值爲非零負數,使用x1-x0獲得的結果是-1,符合非零負數的條件
B指令是最簡單的跳轉指令,一旦遇到B指令,程序會無條件跳轉到B以後所指定的目標地址處執行。
BL指令是另外一個跳轉指令,可是在跳轉以前,它會先將當前標記位的下一條指令存儲在寄存器lr(x30)中,而後跳轉到標記處開始執行代碼,當遇到ret時,會將lr(x30)中存儲的地址從新加載到PC寄存器中,使得程序能返回標記位的下一條指令繼續執行。
.text .global _test label: mov x0, #0x1 mov x1, #0x8 ret _test: mov x0, #0x4 bl label mov x1, #0x3 cmp x1, x0 ret 複製代碼
當處理器工做在arm狀態時,幾乎全部的指令均根據CPSR寄存器中條件碼的狀態和指令的條件域有條件的執行,當指令的執行條件知足時,指令被執行,不然指令被忽略。
每一條ARM指令包含4位的條件碼,位於指令的最高四位[31:28]。條件碼共有16種,每種條件碼可用兩個字符表示,這兩個字符可用添加在指令助記符的後面和指令同時使用。例如:跳轉指令B後可用加上後綴EQ變爲BEQ,表示相等則跳轉,即當CPSR寄存器中的Z標誌置位時發生跳轉。
- (void)test{ int a = 1; int b = 2; if (a == b) { NSLog(@"a==b"); }else{ printf("a!=b"); } } 複製代碼
(lldb) register read cpsr cpsr = 0x80000000 複製代碼
獲得對應16進制的值爲
在16種條件標誌碼中,只有15種可使用,以下圖,第16種(1111)爲系統保留,暫時不能使用
內存操做指令分爲內存讀取和內存寫入指令
LDR(條件) 目的寄存器, <存儲器地址>
複製代碼
LDR指令用於從存儲器中將一個32位的字數據傳送到目的寄存器中。該指令一般用於從存儲器中讀取32位字數據到通用寄存器中,而後對數據進行處理。當程序計數器PC做爲目的寄存器時,指令從存儲器中讀取的字數據被當作目的地址,從而實現程序流程的跳轉。該指令在程序設計中比較經常使用,切尋址方式靈活多樣。示例以下:
LDR x0, [x1] ;將存儲器地址爲x1的字數據讀入寄存器x0 LDR x0, [x1, x2] ;將存儲器地址爲x1+x2的字數據讀入寄存器x0 LDR x0, [x1, #8] ;將存儲器地址爲x1+8的字數據讀入寄存器x0 LDR x0, [x1, x2]! ;將存儲器地址爲x1+x2的字數據讀入寄存器x0,並將新地址x1+x2寫入x1 LDR x0, [x1, #8]! ;將存儲器地址爲x1+8的字數據讀入寄存器x0,並將新地址x1+8寫入x1 LDR x0, [x1], x2 ;將存儲器地址爲x1的字數據讀入寄存器x0,並將新地址x1+x2寫入x1 LDR x0, [x1, x2, LSL#2]! ;將存儲地址爲x1+x2*4的字數據寫入寄存器x0,並將新地址x1+x2*4寫入x1 LDR x0. [x1], x2, LSL#2 ;將存儲地址爲x1的字數據寫入寄存器x0,並將新地址x1+x2*4寫入x1 複製代碼
經過一個簡單的例子來了解LDR的做用:
.text
.global _test
_test:
; ldr指令,找到x1寄存器中存儲的地址,從該地址開始讀取8個字節的數據,存放到x0寄存器中
ldr x0, [x1]
ret
複製代碼
爲何此處是讀取8個字節的數據呢?由於目標寄存器x0能夠存放8個字節的數據,若是將x0換成w0,則讀取4個字節的數據存放到w0中
- (void)viewDidLoad{ [super viewDidLoad]; int a = 5; test(); } 複製代碼
能夠發現,此時的x1寄存器存放着a變量的地址。
(lldb) x 0x000000016f2c52ec 0x16f2c52ec: 05 00 00 00 50 9a e0 31 01 00 00 00 58 0f b4 00 ....P..1....X... 0x16f2c52fc: 01 00 00 00 c7 3b 0f e6 01 00 00 00 50 9a e0 31 .....;......P..1 複製代碼
前4個字節存放的是5,也就是變量a的值
LDUR指令用法和LDR指令相同,區別在於LDUR後的當即數爲負數,以下
LDR x0, [x1, #8] LDUR x0, [x1, #-8] 複製代碼
LDP中的P是pair的簡稱,能夠看出LDP能夠同時操做兩個寄存器
; 如下命令表示,從sp寄存器的地址加上0x30後的地址開始,讀取前8個字節的數據存放到寄存器x29中,讀取後8個字節的數據放入x30寄存器中 ldp x29, x30, [sp, #0x30] 複製代碼
STP指令的格式爲:
STR{條件} 源寄存器, <存儲器地址>
複製代碼
STR指令用於從源寄存器中將一個32位的字數據傳送到存儲器中。示例以下
STR x0, [x1], #8 ;將x0中的字數據寫入以x1爲地址的存儲器中,並將新地址x1+8寫入x1 STR x0, [x1, #8] ;將x0中的字數據寫入以x1+8爲地址的存儲器中 複製代碼
STUR指令和STR指令用法相同,區別在於STUR後的當即數爲負數
STR x0, [x1, #8] STUR x0, [x1, #-8] 複製代碼
STP指令能夠同時操做兩個寄存器
; 如下指令表示,將x29+x30的字數據寫入以sp+0x8爲地址的存儲器中, stp x29, x30, [sp, #0x8] 複製代碼
零寄存器中存放的值爲0,主要做用是進行寄存器的置0操做
#OC代碼 int a = 0; ; 彙編代碼 str wzr, [sp, #0xc] 複製代碼
具體效果是將wzr寄存器中的字數據,也就是0,寫入sp+0xc爲地址的存儲器中
所謂尋址方式就是處理器根據指令中給出的地址信息來尋找物理地址的方法,目前ARM支持如下幾種常見的尋址方式
當即尋址也叫作當即數尋址,是一種特殊的尋址方式,操做數自己就在指令中給出來,只要取出指令也就取到了操做數,這個操做數被稱爲當即數,對應的尋址方式也叫作當即尋址,例如如下指令:
ADD x0, x1, #1 ; x0 ← x1+1 ADD x0, x1, #0x3f ; x0 ← x1+0x3f 複製代碼
在以上兩條指令中,第二個操做數即爲當即數,要求以「#」號爲前綴,對於以16進製表示的當即數,還要求在「#」後加上「0x」或「&」。
寄存器尋址就是利用寄存器中的數值做爲操做數,這種尋址方式是各種微處理器常常採用的一種方式,也是一種執行效率較高的尋址方式,指令以下
ADD x0, x1, x2 ; x0 ← x1+x2
複製代碼
該指令的執行效果是將寄存器x1和x2的內容相加,其結果存放在寄存器x0中
寄存器間接尋址就是以寄存器中的值做爲操做數的地址,而操做數自己存放在存儲器中,例如以下指令
ADD x0, x1, [x2] ; x0 ← x1+[x2]
LDR x0, [x1] ; x0 ← [x1]
STR x0, [x1] ; [x1] ← x0
複製代碼
基址變址尋址就是將寄存器(該寄存器通常稱做基址寄存器)的內容與指令中給出的地址偏移量相加,從而獲得一個操做數的有效地址。變址尋址方式經常使用於訪問某基地址附近的地址單元。採用變址尋址方式的指令有如下常見的幾種形式:
LDR x0, [x1, #4] ; x0 ← [x1+4] LDR x0, [x1, #4]! ; x0 ← [x1+4]、x1 ← x1+4 LDR x0, [x1], #4 ; x0 ← [x1]、x1 ← x1+4 LDR x0, [x1, x2] ; x0 ← [x1+x2] 複製代碼
採用多寄存器尋址方式,一條指令能夠完成多個寄存器值的傳送,這種尋址方式能夠用一條指令完成傳送最多16個通用寄存器的值,指令格式以下:
LDMIA x0, [x1, x2, x3, x4] ; x1 ← [x0]
; x2 ← [x0+4]
; x3 ← [x0+8]
; x4 ← [x0+12]
複製代碼
該指令的後綴IA表示在每次執行完加載/存儲操做後,x0按字長度增長,所以,指令能夠將連續存儲單元的值傳送到x1~x4
與基址變址尋址方式相相似,相對尋址以程序計數器PC的當前值爲基地址,指令中的地址標號爲偏移量,將二者相加之和獲得操做數的有效地址。如下程序段完成子程序的調用和返回,跳轉指令BL就是採用了相對尋址方式:
BL NEXT ; 跳轉到子程序NEXT處執行
......
NEXT
......
MOV PC, LR ; 從子程序返回
複製代碼
堆棧是喲中數據結構,按先進後出(FILO)的方式工做,使用一個稱做堆棧指針的專用寄存器指示當前的操做位置,堆棧指針老是指向棧頂位置。
當棧頂指針指向最後壓入堆棧的數據時,稱爲滿堆棧(Full Stack),而當堆棧指針指向下一個將要放入數據的空位置時,稱爲空堆棧(Empty Stack)
同時、根據堆棧的生成方式,又能夠分爲遞增堆棧(Ascending Stack)和遞減堆棧(Decending Stack),當堆棧由低地址向高地址生成時,稱爲遞增堆棧,當堆棧由高地址向低地址生成時,稱爲遞減堆棧。這樣就有四種類型的堆棧工做方式,ARM微處理器支持這四種類型的堆棧工做方式。即:
在瞭解堆棧操做以前,首先得了解函數的類型,函數類型主要分爲兩種:葉子函數、非葉子函數
瞭解了什麼是葉子函數和非葉子函數,那麼咱們就要從彙編代碼的層面來深刻理解葉子函數和非葉子函數的區別,以及堆棧指針在其中起到的做用。
上文介紹過葉子函數的具體定義,下面經過具體的彙編代碼來深刻了解葉子函數
void leafFuncion(){ int a = 1; int b = 2; } 複製代碼
xcrun --sdk iphoneos clang -S -arch arm64 MyTest.c -o MyTest.s
複製代碼
sub sp, sp, #16 ; sp = sp - 16 orr w8, wzr, #0x2 orr w9, wzr, #0x1 str w9, [sp, #12] str w8, [sp, #8] add sp, sp, #16 ; sp = sp + 16 ret 複製代碼
堆棧指針<font color=red>sp</font>開始指向<font color=red>0x10010</font>,偏移以後指向<font color=red>0x10000</font>,至關於開闢了從<font color=red>0x10000</font>到<font color=red>0x10010</font>這一段內存供函數使用。
複製代碼
爲何要維持堆棧平衡?由於在函數調用以前,堆棧指針sp會偏移一段內存地址,爲當前須要調用的函數分配一段內存空間,在函數調用完成以後將sp指針重置到開始位置,這樣,剛剛分配的那段內存空間就是垃圾內存,下一次再有函數調用的時候,這段內存空間可重複利用。這就作到了堆棧平衡。若是函數調用完成以後不重置sp指針,那麼,若是有足夠多的函數一直調用,最後確定會出現棧溢出的問題。
非葉子函數和葉子函數的區別在因而否有調用其它函數,下面一樣經過具體的彙編代碼來深刻了解非葉子函數
void leafFuncion(){ int a = 1; int b = 2; } void nonLeafFunction(){ int a = 3; int b = 4; leafFuncion(); } 複製代碼
xcrun --sdk iphoneos clang -S -arch arm64 MyTest.c -o MyTest.s
複製代碼
sub sp, sp, #32 ; sp=sp-32 stp x29, x30, [sp, #16] ; 8-byte Folded Spill add x29, sp, #16 ; x29=sp+16 orr w8, wzr, #0x4 orr w9, wzr, #0x3 stur w9, [x29, #-4] str w8, [sp, #8] bl _leafFuncion ldp x29, x30, [sp, #16] ; 8-byte Folded Reload add sp, sp, #32 ; sp=sp+32 ret 複製代碼
開始分析彙編代碼
sub sp, sp, #32指令是執行內存分配的操做,將sp指針向前偏移32位,獲得一片連續的內存空間
stp x29, x30, [sp, #16]指令是將x29(fp)和x30(lr)寄存器中存放的值寫入以sp+16的地址爲起始地址的一段內存空間中去,每一個寄存器佔8個字節的空間。
add x29, sp, #16指令是將sp + 16的地址存放在x29(fp)寄存器中,由此,能夠獲得從sp到x29(fp),這兩個地址之間的一段內存空間就是當前函數可使用的內存空間。
如上圖所示,橙色的那段內存就是咱們可使用的內存空間。
orr w8, wzr, #0x4和orr w9, wzr, #0x3其實就是將4賦值給寄存器w8,將3賦值給寄存器w9
stur w9, [x29, #-4]指令是將w9中存儲的值,也就是3,寫入到以x29(fp)- 4的地址爲開始地址的4個字節的內存中去。str w8, [sp, #8]指令則是將w8中存儲的值4,寫入到以sp+8爲起始地址的4個字節的內存中去,以下
bl _leafFuncion指令則表示跳轉到_leadFunction函數的操做,前面提到過,執行bl指令以前,會將bl指令的下一條彙編指令ldp x29, x30, [sp, #16]的地址存放到lr寄存器中,以便執行完_leadFunction函數以後,能跳轉回ldp x29, x30, [sp, #16]指令繼續執行。這就能夠明白爲何以前須要先存儲lr寄存器中的值,由於一旦執行完bl _leafFuncion指令以後,若是不將lr指令重置爲初始值的話,一旦執行到後面的ret函數,會從新跳到ldp x29, x30, [sp, #16]指令的地址處從新執行,如此反覆。
執行完_leadFunction函數以後,會回到lr中存儲的地址,也就是ldp x29, x30, [sp, #16]指令繼續執行。ldp x29, x30, [sp, #16]指令的做用是以sp+16的地址爲開始地址,依次讀取16個字節的數據,前8個字節的數據存放到x29(fp)寄存器中去,後8個字節的數據存放到x30(lr)寄存器中去。其實就是將x29(fp)和x30(lr)寄存器的值恢復到調用函數以前所存放的值。
最後add sp, sp, #32指令是將sp+32的地址值賦值給sp,其實就是還原sp指針的值,至此整個函數就調用完畢,給當前函數分配的內存空間就成了垃圾內存空間,能夠給以後的函數重複使用。至此,咱們就能夠明白葉子函數和非葉子函數的區別,以及堆棧指針在當前函數調用過程當中起到的做用。
在非葉子函數調用過程當中,sp指針一直指向被分配棧空間的棧頂,因此又叫作棧頂指針,而fp指針指向可用棧空間的棧底,因此又叫作棧底指針。兩個指針所指地址的中間一段內存就是函數可使用的內存空間。
函數執行開始和結束的彙編指令就是用來分配內存以及維持堆棧平衡的操做。