2. ARM彙編語言
2.1. ARM命名規則
ARM是Advanced RISC Machines的縮寫,是典型的RISC(Reduced Instruction Set Computer)架構的CPU。ARM的版本控制的命名規則分成兩類。 一類是基於ARM 架構的版本命名規則,它是一種架構設計的總和;另一類是基於某ARM架構版本的系列處理器的命名規則。
2.1.1. ARM架構命名規則
| ARMv | n | variants | x(variants) |
ARM架構的命名由四部分組成:
- ARMv字符串,這部分固定不變。
- n,ARM指令集版本號,ARM架構版本發佈了7個系列,所以n=[1:7]。其中最新的版本是第7版,Cortex系列CPU採用該版本。
- variants,變種,通常用大寫字母來表示對一類指令或者指令集,變種就是這些大寫字母的連寫。
- x(variants),不支持x後指定的變種。
常見的變種有:
- T,Thumb指令集
- M ,長乘法指令
- E,增強型DSP指令
- J,Java虛擬機Jazelle指令集
- P,LDRD, MCRR, MRRC, PLD和STRD指令
例如,ARMv5TxP表示ARM指令集版本為5,支持Thumb指令集,不支持LDRD, MCRR, MRRC, PLD和STRD指令。
表 1. ARM架構版本 版本 說明
版本 | 說明 |
---|---|
ARMv1[a] | 該版本的原型機是ARM1,沒有用於商業產品 |
ARMv2 | 對v1版進行了擴展,包含了對32位結果的乘法指令和協處理器指令的支持。 |
ARMv3 | ARM公司第一個微處理器ARM6核心是版本3的,它作為IP核、獨立的處理器、具有片上高速緩存、MMU和寫緩衝的集成CPU。 |
ARMv4 | 應用非常廣泛,但僅支持32位ARM指令集。 |
ARMv4T | 同時支持ARM指令集4和Thumb指令集1。 |
ARMv5T | 同時支持ARM指令集5和Thumb指令集2。添加計算開始0個數的clz指令和軟中斷bkpt指令 |
ARMv5TExP | 在ARMv5T基礎上添加增強型DSP指令,但不支持LDRD, MCRR, MRRC, PLD, 和STRD指令 |
ARMv5TE | 在ARMv5T基礎上添加增強型DSP指令。 |
ARMv5TEJ | 在ARMv5TE基礎上支持Jazelle指令集。 |
ARMv6 | 同時支持ARM指令集6和Thumb指令集3。增加和增強了相當多的單指令多數據處理指令。 |
ARMv6K | 添加支持多處理的ARM指令集,以及一些額外的內存模型的特性。 |
ARMv6T2 | 推出Thumb-2技術。為16位和32位指令碼的結合,免去了狀態切換之複雜度。 |
ARMv7A | 高性能運算,針對手機,PDA等手持便攜設備。增加視頻編解碼和3D處理的NEON多媒體技術。 |
ARMv7R | 減少輸入輸出延遲,提高指令預測精度,重視實時處理,針對網略設備,汽車儀表等應用。支持PMSA。 |
ARMv7M | 低成本微處理器應用,去除NEON指令集。 |
[a] ARMv1至ARMv3版本已經廢棄,不再被使用。 |
2.1.2. ARM處理器命名規則
ARM處理器的命名規則如下:
ARM{x}{y}{z}{T}{D}{M}{I}{E}{J}{F}{-S}
各組成部分解釋如下:
- x,處理器系列
- y,存儲管理/保護單元
- z, cache
- T,支持Thumb指令集
- D,支持片上調試
- M,支持快速乘法器
- I,支持Embedded ICE,支持嵌入式跟蹤調試
- E,支持增強型DSP指令
- J,支持Jazelle
- F,具備向量浮點單元VFP
- -S, 可綜合版本
ARM使用一種基於數字的命名法。在早期(1990s),還在數字後面添加字母后綴,用來進一步明細該處理器支持的特性。就拿ARM7TDMI來說,T代表Thumb指令集,D是說支持JTAG調試(Debugging),M意指快速乘法器,I則對應一個嵌入式ICE模塊。後來,這4項基本功能成了任何新產品的標配,於是就不再使用這4個後綴相當於默許了。但是新的後綴不斷加入,包括定義存儲器接口的,定義高速緩存的,以及定義"緊耦合存儲器(TCM)"的,於是形成了新一套命名法,這套命名法一直使用至今。比如ARM1176JZF-S,它實際上默認就支持TDMI功能,除此之外還支持JZF。
- ARM7TDMI、ARM720T、ARM9TDMI、ARM940T、ARM920T、Intel的StrongARM等是基於ARMv4T版本。
- ARM9E-S、ARM966E-S、ARM1020E、ARM1022E以及XScale是ARMv5TE的。
- ARM9EJ-S、ARM926EJ-S、ARM7EJ-S、ARM1026EJ-S是基於ARMv5EJ的。ARM10也採用ARMv5TE。
- ARM1136J(F)-S基於ARMv6主要特性有SIMD、Thumb、Jazelle、DBX、(VFP)、MMU。
- ARM1156T2(F)-S基於ARMv6T2 主要特性有SIMD、Thumb-2、(VFP)、MPU。
- ARM1176JZ(F)-S基於ARMv6KZ 在 ARM1136EJ(F)-S 基礎上增加MMU、TrustZone。
- ARM11MPCore基於ARMv6K 在ARM1136EJ(F)-S基礎上可以包括1-4 核SMP、MMU。
從ARMv6指令集才開始支持SMP,這是由於從該指令集開始提供ldrex和strex系列獨佔(Exclusive)訪問指令,該系列指令可以保證多CPU在同時訪問內存時是互斥的。這也可以從內核代碼中得到應徵:
arch/arm/include/asm/atomic.h
#if __LINUX_ARM_ARCH__ >= 6
......
#else /* ARM_ARCH_6 */
#include <asm/system.h>
#ifdef CONFIG_SMP
#error SMP not supported on pre-ARMv6 CPUs
#endif
......
基於ARMv7架構的ARM處理器不再沿用過去的數字命名方式,而是冠以Cortex前綴,基於ARMv7A的處理器成為Cortex-A系列,基於ARMv7R的處理器成為Cortex-R系列,基於ARMv7M的處理器成為Cortex-M系列,
2.2. 程序狀態寄存器
當前程序狀態寄存器CPSR(Current Program Status Register)可以在任何工作模式下通過mrs指令訪問。它包含了條件碼(Condition code)標誌位,禁止中斷標誌位,處理器工作模式位以及其他狀態和控制信息。每一箇中斷模式還擁有一個單獨的保存的程序狀態寄存器SPSR(Saved Program Status Register),當發生該模式的中斷時,它被用來備份CPSR的值。用戶模式和系統這兩種模式不是中斷模式,所以沒有SPSR寄存器。在這兩種模式下嘗試訪問SPSR結果未知。
圖 1. CPSR和SPSR比特位
如上圖所示,程序狀態寄存器比特位按訪問權限分為四種:
- Reserved bits: 保留位
- User-writable bits: Usr模式可以更新的位:N,Z,C,V,Q,GE[3:0]和E.
- Privileged bits: 特權模式才可更新的位:A,I,F和M[4:0]
- Execution state bits: 執行狀態位J和T:分別表示工作在指令集Jazilla和Thumb,如果J和T同為0 則工作在ARM指令集。不要通過MSR指令改變它,否則結果未知。只可在特權模式更改。
關於J和T的詳細組合如下表所示:
表 2. Memory Hierarchy
J | T | 指令集 |
---|---|---|
0 | 0 | ARM |
0 | 1 | Thumb |
1 | 0 | Jazelle |
1 | 1 | 保留 |
2.3. ARM指令格式
ARM指令的基本格式如下:
<opcode>{<cond>}{S} <Rd>, <Rn/#Num>{, <operand2>}
其中<>號內的項是必須的,{}號內的項是可選的,/表示任選其中之一。各項的說明如下:
- opcode:指令助記符, 如mov,ldr等
- cond:執行條件,如eq,ne等
- S:是否影響CPSR寄存器的值
- Rd:目標寄存器
- Rn/#Num:第1個操作數的寄存器或者立即數
- operand2:第2個操作數
靈活的使用第2個操作數“operand2”能夠提高代碼效率。它有如下的形式:
- #immed_n——常數表達式,n表示立即數的位數,通常為12,也即最大為4096,例如:#0x1000。
- Rm——寄存器方式;例如:r1
- [Rm, shift]——寄存器移位方式;例如:[pc, #-4]
比如:subnes r1, r1, #0xd; 如果Z標誌為0(不等),則執行sub r1, r1, #0xd並根據結果更新CPSR寄存器。 參考資料:ARM嵌入式系統基礎教程(第2版)PPT
指令中的執行條件由CPSR寄存器中的狀態位控制,參考狀態位。 表 3. 條件碼錶
條件碼(二進制) | 條件助記符 | 標誌位 | 含義 |
---|---|---|---|
0000 | eq | Z==1 | 相等 |
0001 | ne | Z==0 | 不等 |
0010 | cs/hs | C==1 | 無符號數大於或等於 |
0011 | cc/lo | C==0 | 無符號數小於 |
0100 | mi | N==1 | 負數 |
0101 | pl | N==0 | 正數或0 |
0110 | vs | V==1 | 溢出 |
0111 | vc | V==0 | 無溢出 |
1000 | hi | C==1 且 Z==0 | 無符號數大於 |
1001 | ls | C==0 且 Z==1 | 無符號數小於或等於 |
1010 | ge | N==V | 有符號數大於或等於 |
1011 | lt | N!=V | 有符號數小於 |
1100 | gt | Z==0 且 N==V | 有符號數大於 |
1101 | le | Z==1 或 N!=V | 有符號數小於或等於 |
1110 | al | 無條件 | 無條件執行(默認) |
2.4. 測試用例
為了進行標誌位等的驗證,這裡使用一個通用的示例程序,它包含兩個文件:
main.S
.section .text
.align 2
.global main
main:
mov r0, #12
mov r1, #34
mov r2, #56
bl print
mov r0, #0
bl quit
注意到main.S中的子程序名為main,這是因為GCC編譯器必須將main作為入口,否則無法使用Glibc的庫函數。注意到mov命令,根據ATPS規則,這裡是給print函數準備參數,這裡可以更改r0和r1中的值,通過print來打印出它們。
print.c
#include <stdio.h>
#include <stdlib.h>
char regs[32][8] =
{
"M0", "M1", "M2", "M3", "M4", "T", "F", "I", /* 0-7 */
"A", "E", " ", " ", " ", "CV", " ", " ", /* 8-15 */
"|", "G", "E", "|", " ", " ", " ", " ", /* 16-23 */
"J", " ", " ", "Q", "V", "C", "Z", "N" /* 24-31 */
};
void format(char *regname, int reg)
{
int i = 0;
if(!reg)
return;
printf("%3s", regname);
for(i = 31; i >= 0; i--)
printf("%3x", reg & (1 << i) ? 1 : 0);
printf("\n");
}
void print(int r0, int r1, int r2)
{
int i = 0;
printf(" General out:\n");
printf(" Hex\tr0 0x%08x\tr1 0x%08x\tr2 0x%08x\n", r0, r1, r2);
printf(" Oct\tr0 %8d\tr1 %8x\tr2 %8d\n\n", r0, r1, r2);
if(r0 || r1 || r2)
{
printf(" Format out:\n ");
for(i = 31; i >= 0; i--)
printf("%3d", i);
printf("\n ");
for(i = 31; i >= 0; i--)
printf("%3s", regs[i]);
printf("\n");
}
format("r0", r0);
format("r1", r1);
format("r2", r2);
}
void quit(int err)
{
exit(err);
}
print.c中的print通過Glibc庫函數printf打印出結果,而quit則通過標準庫函數exit退出運行。另外編譯命令和測試結果如下:
arm-linux-gcc print.c main.S -o test
# ./test
General out:
r0 0x0000000c r1 0x00000022 r2 0x00000038
Format out:
31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
N Z C V O CV I F T M4 M3 M2 M1 M0
r0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0
r1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 0
r2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0
藉助c語言的格式化輸出,能夠很好的觀察狀態位的變化。 2.4.1. CPSR操作
修改main.S指令的如下,獲取cpsr和spsr寄存器標誌位:
main:
mrs r0, cpsr
mrs r1, spsr
mov r2, #0
......
這裡主要用到了mrs命令,它可以直接獲取當前程序狀態寄存器cpsr和保存的程序狀態寄存器spsr,用戶模式並沒有spsr,所以它的值就是cpsr。以上程序結果如下所示:
General out:
r0 0x20000010 r1 0x20000010 r2 0x00000000
Format out:
31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
N Z C V O CV I F T M4 M3 M2 M1 M0
r0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0
r1 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0
可以看到r0和r1的值相同,並且只設置了C標誌。而M模式位為0x10,為用戶模式。通常有些情況下需要設置cpsr,可以通過msr指令實現。
mov r0, #0
msr cpsr_f, r0
......
注意這裡的cpsr_f指明操作標誌位[31:24],這裡將用戶模式可以設置的位於[31:24]中的所有位清零。這裡的f指明瞭傳送的區域,它可以為以下幾種值:
- c:控制標誌位[7:0]
- x:擴展域標誌位[15:8]
- s:狀態域標誌位[23:16]
- f: 條件域標誌位[31:24]
2.4.2. 含s置位的指令
修改main.S指令的以測試含s置位指令:
main:
mov r0, #0 /* without 's' */
mrs r1, cpsr
movs r0, #0 /* with s */
mrs r2, cpsr
bl print
mov r0, #0
bl quit
測試的結果如下所示:
General out:
r0 0x00000000 r1 0x20000010 r2 0x60000010
Format out:
31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
N Z C V O CV I F T M4 M3 M2 M1 M0
r1 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0
r2 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0
可以看到第一條指令並沒有影響標誌位,而movs指令則改變了Z標誌位,也即當前的運算結果為0。
2.4.3. Z標誌條件編碼
首先通過movs賦值r1為0,此時Z標誌位1,r1記錄此時的cpsr值。然後改變r2的值為0,以實現測試moveq和movne的測試。
.macro testz, cmd
movs r0, #0
mrs r1, cpsr
mov r2, #0
\cmd r2, #1
bl print
.endm
.global main
main:
testz moveq
testz movne
mov r0, #0
bl quit
moveq當Z==1時才被執行,顯然此時被執行,r2的值被賦值為1。接著測試movne,它只有在Z==0時才被執行,所以這裡r2的值並沒有被賦值為1,而依然是0。
General out:
r0 0x00000000 r1 0x60000010 r2 0x00000001
Format out:
31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
N Z C V O CV I F T M4 M3 M2 M1 M0
r1 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0
r2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1
General out:
r0 0x00000000 r1 0x60000010 r2 0x00000000
Format out:
31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
N Z C V O CV I F T M4 M3 M2 M1 M0
r1 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0
其他標誌位可以使用相同方法進行測試。
2.5. 原碼和補碼
對於計算機而言,最根本的動作就是高低電位的跳變,從而表示0和1兩種狀態。在計算機中,所有的負數都用補碼錶示,而負數的原碼對於計算機來說沒有任何意義。對於人來說,通常使用十進制,十進制在表示負數時有符號-來表示。但是對於針對計算機而設計的二進制來說,並沒有所謂的符號-來表示負數,而是要拿出一位來表示數的正負——最高位。考慮以下的代碼:
int main()
{
int tmp = -0x1;
printf("tmp:%x\n", tmp);
return 0;
}
儘管編譯時沒有錯誤,但是通常人們並不使用針對計算機設計的二,八或者十六進制添加符號-來表示負數,而是使用十進制-1,或者直接指定符號位:0x80000001。但是注意通過二,八或者十六進製表示的沒有符號-的負數,永遠都被直接編譯進指令中作為補碼使用,否則編譯器將把它們轉換為補碼後再放入指令中。所以0x80000001並不表示-1,而是-2147483647這樣一個數字,這是因為0x80000001是它的補碼,而非-1的。看來補碼的引入並沒有符合人的使用習慣,那麼引入補碼的意義是什麼呢?
模的概念:把一個計量單位稱之為模或模數。例如,時鐘是以12進制進行計數循環的,即以12為模。在時鐘上,時針加上(正撥)12的整數位或減去(反撥)12的整數位,時針的位置不變。14點鐘在捨去模12後,成為(下午)2點鐘(14=14-12=2)。從0點出發逆時針撥10格即減去10小時,也可看成從0點出發順時針撥2格(加上2小時),即2點(0-10=-10=-10+12=2)。因此,在模12的前提下,-10可映射為+2。由此可見,對於一個模數為12的循環系統來說,加2和減10的效果是一樣的;因此,在以12為模的系統中,凡是減10的運算都可以用加2來代替,這就把減法問題轉化成加法問題了(注:計算機的硬件結構中只有加法器,所以大部分的運算都必須最終轉換為加法)。10和2對模12而言互為補數。
同理,計算機的運算部件與寄存器都有一定字長的限制(假設字長為8),因此它的運算也是一種模運算。當計數器計滿8位也就是256個數後會產生溢出,又從頭開始計數。產生溢出的量就是計數器的模,顯然,8位二進制數,它的模數為28=256。在計算中,兩個互補的數稱為"補碼"。 計算機引入補碼後:
- 可以方便地將減法運算轉化成加法運算,運算過程得到簡化。正數的補碼即是它所表示的數的真值,而負數的補碼的數值部份卻不是它所表示的數的真值。採用補碼進行運算,所得結果仍為補碼。
- 0的補碼為0b00000000。但是符號位帶來了負0值,它的補碼與原碼相同為0b10000000,所表示的值為-2N - 1 。對於8位數來說,為-128。
- 若字長為8位,則補碼所表示的範圍為-128~+127;進行補碼運算時,應注意所得結果不應超過補碼所能表示數的範圍。 圖 2. 原碼和補碼的關係
原碼和補碼的關係
2.6. 條件碼標誌位
N(Negative)、Z(Zero)、C(Carry)及V(oVerflow)統稱為條件標誌位。大部分的ARM 指令可以根據CPSR 中的這些條件標誌位來選擇性地執行。它們在以下兩種情況下被改變:
- 比較指令:CMN,CMP,TEQ和TST
- 一些數學運算指令,邏輯和數據傳輸指令有兩種變體:標誌保持;標誌改變,其中標誌改變的指令後綴為S。有些指令沒有標誌改變的變體。標誌改變變體指令通常只有在目標寄存器不為R15(pc)時才會改變標誌位。 在任何一種情況下,這些標誌位發生了改變,這意味著:
- N:它被設置為結果的bit[31]位,當兩個補碼錶示的有符號整數運算時,N=1 表示運算的結果為負數;N=0 表示結果為正數或零。
- Z:Z=0 表示運算的結果不為零。對於CMP 指令,Z=1表示進行比較的兩個數大小相等。
- C:分4 種情況討論C的設置方法: 在加法指令中(包括比較指令CMN),當結果產生了進位,則C=1,表示無符號數運算髮生上溢出;其他情況下C=0。 在減法指令中(包括比較指令CMP),當運算中發生借位則C=0表示無符號數運算髮生下溢出;其他情況下C=1。 對於包含移位操作的非加/減法運算指令,C中包含最後一次溢出的位數數值。 對於其他非加/減法運算指令,C位的值通常不受影響。
- V:對於加/減法運算指令,當操作數和運算結果為二進制的補碼錶示的帶符號數時V=1 表示符號位溢出。通常其他的指令不影響V 位,具體可參考各指令的說明。
- 通過msr指令直接改變CPSR/SPSR標誌位。
- mrc讀協處理器指令,當目的寄存器為r15(pc)時,用來將協處理器中的條件標誌位拷貝到ARM處理器中。
- ldm系列指令,通常用來拷貝SPSR到CPSR,用於中斷返回。
- rfe指令在特權模式下從內存恢復CPSR。
- 數學和邏輯邏輯指令在目的寄存器為r15時,可能拷貝SPSR到CPSR中。可以用於從中斷中返回。
2.6.1. N負數標誌位測試
測試N標誌位的思想很簡單,就是根據指令結果的符號位為0和1來查看N標誌位。
main:
/* save orig cpsr->r0 */
mrs r0, cpsr
/* give a non-Neg value get cpsr->r1 */
movs r3, #1
mrs r1, cpsr
/* give a Neg value get cpsr->r2 */
movs r3, #-1
mrs r2, cpsr
......
輸出結果如下所示,顯然r1的N標誌位為0,而r2則變為了1。
General out:
Hex r0 0x20000010 r1 0x20000010 r2 0xa0000010
Oct r0 536870928 r1 20000010 r2 -1610612720
Format out:
31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
N Z C V Q J | E G | CV E A I F T M4 M3 M2 M1 M0
r0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0
r1 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0
r2 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0
2.6.2. Z零標誌位測試
測試N標誌位的思想很簡單,就是根據指令結果的是否為0來查看Z標誌位。
main:
/* save orig cpsr->r0 */
mrs r0, cpsr
/* give a non-Zero let cpsr->r1 */
movs r3, #1
mrs r1, cpsr
/* give a Zero let cpsr->r2 */
movs r3, #0
mrs r2, cpsr
......
輸出結果如下所示,顯然r1的Z標誌位為0,而r2則變為了1。
General out:
Hex r0 0x20000010 r1 0x20000010 r2 0x60000010
Oct r0 536870928 r1 20000010 r2 1610612752
Format out:
31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
N Z C V Q J | E G | CV E A I F T M4 M3 M2 M1 M0
r0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0
r1 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0
r2 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0
2.6.3. C進位標誌位測試
測試C標誌位相對複雜,包括四種情況。
2.6.3.1. 加法指令進位
加法指令add將所有操作數當做無符號處理,CPU內部使用CarryFrom(Rn + shifter_operand)來計算是否進位,其中rn和shifter_operand分別為第一操作數和第二操作數。測試思想為給第一操作數賦值為0xfffffffe,這保證在第一次加1後,不會進位,再次加1後必定進位。另外注意到r0存數了0xfffffffe + 2的結果。CarryFrom的算法很簡單,在ARM指令集中它通過比較Rn + shifter_operand > 232-1的值決定是否上溢。對於8位和16位的語出操作記作CarryFrom8和CarryFrom16,它們通常被用在字節和半字操作中[1]。
main:
/* clean all condition flags */
msr cpsr_f, #0
/* make sure the add result won't carry and
let cpsr->r1 */
mov r3, #0xfffffffe
adds r3, r3, #1
mrs r1, cpsr
/* carried and let cpsr->r2 */
adds r3, r3, #1
mov r0, r3
......
輸出結果如下所示,顯然r1的C標誌位為0,而由於出現進位r2則變為了1。
Hex r0 0x00000000 r1 0x80000010 r2 0xbeb62ebc
Oct r0 0 r1 80000010 r2 -1095356740
Format out:
31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
N Z C V Q J | E G | CV E A I F T M4 M3 M2 M1 M0
r1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0
r2 1 0 1 1 1 1 1 0 1 0 1 1 0 1 1 0 0 0 1 0 1 1 1 0 1 0 1 1 1 1 0 0
2.6.3.2. 減法指令借位
減法指令add將所有操作數當做無符號處理,CPU內部使用NOT BorrowFrom(Rn - shifter_operand)來計算是否借位,其中rn和shifter_operand分別為第一操作數和第二操作數。BorrowFrom的算法很簡單,在ARM指令集中它通過比較Rn - shifter_operand < 0的值決定是否借位。如果發生借位,那麼C標誌為0,否則為1。
/* clean all condition flags */
msr cpsr_f, #0
/* make sure the sub result won't borrow and
let cpsr->r1 */
mov r3, #0x1
subs r3, r3, #1
mrs r1, cpsr
/* borrowed let cpsr->r2 */
subs r3, r3, #1
mrs r2, cpsr
mov r0, r3
......
測試原理:將作出的被減數設置為1,第一次減1時,不發生借位置C為1,此時r3內容為0,然後再減去1,顯然發生借位,所以置C為1。
General out:
Hex r0 0xffffffff r1 0x60000010 r2 0x80000010
Oct r0 -1 r1 60000010 r2 -2147483632
Format out:
31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
N Z C V Q J | E G | CV E A I F T M4 M3 M2 M1 M0
r0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
r1 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0
r2 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0
注意r0中為1-2的值,也即為-1的補碼。
2.6.3.3. 移位指令
- asr:算術右移指令,將符號位拷貝到空位,若移位量為32,則目標寄存器清0,且最後移除的位保留在C中;若移位量大於32,則目標寄存器和C均被清0。
- lsl:邏輯左移指令,空位補0,若移位量為32,則目標寄存器清0,且最後移出的位保留在C中;若移位量大於32,則目標寄存器和C均被清0。若位移量為0,不影響C標誌。
- lsr:邏輯右移指令,空位補0,若移位量為32,則目標寄存器清0,且最後移出的位保留在C中;若移位量大於32,則目標寄存器和C均被清0。若位移量為0,不影響C標誌。
- ror:數據循環右移指令,影響C標誌。
2.6.3.4. V溢出位
在加減運算中,通過OverflowFrom(Rn + shifter_operand)來判斷是否溢出。OverflowFrom分為兩種情況:加法運算中,如果操作數的符號位相同,但是結果的符號位發生改變;在減法運算中,如果兩操作數符號位不同,結果與第一個操作數的符號位也不同,則溢出,也即超出補碼錶示範圍。
main:
/* clean all condition flags */
msr cpsr_f, #0
/* make sure the adds result won't overflow */
mov r3, #0x6ffffffe
adds r3, r3, #1
mrs r1, cpsr
/* overflowed let cpsr->r2 */
adds r3, r3, #1
mrs r2, cpsr
mov r0, r3
......
這裡以加法為例,首先設置r3為0x7ffffffe,確保第一次加1時,不會溢出,而第二次則會發生結果符號位的改變。
General out:
Hex r0 0x80000000 r1 0x00000010 r2 0x90000010
Oct r0 -2147483648 r1 10 r2 -1879048176
Format out:
31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
N Z C V Q J | E G | CV E A I F T M4 M3 M2 M1 M0
r0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
r1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0
r2 1 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0
顯然r1中的V為0,而r2中的V為1。
2.7. 跳轉指令
2.7.1. b和bl指令
b(Branch)和bl(Branch and Link)指令實現分枝跳轉,並且支持條件跳轉。bl指令與b指令的區別是它在跳轉前會將當前pc的下一條指令地址存入lr寄存器,指令編碼的區別就在於L位是否為1。所以bl適合子程序調用。bl的指令格式如下,signed_immed_24表示24位有符號立即數,顯然它可以表示的大小為+-8M,由於ARM的指令長度總是32bits,所以可以表示+/-32MB的地址空間。 31 28 27 26 25 24
+-----+--------+---+--------------------------------+
|cond | 1 0 1| L | signed_immed_24 |
+-----+--------+---+--------------------------------+
bl指令的偽代碼表示如下:
if ConditionPassed(cond) then
if L == 1 then
LR = address of the instruction after the branch instruction
PC = PC + (SignExtend_30(signed_immed_24) << 2)
`
bl指令格式如下,它的參數是子程序的標籤,並不接受立即數,所以這牽涉到signed_immed_24計算方法。顯然,它是在編譯器編譯過程中確定。
bl{cond} label
- 首先確定bl指令的地址。
- 然後確定bl參數label代表的地址。
- 計算label與bl指令之間的偏移offset,顯然該偏移的單位為字節。
- 根據公式(offset - 8) >> 2 計算指令偏移數,這裡就是signed_immed_24。
- 如果得到的signed_immed_24值不在−33554432 和 33554428範圍內,則編譯器報錯。
offset之所以要減去8,是因為PC + (SignExtend_30(signed_immed_24) << 2)中的pc是指當前正在執行的pc(excute),而pc(excute)= pc(fetch) + 8。指令中的label對應的地址相當於pc(fetch)。這裡用一個簡單的例子說明:
.section .text
.align 2
.global main
main:
mov r0, #12
mov r1, #34
mov r2, #56
bl over
.global over
over:
mov r0, #0
以上是一個簡單的跳轉測試,over標籤與bl指令地址相差1個指令,4個字節。反彙編代碼如下:
00008790 <main>:
8790: e3a0000c mov r0, #12 ; 0xc
8794: e3a01022 mov r1, #34 ; 0x22
8798: e3a02038 mov r2, #56 ; 0x38
879c: ebffffff bl 87a0 <over>
000087a0 <over>:
87a0: e3a00000 mov r0, #0 ; 0x0
ebffffff就是指令碼,eb無需解釋了,而0xffffff就是signed_immed_24,它通過(0x87a0 - 0x879c - 8) >> 2計算而得,這個值就是-1,由於指令中的操作數都是採用補碼錶示,所以0xffffff就是-1的補碼。修改例子如下,此時地址相差3個指令,這保證偏移的指令數為正整數1。
.section .text
.align 2
.global main
main:
mov r0, #12
bl over
mov r1, #34
mov r2, #56
.global over
over:
mov r0, #0
觀察反彙編的結果,顯然指令碼為eb000001,正數1的補碼和原碼相同。
00008790 <main>:
8790: e3a0000c mov r0, #12 ; 0xc
8794: eb000001 bl 87a0 <over>
8798: e3a01022 mov r1, #34 ; 0x22
879c: e3a02038 mov r2, #56 ; 0x38
000087a0 <over>:
87a0: e3a00000 mov r0, #0 ; 0x0
圖 3. bl跳轉示意圖
bl跳轉示意圖
b指令的存在意義在於,有時候無需返回,或者lr寄存器另作它用,此時如需返回需要顯式保存下一 指令地址到其他寄存器。
2.7.2. ldr和adr指令
ldr和adr是另兩個常用來實現跳轉的指令,嚴格來說ldr指令有兩種,根據操作數,它可以是指令,也可以是偽指令。 作為指令的ldr的語法格式如下,通常實現把一個32位的字從內存裝入一個寄存器。
ldr{<cond>} <Rd>,<addr_mode>
指令編碼格式如下:
31 28 27 26 25242322212019 1615 1211 0
+-----+------+-+-+-+-+-+-+----+----+----------------+
|cond | 0 1 |1|P|U|0|W|1| Rn | Rd | addr_mode |
+-----+------+-+-+-+-+-+-+----+----+----------------+
ldr指令根據addr_mode所確定的地址模式將一個32位字讀取到指令中的目標寄存器Rd。如果指令中的尋址方式確定的地址不是字對齊的,則讀出的數值要進行循環右移。所移位數為尋址方式確定的地址bits[1:0]的8倍,也就是說處理器將取到的數值作為字的最低位處理。
addr_mode它確定了指令編碼中的I、P、U、W、Rn和addr_mode位。所有的尋址模式中,都會 確定一個基址寄存器Rn,通常為pc。考慮如下的ldr測試程序:
......
.global main
main:
mov r0, pc
ldr r1, [r0]
ldr r2, main
ldr r0, =main
bl print
b quit
編譯後的反彙編程序如下,顯然對於第一個還有第二個指令來說,它們均是作為指令編譯的,並且是相對於pc(當前正在取指的指令地址,而非正在執行的當前ldr指令的地址)寄存器進行偏移。而第三天指令則是將main的絕對地址0x00008790裝載到r0。所以可以通過ldr指令進行相對偏移來跳轉到C函數
00008790 <main>:
8790: e1a0000f mov r0, pc
8794: e5901000 ldr r1, [r0]
8798: e51f2010 ldr r2, [pc, #-16] ; 8790 <main>
879c: e59f0004 ldr r0, [pc, #4] ; 87a8 <main+0x18>
87a0: ebffff9d bl 861c <print>
87a4: eafffff2 b 8774 <quit>
87a8: 00008790 .word 0x00008790
2.8. 協處理器指令
2.9. ARM彙編偽指令
2.9.1. .macro
宏指令是由.macro和.endm來定義開始和結束的一組指令的集合。它類似於C語言中的宏函數,使用它將可以:
- 使代碼可以重用,減小代碼量。
- 使程序邏輯更加清晰
偽指令格式:
.macro macroname {$param1} {$param2} ...
@commands
.mend
macroname是定義的宏名,$paramx是宏指令的參數,當宏指令被展開式將被替換成相應的值,類似於函數中的形式參數。一個例子是Linux內核中用於打印信息的kputc宏命令。在使用宏參數時必須這樣“$param”表示參數。
.macro kphex,val,len
mov r0, \val
mov r1, #\len
bl phex
.endm
在C語言和彙編語言的交互調用中,使用宏可以簡化參數的傳遞。可以使用.if宏開關來定義宏指令,.exitm可以跳出宏。一個進行移位運算的例子如下:
.macro shiftleft reg, shift
.if \shift < 0
mov \reg, \reg, asr #-\shift
.exitm
.endif
mov \reg, \reg, lsl #\shift
.endm
2.9.2. .rept
.rept 用於重複執行一組指令,它的格式如下:
.rept <repeat>
.endr
repeat指明重複執行的次數。一個例子是Linux內核中用於nop的操作:
.rept 8
mov r0, r0
.endr
2.10. Sandbox
表 4. Memory Hierarchy
圖 4. 內核RAM佈局
[1] 儘管ARM提供了豐富的相關指令,比如uadd8,uadd16等,但並不是所有編譯器都能夠支持這些指令。
上一頁 下一頁 Linux內核學習和研究及嵌入式(ARM)學習和研究的開放文檔 起始頁 3. ARM尋址方式