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

什么是嵌入式單片機?嵌入式單片機詳情匯總
單片機or嵌入式linux

是否要從單片機轉(zhuǎn)為嵌入式Linux

基于51單片機設(shè)計N字節(jié)十六進制除法——以6字節(jié)除以3字節(jié)為例

STM32 終極字節(jié)對齊解析

【M3內(nèi)核篇】關(guān)于“堆棧指針的最低兩位永遠是0,這意味著堆棧總是4字節(jié)對齊的”的理解

評論