咱們本篇文章講的語(yǔ)法不多,因?yàn)檎Z(yǔ)法已經(jīng)有很多文章可以參考學(xué)習(xí),本篇主要講的是怎么去理解匯編。
首先了解計(jì)算機(jī)結(jié)構(gòu)
-
總的來(lái)說(shuō)計(jì)算機(jī)分為CPU、內(nèi)存、硬盤(pán)、外設(shè)。因?yàn)樵蹅兪乔岸碎_(kāi)發(fā),可以忽略外設(shè),所以結(jié)構(gòu)就如下圖。手機(jī)的運(yùn)行內(nèi)存比較常見(jiàn)的是4G、8G,但是相對(duì)動(dòng)輒就128G、256G的硬盤(pán)來(lái)說(shuō),還是很小的。為啥內(nèi)存要那么小呢,和硬盤(pán)有啥區(qū)別呢
- CPU的處理速度比硬盤(pán)的讀取速度快很多,比方說(shuō)CPU一秒可以處理1000個(gè)數(shù)據(jù),但是硬盤(pán)1秒只能讀出10個(gè)數(shù)據(jù),造成CPU的性能發(fā)揮不出來(lái)
- 這時(shí)候內(nèi)存就出來(lái)了,因?yàn)閮?nèi)存的讀取速度比硬盤(pán)快很多,所以可以先把硬盤(pán)的部分?jǐn)?shù)據(jù)加載到內(nèi)存,讓CPU直接從內(nèi)存讀數(shù)據(jù)處理,這樣性能就會(huì)好很多
- 因?yàn)閮?nèi)存的制造成本高,從手機(jī)分級(jí)就能看出來(lái),比如低配4G內(nèi)存 + 128G硬盤(pán),但是高配就可以到8G內(nèi)存 + 256G硬盤(pán),差幾百,硬盤(pán)可以升級(jí)那么多,內(nèi)存就升級(jí)一點(diǎn)。正因?yàn)閮r(jià)格高,所以?xún)?nèi)存一般不大

- CPU的介紹,CPU由3部分組成,分別是運(yùn)算器,控制器,寄存器。
- 運(yùn)算器,顧名思義,就是處理數(shù)據(jù)的,比如運(yùn)算1+1
- 控制器,就是控制著把數(shù)據(jù)從內(nèi)存加載到CPU,并且解析這個(gè)數(shù)據(jù)是干啥的,比如是做加法的話就送到運(yùn)算器進(jìn)行處理
-
寄存器,重點(diǎn)來(lái)了咱們上面說(shuō)了內(nèi)存的存在是為了縮小和CPU處理速度的,但是很不幸的是,雖然內(nèi)存的速度雖然比硬盤(pán)快,但是還是跟不上CPU的速度,所以,在CPU里也有存儲(chǔ)數(shù)據(jù)的地方,他叫寄存器,作用呢和內(nèi)存一樣就是存儲(chǔ)數(shù)據(jù)的,但是他的讀取速度比內(nèi)存更快,當(dāng)然成本也更高。讓CPU里的運(yùn)算器直接從寄存器加載數(shù)據(jù)可以最大限度的發(fā)揮CPU的作用
cpu1.png
匯編要來(lái)了
-
匯編為啥是底層語(yǔ)言
咱們知道,一臺(tái)計(jì)算機(jī)或者手機(jī)可以工作,最核心的東西就是CPU
-
咱們平時(shí)的編程語(yǔ)言直接操作CPU了嗎?答案是并沒(méi)有。比如說(shuō)用Swift、Java或者C語(yǔ)言定義了一個(gè)變量a = 10,b = 20,并且計(jì)算a+b,或者創(chuàng)建了一個(gè)對(duì)象,或者獲取一個(gè)變量的地址,咱們腦海里想的都是在內(nèi)存上開(kāi)辟了一塊空間,放了個(gè)變量a,或者在內(nèi)存上創(chuàng)建了一個(gè)dog對(duì)象,或者獲取內(nèi)存區(qū)域某個(gè)變量的地址。我把平時(shí)的高級(jí)編程語(yǔ)言抽象為“面向內(nèi)存編程”,因?yàn)樵蹅兡X海里都想的是在內(nèi)存上怎么著怎么著,如下圖
CPU、內(nèi)存、硬盤(pán).png -
咱們上面說(shuō)了,為了不讓內(nèi)存拖CPU的后腿,CPU里自帶內(nèi)存,也就是寄存器,匯編就是可以直接操作CPU里的寄存器和內(nèi)存的語(yǔ)言,所以匯編是面向底層的語(yǔ)言。ARM64 有很多個(gè)寄存器,包括X0~X28、LR、SP、PC、CPSR,咱們列舉幾個(gè),以下圖六個(gè)寄存器X1、X2、X3、SP、PC、LR為例講解匯編
寄存器內(nèi)存2.png
-
匯編開(kāi)始啦
寄存器本身就是用于存儲(chǔ)數(shù)據(jù)的,但是寄存器是在CPU內(nèi)部
-
寄存器和寄存器之間傳數(shù)據(jù)
- 如果我想把寄存器X2的賦值給X1怎么操作呢,匯編的寫(xiě)法就是:MOV X1,X2
- 如果我想把X2寄存器里的值和X3里的加起來(lái)放到X1里怎么做呢,匯編的寫(xiě)法就是 ADD X1,X2,X3
- 如果我想用X3寄存器里的值減掉X2里的值放到X1里怎么做呢,匯編的寫(xiě)法就是 SUB X1,X3,X2
-
寄存器和內(nèi)存之間傳數(shù)據(jù)
以上圖為例,比如我想把地址是FFF0的內(nèi)存單元的10取出來(lái)存放到寄存器X1怎么做呢,匯編的寫(xiě)法就是 LDR X1,[FFF0],但是平時(shí)見(jiàn)到的沒(méi)有直接在中括號(hào)里寫(xiě)地址的,一般都是先把要取得內(nèi)存的地址放到另一個(gè)寄存器,比如 MOV X2, FFF0,然后再
LDR X1,[X2]。也就是先把地址放到一個(gè)寄存器里,再根據(jù)寄存器尋址以上圖為例,比如我想把寄存器X3的值寫(xiě)入到內(nèi)存是FFF1的地址怎么寫(xiě)呢,先把地址放到另一個(gè)寄存器中 MOV X2, FFF0
然后再用存儲(chǔ)命令STR X3,[X2]。簡(jiǎn)單的總結(jié):知道為啥匯編語(yǔ)言更底層了吧,因?yàn)檫@種語(yǔ)言可以直接操作CPU,咱們普通語(yǔ)言是做不到的。語(yǔ)法規(guī)則就是除了從CPU往內(nèi)存里存數(shù)據(jù)用的STR相關(guān)的命令是從左往右讀以外,其余的基本是和咱們高級(jí)語(yǔ)言一樣,從右往左
-
指令的加載
-
假如我定義了一個(gè)整型變量int a = 65,那么a在內(nèi)存里的數(shù)據(jù)就是0100 0001(十進(jìn)制就是65),如果我定義了一個(gè)字符變量 char a = 'A',那在內(nèi)存里存的數(shù)是啥,因?yàn)橛?jì)算機(jī)只能存儲(chǔ)二進(jìn)制0和1,所以需要先把'A'轉(zhuǎn)成對(duì)應(yīng)的ASCII碼65,所以實(shí)際上,'A'在計(jì)算機(jī)上存的也是0100 0001(十進(jìn)制就是65)。所有數(shù)據(jù)在內(nèi)存上都是以0和1存儲(chǔ)的,內(nèi)存不知道他是啥類(lèi)型,就看你把他當(dāng)成啥類(lèi)型處理,如下面。
NSInteger a = 65; NSLog(@"%ld",a); NSLog(@"%c",a); -----------------打印結(jié)果如下----------------------- OCTest[35482:8821755] 65 OCTest[35482:8821755] A -
比如咱們開(kāi)發(fā)了一個(gè)90M的軟件,在運(yùn)行的時(shí)候,CPU就開(kāi)始處理,但是CPU需要從哪里開(kāi)始執(zhí)行呢,咱們90M的包里包含代碼,也包含一些全局變量的數(shù)據(jù)等,代碼是可執(zhí)行的,數(shù)據(jù)是用來(lái)參與運(yùn)算的。假如說(shuō) MOV X1,X0 對(duì)應(yīng)的二進(jìn)制是 0110 0100 , 恰巧可執(zhí)行文件的第一行代碼就是0110 0100, CPU怎么知道這行代碼是命令還是數(shù)據(jù),如果當(dāng)成命令,CPU做的就是把寄存器X0的值賦值給X1,如果當(dāng)成數(shù)據(jù),那就代表十進(jìn)制的100。其實(shí)咱們寫(xiě)的代碼在編譯鏈接后生成可執(zhí)行文件時(shí),就已經(jīng)把這90M的包分好了,哪塊是代碼段,哪塊是數(shù)據(jù)段。這樣CPU處理的時(shí)候就不會(huì)混亂了,下圖是用xcode編譯生成的可執(zhí)行文件,可以看出分的很詳細(xì),有代碼段有數(shù)據(jù)段
可執(zhí)行文件2.png- PC寄存器要出現(xiàn)了,PC寄存器俗稱(chēng)PC指針,CPU運(yùn)行哪條指令取決于PC寄存器存的是哪條指令的地址,也就是說(shuō),PC寄存器指向哪,程序就運(yùn)行哪。程序剛開(kāi)始運(yùn)行的時(shí)候PC寄存器會(huì)存儲(chǔ)代碼段的第一行所在內(nèi)存的地址,后續(xù)每執(zhí)行一條指令,PC寄存器會(huì)默認(rèn)加4指向下一條指令(加4是因?yàn)锳RM64匯編的每條指令占4個(gè)字節(jié))。比如下面的程序,剛開(kāi)始運(yùn)行的時(shí)候,PC寄存器存儲(chǔ)著地址是0x10004e1e0,當(dāng)開(kāi)始執(zhí)行第一條指令時(shí),會(huì)默認(rèn)指向下一條指令的地址0x10004e1e4
0x10004e1e0: sub sp, sp, #0x20 ; =0x20 0x10004e1e4: stp x29, x30, [sp, #0x10] 0x10004e1e8: add x29, sp, #0x10 ; =0x10 0x10004e1ec: adrp x8, 3 0x10004e1f0: add x8, x8, #0x3d0 ;
- PC寄存器要出現(xiàn)了,PC寄存器俗稱(chēng)PC指針,CPU運(yùn)行哪條指令取決于PC寄存器存的是哪條指令的地址,也就是說(shuō),PC寄存器指向哪,程序就運(yùn)行哪。程序剛開(kāi)始運(yùn)行的時(shí)候PC寄存器會(huì)存儲(chǔ)代碼段的第一行所在內(nèi)存的地址,后續(xù)每執(zhí)行一條指令,PC寄存器會(huì)默認(rèn)加4指向下一條指令(加4是因?yàn)锳RM64匯編的每條指令占4個(gè)字節(jié))。比如下面的程序,剛開(kāi)始運(yùn)行的時(shí)候,PC寄存器存儲(chǔ)著地址是0x10004e1e0,當(dāng)開(kāi)始執(zhí)行第一條指令時(shí),會(huì)默認(rèn)指向下一條指令的地址0x10004e1e4
-
-
指令的跳轉(zhuǎn)
1. 函數(shù)的調(diào)用:看下面的OC代碼,咱們都知道當(dāng)執(zhí)行到第二行時(shí),會(huì)跳到另一個(gè)函數(shù)test3WithParamB,也就是第6行,當(dāng)執(zhí)行完第8行以后,會(huì)回到上面的第三行繼續(xù)執(zhí)行,也就是函數(shù)跳轉(zhuǎn)。這個(gè)在匯編層面是怎么做到的呢。咱們上面不是剛說(shuō)PC寄存器,執(zhí)行完一條指令會(huì)默認(rèn)加4?,F(xiàn)在怎么還會(huì)跳了呢。1 - (void)test2WithParamA:(NSInteger)a b:(NSInteger)b c:(NSInteger)c { 2 [self test3WithParamB:103 b:104 c:105]; 3 NSInteger total = a + b + c; 4 } 5 6 - (void)test3WithParamB:(NSInteger)a b:(NSInteger)b c: (NSInteger)c { 7 NSInteger total = a + b + c; 8 }- 跳轉(zhuǎn)指令BL
咱們之前說(shuō),當(dāng)開(kāi)始執(zhí)行一條指令的時(shí)候,PC寄存器會(huì)默認(rèn)加4,指向下一條指令的地址。但是這說(shuō)的是默認(rèn),如果遇到函數(shù)跳轉(zhuǎn)就不會(huì)了,比如咱們當(dāng)前執(zhí)行的指令地址是0x10004e10,要跳轉(zhuǎn)的函數(shù)地址是0x10004e1e怎么辦,可以直接把PC寄存的值改成0x10004e1e就可以了。想法是對(duì)的,只是ARM64匯編不允許直接修改PC寄存器,但是可以通過(guò)跳轉(zhuǎn)指令BL,比如 BL 0x10004e1e,這樣就會(huì)把PC寄存器改成0x10004e1e,并且開(kāi)始執(zhí)行0x10004e1e處的指令 - 函數(shù)返回
上面說(shuō)了函數(shù)調(diào)用的跳轉(zhuǎn)指令BL,就是直接拿到被調(diào)用函數(shù)的地址,然后跳過(guò)去。但跳過(guò)去,函數(shù)執(zhí)行完了以后,怎么回去呢,還是以上面的代碼為例,當(dāng)執(zhí)行第2行時(shí),會(huì)跳到第6行,執(zhí)行完第8行后,應(yīng)該回到第3行,可是計(jì)算機(jī)怎么知道回到哪呢,如果不做特殊處理,按照之前的說(shuō)法PC寄存器會(huì)默認(rèn)執(zhí)行第9行的代碼。這時(shí)LR寄存器出場(chǎng)了,LR(x30)通常稱(chēng)X30為程序鏈接寄存器,保存子程序結(jié)束后需要執(zhí)行的下一條指令,其實(shí)咱們?cè)趫?zhí)行跳轉(zhuǎn)指令BL時(shí),CPU除了將PC寄存器里的值修改成要跳轉(zhuǎn)的地址以外,還會(huì)存儲(chǔ)跳轉(zhuǎn)回來(lái)以后要執(zhí)行的指令的地址(以上面的程序?yàn)槔?,就是保存?行的地址),存到哪呢,就是存到了LR寄存器。當(dāng)函數(shù)結(jié)束以后,就把PC寄存器的值,修改為L(zhǎng)R寄存器的值,這樣就相當(dāng)于回到調(diào)用的地方了
- 跳轉(zhuǎn)指令BL
-
參數(shù)問(wèn)題
1.函數(shù)調(diào)用,在匯編層面就是修改PC寄存器的值達(dá)到跳轉(zhuǎn)的目的,但是如果調(diào)用的函數(shù)需要傳參,參數(shù)放哪呢?答案還是寄存器,只不過(guò)用的是普通的寄存器x0 ~ x7: 用于子程序調(diào)用時(shí)的參數(shù)傳遞,以下面的程序?yàn)槔?,調(diào)用test2Wit時(shí),需要傳參,參數(shù)分別是100、101、102,可以看地址是0x10001e1c0的相鄰的三個(gè)指令,就是把0x64(十進(jìn)制的100)、0x65、0x66存到X2、X3、X4。然后就調(diào)用 bl 0x10001e5d4跳走了- (void)test1 { [self test2WithParamA:100 b:101 c:102]; } --------對(duì)應(yīng)的匯編如下------------- OCTest`-[AppDelegate test1]: 0x10001e19c <+0>: sub sp, sp, #0x20 ; =0x20 0x10001e1a0 <+4>: stp x29, x30, [sp, #0x10] 0x10001e1a4 <+8>: add x29, sp, #0x10 ; =0x10 0x10001e1a8 <+12>: adrp x8, 3 0x10001e1ac <+16>: add x8, x8, #0x3c0 ; =0x3c0 0x10001e1b0 <+20>: str x0, [sp, #0x8] 0x10001e1b4 <+24>: str x1, [sp] 0x10001e1b8 <+28>: ldr x0, [sp, #0x8] 0x10001e1bc <+32>: ldr x1, [x8] 0x10001e1c0 <+36>: mov x2, #0x64 0x10001e1c4 <+40>: mov x3, #0x65 0x10001e1c8 <+44>: mov x4, #0x66 0x10001e1cc <+48>: bl 0x10001e5d4 ; symbol stub for: objc_msgSend 0x10001e1d0 <+52>: ldp x29, x30, [sp, #0x10] 0x10001e1d4 <+56>: add sp, sp, #0x20 ; =0x20 0x10001e1d8 <+60>: ret- 那調(diào)到test2WithParamA后,怎么取參數(shù)的呢,看下圖0x1000a2260地址對(duì)應(yīng)的指令,會(huì)把X2、X3、X4的值先存到內(nèi)存里,然后再?gòu)膶?duì)應(yīng)的內(nèi)存地址取來(lái)來(lái)做加法,雖然沒(méi)直接用X2、X3、X4做加法,但是用的值,是從最初的X2、X3、X4里的值。咱們發(fā)現(xiàn)每個(gè)函數(shù)結(jié)束后,都會(huì)有個(gè)ret指令,比如下面0x1000a2288對(duì)應(yīng)的指令,這個(gè)ret的作用就是告訴CPU我這個(gè)函數(shù)結(jié)束了,如果需要返回到調(diào)用函數(shù)的地方,就把PC寄存器的值修改為L(zhǎng)R寄存器里值,這樣就可以調(diào)回去了
- (void)test2WithParama:(NSInteger)a b:(NSInteger)b c:(NSInteger)c {
NSInteger total = a + b + c;
}
-----------對(duì)應(yīng)的匯編如下---------------
OCTest`-[AppDelegate test2WithParama:b:c:]:
0x1000a2254 <+0>: sub sp, sp, #0x30 ; =0x30
0x1000a2258 <+4>: str x0, [sp, #0x28]
0x1000a225c <+8>: str x1, [sp, #0x20]
0x1000a2260 <+12>: str x2, [sp, #0x18]
0x1000a2264 <+16>: str x3, [sp, #0x10]
0x1000a2268 <+20>: str x4, [sp, #0x8]
0x1000a226c <+24>: ldr x0, [sp, #0x18]
0x1000a2270 <+28>: ldr x1, [sp, #0x10]
0x1000a2274 <+32>: add x0, x0, x1
0x1000a2278 <+36>: ldr x1, [sp, #0x8]
0x1000a227c <+40>: add x0, x0, x1
0x1000a2280 <+44>: str x0, [sp]
0x1000a2284 <+48>: add sp, sp, #0x30 ; =0x30
0x1000a2288 <+52>: ret
- SP寄存器
在上面的匯編代碼里,經(jīng)??吹筋?lèi)似[sp, #0x18]的寫(xiě)法,這個(gè)咱們大概說(shuō)一下,只要帶著中括號(hào)[]的一般就是表示內(nèi)存的某個(gè)地址。而SP指向的是內(nèi)存中的棧頂,內(nèi)存分為代碼段和數(shù)據(jù)段,數(shù)據(jù)段里有一個(gè)棧的部分,就是先進(jìn)后出的數(shù)據(jù)結(jié)構(gòu)的內(nèi)存。這種先進(jìn)后出,后進(jìn)先出的數(shù)據(jù)結(jié)構(gòu)就是為了方便臨時(shí)存儲(chǔ)數(shù)據(jù),比如函數(shù)跳轉(zhuǎn),參數(shù)太多的話,就可以先存到棧上,用完就銷(xiāo)毀。比如函數(shù)跳轉(zhuǎn),如果連續(xù)跳轉(zhuǎn),比如A函數(shù)調(diào)到B,還沒(méi)回到A呢,繼續(xù)調(diào)到B,那LR寄存器就一個(gè)怎么辦,沒(méi)法保存好幾個(gè)返回地址,那就可以先保存到內(nèi)存的棧里,等用到的時(shí)候,再取出來(lái)。
總結(jié)
匯編語(yǔ)言是一門(mén)可以直接操作CPU和內(nèi)存的語(yǔ)言



