ARM非對齊訪問和Alignment Fault


 1、指令對齊

A64指令必須word對齊。嘗試在非對齊地址取值會觸發PC alignment fault。

1.1、PC alignment checking

PC(Program Counter)寄存器用來存放下一條執行指令地址,對於AArch64架構,如果PC寄存器低2位不為0,則觸發PC alignment fault。

類似於Instruction Aborts異常,將非對齊地址加載到PC寄存器並不會直接觸發PC alignment fault,只有當CPU嘗試從該地址取指令時才會觸發異常。

當取指異常發生在EL0時,CPU切換到EL1。當取指異常發生時,HCR_EL2.TGE位為1且EL2使能時,CPU切換到EL2。當取指異常發生時,CPU處於其他模式,CPU運行模式不切換。

當發生PC alignment fault時,ESR(Exception Syndrome Register)的EC域置0x22,表明異常級別。

偽代碼AArch64.CheckPCAlignment()執行PC alignment check,偽代碼AArch64.PCAlignmentFault()則主動觸發異常。

 

2、數據訪問對齊

2.1、數據非對齊訪問

         如果被訪問的內存地址不按照被訪問的數據類型的位寬對齊,稱為非對齊訪問。比如int型占4個字節,則訪問int型數據的內存地址需要按照4字節對齊。

2.2、硬件非對齊訪問支持

         MIPS架構不支持非對齊訪問。

         X86架構支持非對齊訪問,其實現機制是將非對齊訪問指令拆分成多條指令執行,結合拼接(或者拆分)指令獲取數據。缺點是犧牲性能。

         ARMv5架構不支持非對齊訪問。

         ARMv6架構開始參考X86架構實現方式支持非對齊訪問,但是是部分內存訪問指令支持。

         ARMv7-M架構中CCR.UNALIGN_TRP位控制是否使能對齊檢查(Alignment Check),ARMv7-A、ARMv7-R、ARMv8架構中SCTLR.A位控制是否使能對齊檢查,默認情況下不使能對齊檢查。

 

 

  如果使能對齊檢查,則任何指令的非對齊訪問均觸發非對齊異常。

  對於A32/T32代碼,如果不使能對齊檢查,則大部分指令的非對齊訪問由CPU處理,如LDR, LDRH, STR, STRH,LDRSH, LDRT, STRT,LDRSHT,LDRHT,STRHT,TBH。其他的數據訪問指令的非對齊訪問都會觸發非對齊異常,如STRD,LDRD。

  對於A64代碼,如果不使能對齊檢查,則所有的load和store指令的非對齊訪問均由CPU處理,但是exclusive load/store, load acquire和store release指令的非對齊訪問則會觸發非對齊異常,包括LDAXR, LDAXRB, LDAXRH, LDAXP, STLXR, STLXRB, STLXRH, STLXP。

  對於SIMD指令,一般都是強制64字節或者128字節對齊,如果發生非對齊訪問,則觸發非對齊訪問異常。

  對於任何內存訪問,如果使用SP指針作為基地址,就必須quadword對齊,否則觸發棧對齊異常。

 

2.3、軟件非對齊訪問支持

         部分MIPS架構,通過在VxWorks內核中對非對齊訪問異常進行處理,通過多次訪存操作和拼接操作來實現非對齊訪問,代價是犧牲性能。ARM架構內核中也有類似的處理方式,可以通過相關的配置來控制其處理方式,詳細內容查看第四節。

 

2.4、編譯器非對齊訪問支持

2.4.1、GCC編譯器

  使能非對齊訪問:-munaligned-access

  禁止非對齊訪問:-mno-unaligned-access

  默認情況下,ARM都是aligned-access的,如果代碼中使用__attribute__((packed))定義的結構體,會出現結構體成員是非對齊的,此時如果沒有使能非對齊訪問會導致觸發abort異常。

2.4.2、編譯器優化

  編譯器一般支持對非對齊訪問代碼的優化,即在編譯階段通過多次內存訪問操作拆分和拼接從而規避非對齊訪問。

  GCC編譯選項-Ox用來指定代碼優化級別,-O0表示不優化,其他優化級別下會對非對齊訪問代碼進行優化,比如將LDRD指令的非對齊訪問拆分成多條LDR指令。

 

3、棧對齊

3.1、SP alignment checking

         當SP(Stack Pointer)寄存器的低4位不為0時,如果當前指令使用SP作為基地址,則觸發棧非對齊異常。

         偽代碼AArch64.CheckSPAlignment()執行stack pointer check,AArch64.SPAlignmentFault()則觸發棧對齊異常。

         類似於Data Aborts異常,將非對齊值加載到SP寄存器並不會直接觸發異常,只有當嘗試從非對齊地址取數據時才會觸發異常。

 

4、ARM Linux內核非對齊訪問

4.1、/proc/cpu/alignment

         Linux內核只針對arm架構實現了非對齊訪問處理機制,主要是針對LDR, STR, LDRD, LDRD, STRD, LDM, STM, LDRH, STRH指令的非對齊訪問進行處理。對於arm64架構,因為ARMv8架構CPU可以處理所有LDR/STR類內存訪問指令的非對齊訪問,因此沒有實現該機制。這樣就會導致如果在arm64架構上運行64位Linux內核,而用戶態為32位應用程序時,如果發生非對齊訪問,則會觸發異常。

宏定義

說明

0

UM_WARM

內核打印Alignment Trap警告,打印進程名,pid,pc,指令,地址,和異常錯誤碼等

1

UM_FIXUP

內核嘗試修復用戶代碼非對齊訪問

2

UM_SIGNAL

發生非對齊訪問時,內核發送SIGBUS信號量給對應進程

         ARMv5架構,該節點默認值為0,即忽略非對齊訪問。

         ARMv6及以上架構,CPU本身部分支持非對齊訪問,因此基本不關心該節點值,但是對於LDM, STM, LDRD和STRD等復合指令進行非對齊訪問時,仍然需要軟件處理,此時,可以將該節點值設置為1,即fix up模式。

         上述三種值是位映射,也可以是組合值。

 

4.2、非對齊訪問異常處理流程

4.2.1、ARM架構

         Linux內核初始化時,通過fs_initcall(alignment_init)加載非對齊訪問處理模塊。alignment_init()函數中首先創建/proc/cpu/alignment節點,然后通過hook_fault_code(FAULT_CODE_ALIGNMENT, do_alignment, SIGBUS, BUS_ADRALN, “alignment exception”)掛do_alignment鈎子函數。當發生非對齊訪問異常時,進入do_alignment中處理異常,根據獲取的/proc/cpu/alignment節點值,分別進入不同的處理分支。

         該部分代碼位於arch/arm/mm/alignment.c中。

 

4.2.2、ARM64架構

  Linux內核在fault_info[]中注冊鈎子函數,當發生非對齊訪問(BUS_ADRALN)時,進入do_bad()函數,給對應進程發送SIGBUS信號量。

  該部分代碼位於arch/arm64/mm/fault.c中。

 

5、什么情況下容易發生非對齊訪問?

  出現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的方式。

 

6、測試代碼

#include <stdio.h>

#include <stdlib.h>

#include <signal.h>

 

void sigbus_handler(int sno)

{

         printf("signal %d captured\n", sno);

         exit(1);

}

 

int main(int argc, char *argv[])

{

         char intarray[8] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88};

 

         signal(SIGBUS, sigbus_handler);

 

         printf("int1 = 0x%08x, int2 = 0x%08x, int3 = 0x%08x, int4 = 0x%08x\n",

                   *((int *)(intarray + 1)),

                   *((int *)(intarray + 2)),

                   *((int *)(intarray + 3)),

                   *((int *)(intarray + 4)));

 

         return 0;

}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM