前言:
爲了方便查看博客,特意申請了一個公衆號,附上二維碼,有興趣的朋友可以關注,和我一起討論學習,一起享受技術,一起成長。
1. 簡述
計算機中內存空間都是按照字節(byte)劃分的,從理論上講似乎對任何類型的變量的訪問可以從任何地址開始,但實際情況是在訪問特定類型變量的時候經常在特定的內存地址訪問,以 2、4、或 8 的倍數的字節塊來讀寫內存,這樣就會對基本數據類型的合法地址作出一些限制,即它的地址必須是 2,4 或 8 的倍數。各種類型數據按照一定的規則在空間上排列,而不是順序的一個接一個的排放,即需要對齊。
關於需要對齊的原因:
(1)爲了提高存取效率,如讀取一個 4 字節的數據,若起始地址爲 4 的倍數,CPU 只需一次讀取,如果不是 4 的倍數,既需要分成兩次讀取在拼接,這就造成了 CPU 效率的降低。
(2)處理器本身的限制,對某些操作,要求數據的內存地址是 2 、4 等整數倍,如果不是,常會造成一些硬件異常(HardFault)。數據的起始地址應具有“對齊”特性。比如:4 字節數據的起始地址應位於 4 字節邊界上,即起始地址能夠被 4 整除。
2. 數據的存儲方式
以下面的結構體爲例:
typedef struct
{
uint8_t data_buff[5];
uint32_t data_length;
uint8_t data_flag;
}st_data_test;
st_data_test g_data_test = {0};
sizof(g_data_test ) = 16。以結構體中最大的類型進行對齊,即 uint32_t 。
佔據存儲空間如下所示:
結構體各變量的首地址如下,均以 4 對齊:
如果我們把結構體的定義稍稍調整,如下:
typedef struct
{
uint8_t data_buff[5];
uint8_t data_flag;
uint32_t data_length;
}st_data_test;
st_data_test g_data_test = {0};
sizof(g_data_test ) = 12 。
在 C 語言中,內存的分配和管理是由操作人員控制的。在嵌入式開發過程中,尤其是存儲資源有限時,就必須合理的分配內存,使用內存。
typedef struct
{
uint8_t type;
uint16_t length;
uint8_t value;
}st_tlv;
st_tlv g_data_tlv = {0};
sizof(g_data_tlv ) = 6 。結構體 g_data_tlv 的大小爲 6 字節,而不是 4 字節。因爲結構體中適宜最大的長度來對齊的,此處就是按照 uint16_t 的長度來對齊。
字節對齊遵從系統字節數與要求的對齊字節數相比,最小原則,在四字節對齊時,局部會按照 2 字節對齊。
3. 數據的對齊方式
(1)數據類型自身的對齊值: char 型數據自身對齊值爲 1 字節,short 型數據爲 2 字節,int / float 型爲 4 字節,double型爲 8 字節;
(2)結構體或類的自身對齊值: 其成員中自身對齊值最大的那個值;
(3)指定對齊值: #pragma pack (value) 時的指定對齊值 value;
(4)數據成員、結構體和類的有效對齊值: 自身對齊值和指定對齊值中較小者,即有效對齊值 = min {自身對齊值,當前指定的pack值}。
每個成員按其類型的對齊參數(通常是這個類型的大小)和指定對齊參數中較小的一個對齊,並且結構的長度必須爲所用過的所有對齊參數的整數倍,不夠就補空字節。
3.1 pragma pack 關鍵字
#pragma pack 的主要作用就是改變編譯器的內存對齊方式。
C 編譯器可通過下面的方式改變對齊邊界。
使用僞指令 #pragma pack(n):C 編譯器將按照 n 個字節對齊;
使用僞指令 #pragma pack(): 取消自定義字節對齊方式。
(1)結構體對齊
#pragma pack(1)
typedef struct
{
uint8_t data_buff[5];
uint32_t data_length;
uint8_t data_flag;
}st_data_test1;
#pragma pack()
st_data_test1 g_st_data_test1;
如上結構體將按照 1 字節對齊,sizeof(g_st_data_test1) = 10。
結構體每個變量首地址如下:
注:GCC 編譯器可以通過 __ attribute((aligned (n))), __ attribute __ ((packed))來修改對齊長度。
(2)棧對齊
棧的對齊方式不受結構體成員對齊選項的影響。總是保持對齊且對齊在 4 字節邊界上。
//--------------------------------------------------------------------------------------------------------
// 函 數 名: stack_test
// 功能說明: 棧測試
// 形 參: 無
// 返 回 值: 無
// 日 期: 2020-04-04
// 備 注: 測試平臺,STM32F103
// 作 者: by 霽風AI
//--------------------------------------------------------------------------------------------------------
void stack_test(void)
{
#pragma pack(push, 1) // 1/2/4/8
struct st_test
{
uint8_t val1;
uint32_t val2;
};
#pragma pack(pop)
uint8_t tmp1;
uint16_t tmp2;
uint32_t tmp3;
double data[2];
struct st_test tag_test;
printf("tmp1 address: %p \r\n", &tmp1);
printf("tmp2 address: %p \r\n", &tmp2);
printf("tmp3 address: %p \r\n", &tmp3);
printf("data[0] address: %p \r\n", &(data[0]));
printf("data[1] address: %p \r\n", &(data[1]));
printf("tag_test address: %p \r\n", &tag_test);
printf("tag_test.val2 address: %p \r\n", &(tag_test.val2));
}
結果輸出:
可以看到變量均是按照 4 字節對齊,每個變量是獨立對齊的,不同於結構體中會將挨着的 uint8_t 和 uint16_t 拼接成 4 字節。另外,可以看到地址是在減小,因爲棧是向下生長的。
3.2 對齊處理
在不同編譯平臺或處理器上,字節對齊會造成消息結構長度的變化。編譯器爲了使字節對齊可能會對消息結構體進行填充,不同編譯平臺可能填充爲不同的形式,大大增加處理器間數據通信的風險。
如下以 32 位處理器爲例,提出一種內存對齊方法以解決上述問題:
(1)對於本地使用的數據結構,爲提高內存訪問效率,採用四字節對齊方式;同時爲了減少內存的開銷,合理安排結構體成員的位置,減少四字節對齊導致的成員之間的空隙,降低內存開銷;
(2)對於處理器之間的數據結構,需要保證消息長度不會因不同編譯平臺或處理器而導致消息結構體長度發生變化,使用一字節對齊方式對消息結構進行緊縮;爲保證處理器之間的消息數據結構的內存訪問效率,採用字節填充的方式自己對消息中成員進行四字節對齊。
(3)數據結構的成員位置要兼顧成員之間的關係、數據訪問效率和空間利用率。順序安排原則是:四字節的放在最前面,兩字節的緊接最後一個四字節成員,一字節緊接最後一個兩字節成員,填充字節放在最後。
typedef struct
{
uint32_t parm1;
uint32_t parm2;
uint16_t length;
uint8_t flag;
uint8_t data_pad; //填充一字節,保證4字節對齊
}s_msg, *p_msg;
3.3 __align(num)
__align(num) 用於修改最高級別對象的字節邊界。在彙編中使用 LDRD 或 STRD 時就要用到此命令 __align(8) 進行修飾限制。來保證數據對象是相應對齊。
這個修飾對象的命令最大是 8 個字節限制,可以讓 2 字節的對象進行 4 字節對齊,但不能讓 4 字節的對象 2 字節對齊。
__align 是存儲類修改,只修飾最高級類型對象,不能用於結構或者函數對象。
示例:
__align(4) uint8_t g_data_buff[DATA_SIZE]; //保證分配的數組空間4字節對齊,同時保證數組首地址可被4整除
3.4 __packed
__packed 進行一字節對齊。
需注意:
(1)不能對 packed 的對象進行對齊;
(2)所有對象的讀寫訪問都進行非對齊訪問;
(3)float 及包含 float 的結構聯合及未用 __packed 的對象將不能字節對齊;
(4)__packed 對局部整型變量無影響;
(5)強制由 unpacked 對象向 packed 對象轉化時未定義。整型指針可以合法定義爲 packed,如 __packed int* p(__packed int 則沒有意義)
如下結構體:
__packed struct send_msg
{
uint8_t head;
uint32_t length;
uint16_t crc;
uint8_t flag;
};
測試程序:
struct send_msg g_send_msg = {0};
data_size = sizeof(g_send_msg);
printf("struct test size is %d \r\n", data_size);
printf("g_send_msg addr is %p \r\n", &g_send_msg);
printf("g_send_msg.head addr is %p \r\n", &g_send_msg.head);
printf("g_send_msg.length addr is %p \r\n", &g_send_msg.length);
printf("g_send_msg.crc addr is %p \r\n", &g_send_msg.crc);
printf("g_send_msg.flag addr is %p \r\n", &g_send_msg.flag);
測試結果:
我們可以看到 sizeof(g_send_msg) = 8(單字節對齊),另外 uint32_t length 的起始地址爲 0x20000007 非 4 字節對齊,針對此,對齊訪問的操作中,必須注意,可能導致訪問硬件錯誤。定義一個局部的變量(位於 stack),也可能引發錯誤,因爲棧是完全 4 字節對齊的。
4. 補充
(1) RISC 指令集處理器( MIPS / ARM):這種處理器的設計以效率爲先,要求所訪問的多字節數據 (short/int/ long) 的地址必須是爲此數據大小的倍數,如 short 數據地址應爲 2 的倍數,long 數據地址應爲 4 的倍數,需是對齊的。