讓你理解ARM64匯編語(yǔ)言

咱們本篇文章講的語(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ū)別呢

    1. CPU的處理速度比硬盤(pán)的讀取速度快很多,比方說(shuō)CPU一秒可以處理1000個(gè)數(shù)據(jù),但是硬盤(pán)1秒只能讀出10個(gè)數(shù)據(jù),造成CPU的性能發(fā)揮不出來(lái)
    2. 這時(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ì)好很多
    3. 因?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、內(nèi)存、硬盤(pán)
  • CPU的介紹,CPU由3部分組成,分別是運(yùn)算器,控制器,寄存器。
    1. 運(yùn)算器,顧名思義,就是處理數(shù)據(jù)的,比如運(yùn)算1+1
    2. 控制器,就是控制著把數(shù)據(jù)從內(nèi)存加載到CPU,并且解析這個(gè)數(shù)據(jù)是干啥的,比如是做加法的話就送到運(yùn)算器進(jìn)行處理
    3. 寄存器,重點(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ǔ)言

    1. 咱們知道,一臺(tái)計(jì)算機(jī)或者手機(jī)可以工作,最核心的東西就是CPU

    2. 咱們平時(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
    3. 咱們上面說(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ù)

      1. 如果我想把寄存器X2的賦值給X1怎么操作呢,匯編的寫(xiě)法就是:MOV X1,X2
      2. 如果我想把X2寄存器里的值和X3里的加起來(lái)放到X1里怎么做呢,匯編的寫(xiě)法就是 ADD X1,X2,X3
      3. 如果我想用X3寄存器里的值減掉X2里的值放到X1里怎么做呢,匯編的寫(xiě)法就是 SUB X1,X3,X2
    • 寄存器和內(nèi)存之間傳數(shù)據(jù)

      1. 以上圖為例,比如我想把地址是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ù)寄存器尋址

      2. 以上圖為例,比如我想把寄存器X3的值寫(xiě)入到內(nèi)存是FFF1的地址怎么寫(xiě)呢,先把地址放到另一個(gè)寄存器中 MOV X2, FFF0
        然后再用存儲(chǔ)命令STR X3,[X2]。

      3. 簡(jiǎn)單的總結(jié):知道為啥匯編語(yǔ)言更底層了吧,因?yàn)檫@種語(yǔ)言可以直接操作CPU,咱們普通語(yǔ)言是做不到的。語(yǔ)法規(guī)則就是除了從CPU往內(nèi)存里存數(shù)據(jù)用的STR相關(guān)的命令是從左往右讀以外,其余的基本是和咱們高級(jí)語(yǔ)言一樣,從右往左

    • 指令的加載

      1. 假如我定義了一個(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
        
      2. 比如咱們開(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
        1. 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            ;   
          
    • 指令的跳轉(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      }
      
      1. 跳轉(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處的指令
      2. 函數(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)用的地方了
    • 參數(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 
      
      1. 那調(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ǔ)言

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容