Lecture 02: ARM 彙編基礎
Contents
- 為什麼學習ARM/ISA彙編
- 從C到彙編
- 理解arm彙編
- 理解機器執行
1 為什麼學習彙編和指令集架構?
1.令人困惑的應用表現
2.指令集架構ISA(Instruction Set Architecture)
- CPU向軟體(應用程式和作業系統)提供的介面。
- 理解軟體在CPU上的執行(OS設計,程式除錯)。
- 作業系統包含體系結構相關的彙編程式碼。
- 作業系統啟動程式碼(棧沒有設定)
- 部分操作C語言無法表達。(e.g.獲取系統狀態,重新整理TLB)
- 部分場景下彙編更加高效(e.g.
memcpy
)
2.2 從C語言到彙編
1.為什麼硬體不能直接執行C
- 硬體設計
(1)高階語言表達能力很強
(2)硬體理解高階語言複雜度過高難以高效設計。 - 機器指令
(1)格式相對固定
(2)功能相對簡單
(3)二進位制編碼
2.編譯過程
二進位制檔案難以理解->彙編較為適合閱讀。
2.3 理解arm彙編
- 在完成程式編寫後,程式被儲存在磁碟中。
- OS載入程式,將其放入記憶體,CPU中的PC指向當前需要執行的第一個彙編指令。每執行一個指令,PC=PC+4.
- 資料剛開始儲存在磁碟。後來會載入到記憶體當中。CPU中具有特殊的儲存單元:暫存器,用於臨時儲存資料。可以用load/store指令來搬運資料。
2.4 常用匯編
2.4.1 資料搬運
2.4.2 算術指令
2.4.3 移位指令
2.4.4 邏輯運算指令
2.4.5 Modified Register
- z=z*48分解成z=z*3與z=z*16,這樣可以採用右移,減少執行時間。(浮點乘法消耗時間遠大於位移)
- Modified register 優勢
- 對運算元進行移位/位擴充套件。
2.4.6 訪存指令
2.4.7 記憶體結構
1.CPU視角下的記憶體:
- 記憶體可以被視為一個很大的位元組陣列。
- 陣列每個元素可以由唯一的地址來索引。
2.記憶體地址
- 記憶體陣列的名稱計為M。M[addr]為addr開始的記憶體單元的內容。addr為記憶體陣列的索引。記憶體單元大小由上下文決定。
- addr的具體格式由定址模式決定。
3.定址模式
(1)基地址模式(索引定址)
- \([r_b]\)
(2)基地址+偏移量
- \([r_b,\text{offset}]\)
(3)前索引定址(定址操作前更新基地址)
- \([r_b,\text{offset}]!\), \(r_b+=\text{offset}\),定址\(M[r_b]\)
(4)後索引定址(定址操作後更新基地址)
- \([r_b],\text{offset}\) 定址\(M[r_b]\);\(r_b+=\text{offset}\).
(5)offset可以是
- 立即數 #imm
- 64位通用暫存器\(r_i\)
- 修改過的暫存器。例如:移位運算
lsl #3
,位擴充套件sxtw
(4)example:
2.4.8 條件碼,分支指令
標籤:.L3
,.L1
分支指令 .cbz
,bne
1.條件碼
- 一組標識位的統稱。
- 由PSTATE暫存器維護。
- N(Negative),Z(zero),C(carry),V(overflow)
- 條件碼保留之前相關指令的執行狀態,其中有
- 帶有s字尾的算術/邏輯指令(
subs
,adds
) - 比較指令
2.條件碼的設定
- 第一類:透過s字尾資料處理指令隱式設定。
adds Rd, Rn, Op2
相當於t=a+b
。
- C:運算產生進位時設定。
- Z:當t=0時被設定。
- N:當t<0時被設定。
- V:當運算產生有符號溢位時被設定。
(a>0 && b>0 && t<0) || (a<0 && b<0 && t>=0)
- 第二類:透過比較指令cmp顯式設定。
cmp src1, src2
計算src1-src2
,不儲存結果,只改變條件碼。
- C:運算不產生借位時設定。
- Z:當運算元相等時被設定。
- N:當src1<src2時被設定。
- V:當運算產生有符號溢位時被設定。
3.跳轉條件
4.跳轉指令
- 直接分支指令
- 標籤對應地址作為跳轉目標
- 無條件分支指令:
b <label>
- 有條件分支指令:
bcond <label>
, bcond=beq,bne,ble,... - 間接分支指令
- 暫存器中地址作為跳轉目標。
br reg
。
2.5 函式呼叫
2.5.1 函式呼叫=無條件跳轉
2.5.2 基本概念
- 術語:
- Caller 呼叫者
- Callee 被呼叫者
2.5.3 函式呼叫與返回指令
函式呼叫bl <label>
,返回ret
函式呼叫
- 指令:
bl <label>
直接呼叫函式blr Rn
間接呼叫,呼叫函式指標。
- 功能:
- 將返回地址儲存在連結暫存器x30
- 跳轉到被呼叫者的入口地址。
返回指令
- 指令
ret
不區分直接呼叫和間接呼叫。
- 功能
- 跳轉到返回地址X30
2.5.4 多級函式呼叫
- [ ]一級:cube呼叫square
- cube中的bl指令將返回地址儲存在LR(X30)中
- square中的ret指令返回到LR(X30)記錄的地址
- [ ]二級:cube呼叫square,square呼叫foo
- LR首先儲存了square返回cube的地址
- 巢狀呼叫時發生覆蓋:LR(X30)儲存foo返回square的地址
2.5.5 函式棧幀
-
棧楨:函式在執行期間使用的一段記憶體
-
生命週期:從被呼叫到返回前
-
作用:存放其區域性狀態,包括:
(1) 存放返回地址
(2) 存放上一個棧楨的位置
(3) 存放區域性變數 -
多級函式呼叫
-
例如,A呼叫B、B呼叫C
-
程式執行中存在多個未返回的函式
-
函式棧楨按照呼叫順序排列
(1) 先被呼叫者後返回,後被呼叫者先返回
(2) 棧:先進後出,後進先出 -
CPU中的另一個特殊暫存器SP
SP: Stack Pointer
指向棧頂(低地址)
2.5.6 函式呼叫返回過程中棧的變化
2.5.7 幀指標FP:X29暫存器
- 棧楨回溯
- 棧楨大小不一
- 如何找到上一個棧楨(如除錯)
(1) 儲存x29(上一個棧楨的SP)
(2) 將當前SP寫入x29(讓callee能儲存)
2.5.8 函式的呼叫,返回與棧
2.6 函式引數與返回值
2.6.1 暫存器傳遞資料
- x0-x7暫存器傳遞前8個引數
- x0作為返回值
2.6.2 傳遞資料
- 呼叫者壓到棧上的資料
- 第8個之後的引數
- 按宣告順序從右到左
Why? 因為引數的數量無法確定。而編譯器讀取引數時只是讀取sp指標以上的記憶體。因此可以得到每個引數的地址。反之則會導致每個引數的記憶體地址無法確定。 - 所有資料對齊到8位元組
- 被呼叫者透過SP+
舉例說明:
void proc(long a1, long *a1p,
int a2, int *a2p,
short a3, short *a3p,
char a4, char *a4p,
char a5, char *a5p)
{
*a1p += a1;
*a2p += a2;
*a3p += a3;
*a4p += a4;
*a5p += a5;
}
void caller(long *n)
{
proc (1,0x2000,3,0x4000,5,0x6000,7,0x8000,9,0xA000);
}
檢視caller的組合語言:
00000000000000a0 <_caller>:
a0: ff c3 00 d1 sub sp, sp, #48
a4: fd 7b 02 a9 stp x29, x30, [sp, #32]
a8: fd 83 00 91 add x29, sp, #32
ac: a0 83 1f f8 stur x0, [x29, #-8]
b0: e9 03 00 91 mov x9, sp
b4: 28 01 80 52 mov w8, #9
b8: 28 01 00 39 strb w8, [x9]
bc: 08 00 94 d2 mov x8, #40960
c0: 28 05 00 f9 str x8, [x9, #8]
c4: 20 00 80 d2 mov x0, #1
c8: 01 00 84 d2 mov x1, #8192
cc: 62 00 80 52 mov w2, #3
d0: 03 00 88 d2 mov x3, #16384
d4: a4 00 80 52 mov w4, #5
d8: 05 00 8c d2 mov x5, #24576
dc: e6 00 80 52 mov w6, #7
e0: 07 00 90 d2 mov x7, #32768
e4: 00 00 00 94 bl 0xe4 <_caller+0x44>
e8: fd 7b 42 a9 ldp x29, x30, [sp, #32]
ec: ff c3 00 91 add sp, sp, #48
f0: c0 03 5f d6 ret
2.7 暫存器儲存
2.7.1 通用暫存器儲存
- 不同函式共享同一批通用暫存器
- 因此能夠透過暫存器傳遞引數和返回值
- 然而,不同的函式對通用暫存器的使用會存在衝突 — 覆蓋
- 避免衝突的思路
- 函式在使用某個暫存器之前儲存該暫存器的值,返回前恢復
- 儲存在哪:函式棧楨中
- 效率問題:有時候可能無需儲存
如:一個函式內不呼叫其他函式
編譯器會盡可能減少冗餘儲存的程式碼
2.7.2 暫存器使用約定
-
呼叫者儲存的暫存器包括 X9~X15
-
呼叫者在呼叫前按需(僅考慮自己是否需要)進行儲存
呼叫者在被呼叫者返回後恢復這些暫存器的值 -
被呼叫者可以隨意使用
這些暫存器呼叫後的值可能發生改變 -
被呼叫者儲存的暫存器包括 X19~X28
-
被呼叫者在使用前進行儲存
-
被呼叫者在返回前進行恢復
-
呼叫者視角:這些暫存器的值在函式呼叫前後不會改變
2.7.3 舉例理解
0000000000000000 <square>:
0: 1b007c00 mul w0, w0, w0
4: d65f03c0 ret
0000000000000008 <cube>:
8: a9be7bfd stp x29, x30, [sp, #-32]! // 開闢棧幀,保留呼叫者的棧幀x29,儲存返回地址x30
c: 910003fd mov x29, sp // 當前幀的棧頂地址寫入x29
10: f9000bf3 str x19, [sp, #16] //被呼叫者使用前儲存呼叫者或之前保留的資料
14: 2a0003f3 mov w19, w0 // 使用資料引數
18: 94000000 bl 0 <square>
1c: 1b137c00 mul w0, w0, w19
20: f9400bf3 ldr x19, [sp, #16] // 從記憶體中恢復資料
24: a8c27bfd ldp x29, x30, [sp], #32 // 返回彈棧
28: d65f03c0 ret
2.8 區域性變數
2.8.1 函式區域性變數存放在函式棧楨中
- 為什麼不直接把區域性變數儲存在暫存器?
- 暫存器數量有限
- 陣列和結構體等複雜資料結構
- 區域性變數可能需要定址 (如&a)
2.8.2 區域性變數
- 區域性變數的分配
- 在分配棧幀時被一起分配
- 區域性變數的釋放
- 在返回前釋放棧幀時釋放
- 區域性變數透過SP相對地址引用
- (例如
ldr x1, [sp, #8]
)
2.8.3 小結