# 概述
# 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 匯編
語言規范
# ARM64 匯編
- 匯編里面要學習的三個重要概念:寄存器、內存模型、指令。
- arm64架構又分為2種執行狀態:
AArch64 Application Level
和AArch32 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位,如圖:
為了函數調用的目的,通用寄存器分為四組(官網文檔):
- 注意,但參數過多、返回值過大時,比如是個成員很多的結構體,通用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 要指揮寄存器,直接跟內存交換數據。所以,除了寄存器,還必須了解內存怎么儲存數據。
程序運行的時候,操作系統會給它分配一段內存,用來儲存程序和運行產生的數據。這段內存有起始地址和結束地址,比如從0x1000
到0x8000
,起始地址是較小的那個地址,結束地址是較大的那個地址。
程序運行過程中,對于動態的內存占用請求(比如新建對象,或者使用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新建一個幀,用來儲存它的內部變量。也就是說,此時同時存在兩個幀:main
和add_a_and_b
。一般來說,調用棧有多少層,就有多少幀。
等到add_a_and_b
運行結束,它的幀就會被回收,系統會回到函數main剛才中斷執行的地方,繼續往下執行。通過這種機制,就實現了函數的層層調用,并且每一層都能使用自己的本地變量。
所有的幀都存放在 Stack,由于幀是一層層疊加的,所以 Stack 叫做棧。生成新的幀,叫做"入棧",英文是 push;棧的回收叫做"出棧",英文是 pop。Stack 的特點就是,最晚入棧的幀最早出棧(因為最內層的函數調用,最先結束運行),這就叫做"后進先出"的數據結構。每一次函數執行結束,就自動釋放一個幀,所有函數執行結束,整個 Stack 就都釋放了。
注意:
Stack 是由內存區域的結束地址開始,
從高位(地址)向低位(地址)分配
。
棧頂置針向低移動,就是分配臨時存儲空間,棧頂置針向高移動,就是釋放臨時存儲空間。
比如,內存區域的結束地址是0x8000,第一幀假定是16字節,那么下一次分配的地址就會從0x7FF0開始;第二幀假定需要64字節,那么地址就會移動到0x7FB0。-
棧中一個數據所分配到的內存中,存儲(讀取)數據時,是
從低位(地址)向高位(地址)讀寫
的。即棧中數據的打印地址(起始地址)與堆中一樣,是低地址開始
。- 情況一:見下面代碼塊中的
stp
、ldp
。 - 情況二:復合類型,如創建一個結構體局部變量,打印成員變量,會發現是從低地址向高地址依次打印出來的
- 注意:基本數據類型的存儲,還涉及到
大端、小端字節序
的概念,即指高位字節在前(后)。
- 情況一:見下面代碼塊中的
-
補充:復合數據類型都是由基本數據類型組成的,基本數據類型的存儲不會帶來空閑(冗余)空間的:
- 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定位有很重大的意義,通過之前幾個步驟的圖解,棧回溯的原理也相對比較清楚了。
- 通過當前的SP,FP可以得到當前函數的stack frame,通過PC可以得到當前執行的地址。
- 在當前棧的FP上方,可以得到Caller(調用者)的FP,和LR。通過偏移,我們還可以獲取到Caller的SP。由于LR保存了Caller下一條指令的地址,所以實際上我們也獲取到了Caller的PC
- 有了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是否被修改過了,如果被修改過了報錯。