ARM汇编基础详解(PS学习汇编的原因)
目錄
- 前言
- 1.GNU 匯編語法
- 2.Cortex-A7 常用匯編指令
- 2.1 處理器內部數據傳輸指令(內部寄存器數據非內存數據)
- 2.2 存儲器訪問指令(RAM)
- 2.3 壓棧和出棧指令(了解)
- 2.4 跳轉指令
- 2.5 算術運算指令
- 2.6 邏輯運算指令
前言
我們在進行嵌入式 Linux 開發的時候是絕對要掌握基本的 ARM 匯編,因為 Cortex-A 芯片一上電 SP 指針(指向一段起始地址,指向棧頂)還沒初始化,C 環境還沒準備好,所以肯定不能運行 C 代碼,必須先用匯編語言設置好 C 環境,比如初始化 DDR、設置 SP指針等等,當匯編把 C 環境設置好了以后才可以運行 C 代碼。所以 Cortex-A 一開始肯定是匯編代碼,其實 STM32 也一樣的,一開始也是匯編,以 STM32F103 為例,啟動文件startup_stm32f10x_hd.s 就是匯編文件,只是這個文件 ST 已經寫好了,我們根本不用去修改,所以大部分學習者都沒有深入的去研究。匯編的知識很龐大,本章我們只講解最常用的一些指令,滿足我們后續學習即可。
至于要寫多少匯編程序,那就看你能在哪一步把C 語言環境準備好。所謂的C語言環境就是保證C 語言能夠正常運行。C 語言中的函數調用涉及到出棧入棧,出棧入棧就要對堆棧進行操作,所謂的堆棧其實就是一段內存,這段內存比較特殊,由SP 指針訪問,SP 指針指向棧頂。芯片一上電SP 指針還沒有初始化,所以C 語言沒法運行,對于有些芯片還需要初始化DDR,因為芯片本身沒有RAM,或者內部RAM 不開放給用戶使用,用戶代碼需要在DDR 中運行,因此一開始要用匯編來初始化DDR 控制器。后面學習Uboot 和Linux 內核的時候匯編是必須要會的。在芯片上電以后用匯編來初始化一些外設,不會涉及到復雜的代碼,而且使用到的指令都是很簡單的,用到的就那么十幾個指令。
1.GNU 匯編語法
如果大家使用過 STM32 的話就會知道 MDK 和 IAR 下的啟動文件 startup_stm32f10x_hd.s其中的匯編語法是有所不同的,將 MDK 下的匯編文件直接復制到 IAR 下去編譯就會出錯,因為 MDK 和 IAR 的編譯器不同,因此對于匯編的語法就有一些小區別。我們要編寫的是 ARM匯編,編譯使用的 GCC 交叉編譯器,所以我們的匯編代碼要符合 GNU 語法。
GNU 匯編語法適用于所有的架構,并不是 ARM 獨享的,GNU 匯編由一系列的語句組成,每行一條語句,每條語句有三個可選部分,如下:
label:instruction @ commentlabe: l即標號,表示地址位置,有些指令前面可能會有標號,這樣就可以通過這個標號得到指令的地址,標號也可以用來表示數據地址。注意 label 后面的“:”,任何以“:”結尾的標識符都會被識別為一個標號。
instruction: 即指令,也就是匯編指令或偽指令
@ 符號:表示后面的是注釋,就跟 C 語言里面的“/”和“/”一樣,其實在 GNU 匯編文件中我們也可以使用“/”和“/”來注釋。
comment: 就是注釋內容。
比如如下代碼:
add: MOVS R0, #0X12 @設置 R0=0X12上面代碼中“add:”就是標號,“MOVS R0,#0X12”就是指令,最后的“@設置 R0=0X12”就是注釋。
注意!ARM 中的指令、偽指令、偽操作、寄存器名等可以全部使用大寫,也可以全部使用小寫,但是不能大小寫混用。
用戶可以使用.section 偽操作來定義一個段,每個段以段名開始,以下一段名或者文件結尾結束,比如:
.section .testsection @定義一個 testsetcion 段匯編系統預定義了一些段名:
| .text | 表示代碼段。 |
| .data | 初始化的數據段。 |
| .bss | 未初始化的數據段。 |
| .rodata | 只讀數據段。 |
匯編程序的默認入口標號是_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 匯編還有其它的偽操作,但是最常見的就是上面這些,如果想詳細的了解全部的偽操作,可以參考《ARM Cortex-A(armV7)編程手冊 V4.0.pdf》的 57 頁。
GNU 匯編同樣也支持函數,函數格式如下:
函數名:函數體返回語句GNU 匯編函數返回語句不是必須的,如下代碼就是用匯編寫的。 Cortex-A7 中斷服務函數:
匯編函數的定義 /* 未定義中斷 */ Undefined_Handler:ldr r0, =Undefined_Handlerbx r0/* SVC 中斷 */ SVC_Handler:ldr r0, =SVC_Handlerbx r0/* 預取終止中斷 */ PrefAbort_Handler:ldr r0, =PrefAbort_Handlerbx r0上述代碼中定義了三個匯編函數:Undefined_Handler、SVC_Handler 和PrefAbort_Handler。以函數 Undefined_Handler 為例我們來看一下匯編函數組成:
- “Undefined_Handler”就是函數名,
- “ldr r0, =Undefined_Handler”是函數體,
- “bx r0”是函數返回語句,“bx”指令是返回指令,函數返回語句不是必須的。
2.Cortex-A7 常用匯編指令
介紹一些常用的 Cortex-A7 匯編指令,如果想系統的了解 Cortex-A7 的所有匯編指令請參考《ARM ArchitectureReference Manual ARMv7-A and ARMv7-R edition.pdf》的 A4章節。
2.1 處理器內部數據傳輸指令(內部寄存器數據非內存數據)
使用處理器做的最多事情就是在處理器內部來回的傳遞數據,常見的操作有:
①、將數據從一個寄存器傳遞到另外一個寄存器。
②、將數據從一個寄存器傳遞到特殊寄存器,如 CPSR 和 SPSR 寄存器。
③、將立即數傳遞到寄存器。
數據傳輸常用的指令有三個:MOV、MRS 和 MSR,這三個指令的用法如表 7.2.1.1 所示:
| MOV | R0 | R1 | 將 R1 里面的數據復制到 R0 中。 |
| MRS | R0 | CPSR | 將特殊寄存器 CPSR 里面的數據復制到 R0 中。 |
| MSR | CPSR | R1 | 將 R1 里面的數據復制到特殊寄存器 CPSR 里中。 |
分別來詳細的介紹一下如何使用這三個指令:
1 、MOV 指令
MOV 指令用于將數據從一個寄存器拷貝到另外一個寄存器,或者將一個立即數傳遞到寄存器里面,使用示例如下:
MOV R0,R1 @將寄存器 R1 中的數據傳遞給 R0,即 R0=R1 MOV R0, #0X12 @將立即數 0X12 傳遞給 R0 寄存器,即 R0=0X122 、MRS 指令
MRS 指令用于將特殊寄存器(如 CPSR 和 SPSR)中的數據傳遞給通用寄存器,要讀取特殊寄存器的數據只能使用 MRS 指令!使用示例如下:
MRS R0, CPSR @將特殊寄存器 CPSR 里面的數據傳遞給 R0,即 R0=CPSR3 、MSR 指令
MSR 指令和 MRS 剛好相反,MSR 指令用來將普通寄存器的數據傳遞給特殊寄存器,也就是寫特殊寄存器,寫特殊寄存器只能使用 MSR,使用示例如下:
MSR CPSR, R0 @將 R0 中的數據復制到 CPSR 中,即 CPSR=R0上面三條指令均不能實現a=b(賦值),因為他們直接操作的是處理器內部寄存器(上篇博文給出了內部寄存器的示意圖),而a=b(賦值)是對內存(RAM)的操作。
下面的存儲器(即RAM,不是SD卡,不是FLASH)訪問指令才可以實現。
2.2 存儲器訪問指令(RAM)
ARM的CPU 不能直接訪問存儲器(RAM)中的數據,I.MX6UL 中的寄存器就是 RAM 類型的,我們用匯編來配置 I.MX6UL 寄存器的時候需要借助存儲器訪問指令,一般先將要配置的值寫入到 Rx(x=0~12)(通用)寄存器中,然后借助存儲器訪問指令將 Rx 中的數據寫入到 I.MX6UL 寄存器中。讀取 I.MX6UL 寄存器也是一樣的,只是過程相反。常用的存儲器訪問指令有兩種:LDR 和STR,用法如表:
| LDR Rd, [Rn , #offset] | 從存儲器(RAM) Rn+offset 的位置讀取數據存放到 Rd 中。 |
| STR Rd, [Rn, #offset] | 將 Rd 中的數據寫入到存儲器(RAM) 中的 Rn+offset 位置。 |
分別來詳細的介紹一下如何使用這兩個指令(寫驅動最常用的兩個指令):
1 、LDR 指令
LDR 主要用于從存儲器(RAM)加載數據到寄存器 Rx 中,LDR 也可以將一個立即數加載到寄存器 Rx中,LDR 加載立即數的時候要使用“=”,而不是“#”。在嵌入式開發中,LDR 最常用的就是讀取 CPU 的寄存器值,比如 I.MX6UL 有個寄存器 GPIO1_GDIR,其地址為 0X0209C004,我們現在要讀取這個寄存器中的數據,示例代碼如下:
示例代碼 LDR 指令使用LDR R0, =0X0209C004 @將寄存器地址 0X0209C004 加載到 R0 中,即 R0=0X0209C004LDR R1, [R0] @讀取地址 0X0209C004 中的數據到 R1 寄存器中上述代碼就是讀取寄存器 GPIO1_GDIR 中的值,讀取到的寄存器值保存在 R1 寄存器中,上面代碼中 offset 是 0,也就是沒有用到 offset。
2 、STR 指令
LDR 是從存儲器讀取數據,STR 就是將數據寫入到存儲器中,同樣以 I.MX6UL 寄存器GPIO1_GDIR 為例,現在我們要配置寄存器 GPIO1_GDIR 的值為 0X2000002,示例代碼如下:
示例代碼 STR 指令使用LDR R0, =0X0209C004 @將寄存器地址 0X0209C004 加載到 R0 中,即 R0=0X0209C004LDR R1, =0X20000002 @R1 保存要寫入到寄存器的值,即 R1=0X20000002STR R1, [R0] @將 R1 中的值寫入到 R0 中所保存的地址中LDR 和 STR 都是按照字進行讀取和寫入的,也就是操作的 32 位數據,如果要按照字節、半字進行操作的話可以在指令“LDR”后面加上 B 或 H,比如按字節操作的指令就是 LDRB 和STRB,按半字操作的指令就是 LDRH 和 STRH。
2.3 壓棧和出棧指令(了解)
我們通常會在 A 函數中調用 B 函數,當 B 函數執行完以后再回到 A 函數繼續執行。要想在跳回 A 函數以后代碼能夠接著正常運行,那就必須在跳到 B 函數之前將當前處理器狀態保存起來(就是保存 R0~R15 這些寄存器值),當 B 函數執行完成以后再用前面保存的寄存器值恢復R0~R15 即可。保存 R0~R15 寄存器的操作就叫做現場保護,恢復 R0~R15 寄存器的操作就叫做恢復現場。在進行現場保護的時候需要進行壓棧(入棧)操作,恢復現場就要進行出棧操作。壓棧的指令為 PUSH,出棧的指令為 POP,PUSH 和 POP 是一種多存儲和多加載指令,即可以一次操作多個寄存器數據,他們利用當前的棧指針 SP 來生成地址,PUSH 和 POP 的用法如表所示:
| PUSH | 將寄存器列表存入棧中。 |
| POP | 從棧中恢復寄存器列表。 |
假如我們現在要將 R0~R3 和 R12 這 5 個寄存器壓棧,當前的 SP 指針指向 0X80000000,處理器的堆棧是向下增長的,使用的匯編代碼如下:
PUSH {R0~R3, R12} @將 R0~R3 和 R12 壓棧壓棧完成以后的堆棧如圖所示:
就是分兩步對 R0~R3,R2 和 LR 進行壓棧以后的堆棧模型,如果我們要出棧的話就是使用如下代碼:
出棧的就是從棧頂,也就是 SP 當前執行的位置開始,地址依次減小來提取堆棧中的數據到要恢復的寄存器列表中。PUSH 和 POP 的另外一種寫法是“STMFD SP!”和“LDMFD SP!”,
因此上面的匯編代碼可以改為:
STMFD 可以分為兩部分:STM 和 FD,同理,LDMFD 也可以分為 LDM 和 FD。看到 STM和 LDM 有沒有覺得似曾相識(不是 STM32 啊啊啊啊),前面我們講了 LDR 和 STR,這兩個是數據加載和存儲指令,但是每次只能讀寫存儲器中的一個數據。STM 和 LDM 就是多存儲和多加載,可以連續的讀寫存儲器中的多個連續數據。FD 是 Full Descending 的縮寫,即滿遞減的意思。根據 ATPCS 規則,ARM 使用的 FD 類型的堆棧,SP 指向最后一個入棧的數值,堆棧是由高地址向下增長的,也就是前面說的向下增長的堆棧,因此最常用的指令就是 STMFD 和 LDMFD。STM 和 LDM 的指令寄存器列表中編號小的對應低地址,編號高的對應高地址。
2.4 跳轉指令
有多種跳轉操作,比如:
①、直接使用跳轉指令 B、BL、BX 等。
②、直接向 PC 寄存器里面寫入數據。
上述兩種方法都可以完成跳轉操作,但是一般常用的還是 B、BL 或 BX,用法如表:
| B(回不來) | 跳轉到 label,如果跳轉范圍超過了+/-2KB,可以指定 B.W使用 32 位版本的跳轉指令, 這樣可以得到較大范圍的跳轉 |
| BX | 間接跳轉,跳轉到存放于 Rm 中的地址處,并且切換指令集 |
| BL(回得來) | 跳轉到標號地址,并將返回地址保存在 LR 中。 |
| BLX | 結合 BX 和 BL 的特點,跳轉到 Rm 指定的地址,并將返回地址保存在 LR 中,切換指令集。 |
我們重點來看一下 B 和 BL 指令,因為這兩個是我們用的最多的,如果要在匯編中進行函數調用使用的就是 B 和 BL 指令:
1 、B 指令
這是最簡單的跳轉指令,B 指令會將 PC 寄存器的值設置為跳轉目標地址,一旦執行 B 指令,ARM 處理器就會立即跳轉到指定的目標地址。如果要調用的函數不會再返回到原來的執行處,那就可以用 B 指令,如下示例:
示例代碼 B 指令示例_start:ldr sp,=0X80200000 @設置棧指針b main @跳轉到 main 函數上述代碼就是典型的在匯編中初始化 C運行環境,然后跳轉到C文件的 main函數中運行,上述代碼只是初始化了 SP 指針,有些處理器還需要做其他的初始化,比如初始化 DDR 等等。因為跳轉到 C 文件以后再也不會回到匯編了,所以在第 4 行使用了 B 指令來完成跳轉。
2 、BL 指令
BL 指令相比 B 指令,在跳轉之前會在寄存器 LR(R14)中保存當前 PC 寄存器值,所以可以通過將 LR 寄存器中的值重新加載到 PC 中來繼續從跳轉之前的代碼處運行,這是子程序調用一個基本但常用的手段。比如 Cortex-A 處理器的 irq 中斷服務函數都是匯編寫的,主要用匯編來實現現場的保護和恢復、獲取中斷號等。但是具體的中斷處理過程都是 C 函數,所以就會存在匯編中調用 C 函數的問題。而且當 C 語言版本的中斷處理函數執行完成以后是需要返回到irq 匯編中斷服務函數,因為還要處理其他的工作,一般是恢復現場。這個時候就不能直接使用B 指令了,因為 B 指令一旦跳轉就再也不會回來了,這個時候要使用 BL 指令,示例代碼如下:
示例代碼 BL 指令示例 1 push {r0, r1} @保存 r0,r1 2 cps #0x13 @進入 SVC 模式,允許其他中斷再次進去 3 5 bl system_irqhandler @加載 C 語言中斷處理函數到 r2 寄存器中 6 7 cps #0x12 @進入 IRQ 模式 8 pop {r0, r1} 9 str r0, [r1, #0X10] @中斷執行完成,寫 EOIR上述代碼中第 5 行就是執行 C 語言版的中斷處理函數,當處理完成以后是需要返回來繼續執行下面的程序,所以使用了 BL 指令。
2.5 算術運算指令
匯編中也可以進行算術運算, 比如加減乘除,常用的運算指令用法如表 所示:
| ADD Rd, Rn, Rm | Rd = Rn + Rm | 加法運算,指令為 ADD |
| ADD Rd, Rn, #immed | Rd = Rn + #immed | 加法運算,指令為 ADD |
| ADC Rd, Rn, Rm | Rd = Rn + Rm + 進位 | 帶進位的加法運算,指令為 ADC |
| ADC Rd, Rn, #immed | Rd = Rn + #immed +進位 | 帶進位的加法運算,指令為 ADC |
| SUB Rd, Rn, Rm | Rd = Rn – Rm | 減法 |
| SUB Rd, #immed | Rd = Rd - #immed | 減法 |
| SUB Rd, Rn, #immed | Rd = Rn - #immed | 減法 |
| SBC Rd, Rn, #immed | Rd = Rn - #immed – 借位 | 帶借位的減法 |
| SBC Rd, Rn ,Rm | Rd = Rn – Rm – 借位 | 帶借位的減法 |
| MUL Rd, Rn, Rm | Rd = Rn * Rm | 乘法(32 位) |
| UDIV Rd, Rn, Rm | Rd = Rn / Rm | 無符號除法 |
| SDIV Rd, Rn, Rm | Rd = Rn / Rm | 有符號除法 |
在嵌入式開發中最常會用的就是加減指令,乘除基本用不到。
2.6 邏輯運算指令
我們用 C 語言進行 CPU 寄存器配置的時候常常需要用到邏輯運算符號,比如“&”、“|”等邏輯運算符。使用匯編語言的時候也可以使用邏輯運算指令,常用的運算指令用法如表 所示:
邏輯運算指令都很好理解,后面時候匯編配置 I.MX6UL 的外設寄存器的時候可能會用到
總結
以上是生活随笔為你收集整理的ARM汇编基础详解(PS学习汇编的原因)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 分享一个最终幻想勇气启示录的脚本,能自动
- 下一篇: 为什么学习线性代数_工程应用简介