您好,登錄后才能下訂單哦!
GNU C 通過 attribute 來聲明 aligned 和 packed 屬性,指定一個變量或類型的對齊方式。這兩個屬性用來告訴編譯器:在給變量分配存儲空間時,要按指定的地址對齊方式給變量分配地址。如果你想定義一個變量,在內存中以8字節地址對齊,就可以這樣定義。
int a __attribute__((aligned(8));
通過 aligned 屬性,我們可以直接顯式指定變量 a 在內存中的地址對齊方式。aligned 有一個參數,表示要按幾字節對齊,使用時要注意地址對齊的字節數必須是2的冪次方,否則編譯就會出錯。
一般情況下,當我們定義一個變量,編譯器會按照默認的地址對齊方式,來給該變量分配一個存儲空間地址。如果該變量是一個 int 型數據,那么編譯器就會按4字節或4字節的整數倍對齊;如果該變量是一個 short 型數據,那么編譯器就會按2字節或2字節的整數倍邊界對齊;如果是一個 char 類型的變量,那么編譯器就會按照1字節對齊。
int a = 1;
int b = 2;
char c1 = 3;
char c2 = 4;
int main(void)
{
printf("a: %p\n",&a);
printf("b: %p\n",&b);
printf("c1:%p\n",&c1);
printf("c2:%p\n",&c2);
return 0;
}
在上面的程序中,我們分別定義2個 int 型變量,2個 char 型變量,然后分別打印它們的地址,運行結果如下。
a: 00402000
b: 00402004
c1: 00402008
c2: 00402009
通過運行結果我們可以看到,對于 int 型數據,其在內存中的地址都是以4字節或4字節整數倍對齊的。而 char 類型的數據,其在內存中是以1字節對齊的。變量 c2 就直接分配到了 c1 變量的下一個存儲單元,不用像 int 數據那樣考慮4字節對齊。接下來,我們修改一下程序,指定變量 c2 按4字節對齊。
int a = 1;
int b = 2;
char c1 = 3;
char c2 __attribute__((aligned(4))) = 4;
int main(void)
{
printf("a: %p\n",&a);
printf("b: %p\n",&b);
printf("c1:%p\n",&c1);
printf("c2:%p\n",&c2);
return 0;
}
運行結果如下。
a: 00402000
b: 00402004
c1: 00402008
c2: 0040200C
通過運行結果可以看到,字符變量 c2 由于使用 aligned 屬性聲明按照4字節邊界對齊,所以編譯器不可能再給其分配 0x00402009 這個地址,因為這個地址不是4字節對齊的。編譯器空出3個字節單元,直接從 0x0040200C 這個地址上給變量 c2 分配存儲空間。
通過 aligned 這個屬性聲明,我們雖然可以顯式指定變量的地址對齊方式,但是也會因邊界對齊造成一定的內存空洞,浪費一定的內存空間。比如在上面這個程序中,0x00402009~0x0040200b 這三個地址空間的存儲單元就沒有被使用。
既然地址對齊會造成一定的內存空洞,那我們為什么還要按照這種對齊方式去存儲數據呢?一個主要原因就是,這種對齊設置可以簡化 CPU 和內存 RAM 之間的接口和硬件設計。比如一個32位的計算機系統,CPU 讀取內存時,硬件設計上可能只支持4字節或4字節倍數對齊的地址訪問,CPU 每次往內存 RAM 讀寫數據時,一個周期可以讀寫4個字節。如果我們把一個數據放在4字節對齊的地址上,那么CPU一次就可以把數據讀寫完畢;如果我們把一個 int 型數據放在一個非4字節對齊的地址上,那 CPU 就要分2次才能把這個4字節大小的數據讀寫完畢。
為了配合計算機的硬件設計,編譯器在編譯程序時,對于一些基本數據類型,比如 int、char、short、float 等,會按照其數據類型的大小進行地址對齊,按照這種地址對齊方式分配的存儲地址,CPU 一次就可以讀寫完畢。雖然邊界對齊會造成一些內存空洞,浪費一些內存單元,但是在硬件上的設計卻大大簡化了。這也是編譯器給我們定義的變量分配地址時,不同類型變量按不同字節數地址對齊的原因。
除了 int、char、short、float 這些基本類型數據,對于一些復合類型數據,也要滿足地址對齊要求。
結構體作為一種復合數據類型,編譯器在給一個結構體變量分配存儲空間時,不僅要考慮結構體內各個基本成員的地址對齊,還要考慮結構體整體的對齊。為了結構體內的成員地址對齊,編譯器可能會在結構體內填充一些空間;為了結構體整體對齊,編譯器可能會在結構體的末尾填充一些空間。
接下來,我們定義一個結構體,結構體內定義 int、char 和 short 三種成員,并打印結構體的大小和各個成員的地址。
struct data{
char a;
int b ;
short c ;
}
int main(void)
{
struct data s;
printf("size:%d\n",sizeof(s));
printf("a:%p\n",&s.a);
printf("b:%p\n",&s.b);
printf("c:%p\n",&s.c);
}
程序運行結果如下。
size: 12
&s.a: 0028FF30
&s.b: 0028FF34
&s.c: 0028FF38
我們可以看到,因為結構體的成員 b 需要4字節對齊,編譯器在給成員 a 分配完空間后,接著會空出3個字節,在滿足4字節對齊的 0x0028FF34 地址處才給成員 b 分配存儲空間。接著是 short 類型的成員 c 占據2字節的存儲空間。三個結構體成員一共占據4+4+2=10字節的存儲空間,根據結構體的對齊規則,結構體的整體對齊要向結構體所有成員中最大對齊字節數或其整數倍對齊,或者說結構體的整體長度要為其最大成員字節數的整數倍,如果不是整數倍要補齊。因為結構體最大成員 int 為4個字節,或者說按4字節的整數倍對齊,所以結構體的長度要為4的整數倍,要在結構體的末尾補充2個字節,所以最后結構體的 size 為12個字節。
結構體成員中,不同的排放順序,可能也會導致結構體的整體長度不一樣,我們修改一下上面的程序。
struct data{
char a;
short b ;
int c ;
};
int main(void)
{
struct data s;
printf("size: %d\n",sizeof(s));
printf("&s.a: %p\n",&s.a);
printf("&s.b: %p\n",&s.b);
printf("&s.c: %p\n",&s.c);
}
程序運行結果如下。
size: 8
&s.a: 0028FF30
&s.b: 0028FF32
&s.c: 0028FF34
我們調整了一些成員順序,你會發現,char 型變量 a 和 short 型變量 b,分配在了結構體的前4個字節存儲空間中,而且都滿足各自的地址對齊,整個結構體大小是8字節,只造成一個字節的內存空洞。我們繼續修改程序,讓 short 型的變量 b 按4字節對齊:
struct data{
char a;
short b __attribute__((aligned(4)));
int c ;
};
程序運行結果如下。
size: 12
&s.a: 0028FF30
&s.b: 0028FF34
&s.c: 0028FF38
你會發現,結構體的大小又重新變為12個字節。這是因為,我們顯式指定 short 變量以4字節地址對齊,導致變量 a 的后面填充了3個字節空間。int 型變量 c 也要4字節對齊,所以變量 b 的后面也填充了2個字節,導致整個結構體的大小為12字節。
我們不僅可以顯式指定結構體內某個成員的地址對齊,也可以指定整個結構體的對齊方式。
struct data{
char a;
short b;
int c ;
}__attribute__((aligned(16)));
程序運行結果如下。
size: 16
&s.a: 0028FF30
&s.b: 0028FF32
&s.c: 0028FF34
在這個結構體中,各個成員一共占8個字節。通過前面學習我們知道,整個結構體的對齊只要是最大成員對齊字節數的整數倍即可。所以這個結構體整體就以8字節對齊,結構體的整體長度為8字節。但是我們在這里,顯式指定結構體整體以16字節對齊,所以編譯器就會在這個結構體的末尾填充8個字節以滿足16字節對齊的要求,導致結構體的總長度變為16字節。
通過 aligned 屬性,我們可以顯式指定一個變量的對齊方式,那么,編譯器就一定會按照我們指定的大小對齊嗎?非也!
我們通過這個屬性聲明,其實只是建議編譯器按照這種大小地址對齊,但不能超過編譯器允許的最大值。一個編譯器,對每個基本數據類型,都有默認的最大邊界對齊字節數。如果你超過了,不好意思,我不奉陪,編譯器只能按照它規定的最大對齊來給你的變量分配地址。
char c1 = 3;
char c2 __attribute__((aligned(16))) = 4 ;
int main(void)
{
printf("c1:%p\n",&c1);
printf("c2:%p\n",&c2);
return 0;
}
在這個程序中,我們指定 char 型的變量 c2 以16字節對齊,然后運行結果為:
c1:00402000
c2:00402010
我們可以看到,編譯器給 c2 分配的地址就是16字節地址對齊的,如果我們繼續修改 c2 變量按32字節對齊,你會發現程序的運行結果不再會有變化,編譯器還會分配一個16字節對齊的地址,因為已經超過編譯器允許的最大值了。
aligned 屬性一般用來增大變量的地址對齊,元素之間因為地址對齊會造成一定的內存空洞。而 packed 屬性則與之相反,用來減少地址對齊,用來指定變量或類型使用最可能小的地址對齊方式。
struct data{
char a;
short b __attribute__((packed));
int c __attribute__((packed));
};
int main(void)
{
struct data s;
printf("size: %d\n",sizeof(s));
printf("&s.a: %p\n",&s.a);
printf("&s.b: %p\n",&s.b);
printf("&s.c: %p\n",&s.c);
}
在這個程序中,我們將結構體的成員 b 和 c 使用 packed 屬性聲明,就是告訴編譯器,盡量使用最可能小的地址對齊給它們分配地址,盡可能地減少內存空洞。程序的運行結果如下。
size: 7
&s.a: 0028FF30
&s.b: 0028FF31
&s.c: 0028FF33
通過結果我們看到,結構體內各個成員地址的分配,使用最小1字節的對齊方式,導致整個結構體的大小只有7個字節。
這個特性在底層驅動開發中還是非常有用的。比如,你想定義一個結構體,封裝一個 IP 控制器的各種寄存器。在 ARM 芯片中,每一個控制器的寄存器地址空間一般是連續存在的。如果考慮數據對齊,結構體內有空洞,這樣就跟實際連續的寄存器地址不一致了,使用 packed 就可以避免這個問題,結構體的每個成員都緊挨著依次分配存儲地址,這樣就避免了各個成員元素因地址對齊而造成的內存空洞。
struct data{
char a;
short b ;
int c ;
}__attribute__((packed));
我們對整個結構體添加 packed 屬性,和分別對每個成員添加 packed 屬性,效果是一樣的。修改結構體后,程序的運行結果跟上面程序運行結果相同——結構體的大小為7,結構體內各成員地址相同。
在 Linux 內核中,我們經常看到 aligned 和 packed 一起使用,即對一個變量或類型同時使用 aligned 和 packed 屬性聲明。這樣做的好處是,既避免了結構體內因地址對齊產生的內存空洞,又指定了整個結構體的對齊方式。
struct data{
char a;
short b ;
int c ;
}__attribute__((packed,aligned(8)));
int main(void)
{
struct data s;
printf("size: %d\n",sizeof(s));
printf("&s.a: %p\n",&s.a);
printf("&s.b: %p\n",&s.b);
printf("&s.c: %p\n",&s.c);
}
程序運行結果如下。
size: 8
&s.a: 0028FF30
&s.b: 0028FF31
&s.c: 0028FF33
在這個程序中,結構體 data 雖然使用 packed 屬性聲明,整個長度變為7,但是我們同時又使用了 aligned(8) 指定其按8字節地址對齊,所以編譯器要在結構體后面填充1個字節,這樣整個結構體的大小就變為8字節,按8字節地址對齊。
本教程根據 C語言嵌入式Linux高級編程視頻教程 第05期 改編,電子版書籍可加入QQ群:475504428 下載,更多嵌入式視頻教程,可關注:
微信公眾號:宅學部落(armlinuxfun)
51CTO學院-王利濤老師:http://edu.51cto.com/sd/d344f
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。