文章標題

一、引言

最近終於有時間把《X86彙編語言:從實模式到保護模式》這本書好好讀了一遍,真是暢快!這本書從書名看,是一本講x86彙編語言的書,但它真正的價值是一步一步帶領讀者進入操作系統的世界,寫得非常的好,很佩服作者能夠透過這本書將操作系統的原理講清楚!

本人是搞嵌入式開發的,讀完這本書後,我覺得完全可以按照此書的寫作思路來寫一本《從ARM彙編到操作系統》的書,雖然自己對ARM的掌握還很有限,但是我覺得值得嘗試一下,一方面可以將自己這幾年積累的知識總結一下,另一方面可以系統得學習一下ARM相關的知識。

二、硬件平臺

由於手頭有一塊閒置的ARM9的板子,所以打算就用這塊板子作爲實踐的平臺。這塊板子的配置爲:
CPU:Atmel公司的At91SAM9260
SDRAM:
DataFlash:
NandFlash:

(1)啓動

ARM上電執行第一條代碼是從 0x0開始的,這是誰都不能改變的。但是0x0的地址可以通過很多方法進行映射,執行不同的代碼。有些廠家在裸ARM芯片上增加了一小段啓動代碼,用以完成從特殊設備上加載代碼,啓動ARM。這種情況是非常多的,一些設計良好的ARM芯片,都可以從SPI Flash上加載,從UART上加載,大大的簡化了ARM單板第一次燒寫程序的複雜度。

AT91SAM9260是一款設計良好的ARM芯片。支持從NandFlash和DataFlash(SPI)上啓動。從芯片手冊上可知,DataFlash支持從SPI0口的NPCS0或者NPCS1啓動,或者從其他設備啓動。

AT91SAM9260的內存佈局圖:
這裏寫圖片描述

芯片內部的存儲空間:

這裏寫圖片描述

芯片內部的映射圖:

這裏寫圖片描述

系統總是從地址0x0啓動,芯片復位後,片內ROM被同時映射到地址0x0000_0000和0x0010_0000。REMAP(重映射)允許將芯片內部的SRAM0映射到地址0x0000_0000,如上圖所示,當REMAP=1時,芯片內部的SRAM0映射到地址0x0000_0000。當REMAP=0時,BMS決定是從芯片內部ROM啓動還是從EBI_NCS0片外存儲啓動。

這裏寫圖片描述

這裏寫圖片描述

當系統啓動選擇片內啓動(BMS=1)時,程序上電後,內部ROM被映射在0x0000_0000地址,啓動內部ROM的固化程序(Boot Program),將自動檢測DataFlash或者NandFlash中前48個字節的數據,如果數據正確,則爲有效代碼,這時候系統自動將存在DataFlash或者NandFlash中的有效代碼拷貝至SRAM0中去,接下來需要進行存儲器的REMAP(即REMPA=1),經過REMAP後,SRAM0從映射前的0x0020_0000地址被映射到了0x0000_0000地址並且程序從此處開始執行。

當系統啓動選擇片外啓動(BMS=0)時,程序上電後,EBI_NCS0(片選0)被映射在0x0000_0000地址,如果EBI_NCS0連接的是NorFlash,則可以直接從NorFlash啓動。

內部ROM的固化程序Boot Program完成了如下工作:

這裏寫圖片描述

這裏寫圖片描述

三、ARM處理器

四、ARM處理器模式

ARM處理器在運行過程中可以在不同的處理模式間切換,但任一時刻只能處在一種模式下。在ARM11之前,ARM處理器一共有7種模式,從ARM11以後開始增加了第8種模式(Secure Mode)。處理器在運行代碼時,會根據情況在7種模式間不停切換。對於Linux系統來說,只用到了usr、irq、svc、abt和und這5種模式。

7種模式具體如下:

(1)用戶模式(User,usr):正常程序執行的模式
(2)快速中斷模式(FIQ,fiq):用於高速數據傳輸和通道處理
(3)外部中斷模式(IRQ,irq):用於通常的中斷處理
(4)特權模式(Supervisor,svc):供操作系統使用的一種保護模式
(5)數據訪問中止模式(Abort,abt):用於虛擬存儲及存儲保護
(6)未定義指令中止模式(Undefined,und):用於支持通過軟件仿真硬件的協處理器
(7)系統模式(System,sys):用於運行特權級的操作系統任務

除了用戶模式之外的其他6種處理器模式稱爲特權模式(Privileged Modes)。在這些模式下,程序可以訪問所有的系統資源,也可以任意地進行處理器模式的切換。其中,除系統模式外,其他5種特權模式也稱爲異常模式。

處理器模式可以通過軟件控制進行切換,也可以通過外部中斷或異常處理過程進行切換。大多數的用戶程序運行在用戶模式下。此時,應用程序不能夠訪問一些受操作系統保護的系統資源。應用程序也不能直接進行處理器模式的切換。當需要進行處理器模式的切換時,應用程序可以產生異常處理,在異常處理過程中進行處理器模式的切換。這種體系結構可以使操作系統控制整個系統的資源。當應用程序發生異常中斷時,處理器進入相應的異常模式。在每一種異常模式中都有一組寄存器,供相應的異常處理程序使用,這樣就可以保證在進入異常模式時,用戶模式下的寄存器(保存可程序運行狀態)不被破壞。

系統模式並不是通過異常過程進入的,它和用戶模式具有完全一樣的寄存器。但是系統模式屬於特權模式,能夠訪問所有的系統資源,也可以直接進行處理器模式的切換,它主要供操作系統任務使用。通常操作系統任務需要訪問所有的系統資源,同時該任務仍然使用用戶模式的寄存器組,而不是使用異常模式下相應的寄存器組,這樣可以保證當異常中斷髮生時任務狀態不被破壞。

五、ARM寄存器

ARM處理器共有37個寄存器,其中包括:
(1)31個通用寄存器,包括程序計數器(pc)在內。這些寄存器都是32位寄存器。
(2)6個狀態寄存器,這些寄存器都是32位寄存器。

ARM處理器共有7種不同的處理器模式,在每一種處器模式下都有一組相應的寄存器組。任意時刻(也就是任意的處理器模式下),可見的寄存器包括15個通用寄存器(R0~R14)、一個或兩個狀態寄存器及程序計數器(pc)。在所有的寄存器中,有些是各模式共用的同一個物理寄存器;有一些寄存器是各模式自己擁有的獨立的物理寄存器。

這裏寫圖片描述

通用寄存器可以分爲以下3類:

(1)未備份寄存器,包括R0~R7

對於每一個未備份寄存器來說,在所有的處理器模式下指的都是同一個物理寄存器。

(2)備份寄存器,包括R8~R14

對於備份寄存器R8~R12來說,每個寄存器對應兩個不同的物理寄存器。當中斷處理非常簡單,僅使用R8~R12寄存器時,FIQ處理程序可以不必執行保存和恢復中斷現場的指令,從而可以使中斷處理過程非常迅速。
對於備份寄存器R13和R14來說,每個寄存器對應6個不同的物理寄存器,其中一個是用戶模式和系統模式共用的,另外的5個對應於其他5種處理器模式。

(3)程序計數器(pc),即R15

上面的寄存器中,R0~R15是通用寄存器,可用來存儲任意值;CPSR/SPSR是狀態寄存器,用於獲取ARM當前狀態以及模式控制。
R13:常用作棧指針(sp),保存當前處理器模式的堆棧的棧頂地址
R14:鏈接寄存器(lr),在調用子程序時保存返回地址
R15:程序計數器(pc),處理器要取的下一條指令的地址
CPSR:通用狀態寄存器
SPSR:備份狀態寄存器(當中斷或異常發生時保存CPSR,用戶模式下沒有SPSR)

六、ARM異常中斷

當一個異常或中斷髮生時,ARM處理器會把程序計數器(pc)設置到一個特定的地址範圍,然後從這些地址中加載指令執行;這一特定的地址範圍稱爲中斷/異常向量表(vector-table);

中斷/異常向量表是由軟件開發人員準備的,軟件人員要負責在向量表中放置一系列跳轉指令,跳轉到專門處理某個異常或中斷的子程序(這些子程序也由軟件人員負責準備);

中斷/異常向量表可以放置在低地址0x0000_0000處,或者高地址0xFFFF_0000處;當ARM處理器復位時,處理器採用物理地址,此時從地址0x0處查找異常向量表;當程序準備好頁表並使能MMU後,處理器將採用虛擬地址,此時應當到地址0xFFFF_0000處查找異常向量表。

當一個異常或中斷髮生時,處理器會掛起正常的操作,轉而從向量表的對應位置裝載指令執行。

(1)異常/中斷向量表的位置分佈

異常/中斷             地址
復位                  0x0
未定義指令             0x4或0xffff,0004
軟件中斷(SWI)          0x8或0xffff,0008
取指令異常             0xc或0xffff,000c
取數據異常             0x10或0xffff,0010
保留                  0x14或0xffff,0014
中斷IRQ               0x18或0xffff,0018
快速中斷FIQ            0x1c或0xffff,001c

(2)說明

復位(RESET):
處理器上電後執行的第一條指令的位置。這條指令將跳轉到初始化代碼。復位異常中斷通常發生在如下情況下:
a、系統加電時
b、系統復位時
c、跳轉到復位中斷向量處執行,稱爲軟復位

未定義指令(UNDEF):
處理器無法對一條指令譯碼時會產生異常,跳轉到這裏

軟件中斷(SWI):
處理器執行一條SWI指令後跳轉到此處,此時,ARM核從非特權的USR模式切換到特權的SVC模式;
Linux從用戶態到內核態的切換就是通過SWI指令實現的。用戶應用程序使用的系統調用(如open,read,write)就是通過軟件中斷切換到內核中

預取指中止(PART):
處理器試圖從一個未獲得正確訪問權限的地址取指令時發生

預取數據中止(DART):
處理器試圖從一個未獲得正確訪問權限的地址取數據時發生

中斷請求(IRQ):
當外設給處理器發來一箇中斷請求後,會跳轉到此處

快速中斷請求(FIQ):
外設可以將自己產生的中斷設置爲FIQ,比IRQ優先級高

七、ARM存儲系統

(1)ARM的存儲空間

ARM體系使用單一的平板地址空間。
a、該地址空間可以被看作是2^32個字節單元,這些字節單元的地址是一個無符號的32位數值,其取值範圍爲0~2^32-1。
b、該地址空間可以被看作是2^30個字單元,這些字單元的地址可以被4整除,也就是該地址的低兩位爲0。
c、該地址空間可以被看作是2^31個半字單元,這些半字單元的地址可以被2整除,也就是該地址的最低位爲0。

(2)ARM的存儲格式

ARM體系中的數據存儲格式有兩種:big-endian和little-endian,即大端和小端格式

(3)非對齊的存儲訪問操作

在ARM中,通常希望字單元的地址是字對齊的(地址低兩位爲0),半字單元的地址是半字對齊的(地址的最低位爲0)。在存儲訪問操作中,如果存儲單元的地址沒有遵守上述的對齊規則,則稱爲非對齊的存儲訪問操作。

a、非對齊的指令預取操作
b、非對齊的數據訪問操作

八、ARM彙編程序設計

(1)交叉編譯工具

源文件需要經過編譯才能生成可執行文件。PC上的編譯工具鏈爲gcc、ld、objcopy、objdump等,ARM平臺上必須使用交叉編譯工具arm-linux-gcc、arm-linux-ld等。

一個C/C++文件要經過如下4步才能變成可執行文件:
A、預處理:生成 .i文件,將要包含的文件插入原文件中、將宏定義展開、根據條件編譯選擇要使用的代碼,使用arm-linux-cpp工具
B、編譯:生成 .s文件(彙編代碼),使用ccl工具
C、彙編:生成 .o文件(ELF目標文件,機器代碼),使用arm-linux-as工具
D、連接:生成可執行文件,將生成的OBJ文件和系統庫的OBJ文件、庫文件連接起來,使用arm-linux-ld工具,被傳遞給連接器的文件,通常包括以下兩種:
a、.o目標文件(OBJ文件)
b、.a歸檔庫文件

1、arm-linux-gcc:

常用選項(區分大小寫):
-c
完成預處理、編譯和彙編,不執行連接,生成.o文件

-S
完成預處理和編譯,不進行彙編和連接,生成.s文件

-E
完成預處理後停止,不執行後續動作,預處理後的代碼直接在控制檯輸出

-o filename
指定輸出的文件名,和上面的選項配套使用

$>arm-linux-gcc hello.c -S -o hello.s
$>arm-linux-gcc hello.c -E -o hello.i

-v
顯示編譯的詳細過程(verbose)

-Wall
打開所有的警告信息

代碼優化:
-O0: 不優化
-O1: 打開一定的優化選項
-O2: 除了不進行循環展開和函數內嵌,執行幾乎所有的優化(常見)
-O3: 在O2的基礎上增加了”-finline-functions”選項

-I/-L/-l
-I:增加頭文件的搜索路徑(默認爲/usr/include等)
-L:增加庫文件的搜索路徑(默認爲/usr/lib等)
-l:指明程序要鏈接到的庫,可以是動態庫(.so)或靜態庫(.a)

$>arm-linux-gcc -I/var/include/ -L/var/lib/ -labc -o hello hello.c

在/var/include下檢索頭文件
在/var/lib下檢索庫文件
Linux下的庫文件都以lib開頭,因此鏈接的庫爲libabc.so(如果沒有動態庫,則鏈接.a靜態庫)

-nostdlib
不連接系統標準啓動文件和標準庫文件,常用於編譯uboot和內核。這些程序和普通的應用程序不同,不需要標準啓動文件和庫。

-static
鏈接靜態庫,而不是動態庫。編譯器會把整個的.a庫加入應用程序

$>arm-linux-gcc -static -o hello hello.c
$>file hello

2、arm-linux-ld:

ld爲鏈接器,用於將目標文件、庫文件等鏈接成可執行的elf應用程序;arm-linux-gcc在生成a.out時會默認調用ld;編譯較大型軟件的常見用法是,由arm-linux-gcc生成多個.o文件,然後由arm-linux-ld統一生成一個可執行文件。

選項“-T”:可以直接使用它來指定代碼段、數據段、BSS段的起始地址,也可以用來指定一個連接腳本,在腳本中進行更復雜的地址設置。

“-T”選項只用於連接Bootloader、內核等“沒有底層軟件支持”的軟件;連接運行於操作系統之上的應用程序時,無需指定“-T”選項,它們使用默認的方式進行連接。

格式如下:
-Ttext startaddress
-Tdata startaddress
-Tbss startaddress

3、arm-linux-objcopy

被用來複制一個目標文件的內容到另一個文件中,可使用不同於源文件的格式來輸出目標文件,即文件格式轉換工具,常用於將elf格式的可執行應用程序轉化爲bin格式的程序。

$>arm-linux-objcopy -O binary led led.bin

-O binary指明輸出bin格式的應用程序,最後面的兩個參數分別爲input-file和output-file

4、arm-linux-objdump

用於顯示二進制文件信息,常用於對.o文件或可執行文件進行反彙編。

常用選項:
-d
反彙編可執行段
-D
反彙編所有段
-f
顯示文件的整體頭部摘要信息
-h
顯示文件中各個段的頭部摘要信息
-m arm
指定反彙編時使用的架構
-b binary
指定要反彙編的文件格式

$>arm-linux-objdump -D led01
$>arm-linux-objdump -D -b binary -m arm led01.bin

5、arm-linux-nm

用於顯示.o或elf程序中的符號。

$>arm-linux-nm hello

(2)ARM彙編語言程序格式

ARM彙編語言以段(section)爲單位組織源代碼。段是相對獨立的、具有特定名稱的、不可分割的指令或者數據序列。段又可以分爲代碼段和數據段,代碼段存放執行代碼,數據段存放代碼執行時需要用到的數據。一個ARM源程序至少需要一個代碼段,大的程序可以包含多個代碼段和數據段。

ARM彙編語言源程序經過彙編處理後生成一個可執行的映像文件,該可執行的映像文件包含以下3部分:

a、一個或多個代碼段,代碼段通常是隻讀的
b、零個或多個包含初始值的數據段,這些數據段通常是可讀可寫的
c、零個或多個不包含初始值的數據段,這些數據段被初始化爲0,通常是可讀可寫的

連接器根據一定的規則將各個段安排到內存中的相應位置。源程序中段之間的相鄰關係與可執行映像文件中段之間的相鄰關係並不一定相同。

(3)ARM指令集

A、ARM指令集的版本

ARM採用精簡指令架構(RISC),其指令集目前已經發展到v8,從軟件兼容性方面考慮,新的指令集一般主要增加一些DSP、多媒體處理等方面的功能,很少會修改或廢棄舊的指令;但是ARM並不像INTEL一樣嚴格保證向後兼容,這會導致一些老的程序不能在新的處理器上運行。

B、ARM指令的基本語法

ARM指令通常帶有2個或3個操作數,例如加法指令

add Rd, Rn, Rm

Rd:目標寄存器
Rn: 源寄存器1
Rm:源寄存器2

add r3, r1, r2

把存放在寄存器r1和r2中的值相加,然後把結果放到r3中。

C、指令的長度和書寫方法

ARM指令編碼後每條32位;爲了節省內存空間,也可以編碼爲16位的thumb指令。

書寫ARM彙編指令時,可不區分大小寫,但建議格式統一,要麼統一用大寫,要麼統一用小寫。

D、註釋方法

如果是使用arm公司提供的彙編器,則分號“;”後面的內容爲註釋;如果使用arm-linux-as,則將”@”後面的內容視爲註釋。

E、ARM-Thumb過程調用標準(ATPCS)

當一個項目中既包括用彙編語言寫的源文件,也包括用C語言寫的源文件時,就必然要涉及到彙編代碼和C函數之間的調用問題;此時需要定義一套規範,以指明函數調用時如何傳遞參數,如何傳遞返回值等;針對這一要求,ARM提出了一套標準,稱爲ARM-Thumb過程調用標準(ATPCS),其中規定了如何通過寄存器來傳遞函數的參數和返回值。

ATPCS標準中最主要的規則如下:
a、一個函數中最前面的四個參數通過ARM的前4個寄存器r0、r1、r2和r3傳遞
b、從第5個參數開始,以後的參數通過遞減滿堆棧傳遞
c、函數返回的整型變量通過寄存器r0傳遞

函數參數傳遞圖:

sp+16 參數8
sp+12 參數7
sp+8 參數6
sp+4 參數5
sp 參數4
r3 參數3
r2 參數2
r1 參數1
r0 參數0 返回值

r0-r3:用來存放函數的前4個參數,r0還用於保存函數的返回值
r4-r12: 可以用來分配局部變量,但使用前要通過堆棧保存
r13:棧指針
r14:函數調用時保存返回地址
r15:pc

函數調用的優化原則:儘量限制一個函數的參數,不要超過4個。

F、常用指令

1、數據處理指令:mov/mvn

最簡單的ARM指令,執行的結果就是把一個立即數N送到目標寄存器Rd,N可以是通用寄存器,也可以是常量。語法:

  mov{<cond>}{S} Rd, N
  mvn{<cond>}{S} Rd, ~N

mov:把一個32位數存入寄存器Rd,即Rd = N
mvn:把一個32位數按位取反後存入寄存器Rd
N爲立即數,可以是一個寄存器Rn,也可以是一個使用”#”作爲前綴的常量。指令可支持條件執行,可以附加S位。

mov r7, r5
mov r4, #0xbd00

2、算術指令:add/sub

用於實現32位有符號數或無符號數的加法和減法。語法:

  add{<cond>}{S} Rd, Rn, N
  sub{<cond>}{S} Rd, Rn, N

add:32位加法,Rd=Rn+N
sub:32位減法,Rd=Rn-N
N爲立即數,可以是一個寄存器Rm,也可以是一個使用”#”作爲前綴的常量,還可以包括移位

sub r0, r1, r2
add r0, r1, r1

3、邏輯指令

對2個源操作數進行按位的邏輯操作,結果存儲到目標寄存器中。語法:

and{<cond>}{S} Rd, Rn, N
orr{<cond>}{S} Rd, Rn, N
eor{<cond>}{S} Rd, Rn, N
bic{<cond>}{S} Rd, Rn, N

and:32位邏輯與;Rd=Rn & N
orr:32位邏輯或;Rd=Rn | N
eor:32位邏輯異或;Rd=Rn ^ N
bic:32位邏輯位清除;Rd=Rn &~ N
N可以是寄存器,也可以是常量,還可以包括移位。

and r0, r1, r2
orr r0, r1, r2
eor r0, r1, r2
bic r0, r1, r2

bic指令通常用於清除寄存器的某個特定位,比如清除CPSR寄存器的I位和F位來使能中斷;eor指令常用於反轉某些位。

4、比較指令

比較指令用於對一個寄存器中的值和一個立即數進行比較或測試。比較指令根據結果更新cpsr的標誌位,但不影響其它的寄存器。當比較指令改變了標誌位後,後續指令就可以通過條件執行來改變程序的執行流程。比較指令不使用S後綴就可以改變標誌位。語法:

cmp{<cond>} Rn, N
teq{<cond>} Rn, N
tst{<cond>} Rn, N

cmp:比較(根據Rn-N設置標記位)
teq:等值比較(根據Rn^N設置標記位)
tst:位測試(根據Rn&N設置標記位)

cmp r0, r9

5、分支指令(branch)

分支指令可以改變程序的執行流程或者調用子程序。這種指令使得程序可以實現子程序調用,if-else結構以及循環等;執行流程的改變會迫使程序計數器pc指向一個新的地址。語法:

b{<cond>}  label
bl{<cond>} label

b:跳轉,pc=label
bl:帶返回的跳轉,pc=label,lr=bl指令後面一條指令的地址

b指令一般用於實現C代碼中的if-else分支,或者for/while循環;bl指令一般用於實現函數調用/返回。

myadd:
  add r0, r1, r2
  ...
  b myadd
  ...

標號放在一行的開始處,後面加冒號。

@函數定義
myfunc:
  ...
  mov    pc, lr @返回

  @當r0等於0時調用函數myfunc
  cmp    r0, #0
  bleq   myfunc 
  ...

帶條件的跳轉bleq

1:
  ...
1:
  ...
  b 1b @backward/forward
  ...
1:
  ...

在一個.s文件中,字母開頭的標號視爲全局標號,是不能重名的;而純數字的標號認爲是局部標號,可以重名。

6、單寄存器load-store指令

load-store指令用於在內存和通用寄存器之間交換數據;ARM是不能直接操作內存中的數據的,必須首先把數據用load指令從內存讀取到通用寄存器中,然後才能進行計算,最後再用store指令將運算的結果寫回內存。語法:

ldr{<cond>} Rd, address
str{<cond>} Rd, address

ldr:把一個word(32位)從內存讀入通用寄存器Rd
str:把一個通用寄存器的內容(32位)寫入內存

如果需要讀寫16位數據,可以用ldrh/strh
如果需要讀寫8位數據,可以用ldrb/strb

address是內存中的地址,可以直接寫一個標號;也可以先把基地址存到寄存器中,然後採用[基地址+/-偏移量]的方式訪問內存。偏移量也可以有多種表示,可以直接用常數作爲偏移量,也可以用另一個寄存器做偏移量。

[Rn, #+/-offset_12]
[Rn, +/-Rm]
[Rn, +/-Rm, shift_imm]

ldr和str需要裝載和存儲地址對齊的數據,如ldr只能從0、4、8等地址裝載32位的word。

例1:

@在r1中保存基地址
@將內存中的數據讀入r0
@不能寫成ldr r0, [#0x8000]
mov r1, #0x8000
ldr r0, [r1, #0x20]

例2:

@將r0中的數據寫入內存
mov r1, #0x9000
str r0, [r1]
str r0, [r1, #-8]

ldr/str指令的尋址方式:

基地址尋址:

mov r1, #0x8000
ldr r0, [r1]
@從地址單元0x8000讀4個字節,存入r0
@r1的內容不變

基地址加偏移尋址:

mov r1, #0x8000
ldr r0, [r1, #8]
mov r2, #0x100
ldr r3, [r1, r2]
@從地址單元0x8008讀4個字節,存入r0
@從地址單元0x8100讀4個字節,存入r3
@r1的內容不變

基地址加偏移並更新基地址:

mov r1, #0x8000
ldr r0, [r1, #8]!
ldr r2, [r1, #4]
@從地址單元0x8008讀4個字節,存入r0
@r1的內容變爲0x8008
@從地址單元0x800C讀4個字節,存入r2

注意:!的作用是在指令執行完畢後,更新基地址;

只更新基地址:

mov r1, #0x8000
ldr r0, [r1], #8
ldr r2, [r1]
@從地址單元0x8000讀4個字節,存入r0
@r1的內容變爲0x8008
@從地址單元0x8008讀4個字節,存入r2

基於標號尋址:

在彙編中,標號實際上也是地址,因此可以直接在ldr/str指令後面使用標號。

abc:
  .word 0x12345678
  ...
  ldr r0, abc
  @從地址單元abc讀4個字節,存入r0

7、多寄存器傳輸指令ldm/stm

爲了提高效率,ARM還提供了和內存單元交換多個word的指令,這種指令常用於數據拷貝,入棧出棧等操作;多寄存器傳輸指令是執行時間最長的ARM指令。語法:

ldm{<cond>}<尋址模式> Rn{!}, <Regs>{^}
stm{<cond>}<尋址模式> Rn{!}, <Regs>{^}

ldm:從內存中取出數據裝載到多個寄存器中
stm:將多個寄存器中的內容保存到內存中
Rn中保存要訪問的內存單元的基地址
!的作用是在指令執行完畢後,更新基地址
Regs是指令涉及的通用寄存器列表
^可以將spsr寄存器的值賦給cpsr

ldm/stm指令的尋址模式:IA/IB/DA/DB

IA(執行後增加,Increase After)

  mov   r7,  #0x8000
  ldmia r7!, {r0-r2,r5}
  @從0x8000開始連續讀16個字節(地址遞增)
  @0x8000~0x8003的內容存入r0
  @0x8004~0x8007的內容存入r1
  @0x8008~0x800b的內容存入r2
  @0x800c~0x800f的內容存入r5
  @將r7的內容更新爲0x8010

IB(執行前增加,Increase Before)

  mov   r7,  #0x8000
  ldmib r7!, {r5,r0-r2}
  @從0x8004開始連續讀16個字節(地址遞增)
  @按順序存入r0,r1,r2,r5
  @將r7的內容更新爲0x8010

DA(執行後減少,Decrease After)

  mov   r7,  #0x8000
  ldmda r7!, {r0-r2,r5}
  @從0x8000開始連續讀16個字節(地址遞減)
  @0x8000~0x8003的內容讀入r5
  @0x7ffc~0x7fff的內容讀入r2
  @0x7ff8~0x7ffb的內容讀入r1
  @0x7ff4~0x7ff7的內容讀入r0
  @將r7的內容更新爲0x7ff0

DB(執行前減少,Decrease Before)

  mov   r7,  #0x8000
  ldmdb r7!, {r0-r2,r5}
  @從0x8000開始連續讀16個字節(地址遞減)
  @0x7ffc~0x7fff的內容讀入r5
  @0x7ff8~0x7ffb的內容讀入r2 
  @0x7ff4~0x7ff7的內容讀入r1 
  @0x7ff0~0x7ff3的內容讀入r0 
  @將r7的內容更新爲0x7ff0

stm/ldm指令常用於棧的操作以及數據拷貝:

入棧和出棧:

  @設置棧頂爲0x8000
  mov   sp, #0x8000
  stmib sp!, {r1-r3}
  ...
  ldmda sp!, {r1-r3}

C代碼默認使用stmdb和ldmia的組合

數據拷貝:

  @r9存放數據的源地址(0x8000)
  @r10存放數據大小(0x100)
  @r11存放目的地址(0xA000)
  mov   r9,  #0x8000
  mov   r10, #0x100
  mov   r11, #0xA000
loop:
  ldmia r9!, {r0-r7}
  stmia r11!, {r0-r7}
  sub   r10, r10, #32
  cmp   r10, #0
  bgt   loop

8、軟件中斷指令swi/svc

中斷是由硬件產生的異常,而軟件中斷是通過執行特定的指令產生的異常。ARM定義了指令swi(ARMv7後更名爲svc)來產生一個軟件中斷異常;當處理器執行完swi指令後,不會繼續執行swi後面的指令,而是首先將CPU切換到svc模式,然後將pc設定爲異常向量表基地址+0x8,並從此處繼續執行。swi/svc指令的最主要工作就是將cpu從非特權的usr模式(對應linux的用戶態)提升到特權的svc模式(對應Linux的內核態)。在Linux用戶態執行的open/read等系統調用,在ARM平臺上就是通過swi指令實現的。通常在用戶模式下調用swi,從而使處理器從非特權模式(usr)切換到特權模式(svc)。

語法:

  swi{<cond>} number

指令後面的數字爲系統調用號,最大爲0xffffff。系統調用號直接編譯到指令代碼中。

執行swi指令後,處理器的執行步驟如下:
–> 在lr_svc寄存器中記錄swi指令後面一條指令的地址
–> 將cpsr的值(執行swi指令時所處的處理器狀態)存入spsr_svc
–> pc = 異常向量表基地址 + 0x8
–> 將cpu切換到svc模式
–> cpsr_I = 1(屏蔽IRQ中斷)

執行編號爲123的系統調用:

swi 123
@執行前,cpsr=nzcVift_USER
@執行前,pc=0x8000
@執行後,cpsr=nzcVift_SVC
@執行後,spsr=nzcVift_USER
@執行後,pc=0x08, lr=0x8004
@可以用r0傳遞參數

軟件中斷的處理函數,應該從異常向量表的0x8單元處跳轉過來:

swi_handler:
    stmdb sp!, {r0-r12,lr}
    ldr     r0, [lr, #-4]
    bic     r0, r0, #0xff000000
    bl  swi_service     
    ldmia sp!, {r0-r12, pc}^
@將r0-r12,lr壓棧
@讀swi指令的編碼,放入r0
@取出swi指令的後24位(即系統調用號)
@根據系統調用號調用相應的處理程序
@出棧,並跳轉回swi後面的指令

在重新裝載pc的時候,除非明確要求,否則不會把spsr中的內容恢復到cpsr中去。要恢復cpsr的方法是在寄存器列表後跟隨一個”^”。

9、狀態寄存器訪問指令mrs/msr

要讀寫ARM的狀態寄存器cpsr和spsr,需要使用兩條特殊的指令:mrs和msr。語法:

  mrs{<cond>} Rd, <cpsr|spsr>
  msr{<cond>} <cpsr|spsr>_<field>, Rm | #立即數

mrs:將狀態寄存器的內容讀入通用寄存器Rd
msr:設定狀態寄存器或其中的某個域
field:用msr指令寫cpsr/spsr時,可以附加field標誌從而隻影響cpsr/spsr的部分位
支持的field標誌有:
c – control域(bit[7:0])
x – extension域(bit[15:8])
s – status域(bit[23:16])
f – flags域(bit[31:24])

使能IRQ:

mrs r1, cpsr
bic r1, r1, #0x80
msr cpsr_c, r1
@執行前,cpsr=nzcvIFt_SVC
@執行後,cpsr=nzcviFt_SVC

10、協處理器訪問指令

ARM提供了專門的協處理器訪問指令,協處理器可提供附加的計算能力,如浮點數運算、多媒體數據運算等,也可用於控制cache和MMU等,協處理器訪問指令的格式必須參考對應的ARM手冊。語法:

mrc{<cond>} cp, opcode1, Rd, Cn, Cm{, opcode2}
mcr{<cond>} cp, opcode1, Rd, Cn, Cm{, opcode2}

mrc:把數據從協處理器內部的寄存器讀入ARM的通用寄存器
mcr:把通用寄存器中的數據寫入協處理器內部的寄存器
cp:協處理器的編號,範圍p0~p15
Rd:ARM中的通用寄存器,範圍r0~r12
opcode1:操作碼1,範圍0~7
opcode2:操作碼2,範圍0~7
Cn:協處理器中的主寄存器,範圍c0~c15
Cm:協處理器中的輔助寄存器,範圍c0~c15
注意!op1, op2, Cn, Cm組合在一起代表特定的操作,其組合方式只能參考對應的手冊。

11、ARM的僞指令

爲了滿足編程的需要,ARM還提供了一些僞指令。僞指令的使用方式和前面的指令一樣,但彙編器(as)在彙編時,會首先根據具體情況把僞指令轉換爲前面介紹過的真正的ARM指令,然後再進行編碼。

(1)壓棧和出棧:

爲了更好地使用堆棧,ARM根據棧的不同使用狀態,提供了專門的入棧/出棧指令。這些指令會被彙編器轉換爲前面介紹的stm/ldm系列指令。

堆棧的分類:
從棧指針的生長方向上來劃分,棧可以分爲遞增堆棧和遞減堆棧
從棧指針指向的單元是否被使用上來劃分,棧可以分爲滿堆棧和空堆棧

根據這兩種分類,棧的使用可分爲4種:
A. 遞增滿堆棧(FA, full ascend)
B. 遞減滿堆棧(FD, full descend)
C. 遞增空堆棧(EA, empty ascend)
D. 遞減空堆棧(ED, empty descend)

在Uboot或Linux中,默認採用遞減滿的方式使用堆棧。

遞減滿堆棧:

  mov   sp, #0x8000
  stmfd sp!, {r0-r2}
  ...
  ldmfd sp!, {r0-r2}
  @棧指針sp指向堆棧的頂部0x8000
  @壓棧後,0x7ffc存r0, 0x7ff8存r1, 0x7ff4存r2
  @sp更新到0x7ff4(指向的單元中包含有效數據)
  @出棧後,sp重新指向棧頂0x8000
  @stmfd = stmdb, ldmfd = ldmia

(2)常量和地址裝載:

ARM指令採用32位編碼,由於操作碼要佔用空間,因此不可能在一條指令中編碼一個32位的立即數,爲方便編程,ARM增加了一條僞指令,用於把一個32位的立即數存入寄存器。ARM還提供了一條僞指令,用於獲得當前的運行地址。

語法:

ldr Rd, =constant
adr Rd, label

ldr:常量裝載僞指令,Rd=32位常量
adr:地址裝載僞指令,Rd=32位相對地址

例1:

ldr r0, =0x12345678
@僞指令由編譯器展開爲ARM指令,反彙編一下看看

例2:

abc:
  adr   r0, abc
  @將標號abc在內存中的實際地址存儲到r0中

例3:

abc:
  ldr r0, =abc
  ldr r1, abc
  @將鏈接時指定的標號abc的地址值賦給r0
  @單寄存器ldr指令,將標號abc所指地址的內存的一個word讀入r1寄存器

12、gcc編譯器支持的僞彙編指令

在ARM彙編語言程序設計中,有一些特殊指令助記符,這些助記符與ARM指令不同,沒有相對應的操作碼,通常稱這些特殊指令助記符爲僞彙編指令或僞指令。僞指令類似於C語言中的include,define等,僅在程序的彙編過程中起作用,一旦彙編結束,僞指令的使命就完成。

僞彙編是編譯器定義的,如果不是使用gcc,而是armcc,則編譯器支持不同格式的僞彙編。

常用的僞彙編指令:

(1)標號(label)
gcc中的標號通過後面帶的冒號來識別,例如:

.global add
add:
  add r0, r0, r1
  mov pc, lr

(2).global/.globl
將symbol定義爲全局標號,例如:

 .global MyAsmFunc

(3).byte
聲明一個字節(8bit),相當於c語言中的char,例如:

hello:
  .byte 25, 0x11, 'A'

(4).hword/.short
聲明一個半字(16bit),相當於c語言中的short,例如:

hello:
  .hword 2, 0xFFE0

(5).word/.int/.long
聲明一個字(32bit),相當於C語言中的int,例如:

hello:
  .word 144511, 0x223344
  .word 0x11

(6).ascii
聲明一個字符串(非零結束符),例如:

hello:
  .ascii "Ascii text is here"

(7).asciz/.string
聲明一個字符串(以0爲結束符),例如:

.asciz "Zero Terminated Text"
.string "My Cool String\n"

(8).fill
語法:

.fill repeat {, size} {, value}

數據填充,次數爲repeat次,每次填充size個字節,填充內容爲value,size缺省爲1, value缺省爲0。例如:

.fill 32, 4, 0xFFFFFFFF

(9).space
語法:

.space size {, value}

用value填充size個字節,value缺省爲0。例如:

.space 25, 0b11001100

(10).arm/.code 32
定義以下代碼使用ARM指令集編譯。

.arm

(11).include
包含文件,可以包含頭文件或其他彙編文件

.include "hardware.bin"

更常見的頭文件包含方式爲:

#include <linux/config.h>

(12).align/.balign
語法:

.balign {alignment} {, fill} {, max}

以alignment個字節對齊。

(13).end
標記彙編文件的結束行,即標號後的代碼不作處理。

.end

(14).section
定義elf文件中包含的段,可以是.text、.data、.bss等。

.section 
.bss

(15).text/.data/.bss
將符號後面的代碼或數據編譯到指定段中。

.text
.data
.bss

(16).if/.elseif/.else/.endif
條件編譯,例如:

.if (2+2)
...
.endif

(17).ifdef
若symbol已定義,編譯以下代碼:

.ifdef _test_i_

(18).ifndef
若symbol沒定義,編譯以下代碼:

.ifndef _test_i_

(19).rept/.endr
重複編譯.rept與.endr之間的語句count次,例如:

.rept   8
    mov r0, r0
.endr

(20).equ/.equiv
將符號替換爲表達式的結果,類似於c中的define,例如:

.equ swapper_pg_dir, KERNEL_RAM_ADDR - 0x4000
.equiv Version, "0.2"

(21).req
爲ARM的通用寄存器起個名字,語法:

  <register_name> .req <register_name>

例如:

acc  .req  r0
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章