iOS需要了解的ARM64匯編

# 概述
# iOS相關的指令集及對應的ARM匯編語言
# ARM64匯編 
  ## 寄存器
    ### R0-R30(包括FP、LR)
    ### 一些特殊寄存器:SP、PC、V0-V31、SPRS
  ## 內存模型
    ### 堆
    ### 棧
    ### 棧回溯
  ## 指令格式及常見指令
  ## ARM指令的二進制編碼
    ### 匯編指令對應的二進制編碼格式
    ### 條件執行
# 匯編層次看高級語言
# GCC內聯匯編
# 參考鏈接
# 關于intel、AT&T匯編的簡單了解

# 概述


早期的程序員發現機器語言在閱讀、書寫方面的問題,是如此的難以辨別和記憶,需要記住所有抽象的二進制碼,為了解決這個問題,匯編語言就產生了。匯編語言是各種CPU提供的機器指令的助記符的集合,人們可以用匯編語言直接控制硬件系統進行工作。

匯編語言的主體是匯編指令。匯編指令和機器指令的差別在于指令的表示方法上。匯編指令是機器指令便于記憶的書寫格式。

匯編語言與硬件關聯很深,所以涉及到的知識點有很多,如:寄存器、端口、尋址方式、內外中斷、以及指令的實現原理等,額,如果想了解這些知識點,可以閱讀《匯編語言(第3版)》 王爽著。本篇博客類似閱讀手冊,主要記錄一些常見的寄存器、以及不同匯編語言規范中指令的編寫風格(intel及AT&T的篇幅很少,畢竟我是一個iOSer,以移動端主流ARM64匯編為例)。

# iOS相關的指令集及對應的ARM匯編語言


作為iOS開發工程師,主要需要了解的匯編語言是:

  • iOS模擬器:兼容x86指令集,對應 AT&T 匯編語言規范
  • iOS真機設備:兼容ARM指令集,對應 ARM 匯編語言規范
iPhone指令集

# ARM64 匯編


  • 匯編里面要學習的三個重要概念:寄存器、內存模型、指令。
  • arm64架構又分為2種執行狀態: AArch64 Application LevelAArch32 Application Level(后者是為了兼容以前的32bit的程序)
    • AArch64執行A64指令,使用64bit的通用寄存器;
    • AArch32執行A32/T32指令,使用32bit的通用寄存器;

## 先放代碼 — Hello world

#include <stdio.h>
int main(){
    printf("hello, world\n");
    return 0;
}

生成匯編文件:xcrun --sdk iphoneos clang -S -arch arm64 helloworld.c。也可以在XCode中,Product -> Perform Action -> Assemble 來生成匯編文件。

    .section    __TEXT,__text,regular,pure_instructions
    .build_version ios, 13, 2   sdk_version 13, 2
    .globl  _main                   ; -- Begin function main
    .p2align    2
_main:                                  ; @main
    .cfi_startproc
; %bb.0:
    sub sp, sp, #32             ;sub 減法; sp = sp - 32Byte
    stp x29, x30, [sp, #16]     ;stp 寄存器存儲到內存上,依次存兩個;保存x29(FP),和x30(LR) 到sp+16Byte上的16個Byte
    add x29, sp, #16            ;add 加法;把sp+16Byte的結果寫入x29(FP);
    .cfi_def_cfa w29, 16
    .cfi_offset w30, -8
    .cfi_offset w29, -16
    stur    wzr, [x29, #-4]     ;stur 寄存器內容存儲到內存;把wzr(零寄存器)中的數據寫入 x29(FP)減 4Byte 的內存
    adrp    x0, l_.str@PAGE     ;adrp 讀取地址到寄存器;把符號l.str所在的Page讀入x0
    add x0, x0, l_.str@PAGEOFF  ;x0 = x0 + l.str所在Page的偏移量
    bl  _printf                 ;bl 子程序調用;調用printf函數
    mov w8, #0                  ;mov 傳送指令;0寫入x8
    str w0, [sp, #8]            ;w0寫入sp+8的內存
    mov x0, x8                  ;x8寫入x0
    ldp x29, x30, [sp, #16]     ;sp+16Byte處的內存的兩個8Byte,分別寫入x29, x30
    add sp, sp, #32             ;sp = sp + 32Byte
    ret
    .cfi_endproc
                                        ; -- End function
    .section    __TEXT,__cstring,cstring_literals
l_.str:                                 ; @.str
    .asciz  "hellom, world\n"

.subsections_via_symbols

匯編代碼幾個規則:

  • .(點)開頭的是匯編器指令。匯編器指令是告訴匯編器如何生成機器碼的,閱讀匯編代碼的時候通常可以忽略掉。

    • .section __TEXT,__text,regular,pure_instructions:表示接下來的內容在生成二進制代碼的時候,應該生成到Mach-O文件__TEXT(Segment)中的__text(Section)
    • .cfi_startproc:用在每個函數的開始,用于初始化一些內部數據結構
    • .cfi_endproc:在函數結束的時候使用與.cfi_startproc相配套使用
    • .cfi_def_cfa <register>, <offset>:從寄存器中獲取地址并向其添加偏移量
    • .cfi_offset <register>, <offset>:寄存器以前的值保存在CFA的offset偏移處
  • :(冒號)結尾的是標簽(Label)。代表一個地址,在需要時可以使用跳轉指令跳轉到標簽處執行。其中,以小寫字母l開頭的是本地(local)標簽,只能用于函數內部。

## ARM中的寄存器

CPU 本身只負責運算,不負責儲存數據。數據一般都儲存在內存之中,CPU 要用的時候就去內存讀寫數據。但是,CPU 的運算速度遠高于內存的讀寫速度,為了避免被拖慢,CPU 都自帶一級緩存和二級緩存。基本上,CPU 緩存可以看作是讀寫速度較快的內存。

但是,CPU 緩存還是不夠快,另外數據在緩存里面的地址是不固定的,CPU 每次讀寫都要尋址也會拖慢速度。因此,除了緩存之外,CPU 還自帶了寄存器(register),用來儲存最常用的數據。也就是說,那些最頻繁讀寫的數據(比如循環變量),都會放在寄存器里面,CPU 優先讀寫寄存器,再由寄存器跟內存交換數據。

寄存器不依靠地址區分數據,而依靠名稱。每一個寄存器都有自己的名稱,我們告訴 CPU 去具體的哪一個寄存器拿數據,這樣的速度是最快的。有人比喻寄存器是 CPU 的零級緩存。

這里介紹一下arm64常見的一些寄存器:

### 通用寄存器R0 – R30

r0 - r30 是31個通用整形寄存器。每個寄存器可以存取一個64位大小的數。 當使用 x0 - x30訪問時,它就是一個64位的數。當使用 w0 - w30訪問時,訪問的是這些寄存器的低32位,如圖:

為了函數調用的目的,通用寄存器分為四組(官網文檔):

ARM64 通用寄存器
  • 注意,但參數過多、返回值過大時,比如是個成員很多的結構體,通用x0-x7不夠用,會通過棧來傳遞
### 一些特殊寄存器
  • ZR:zero register 零寄存器,與通用寄存器一樣,x、w分別代表64/32位(XZR/WZR),作用就是0,寫進去代表丟棄結果,拿出來是0.
  • SP:Stack Pointer 保存棧指針。在指令編碼中,使用 SP/WSP來進行對SP寄存器的訪問。
  • PC:程序計數器,俗稱PC指針,總是指向即將要執行的下一條指令。在arm64中,軟件是不能改寫PC寄存器的。
  • V0 – V31:向量寄存器,也可以說是浮點型寄存器。它的特點是每個寄存器的大小是 128 位的。 分別可以用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:狀態寄存器,用于存放程序運行中一些狀態標識。不同于編程語言里面的if else。在匯編中就需要根據狀態寄存器中的一些狀態來控制分支的執行。狀態寄存器又分為 The Current Program Status Register (CPSR)The Saved Program Status Registers (SPSRs)。 一般都是使用CPSR, 當發生異常時, CPSR會存入SPSR。當異常恢復,再拷貝回CPSR
    不同于其他寄存器,其他寄存器是用來存放數據的,都是整個寄存器具有一個含義。而CPSR寄存器是按位起作用的,也就是說,它的每一位都有專門的含義,記錄特定的信息。
    • CPSR寄存器是32位的
    • CPSR的低8位(包括I、F、T和M[4:0])稱為控制位,程序無法修改,除非CPU運行于特權模式下,程序才能修改控制位。
    • N、Z、C、V均為條件標志位,分別代表運算過程中產生的狀態。它們的內容可被算術或邏輯運算的結果所改變,并且可以決定某條指令是否被執行。

還有一些系統寄存器,還有 FPSR FPCR是浮點型運算時的狀態寄存器等。基本了解上面這些寄存器就可以了。

## 內存模型

### 堆

寄存器只能存放很少量的數據,大多數時候,CPU 要指揮寄存器,直接跟內存交換數據。所以,除了寄存器,還必須了解內存怎么儲存數據。

程序運行的時候,操作系統會給它分配一段內存,用來儲存程序和運行產生的數據。這段內存有起始地址和結束地址,比如從0x10000x8000,起始地址是較小的那個地址,結束地址是較大的那個地址。

程序運行過程中,對于動態的內存占用請求(比如新建對象,或者使用malloc命令),系統就會從預先分配好的那段內存之中,劃出一部分給用戶,具體規則是從起始地址開始劃分(實際上,起始地址會有一段靜態數據,這里忽略)。舉例來說,用戶要求得到10個字節內存,那么從起始地址0x1000開始給他分配,一直分配到地址0x100A,如果再要求得到22個字節,那么就分配到0x1020

這種因為用戶主動請求而劃分出來的內存區域,叫做 Heap(堆)。它由起始地址開始,從低位(地址)向高位(地址)增長。Heap 的一個重要特點就是不會自動消失,必須手動釋放,或者由垃圾回收機制來回收。

### 棧

Stack 是由于函數運行而臨時占用的內存區域。或者說棧是指令執行時存放臨時變量的內存空間。一個函數對應一幀,fp指向當前frame的棧底,sp指向棧頂

int main() {
   int a = 2;
   int b = 3;
}

上面代碼中,系統開始執行main函數時,會為它在內存里面建立一個幀(frame),所有main的內部變量(比如a和b)都保存在這個幀里面。main函數執行結束后,該幀就會被回收,釋放所有的內部變量,不再占用空間。

如果函數內部調用了其他函數,會發生什么情況?

int main() {
   int a = 2;
   int b = 3;
   return add_a_and_b(a, b);
}

上面代碼中,main函數內部調用了add_a_and_b函數。執行到這一行的時候,系統也會為add_a_and_b新建一個幀,用來儲存它的內部變量。也就是說,此時同時存在兩個幀:mainadd_a_and_b。一般來說,調用棧有多少層,就有多少幀

等到add_a_and_b運行結束,它的幀就會被回收,系統會回到函數main剛才中斷執行的地方,繼續往下執行。通過這種機制,就實現了函數的層層調用,并且每一層都能使用自己的本地變量。

所有的幀都存放在 Stack,由于幀是一層層疊加的,所以 Stack 叫做棧。生成新的幀,叫做"入棧",英文是 push;棧的回收叫做"出棧",英文是 pop。Stack 的特點就是,最晚入棧的幀最早出棧(因為最內層的函數調用,最先結束運行),這就叫做"后進先出"的數據結構。每一次函數執行結束,就自動釋放一個幀,所有函數執行結束,整個 Stack 就都釋放了。

注意:

  • Stack 是由內存區域的結束地址開始,從高位(地址)向低位(地址)分配
    棧頂置針向低移動,就是分配臨時存儲空間,棧頂置針向高移動,就是釋放臨時存儲空間。
    比如,內存區域的結束地址是0x8000,第一幀假定是16字節,那么下一次分配的地址就會從0x7FF0開始;第二幀假定需要64字節,那么地址就會移動到0x7FB0。

  • 棧中一個數據所分配到的內存中,存儲(讀取)數據時,是從低位(地址)向高位(地址)讀寫的。即棧中數據的打印地址(起始地址)與堆中一樣,是低地址開始

    • 情況一:見下面代碼塊中的stpldp
    • 情況二:復合類型,如創建一個結構體局部變量,打印成員變量,會發現是從低地址向高地址依次打印出來的
    • 注意:基本數據類型的存儲,還涉及到大端、小端字節序的概念,即指高位字節在前(后)。
  • 補充:復合數據類型都是由基本數據類型組成的,基本數據類型的存儲不會帶來空閑(冗余)空間的:

    • char類型的數據值為單個字符,ASCII碼值對應為0-255,正好一個字節存儲。
    • int類型,比如int = 1,int占4字節,存的時候會存0x00000001,即會轉成8位的16進制表示存儲,占滿4字節
    • 冗余空間的產生,往往是因為一些比如對齊之類的存儲策略造成的

下面的圖簡單的描述了 main 調用方法 printf 時,棧是如何劃分的:

下面是方法的調用過程,分別對應方法頭、方法尾。

//x29就是fp, x30就是lr
//方法頭:保存當前函數/子程序(main)的棧底FP、LR(main結束后需要執行的下一條指令)
sub  sp, sp, #32             // sub 減法; sp = sp - 32Byte
stp  x29, x30, [sp, #16]    // stp 寄存器存儲到內存上;保存x29(FP)、x30(LR)到sp+16Byte上的16個Byte(通用寄存器,用x訪問,表示64位,8Byte)
add  x29, sp, #16            // add 加法;把sp+16Byte寫入x29(FP),保存即將執行函數的棧底

bl  _printf //子程序調用。// 跳轉到_printf方法處,同時將該行的下一個指令的地址復制到 lr。作用也很好理解:當printf執行完了之后要返回來繼續執行,但是計算機要如何知道返回到哪執行呢? 就是靠lr記錄了返回的地址,方法才能得以正常返回。
// 本來LR中存儲的是LR(main),是記錄main函數執行完需要返回執行的下一條指令。在發生bl _printf后,LR存儲的是printf函數執行完需要執行的下一條執行。這里沒顯示printf函數的匯編代碼,在其中還有一個ret,會返回到這個LR

//方法尾
ldp x29, x30, [sp, #16]     // 將sp+16Byte后的兩個8Byte,分別存入FP、LR,恢復為FP(main),LR(main)
add sp, sp, #32              // sp = sp + 32Byte
// 這一步執行完之后,fp就執行了圖中FP(main);sp指向了 SP(main);lr恢復成main執行完后的返回地址。 
// 這個時候狀態已經完全恢復到了 main 的環境
ret    // 返回指令,這一步直接執行lr的指令。

總結:

  • 方法頭、尾的作用就是調用前保存程序狀態,調用后恢復程序狀態。
  • 如果一個函數內部沒有其他函數調用,也就沒有這幾行方法頭、尾了,比如一個最簡單的程序如:
#include <stdio.h>
void nothing(){
    return;
}
//匯編代碼中就一行ret指令,如下:
_nothing:                               ; @nothing
    .cfi_startproc
; %bb.0:
    ret
    .cfi_endproc

關于參數及返回值的傳遞,具有以下規則(贅述一遍,前面講寄存器時提過):

  • 當函數參數個數小于等于8個的時候,x0-x7依次存儲前8個參數
  • 參數個數大于8個的時候,多余的參數會通過棧傳遞
  • 方法通常通過x0返回數據,如果返回的數據結構較大,則通過x8將數據的地址進行返回(寄存器最大為8字節,超過8字節的返回值,一個寄存器就傳遞不了了)
  • 在Intel 32位匯編中
    • 小于等于4字節,函數將返回值存儲在eax中(32位機器,eax本身只有4個字節)
    • 5~8字節,幾乎所有的調用慣例(調用約定)都是采用eax和edx 聯合返回的方式進行的,eax存儲返回值的低4字節,edx存儲返回值的高4字節
    • 大于8字節,在棧上臨時開辟一塊內存區域作為中轉,eax返回數據的地址。返回時,先將值寫入到這一塊指定的棧內存,外部程序使用時再讀取,多了兩次內存讀寫,造成額外開銷。(參考《程序員的自我修養—第10章內存》)
### Stack backtrace

棧回溯對代碼調試和crash定位有很重大的意義,通過之前幾個步驟的圖解,棧回溯的原理也相對比較清楚了。

  1. 通過當前的SP,FP可以得到當前函數的stack frame,通過PC可以得到當前執行的地址。
  2. 在當前棧的FP上方,可以得到Caller(調用者)的FP,和LR。通過偏移,我們還可以獲取到Caller的SP。由于LR保存了Caller下一條指令的地址,所以實際上我們也獲取到了Caller的PC
  3. 有了Caller的FP,SP和PC,我們就可以獲取到Caller的stack frame信息,由此遞歸就可以不獲取到所有的Stack Frame信息。

棧回溯的過程中,我們拿到的是函數的地址,又是如何通過函數地址獲取到函數的名稱和偏移量的呢?

  • 對于系統的庫,比如CoreFoundation我們可以直接通過系統的符號表拿到
  • 對于自己代碼,則依賴于編譯時候生成的dsym文件。

這個過程我們稱之為symbolicate,對于iOS設備上的crash log,我們可以直接通過XCode的工具symbolicatecrash來符號化:

cd /Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources
./symbolicatecrash ~/Desktop/1.crash ~/Desktop/1.dSYM > ~/Desktop/result.crash

當然,可以用工具dwarfdump去查詢一個函數地址:

dwarfdump --lookup 0x000000010007528c  -arch arm64 1.dSYM

## 指令格式及常見指令

ARM作為精簡指令集(RISC),所有 ARM 指令(RISC)的長度都是 32 位。行成對比的是復雜指令集(CISC,如x86),指令長度不同,最長的指令長達15 bytes,等于120位。

ARM指令使用的基本格式如下:<opcode>{<cond>}{S} <Rd>,<Rn>,{<operand2>}

  • Opcode:操作碼;指令助記符,如LDR、STR等。
  • Cond:可選的條件碼;執行條件,如EQ、NE等。
  • S:可選后綴;若指定S,則根據指令執行結果更新CPSR中的條件碼
  • Rd:目標寄存器
  • Rn:存放在第1操作數的寄存器。
  • operand2:第2個操作數。
  • “< >”:“< >”內的項是必需的,例如,<opcode>是指令助記符,這是必須書寫的。
  • “{ }”:“{ }”內的(ˇ?ˇ) 項是可選的,例如,{< code>}為指令執行條件,是可選項。若不書寫,則使用默認條件AL(無條件執行)。

ARM處理器的指令集可以分為跳轉指令、數據處理指令、程序狀態寄存器(PSR)處理指令、加載/存儲指令、協處理器指令和異常產生指令6大指令。

本文只列舉一些常見的基本指令,可以正常閱讀匯編代碼即可。有幾個注意點:

  • 寄存器:為標號,不加前綴
  • 操作數順序:目標操作數在左,源操作數在右
  • 立即數:前加#作為前綴
  • 尋址格式
;尋址格式:
    [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
    舉例:
      ldr x0, [x1]              // 從`x1`指向的地址里面取出一個 64 位大小的數存入 `x0`
      ldp x1, x2, [x10, #0x10]  // 從 x10 + 0x10 指向的地址里面取出 2個 64位的數,分別存入x1, x2
      str x5, [sp, #24]         // 把x5的值(64位數值)存到 sp+24 指向的內存地址上
      stp x29, x30, [sp, #-16]! // 把 x29, x30的值存到 sp-16的地址上,并且把 sp-=16. 
      ldp x29, x30, [sp], #16   // 從sp地址取出 16 byte數據,分別存入x29, x30. 然后 sp+=16;

除此之外,還有兩種地址表示方式(相對尋址):

  • 程序相對地址(程序相對的表達式):是命名寄存器的值加上或減去一個數字常數
  • 寄存器相對地址(寄存器相對的表達式):表示為相對當前程序計數器 (PC) 的偏移量。它通常是標簽與數字
    表達式的組合(如ADR指令)

由于篇幅原因,只列舉了常用的一些,更多的可以跳轉ARM64指令簡易手冊查閱。全面的可以查看ARM官網文檔。如果想看中文版的資料可以看《匯編器指南》— 第二章、第四章

;數據處理指令  
    MOV    X1,X0              將寄存器X0的值傳送到寄存器X1。MOV:從另一個寄存器、被移位的寄存器或將一個立即數加載到目的寄存器。
    ;算術運算:ADD SUB MUL … 等加減乘除運算
    ADD    X0,X1,X2          寄存器X1和X2的值相加后傳送到X0
    SUB    X0,X1,X2          寄存器X1和X2的值相減后傳送到X0
    MUL
    add  x14, x4, x27, lsl #1  算術運算也可以與邏輯位移運算一起用,意思是把  (x27 << 1) + x4 = x14;
    ;擴展位數運算:有 zero extend(高位補0) 和 sign extend(高位填充和符號位一致,一般有符號數用這個)。 一般用來補齊位數。常和算術運算配合一起.
    add  w20, w30, w20, uxth   算術運算也可以與擴展位數運輸算一起,意思是取 w20的低16位,無符號補齊到32位后再進行  w30 + w20的運算
    ;邏輯運算指令
    LSL                        邏輯左移
    LSR                        邏輯右移
    ASR                        算術右移
    ROR                        循環右移
    AND    X0,X0,#0xF        與。X0的值與0xF相位與后的值傳送到X0
    ORR    X0,X0,#9          或。X0的值與9相位或后的值傳送到X0
    EOR    X0,X0,#0xF        異或。X0的值與0xF相異或后的值傳送到X0

;寄存器加載/存儲指令
    LDR    X5,[X6,#0x08]           ld(load): X6寄存器加0x08的和的地址值內的數據傳送到X5
    LDP    x29, x30, [sp, #0x10]     ldp(load pair):是ldr 的變種指令,可以同時操作兩個寄存器,從指定內存處讀取兩個數據到寄存器
    STR    X0, [SP, #0x8]            st:store, str:往內存中寫數據(偏移值為正); X0寄存器的數據傳送到SP+0x8地址值指向的存儲空間
    STUR   w0, [x29, #-0x8]          往內存中寫數據(偏移值為負)
    STP    x29, x30, [sp, #0x10]     stp(store pair):是str 的變種指令,可以同時操作兩個寄存器,將一對寄存器中的值,入棧,存放到指定內存處
    ADR      ;將一個立即值與 pc 值相加,并將結果寫入目標寄存器
    ADRP     ;以頁為單位的大范圍的地址讀取指令,這里的P就是page的意思。取得page的基地址存入寄存器
      示例: adrp    x0, l_.str@PAGE         ;將符號l.str所在的page基址讀入x0
            add     x0, x0, l_.str@PAGEOFF  ;x0 = x0 + l.str所在page中的偏移量

;跳轉和控制指令
    CBZ      ;比較(Compare),如果結果為零(Zero)就轉移(只能跳到后面的指令)
    CBNZ     ;比較,如果結果非零(Non Zero)就轉移(只能跳到后面的指令)
    CMP      ;比較指令,相當于SUBS,影響程序狀態寄存器CPSR,關于CPSR的幾個狀態值,前面寄存器節已經講過

    B{條件} 目標地址        ;跳轉指令,可帶條件跳轉與cmp配合使用。一般是本方法內的跳轉,如while循環,if else等。
    BL       ;帶返回的跳轉指令, 返回地址保存到LR(X30)。存了LR也就意味著可以返回到本方法繼續執行。一般用于不同方法之間的調用
    RET      ;子程序返回指令,返回地址默認保存在LR(X30)

;異常產生指令
    SWI(Software Interrupt)    軟件中斷指令。用于產生軟中斷,從而實現處理器從用戶模式變換到管理模式,CPSR保存到管理模式的SPSR中,執行轉移到SWI向量,在其他模式下也可以使用SWI指令,處理器同樣切換到管理模式。
    BKPT(BreakPoint)           斷點中斷指令。產生一個預取異常(prefetch abort),它常被用來設置軟件斷點,在調試程序時十分有用。當系統中存在調試硬件時,該指令被忽略。
  • ARM指令中,不支持將立即數直接寫入內存,需要先通過mov寫入寄存器,然后通過str將寄存器中的值存儲進內存

## ARM指令的二進制編碼

### 對應的二進制編碼格式

ARM指令集是以32位二進制編碼的方式給出的,大部分的指令編碼中定義了第一操作數、第二操作數、目的操作數、條件標志影響位以及每條指令所對應的不同功能實現的二進制位。每條32位ARM指令都具有不同的二進制編碼方式,與不同的指令功能相對應

如圖所示表示了ARM指令集編碼。

### 條件執行

ARM指令的一個重要特點就是所有指令都是帶有條件的,就是說匯編中可以根據狀態寄存器中的一些狀態來控制分支的執行。

在ARM的指令編碼表中,統一占用編碼的最高4位[31:28]來表示條件碼。每種條件碼用兩個英文縮寫字符表示其含義,可添加在指令助記符的后面,表示指令執行時必須要滿足的條件。ARM指令根據CPSR中的條件位自動判斷是否執行指令。在條件滿足時,指令執行;否則,指令被忽略。

例如,數據傳送指令MOV加上條件后綴EQ后成為MOVEQ,表示“相等則執行傳送”,“不相等則本條指令不執行”,即只有當CPRS中的Z標志為1時,才會發生數據傳送。ARM指令集編碼表列舉了4位條件碼的16種編碼中能為用戶所使用的15種,而編碼1111為系統暫不使用的保留編碼。

舉例

看下面幾行匯編指令:

cmp x2, #0         // x2 - 0 = 0。  狀態寄存器標識zero: PSTATE.NZCV.Z = 1
b.ne  0x1000d48f0  // ne就是個condition code, 這句的意思是,當判斷狀態寄存器 NZCV.Z != 1才跳轉,因此這句不會跳轉

0x1000d4ab0 bl testFuncA               // 跳轉方法,這個時候 lr 設置為 0x1000d4ab4
0x1000d4ab4 orr x8, xzr, #0x1f00000000 // testFuncA執行完之后跳回lr就周到了這一行

# 內聯匯編


用匯編編寫的程序雖然運行速度快,但開發速度非常慢,效率也很低。如果只是想對關鍵代碼段進行優化,或許更好的辦法是將匯編指令嵌入到 C 語言程序中,從而充分利用高級語言和匯編語言各自的特點。但一般來講,在 C 代碼中嵌入匯編語句要比"純粹"的匯編語言代碼復雜得多,因為需要解決如何分配寄存器,以及如何與C代碼中的變量相結合等問題。

GCC 提供了很好的內聯匯編支持,最基本的格式是:__asm__("asm statements");

更詳細,可參考Linux 匯編語言開發指南—第七節

# 匯編層次看高級語言


匯編層面上只有寄存器、內存及數據(地址(無符號整數)、數字(定點、浮點)、字符、邏輯數)

  • 指針:本質上就是一個變量的地址。
  • 結構體:本質上就是按照一定規則分配的連續內存。
    • 結構體作為參數時,將成員通過連續的通用寄存器或者浮點型寄存器傳入。當結構體過大(成員過多、復雜)的時候,作為參數和返回值時,通過棧來傳遞,這一點和函數的參數個數過多的時候類似。
    • 舉例:當使用printf直接打印結構體變量時(一般不這么使用,而是打印結構體.成員變量),不是直接打印地址,而是打印成員。前面有多少個打印字符,就會打印出多少個成員變量的值。(如果打印字符多于成員數,會打印出一些隨機的東西)
  • 數組
    • 數組作為函數參數的時候,是以指針的方式傳入的,比如這個例子中,是把sp+12Byte的地址作為參數放到x0中,傳遞給logArray函數的。
    • 初始化數組的變量是存儲在代碼段的常量區,.section __TEXT,__const
    • 在編譯過后,會在變量區域的上下各插入一個___stack_chk_guard,在方法執行完畢后,檢查棧上的___stack_chk_guard是否被修改過了,如果被修改過了報錯。

# 關于intel、AT&T匯編的簡單了解


Intel、AT&T匯編

# 參考鏈接:


最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,967評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,273評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,870評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,742評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,527評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,010評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,108評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,250評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,769評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,656評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,853評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,371評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,103評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,472評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,717評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,487評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,815評論 2 372

推薦閱讀更多精彩內容