1. 程式人生 > 實用技巧 >ARM雜散知識

ARM雜散知識

畫重點:
1.儲存器格式:重點是大小端識別 經常考
2.對齊後結構體佔用空間大小:使用aligned,packed,#pragma pack()三種方式都要會

Thumb指令集

Thumb指令集能夠以16位的系統開銷得到32位的系統性能

正常ARM指令PC+4,Thumb指令PC+2

Thumb指令集與ARM指令集的區別

Thumb指令集沒有協處理器指令、訊號量指令以及訪問CPSR或SPSR的指令,沒有乘加指令及64位乘法指令等,且指令的第二運算元受到限制;除了跳轉指令B有條件執行功能外,其他指令均為無條件執行;大多數Thumb資料處理指令採用2地址格式。Thumb指令集與ARM指令集的區別一般有如下幾點:

Ø 跳轉指令
程式相對轉移,特別是條件跳轉與ARM程式碼下的跳轉相比,在範圍上有更多的限制,轉向子程式是無條件的轉移。
Ø 資料處理指令
資料處理指令是對通用暫存器進行操作,在大多數情況下,操作的結果須放入其中一個運算元暫存器中,而不是第三個暫存器中。
資料處理操作比ARM狀態的更少,訪問暫存器R8—R15受到一定限制。
(除MOV和ADD指令訪問暫存器R8—R15外,其他資料處理指令總是更新CPSR中ALU狀態標誌)
訪問暫存器R8—R15的Thumb資料處理指令不能更新CPSR中的ALU狀態標誌
Ø 單暫存器載入和儲存指令
在Thumb狀態下,單暫存器載入和儲存指令只能訪問暫存器R0—R7
Ø 批量暫存器載入和儲存指令
LDM和STM指令可以將任何範圍為R0——R7的暫存器子集載入或儲存

ARM處理器模式

處理器模式 描述
使用者模式(User,usr) 正常程式執行的模式
快速中斷模式(FIQ,fiq) 用於高速資料傳輸和通道處理
外部中斷模式(IRQ,irq) 用於通常的中斷處理
特權模式(Supervisor,sve) 供作業系統使用的一種保護模式
資料訪問中止模式(Abort,abt) 用於虛擬儲存及儲存保護
未定義指令中止模式(Undefined,und) 用於支援通過軟體模擬硬體的協處理器
系統模式(System,sys) 用於執行特權級的作業系統任務

除了使用者模式以外的其他6種處理器模式稱為特權模式(Privileged Modes)。除系統模式外,其他模式又稱為異常模式

處理器模式可以通過軟體切換,也可以通過外部終端或異常處理過程進行切換,大多數的使用者程式執行在使用者模式下(這時使用者程式不能訪問一些受作業系統保護的系統資源,應用程式也不能直接進行處理器模式的切換)。當需要進行處理器模式的切換時,應用程式可以產生異常處理,在異常處理過程中進行處理器模式的切換。

每一種異常模式都有一組暫存器,供相應的異常處理程式使用,以保證在進入異常模式時,使用者模式下的暫存器不會被破壞。

系統模式並不是通過異常模式進入的,它和使用者模式具有完全一樣的暫存器,但是系統模式屬於特權模式,可以訪問所有的系統資源,也可以直接進行處理器模式切換。

ARM暫存器

ARM處理器共有37個暫存器,包括:

  • 31個通用暫存器,包括程式計數器(PC)在內,這些暫存器都是32位暫存器。
  • 6個狀態暫存器,這些暫存器都是32位暫存器,但目前只使用了其中的12位。

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

更詳細的描述可見http://blog.chinaunix.net/uid-20443992-id-5700979.html

ARM體系的異常中斷

異常中斷種類:

異常中斷名稱 含義
復位(Reset) 復位異常通常用在下面幾種情況下:
系統上電時
系統復位時
跳轉到復位中斷處向量執行,稱為軟復位
未定義的指令(undefined instruction) 當ARM處理器或者系統中的協處理器認為當前指令未定義時,
產生未定義的指令異常中斷,可以通過該異常中斷機制模擬浮點向量運算
軟體中斷(software interrupt SWI) 使用者定義的中斷指令,可用於使用者模式下的程式呼叫特權操作指令,
在實時作業系統鍾可以通過該機制實現系統呼叫功能
指令預取中止(Prefetch Abort) 處理器預取的指令地址不存在,或者該地址不允許當前指令訪問,
處理器產生指令預取中止異常中斷
資料訪問中止(Data Abort) 處理器資料訪問指令的目標地址不存在,或者該地址不允許當前指令訪問,
處理器產生資料訪問中止異常中斷
外部中斷請求(IRQ) 處理器的外部中斷請求引腳有效,且CPSR暫存器的I控制位被清除時產生,
系統中各種外設通常通過這種方式產生中斷請求處理器服務
快速中斷請求(FIQ) 當處理器的外部快速中斷請求引腳有效,且CPSR暫存器的F控制位被清除時產生

ARM體系中儲存系統

地址空間

​ ARM體系使用單一的平板地址空間,該地址空間大小為232個8位位元組,這些位元組單元的地址是一個無符號的32位數值,其取值範圍為0到232-1。

​ ARM的地址空間也可以看作是230個32位的字單元,這些字單元的地址可以被4整除,即該地址的低兩位為0b00,地址為A的字資料包括地址為A,A+1,A+2,A+3這4個位元組單元的內容

儲存器格式

​ ARM體系中,每個字單元包含4個位元組單元或者兩個半字單元。

​ 在字單元中,4個位元組哪一個是高位位元組,哪一個是低位位元組則有兩種不同的格式:big-endianlittle-endian格式

big-endian格式中,對於地址為A的字單元包括位元組單元A、A+1、A+2、A+3,其中位元組單元從高位到低位位元組順序為:A、A+1、A+2、A+3,其格式大概為:

31-24bit 23-16bit 15-8bit 7-0bit
A A+1 A+2 A+3

little-endian格式中,對於地址為A的字單元包括位元組單元A、A+1、A+2、A+3,其中位元組單元從高位到低位位元組順序為:A+3、A+2、A+1、A,其格式大概為:

31-24bit 23-16bit 15-8bit 7-0bit
A+3 A+2 A+1 A

關於大小端的判斷:

bool isBig()
{
	int a = 1;
	char *p = (char*)&a;
	if(*p == 1)
	{
		return false;//小端
	}
	else
	{
		return true;//大端
	}
}
int main()
{
    if(!isBig())
	{
		printf("Is little\n");
	}
	else
	{
		printf("Is big\n");
	}
	return 0;
}

非對齊的儲存訪問操作

非對齊的指令預取操作

無論處理器處於ARM還是Thumb狀態,如果寫入到暫存器PC的值是非對齊的(ARM下低兩位不為0,Thumb下最低位不為0)則要麼指令執行的結果不可預知,要麼地址值中後面的2位或1位被忽略。

非對齊的資料訪問操作

對於Load/Store操作,如果是非對齊的資料訪問操作,系統定義了下面3種可能的結果:

  • 執行的結果不可預知
  • 忽略字單元地址的低兩位(即訪問地址為 destaddr & 0XFFFFFFFC 的字單元);忽略半字單元地址的最低位(即訪問地址為 destaddr & 0XFFFFFFFE 的半字單元)
  • 由儲存系統進行忽略,比如定址0X20000001的地址,該值被原封不動地傳進儲存系統,和第二條類似

什麼情況容易發生非對齊訪問

出現alignment fault問題,通常是使用者編寫的程式碼導致。估計很多程式猿在編寫程式碼(特別是c/c++程式碼)時,從未考慮過這樣的問題,那是因為多數可能都在X86架構下的進行程式碼開發,而且沒有考慮過程式碼的移植性,如前面所說X86硬體會自動處理非對齊問題,使用者感知不到,但這種情況下,由此帶來的效能損耗,使用者可能也關注不到了。另一方面,部分情況下,編譯器也會自動做padding處理(如對結構體的自動填充對齊),這也進一步讓程式猿們減少了對alignment fault的關注。

最常見的可能導致alignment fault的程式碼編寫方式如:

1) 指標轉換

  將低位寬型別的指標轉換為高位寬型別的指標,如:將char * 轉為int *,或將void *轉為結構體指標。這類操作是導致alignment fault的最主要的來源,在分析定位問題時,需要特別關注。對於出現異常卻又必須這樣使用的場景,對這類轉換後的指標進行訪問時,如果不能確認其對應的地址是對齊的,則應該使用memcpy訪問(memcpy方式不存在對齊問題)。另外,建議轉換後立即使用,不要將其傳遞到其他函式和模組,防止擴充套件,帶來潛在的問題。

2) 使用packed屬性或者編譯選項(筆試面試經常會問到的問題)

  這樣的操作會關閉編譯器的自動填充功能,從而使結構體中各個欄位緊湊排列,如果排列時未處理好對齊,則可能導致alignment fault。一些場景下(核心中也較常見)確實需要使用者自行緊湊排列結構體,可節省空間(在記憶體資源稀缺的場景下,很有用),此時需要特別關注對齊問題,建議通過填充的方法儘量對齊,如此可能會導致空間浪費,但是會提升訪問效能,典型的“以空間換時間”的思路。如果對空間有強烈要求,而可以接受效能損失,也可以不考慮對齊,不做padding,但在訪問這些結構體的資料時,需要全部使用memcpy的方式。

用__attribute__進行對齊(aligned和packed

比如如下程式碼:

struct stu{
    char sex;
    int length; ;
    char name[2];
    char value[15];
}__attribute__ ((aligned (1)));

或者是如下程式碼:

typedef struct {
	/*	frame control format.  */
	union {
		uint16_t fcf;
		struct {
			uint16_t type : 3;
			uint16_t security : 1;
			uint16_t frame_pending : 1;
			uint16_t ar : 1;
			uint16_t panid_compression : 1;
			uint16_t reserved : 3;
			uint16_t dest_addr_mode : 2;
			uint16_t frame_version : 2;
			uint16_t src_addr_mode : 2;
		} __attribute__ ((packed)) fcf_s;
	} __attribute__ ((packed)) fcf_u;
	/*	sequence number.  */
	uint8_t seq_num;
} __attribute__ ((packed)) mhr_t;

可見 常用的方法有__attribute__((aligned(n)))和__attribute__((packed))兩種:

aligned方法

​ 這種方法可能更常用也更常考一些,多用於進行對齊操作,使用方法為:

​ aligned後接要對齊的位元組數標準n,當結構體內成員佔用的最大位元組數不超過n時,按照這個數n進行對齊,當超過n時按照結構體內成員佔用的最大位元組數進行對齊。例如上述程式碼中對齊標準為1,但結構體中的最大不可拆分成員變數為int,其佔用位元組數為4,1<4,因此按照4位元組方式進行對齊,對齊結果如下圖所示,每行為一個字(32bit 4位元組),由於對齊標準為4,sex和length就不能放在同一字中,空白的部分被reserved填充(由編譯器進行操作)。由此也能推得,當n<=4時對齊後的記憶體排布都是下圖方式,佔用記憶體為28

​ 當n=8時,由於4<8,因此按照8位元組方式進行對齊,對齊結果如下圖:其中每行有8個位元組,最終佔用記憶體為32

這時候產生了一個疑問:為什麼sex和lenghth、name不能放到同一個8位元組記憶體中呢?

這是因為:每個成員所在記憶體的地址必須為其大小的整數倍,即length的大小為4,如果緊接著sex排放其記憶體地址將是……1,不滿足4的整數倍。

packed方法

​ packed方法與編譯器聯絡更密切,對於不同的編譯器來說得到的結果可能不一樣,如下的程式碼,我在codeblocks上編譯和在linux下用gcc編譯得到的大小是不同的,在gcc下編譯時返回結果為5,證明對其進行了緊湊型編譯,而在codeblocks中編譯執行結果仍為8,(不知道是不是編譯器環境設定錯誤),不過可以確定的是,packed方法可以讓編譯器不按照對齊規則進行編譯,而是使內部變數連續地排布,這對一些跨平臺的移植很有幫助,比如各種跨平臺的協議棧等

struct stu{
    char sex;
    int length;
}__attribute__((packed)) ;
“:”符號的引入(位域操作)

​ 如下面程式碼塊中就是IEEE802.15.4協議中幀頭部的定義,由於需要嚴格按照下圖中的規範中各位進行解析,所以必須按照特定的位數進行對齊,而在一個變數中對其進行定義顯然有些麻煩,因此位域操作使得程式碼可以很明朗地對這類結構進行描述。

​ 位域操作的用法是:在變數中,只取變數最低幾位,並將其命名為這個變數名,而這個位數取決於":"後面的數字。比如下面的表格和程式碼中的各位是一一對應的,使用時也可以在一個結構體中直接通過變數名對某一位進行讀寫,這樣做還不會浪費額外的空間。

typedef struct {
	/*	frame control format.  */
	union {
		uint16_t fcf;
		struct {
			uint16_t type : 3;
			uint16_t security : 1;
			uint16_t frame_pending : 1;
			uint16_t ar : 1;
			uint16_t panid_compression : 1;
			uint16_t reserved : 3;
			uint16_t dest_addr_mode : 2;
			uint16_t frame_version : 2;
			uint16_t src_addr_mode : 2;
		} __attribute__ ((packed)) fcf_s;
	} __attribute__ ((packed)) fcf_u;
	/*	sequence number.  */
	uint8_t seq_num;
} __attribute__ ((packed)) mhr_t;
位域的儲存

C語言標準並沒有規定位域的具體儲存方式,不同的編譯器有不同的實現,但它們都儘量壓縮儲存空間。

位域的具體儲存規則如下:

  1. 當相鄰成員的型別相同時,如果它們的位寬之和小於型別的 sizeof 大小,那麼後面的成員緊鄰前一個成員儲存,直到不能容納為止;如果它們的位寬之和大於型別的 sizeof 大小,那麼後面的成員將從新的儲存單元開始,其偏移量為型別大小的整數倍。

  2. 當相鄰成員的型別不同時,不同的編譯器有不同的實現方案,GCC 會壓縮儲存,而 VC/VS 不會。

  3. 如果成員之間穿插著非位域成員,那麼不會進行壓縮。

無名位域一般用來作填充或者調整成員位置。因為沒有名稱,無名位域不能使用。

編譯選項對齊

#pragma pack (n)           	 // 作用:C編譯器將按照n個位元組對齊。
#pragma pack ()              // 作用:取消自定義位元組對齊方式。
#pragma pack (push,1)     	 // 作用:是指把原來對齊方式設定壓棧,並設新的對齊方式設定為一個位元組對齊
#pragma pack(pop)            // 作用:恢復對齊狀態