結構對齊--__packed與#pragma pack
Arm結構體gcc記憶體邊界對齊問題
這段時間移植公司的linux i386程式到Arm linux平臺,本以為是件工作量很小的事情,以為只要改幾個驅動程式就OK了,沒想到在應用程式這一塊卡了很長時間。其中最煩的事情就莫過於結構體記憶體邊界對齊了。搞了這麼久,終於終結了一些小經驗。
預設情況下,在32位cpu裡,gcc對於結構體的對齊方式是按照四個位元組來對齊的。看以下結構體
typedef struct pack{
char a;
int b;
short c;
}pack;
對於Pack結構體,預設情況下在arm/386平臺下(別的平臺沒試過)sizeof(pack)=12,求解過程如下:
sizeof(char)=1;
下一個int b,由於是四個位元組,要求b的開始地址從32的整數倍開始,故需要在a後面填充3個沒用的位元組,記為dump(3),sizeof(b)=4,此時相當於結構體擴充為
char a;
char dump(3);
int b;
看short c,現在c的前面有8個位元組,c是兩個位元組,c的開始地址是從16的整數開始,在b前面不需再加東西.此時對於結構體來說,sizeof(pack)=10,但是這不是最終結果,最後總的位元組數也要能被4個位元組整除,所以還需在short c後面再加
dump(2);
故總的位元組數為12.
當然以上說的只是簡單的情況,下面談談Arm,x86在gcc裡關於記憶體邊界位元組對齊的區別.對於同樣的結構體,在386下
#prama pack(1)
後,sizeof(pack)=1 4 2=7
而在arm下同樣的操作sizeof(pack)=1 4 2 1=8,即雖然b根a之間不要填充但總的長度必須要是4的整數倍.
在ARM 下要使結構體按指定位元組對齊,可行的方法
1.在makefile里加-fpack-struct 選項,這樣的話對所有的結構按一位元組對齊.
不得不說,確實有那麼些質量較差的程式可能需要你部分自然對齊,部分一字 節對齊,此時
2. typedef struct pack{
}__attribute__((packed))
可利用__attribute__屬性
當然最後的方式,還是自己去看ARM體系結構與gcc編譯選項了。
------------------------------------------------------------------------------------------------------------
淺談結構體對齊問題
#include
int main() {
struct ms {
double x;
char a;
int y;
};
// }__attribute__((packed));
printf("%d\n", sizeof(struct ms));
return 0;
}
linux上執行,結果為16;如果採用註釋的那一行,則結果為13
原文:: http://dev.csdn.net/article/48/48195.shtm
什麼是記憶體對齊
考慮下面的結構:
struct foo
{
char c1;
short s;
char c2;
int i;
};
假設這個結構的成員在記憶體中是緊湊排列的,假設c1的地址是0,那麼s的地址就應該是1,c2的地址就是3,i的地址就是4。也就是
c1 00000000, s 00000001, c2 00000003, i 00000004。
可是,我們在Visual c/c++ 6中寫一個簡單的程式:
struct foo a;
printf("c1 %p, s %p, c2 %p, i %p\n",
(unsigned int)(void*)&a.c1 - (unsigned int)(void*)&a,
(unsigned int)(void*)&a.s - (unsigned int)(void*)&a,
(unsigned int)(void*)&a.c2 - (unsigned int)(void*)&a,
(unsigned int)(void*)&a.i - (unsigned int)(void*)&a);
執行,輸出:
c1 00000000, s 00000002, c2 00000004, i 00000008。
為什麼會這樣?這就是記憶體對齊而導致的問題。
為什麼會有記憶體對齊
以下內容節選自《Intel Architecture 32 Manual》。
字,雙字,和四字在自然邊界上不需要在記憶體中對齊。(對字,雙字,和四字來說,自然邊界分別是偶數地址,可以被4整除的地址,和可以被8整除的地址。)
無論如何,為了提高程式的效能,資料結構(尤其是棧)應該儘可能地在自然邊界上對齊。原因在於,為了訪問未對齊的記憶體,處理器需要作兩次記憶體訪問;然而,對齊的記憶體訪問僅需要一次訪問。
一個字或雙字運算元跨越了4位元組邊界,或者一個四字運算元跨越了8位元組邊界,被認為是未對齊的,從而需要兩次匯流排週期來訪問記憶體。一個字起始地址是奇數但卻沒有跨越字邊界被認為是對齊的,能夠在一個匯流排週期中被訪問。
某些操作雙四字的指令需要記憶體運算元在自然邊界上對齊。如果運算元沒有對齊,這些指令將會產生一個通用保護異常(#GP)。雙四字的自然邊界是能夠被16 整除的地址。其他的操作雙四字的指令允許未對齊的訪問(不會產生通用保護異常),然而,需要額外的記憶體匯流排週期來訪問記憶體中未對齊的資料。
編譯器對記憶體對齊的處理
預設情況下,c/c++編譯器預設將結構、棧中的成員資料進行記憶體對齊。因此,上面的程式輸出就變成了:
c1 00000000, s 00000002, c2 00000004, i 00000008。
編譯器將未對齊的成員向後移,將每一個都成員對齊到自然邊界上,從而也導致了整個結構的尺寸變大。儘管會犧牲一點空間(成員之間有空洞),但提高了效能。
也正是這個原因,我們不可以斷言sizeof(foo) == 8。在這個例子中,sizeof(foo) == 12。
如何避免記憶體對齊的影響
那麼,能不能既達到提高效能的目的,又能節約一點空間呢?有一點小技巧可以使用。比如我們可以將上面的結構改成:
struct bar
{
char c1;
char c2;
short s;
int i;
};
這樣一來,每個成員都對齊在其自然邊界上,從而避免了編譯器自動對齊。在這個例子中,sizeof(bar) == 8。
這個技巧有一個重要的作用,尤其是這個結構作為API的一部分提供給第三方開發使用的時候。第三方開發者可能將編譯器的預設對齊選項改變,從而造成這個結構在你的發行的DLL中使用某種對齊方式,而在第三方開發者哪裡卻使用另外一種對齊方式。這將會導致重大問題。
比如,foo結構,我們的DLL使用預設對齊選項,對齊為
c1 00000000, s 00000002, c2 00000004, i 00000008,同時sizeof(foo) == 12。
而第三方將對齊選項關閉,導致
c1 00000000, s 00000001, c2 00000003, i 00000004,同時sizeof(foo) == 8。
如何使用c/c++中的對齊選項
vc6中的編譯選項有 /Zp[1|2|4|8|16] ,/Zp1表示以1位元組邊界對齊,相應的,/Zpn表示以n位元組邊界對齊。n位元組邊界對齊的意思是說,一個成員的地址必須安排在成員的尺寸的整數倍地址上或者是n的整數倍地址上,取它們中的最小值。也就是:
min ( sizeof ( member ), n)
實際上,1位元組邊界對齊也就表示了結構成員之間沒有空洞。
/Zpn選項是應用於整個工程的,影響所有的參與編譯的結構。
要使用這個選項,可以在vc6中開啟工程屬性頁,c/c++頁,選擇Code Generation分類,在Struct member alignment可以選擇。
要專門針對某些結構定義使用對齊選項,可以使用#pragma pack編譯指令。指令語法如下:
#pragma pack( [ show ] | [ push | pop ] [, identifier ] , n )
意義和/Zpn選項相同。比如:
#pragma pack(1)
struct foo_pack
{
char c1;
short s;
char c2;
int i;
};
#pragma pack()
棧記憶體對齊
我們可以觀察到,在vc6中棧的對齊方式不受結構成員對齊選項的影響。(本來就是兩碼事)。它總是保持對齊,而且對齊在4位元組邊界上。
驗證程式碼
#include
struct foo
{
char c1;
short s;
char c2;
int i;
};
struct bar
{
char c1;
char c2;
short s;
int i;
};
#pragma pack(1)
struct foo_pack
{
char c1;
short s;
char c2;
int i;
};
#pragma pack()
int main(int argc, char* argv[])
{
char c1;
short s;
char c2;
int i;
struct foo a;
struct bar b;
struct foo_pack p;
printf("stack c1 %p, s %p, c2 %p, i %p\n",
(unsigned int)(void*)&c1 - (unsigned int)(void*)&i,
(unsigned int)(void*)&s - (unsigned int)(void*)&i,
(unsigned int)(void*)&c2 - (unsigned int)(void*)&i,
(unsigned int)(void*)&i - (unsigned int)(void*)&i);
printf("struct foo c1 %p, s %p, c2 %p, i %p\n",
(unsigned int)(void*)&a.c1 - (unsigned int)(void*)&a,
(unsigned int)(void*)&a.s - (unsigned int)(void*)&a,
(unsigned int)(void*)&a.c2 - (unsigned int)(void*)&a,
(unsigned int)(void*)&a.i - (unsigned int)(void*)&a);
printf("struct bar c1 %p, c2 %p, s %p, i %p\n",
(unsigned int)(void*)&b.c1 - (unsigned int)(void*)&b,
(unsigned int)(void*)&b.c2 - (unsigned int)(void*)&b,
(unsigned int)(void*)&b.s - (unsigned int)(void*)&b,
(unsigned int)(void*)&b.i - (unsigned int)(void*)&b);
printf("struct foo_pack c1 %p, s %p, c2 %p, i %p\n",
(unsigned int)(void*)&p.c1 - (unsigned int)(void*)&p,
(unsigned int)(void*)&p.s - (unsigned int)(void*)&p,
(unsigned int)(void*)&p.c2 - (unsigned int)(void*)&p,
(unsigned int)(void*)&p.i - (unsigned int)(void*)&p);
printf("sizeof foo is %d\n", sizeof(foo));
printf("sizeof bar is %d\n", sizeof(bar));
printf("sizeof foo_pack is %d\n", sizeof(foo_pack));
return 0;
}
-----------------------------------------------------------------------------------------------------------在結構中,編譯器為結構的每個成員按其自然對界條件分配空間;各個成員按照它們被宣告的順序在記憶體中順序儲存,第一個成員的地址和整個結構的地址相同。在預設情況下,c編譯器為每一個變數或是資料單元按其自然對界條件分配空間
例如,下面的結構各成員空間分配情況
struct test {
char x1;
short x2;
float x3;
char x4;
};
結構的第一個成員x1,其偏移地址為0,佔據了第1個位元組。第二個成員x2為short型別,其起始地址必須2位元組對界,因此,編譯器在x2和x1之間填充了一個空位元組。結構的第三個成員x3和第四個成員x4恰好落在其自然對界地址上,在它們前面不需要額外的填充位元組。在test結構中,成員x3要求 4位元組對界,是該結構所有成員中要求的最大對界單元,因而test結構的自然對界條件為4位元組,編譯器在成員x4後面填充了3個空位元組。整個結構所佔據空間為12位元組。
現在你知道怎麼回事了吧?
更改c編譯器的預設分配策略
一般地,可以通過下面的兩種方法改變預設的對界條件:
· 使用偽指令#pragma pack ([n])
· 在編譯時使用命令列引數
#pragma pack ([n])偽指令允許你選擇編譯器為資料分配空間所採取的對界策略:
例如,在使用了#pragma pack (1)偽指令後,test結構各成員的空間分配情況就是按照一個位元組對齊了
#pragma pack(push) //儲存對齊狀態
#pragma pack(1)
#pragma pack(pop)
1 ANSI C規定
ANSI C規定一種結構型別的大小是它所有欄位的大小以及欄位之間或欄位尾部的填充區大小之和。
填充區就是為了使結構體欄位滿足記憶體對齊要求而額外分配給結構體的空間。
那麼結構體本身有對齊要求,ANSI C標準規定結構體型別的對齊要求不能比它所有欄位中要求最嚴格的那個寬鬆,可以更嚴格。
2 WIN32和Linux平臺下的對齊基本規則
許多實際的計算機系統對基本型別資料在記憶體中存放的位置有限制,它們會要求這些資料的首地址的值是某個數k
(通常它為4或8)的倍數,這就是所謂的記憶體對齊,而這個k則被稱為該資料型別的對齊模數(alignment modulus)。
Win32平臺下的微軟C編譯器(cl.exe for 80x86)在預設情況下采用如下的對齊規則:
任何基本資料型別T的對齊模數就是T的大小,即sizeof(T)。比如對於double型別(8位元組),就要求該型別資料的地址總是8的倍數,
而char型別資料(1位元組)則可以從任何一個地址開始。
Linux下的GCC奉行的是另外一套規則(在資料中查得,並未驗證,如錯誤請指正):
任何2位元組大小(包括單位元組嗎?)的資料型別(比如short)的對齊模數是2,而其它所有超過2位元組的資料型別(比如long,double)
都以4為對齊模數。
3 vc6中對齊處理
vc6中的編譯選項有 /Zp[1|2|4|8|16] ,/Zp1表示以1位元組邊界對齊,相應的,/Zpn表示以n位元組邊界對齊。
n位元組邊界對齊的意思是說,一個成員的地址必須安排在成員的尺寸的整數倍地址上或者是n的整數倍地址上,取它們中的最小值。
也就是:
min ( sizeof ( member ), n)
實際上,1位元組邊界對齊也就表示了結構成員之間沒有空洞。
要使用這個選項,可以在vc6中開啟工程屬性頁,c/c++頁,選擇Code Generation分類,在Struct member alignment可以選擇。
/Zpn選項是應用於整個工程的,影響所有的參與編譯的結構。該預設/Zp 緊湊值為/Zp8,也就是說,預設是8位元組對齊。
要專門針對某些結構定義使用對齊選項,可以使用#pragma pack編譯指令。
#pragma pack(push) //儲存對齊狀態
#pragma pack(1) // 1 bytes對齊
typedef struct
{
double dValue1;
char u8Value2;
int u32Value3;
} ASampleStructor;
#pragma pack(pop)//恢復對齊狀態
上例中,size值為13,說明1位元組對齊後,該結構總長為13位元組。去掉對齊後,為16位元組。
4 ARM平臺中的對齊
在ARM平臺的編譯器中,沒有提供象“#pragma pack”這麼豐富的帶引數對齊指令,只有一個關鍵字“__packed”。
__packed 限定符將所有有效型別的對齊邊界設定為 1,如果一個結構沒有這個限定符,預設向表數能力最強的那個資料型別對齊。
typedef __packed struct
{
double dValue1;
char u8Value2;
int u32Value3;
} ASampleStructor;
上例中,size值為13,說明1位元組對齊後,該結構總長為13位元組。去掉__packed對齊後,為16位元組。
5 一種與對齊相關的可隨時執行在VC環境或ARM(Keil)環境下的定義
我們時常會把嵌入式平臺上的程式碼拿到PC環境中去測試,這時環境的差異將使程式碼移植變得困難。
下面這種結構,將會使移植變得非常輕鬆。
#ifdef WIN32
#define __packed //在VC環境下,將此關鍵字定義為空
#pragma pack(push) //儲存對齊狀態
#pragma pack(1) // 1 bytes對齊
#endif
typedef __packed struct
{
double dValue1;
char u8Value2;
int u32Value3;
} ASampleStructor;
#ifdef WIN32
#pragma pack(pop)//恢復對齊狀態
#endif
這樣,無論是ARM還是VC,都可以編譯,程式碼的同步將非常簡單。
不過呢,還有一種方法更為簡單,可以使程式碼執行在ARM(Keil)或VC環境下。
#pragma pack(push,1)
typedef struct
{
double dValue1;
char u8Value2;
int u32Value3;
} ASampleStructor;
#pragma pack(pop)