本文主要介紹了嵌入式單片機編程中,為什么大多時候要保證堆棧8字節對齊的問題。
字節對齊原則
1. 結構(struct)(或聯合(union)) 中的第一個數據成員放在 offset 為 0 的地方,以后每個數據成員存儲的起始位置,要從該成員大小或者成員的子成員大小(只要該成員有子成員,比如說是數組,結構體等)的整數倍開始(比如 int 型變量在 32 位編譯環境下為 4 字節,則要從 4 的整數倍地址開始存儲);
2. 如果一個結構里有某些結構體成員,則結構體成員要從其內部最大元素大小的整數倍地址開始存儲.
如:struct a 里存有 struct b, b 里有 char, int , double 等元素,那 b 應該從 8 的整數倍開始存儲.;
3. 結構體的總大小,也就是 sizeof 的結果,必須是其內部最大成員的整數倍,不足的要補齊;
總大小為最大成員變量大小的整數倍,sizeof(AA) = 24;sizeof(BB) = 32。
#pragma pack():
在代碼前加一句 #pragma pack(1),會發現 sizeof(AA) = 17;sizeof(BB) = 24;
AA 是 8+4+1+4=17;
BB 是 5+4+8+4+2+1=24;
這就是理想中的沒有內存對齊的情況,所以 #pragma pack(1) 是告訴編譯器,所有的對齊都按照1的整數倍對齊,換句話說就是沒有對齊規則。
即#pragma pack(n)就是所有的對齊都按照n的整數倍對齊。
Vc,Vs等編譯器默認是 #pragma pack(8),所以測試我們的規則會正常;
gcc 默認是 #pragma pack(4),并且 gcc 只支持 1, 2, 4 對齊。套用三原則里計算的對齊值是不能大于 #pragma pack 指定的n值。
為什么要保證堆棧8字節對齊
AAPCS 規則要求堆棧保持 8 字節對齊。如果不對齊,調用一般的函數也是沒問題的。但是當調用需要嚴格遵守 AAPCS 規則的函數時可能會出錯。
例如調用 sprintf 輸出一個浮點數時,棧必須是 8 字節對齊的,否則結果可能會出錯。
實驗驗證:
1.在 A 處設置斷點,讓程序全速運行至 A
2.在 MDK 中修改 MSP 的值使 MSP 滿足 8 字節對齊
3.全速運行程序,觀察 buf 中的字符為 1.234 結果正確
4.回到第 2 步,修改 MSP 使之只滿足 4 字節對齊而不滿足 8 字節對齊
5.全速運行程序,觀察 buf 中的字符為 -2.000 結果錯誤
該實驗證明了調用 sprintf 輸出一個浮點數必須要保證棧 8 字節對齊。
編譯器為我們做了什么
先看一個實驗:
保證初始的時候堆棧是 8 字節對齊的;
1.在 A 處設置斷點;
2.全速運行至 A,觀察 MSP=0x2000025c,沒有 8 字節對齊;
3.略微修改一下 main 函數代碼如下,其他部分代碼不變;
4.同樣在 A 處設置斷點;
5.全速運行至 A,觀察 MSP=0x200002d8,這次 8 字節對齊了; 這個實驗說明了如果編譯器發現了某個函數需要調用浮點庫時會自動調整編譯生成的匯編代碼,從而保證調用這些浮點庫函數時堆棧是8字節對齊的。
換句話說如果我們保證了棧初始的時候是8字節對齊的,那么編譯器可以保證以后調用浮點庫時堆棧仍是8字節對齊的。
os下應該怎樣設置任務堆棧
由上面的討論可知給任務分配棧時需要保證棧是 8 字節對齊的,不然在該任務中凡是調用 sprintf 的函數均會出錯,因為棧一開始就是不對齊的。
是否保證了棧初始是8字節對齊了就萬事大吉了呢。no!大家請看一種特殊的情況:
mian函數的反匯編如下:
保證初始的時候堆棧是 8 字節對齊的;
1.在 A 處設置斷點;
2.全速運行至 A,觀察此時 MSP=0x200002e4 未對齊;
3.在 MDK 中將 SVC 的掛起位置 1;
4.在 B 處設置斷點;
5.全速運行至 B,觀察此時 MSP=0x200002b4 未對齊;
6.繼續全速執行,觀察 buf 中的字符為: -2.000 出錯了;
這個實驗說明了即使保證棧初始是 8 字節對齊的,編譯器也只能保證在調用 sprintf 那個時刻棧是 8 字節對齊的,但不能保證任意時刻棧都是 8 字節對齊的,如果恰巧在 MSP 沒有 8 字節對齊的時刻發生了中斷,而中斷中又調用了 sprintf,這種情況下仍會出錯。
Cortex-M3 內核為我們做了什么
Cortex-M3 內核提供了一種硬件機制來解決上述這種中斷中棧不對齊問題。
CM3 中可以把 NVIC 配置控制寄存器的 STKALIGN 置位,來保證中斷中的棧 8 字節對齊。
具體實現過程如下:當發生中斷時由硬件自動檢測 MSP 是否 8 字節對齊,如果對齊了,則不進行任何操作,如果沒有對齊,則自動將 MSP 減 4 這樣便對齊了,同時將 xPSR 的第 9 位置位來記錄這個 MSP 的非正常的變化,在中斷返回若發現 xPSR 的第 9 位是置位的則自動將 MSP 加 4 調整回原來的值。
實驗驗證:
mian函數的反匯編如下:
1.在 A 處設置斷點;
2.全速運行至 A,觀察此時 MSP=0x200002e4 未對齊;
3.在 MDK 中將 SVC 的掛起位置 1,同時將 0xE000ED14 處的值由 0x00000000 改為 0x00000200(即將 NVIC 配置控制寄存器的 STKALIGN 置位)
4.在 B 處設置斷點;
5.全速運行至 B,觀察此時 MSP=0x200002b0 對齊了;
6.觀察中斷返回時的 MSP=0x200002e4 調整回來了;
7.繼續全速執行,觀察 buf 中的字符為:1.234 正確;
這個實驗說明了將NVIC配置控制寄存器的STKALIGN置位可以保護中斷時棧仍是8字節對齊
總結
綜上所述,為了能夠安全的使用嚴格遵守AAPCS規則的函數(比如sprintf)需要做到以下幾點:
保證MSP在初始的時候是8字節對齊的
如果用到OS的話,需要保證給每個任務分配的棧是保持8字節對齊的
如果用的是基于CM3內核的處理器,需將NVIC配置控制寄存器的STKALIGN置位。
審核編輯:劉清
-
處理器
+關注
關注
68文章
19396瀏覽量
230712 -
寄存器
+關注
關注
31文章
5363瀏覽量
120915 -
存儲器
+關注
關注
38文章
7525瀏覽量
164159 -
SVC控制器
+關注
關注
0文章
2瀏覽量
5245 -
嵌入式單片機
+關注
關注
0文章
10瀏覽量
2280
原文標題:對堆棧 8 字節對齊問題的討論
文章出處:【微信號:c-stm32,微信公眾號:STM32嵌入式開發】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論