GNU 彙編語法適用於所有的架構,並不是 ARM 獨享的,GNU 彙編由一系列的語句組成,
每行一條語句,每條語句有三個可選部分,如下:
label: instruction@comment |
---|
label 即標號,表示地址位置,有些指令前面可能會有標號,這樣就可以通過這個標號得到指令的地址,標號也可以用來表示數據地址。注意 label 後面的「:」,任何以「:」結尾的識別符號都會被識別爲一個標號。
instruction 即指令,也就是彙編指令或僞指令。
@符號,表示後面的是註釋,就跟 C 語言裏面的「/」和「/」一樣,其實在 GNU 彙編檔案中我們也可以使用「/」和「/」來註釋。
comment 就是註釋內容。
比如如下程式碼:
add:
MOVS R0, #0X12 @設定R0=0X12
上面程式碼中「add:」就是標號,「MOVS R0,#0X12」就是指令,最後的「@設定 R0=0X12」就是註釋。
注意!ARM 中的指令、僞指令、僞操作、暫存器名等可以全部使用大寫,也可以全部使用小寫,但是不能大小寫混用。
使用者可以使用.section 僞操作來定義一個段,彙編系統預定義了一些段名:
.text 表示程式碼段。
.data 初始化的數據段。
.bss 未初始化的數據段。
.rodata 只讀數據段。
我們當然可以自己使用.section 來定義一個段,每個段以段名開始,以下一段名或者檔案結
尾結束,比如:
.section .testsection @定義一個 testsetcion 段
彙編程式的預設入口標號是_start,不過我們也可以在鏈接指令碼中使用 ENTRY 來指明其它
的入口點,下面 下麪的程式碼就是使用_start 作爲入口標號:
.global _start
_start:
ldr r0, =0x12 @r0=0x12
上面程式碼中.global 是僞操作,表示_start 是一個全域性標號,類似 C 語言裏面的全域性變數一
樣,常見的僞操作有:
.byte 定義單位元組數據,比如.byte 0x12。
.short 定義雙位元組數據,比如.short 0x1234。
.long 定義一個 4 位元組數據,比如.long 0x12345678。
.equ 賦值語句,格式爲:.equ 變數名,表達式,比如.equ num, 0x12,表示 num=0x12。
.align 數據位元組對齊,比如:.align 4 表示 4 位元組對齊。
.end 表示原始檔結束。
.global 定義一個全域性符號,格式爲:.global symbol,比如:.global _start。
GNU 彙編同樣也支援函數,函數格式如下:
函數名: 函數體
返回語句
GNU 彙編函數返回語句不是必須的,如下程式碼就是用匯編寫的Cortex-A7中斷服務函數:
/* 未定義中斷 */
Undefined_Handler:
ldr r0, = Undefine_Handler
bx r0
/* SVC中斷 */
SVC_Handler:
ldr r0, = SVC_Handler
bx r0
/*預取中斷*/
PrefAbort_Handler:
ldr r0, PerfAbort_Hander
bx r0
上述程式碼中定義了三個彙編函數:Undefined_Handler、SVC_Handler 和
PrefAbort_Handler。以函數 Undefined_Handler 爲例我們來看一下彙編函陣列成,
「Undefined_Handler」就是函數名,「ldr r0, =Undefined_Handler」是函數體,「bx r0」是函數
返回語句,「bx」指令是返回指令,函數返回語句不是必須的。
使用處理器做的最多的事情就是在處理器內部來回的傳遞數據,常見的操作有:
①、將數據從一個暫存器傳遞到另外一個暫存器。
②、將數據從一個暫存器傳遞到特殊暫存器,如 CPSR 和 SPSR 暫存器。
③、將立即數傳遞到暫存器。
數據傳輸常用的指令有三個:MOV、MRS 和 MSR,這三個指令的用法如下表所示:
MOV 指令用於將數據從一個暫存器拷貝到另外一個暫存器,或者將一個立即數傳遞到寄
存器裏面,使用範例如下:
MOV R0, R1 @將暫存器R1中的數據傳遞給R0, 即 R0 = R1
MOV R0, #0X12 @將0X12傳遞給R0暫存器, 即R0 = 0X12
MRS指令用於將特殊暫存器(如CPSR 和 SPSR)中的數據傳遞給通用暫存器,要讀取特殊暫存器的數據只能用MRS指令!使用範例如下:
MRS R0, CPSR @將特殊暫存器CPSR裏面的數據傳遞給R0, 即R0 = CPSR
MSR 指令和MRS剛好相反,MSR指令用來將普通暫存器的數據傳遞給特殊暫存器,也就是寫特殊暫存器,寫特殊暫存器,也就是寫特殊暫存器,寫特殊暫存器只能用MSR, 使用範例如下:
MSR R0, CPSR @ 將R0的數據複製到CPSR中,即CPSR=R0
ARM 不能直接存取記憶體,比如 RAM 中的數據,I.MX6UL 中的暫存器就是 RAM 型別
的,我們用匯編來設定 I.MX6UL 暫存器的時候需要藉助記憶體存取指令,一般先將要設定的值
寫入到 Rx(x=0~12)暫存器中,然後藉助記憶體存取指令將 Rx 中的數據寫入到 I.MX6UL 暫存器中。讀取 I.MX6UL 暫存器也是一樣的,只是過程相反。常用的記憶體存取指令有兩種:LDR 和 STR,用法如下表所示:
指令 | 描述 |
---|---|
LDR Rd, [Rn, #offset | 從記憶體Rn+offset的位置讀取數據存放到Rd中 |
STR Rd,[Rn, %offset | 將Rd中的數據寫入到記憶體中的Rn+offset位置。 |
LDR主要用於儲存載入數據到暫存器Rx中,LDR也可以將一個立即數載入到暫存器Rx中,LDR載入立即數的時候要使用"=",而不是「#」。在嵌入式開發中。LDR最常用的就是讀CPU的暫存器值,比如 I.MX6UL 有個暫存器 GPIO1_GDIR,其地址爲 0X0209C004,我們
現在要讀取這個暫存器中的數據,範例程式碼如下:
LDR R0, = 0X0209C004 @ 將暫存器地址0X209C004載入到R0中,即R0=0X0209C004
LDR R1, [R0] @讀取地址0X0209C004 中的數據到R1暫存器中
上述程式碼就是讀取暫存器 GPIO1_GDIR 中的值,讀取到的暫存器值儲存在 R1 暫存器中,
上面程式碼中 offset 是 0,也就是沒有用到 offset。
LDR是從記憶體讀取數據, STR就是將數據寫入到記憶體中,同樣以I.MX6UL 暫存器
GPIO1_GDIR 爲例,現在我們要設定暫存器 GPIO1_GDIR 的值爲 0X2000002,範例程式碼如下:
LDR R0, = 0X0209C004 @將暫存器地址0X0209C004 載入到R0中, 即R0 = 0X0209C004
LDR R1, = 0X20000002 @R1儲存要寫入到暫存器的值, 即R1 = 0X20000002
LDR 和 STR 都是按照字進行讀取和寫入的,也就是操作的 32 位數據,如果要按照位元組、
半字進行操作的話可以在指令「LDR」後面加上 B 或 H,比如按位元組操作的指令就是 LDRB 和 STRB,按半字操作的指令就是 LDRH 和 STRH。
我們通常會在 A 函數中呼叫 B 函數,當 B 函數執行完以後再回到 A 函數繼續執行。要想
在跳回 A 函數以後程式碼能夠接着正常執行,那就必須在跳到 B 函數之前將當前處理器狀態儲存
起來(就是儲存 R0~R15 這些暫存器值),當 B 函數執行完成以後再用前面儲存的暫存器值恢復
R0~R15 即可。儲存 R0~R15 暫存器的操作就叫做現場保護,恢復 R0~R15 暫存器的操作就叫做
恢復現場。在進行現場保護的時候需要進行壓棧(入棧)操作,恢復現場就要進行出棧操作。壓棧
的指令爲 PUSH,出棧的指令爲 POP,PUSH 和 POP 是一種多儲存和多載入指令,即可以一次
操作多個暫存器數據,他們利用當前的棧指針 SP 來生成地址,PUSH 和 POP 的用法如下表所示:
假如我們現在要將 R0~R3 和 R12 這 5 個暫存器壓棧,當前的 SP 指針指向 0X80000000,處理器的堆疊是向下增長的,使用的彙編程式碼如下:
PUSH {R0~R3, R12} @將R0~R3和R12壓棧
壓棧完成以後的堆疊如下圖所示
就是對R0~R3,R12進行壓棧以後的堆疊示意圖,此時的SP指向了0X7FFFFFEC,
假如我們現在要再將 LR 進行壓棧,彙編程式碼如下:
PUSH {LR} @將LR進行壓棧
對 LR 進行壓棧完成以後的堆疊模型如下圖所示
這就是分兩步對 R0~R3,R2 和 LR 進行壓棧以後的堆疊模型,如果我們要出棧的話
就是使用如下程式碼:
下面 下麪展示一些 內聯程式碼片
。
POP {LR} @先恢復LR
POP {R0~R3, R12} @再恢復R0~R3, R12
出棧的就是從棧頂,也就是當前執行的位置開始, 地址依次減小來提取堆疊的數據到恢復的暫存器列表中。
有多種跳轉操作,比如:
1、直接使用跳轉指令B、BL、BX等。
2、直接向PC暫存器裏面寫數據。
上述兩種方法都可以完成跳轉操作,但是一般常用的還是 B、BL 或 BX,用法如下表
這是最簡單的跳轉指令,B指令會將PC暫存器的值設定爲跳轉目標值,一旦執行B指令,ARM處理器就會跳轉到指定的目標地址。如果要呼叫的函數不會再返回到原來的執行地址處,那就可以用B指令,如下範例:
_start:
ldr sp, = 0X80200000 @
b mian
上述程式碼就是典型的在彙編中初始化 C 執行環境,然後跳轉到 C 檔案的 main 函數中執行,上述程式碼只是初始化了 SP 指針,有些處理器還需要做其他的初始化,比如初始化 DDR 等等。因爲跳轉到 C 檔案以後再也不會回到彙編了,所以在第 4 行使用了 B 指令來完成跳轉。
BL指令相比B指令,在跳轉會在暫存器LR(R14)中儲存當前PC暫存器值,所以可以通過將LR暫存器中的值重新載入到PC來繼續從跳轉之前的程式碼執行,這是子程式呼叫
一個基本但常用的手段。比如 Cortex-A 處理器的 irq 中斷服務函數都是彙編寫的,主要用匯編
來實現現場的保護和恢復、獲取中斷號等。但是具體的中斷處理過程都是 C 函數,所以就會存
在彙編中呼叫 C 函數的問題。而且當 C 語言版本的中斷處理常式執行完成以後是需要返回到
irq 彙編中斷服務函數,因爲還要處理其他的工作,一般是恢復現場。這個時候就不能直接使用
B 指令了,因爲 B 指令一旦跳轉就再也不會回來了,這個時候要使用 BL 指令,範例程式碼如下
push {r0, r1} @儲存r0, r1
cps #13 @進入SVC模式,允許其他中斷再次進入
bl system_irqhandler @載入 C 語言中斷處理常式到 r2 暫存器中
cps #0x12 @進入 IRQ 模式
pop {r0, r1}
str r0, [r1, #0X10] @中斷執行完成,寫 EOIR
上述程式碼中第 5 行就是執行 C 語言版的中斷處理常式,當處理完成以後是需要返回來繼續
執行下面 下麪的程式,所以使用了 BL 指令。
彙編中也可以進行算術運算, 比如加減乘除,常用的運算指令用法如下表所示:
我們用 C 語言進行 CPU 暫存器設定的時候常常需要用到邏輯運算子號,比如「&」、「|」等邏輯運算子。使用匯編語言的時候也可以使用邏輯運算指令,常用的運算指令用法如下表所示: