嵌入式C語(yǔ)言自我修養(yǎng)-地址對(duì)齊的核心思想
嵌入式C語(yǔ)言自我修養(yǎng)-地址對(duì)齊的核心思想

\\\插播一條:我自己在今年年初錄制了一套還比較系統(tǒng)的入門(mén)單片機(jī)教程,想要的同學(xué)找我拿就行了免費(fèi)的(禾厶-亻言-手戈)。最近比較閑,帶做畢設(shè),帶學(xué)生參加省級(jí)以上比賽///綠色圖標(biāo)【?で】liutianwang123

7.1屬性聲明:aligned
GNU C通過(guò) __atttribute__來(lái)聲明 aligned和 packed屬性,指定一個(gè)變量或類型的對(duì)齊方式。這兩個(gè)屬性用來(lái)告訴編譯器:在給變量分配存儲(chǔ)空間時(shí),要按指定的地址對(duì)齊方式給變量分配地址。如果你想定義一個(gè)變量,在內(nèi)存中以8字節(jié)地址對(duì)齊,就可以這樣定義。
inta__attribute__((aligned(8));
通過(guò) aligned屬性,我們可以直接顯式指定變量 a在內(nèi)存中的地址對(duì)齊方式。aligned有一個(gè)參數(shù),表示要按幾字節(jié)對(duì)齊,使用時(shí)要注意地址對(duì)齊的字節(jié)數(shù)必須是2的冪次方,否則編譯就會(huì)出錯(cuò)。

什么是數(shù)據(jù)對(duì)齊
一般情況下,當(dāng)我們定義一個(gè)變量,編譯器會(huì)按照默認(rèn)的地址對(duì)齊方式,來(lái)給該變量分配一個(gè)存儲(chǔ)空間地址。如果該變量是一個(gè) int型數(shù)據(jù),那么編譯器就會(huì)按4字節(jié)或4字節(jié)的整數(shù)倍對(duì)齊;如果該變量是一個(gè) short型數(shù)據(jù),那么編譯器就會(huì)按2字節(jié)或2字節(jié)的整數(shù)倍邊界對(duì)齊;如果是一個(gè) char類型的變量,那么編譯器就會(huì)按照1字節(jié)對(duì)齊。
inta=1;
intb=2;
charc1=3;
charc2=4;
intmain(void)
{
printf("a: %p\n",&a);
printf("b: %p\n",&b);
printf("c1:%p\n",&c1);
printf("c2:%p\n",&c2);
return0;
}
在上面的程序中,我們分別定義2個(gè) int型變量,2個(gè) char型變量,然后分別打印它們的地址,運(yùn)行結(jié)果如下。
a:00402000b:00402004c1:00402008c2:004這個(gè)地址不是4字節(jié)對(duì)齊的。編譯器空出3個(gè)字節(jié)單元,直接從 0x0040200C這個(gè)地址上給變量 c2分配存儲(chǔ)空間。
為什么要數(shù)據(jù)對(duì)齊?
通過(guò) aligned這個(gè)屬性聲明,我們雖然可以顯式指定變量的地址對(duì)齊方式,但是也會(huì)因邊界對(duì)齊造成一定的內(nèi)存空洞,浪費(fèi)一定的內(nèi)存空間。比如在上面這個(gè)程序中,0x00402009~0x0040200b這三個(gè)地址空間的存儲(chǔ)單元就沒(méi)有被使用。
既然地址對(duì)齊會(huì)造成一定的內(nèi)存空洞,那我們?yōu)槭裁催€要按照這種對(duì)齊方式去存儲(chǔ)數(shù)據(jù)呢?一個(gè)主要原因就是,這種對(duì)齊設(shè)置可以簡(jiǎn)化 CPU和內(nèi)存 RAM之間的接口和硬件設(shè)計(jì)。比如一個(gè)32位的計(jì)算機(jī)系統(tǒng),CPU讀取內(nèi)存時(shí),硬件設(shè)計(jì)上可能只支持4字節(jié)或4字節(jié)倍數(shù)對(duì)齊的地址訪問(wèn),CPU每次往內(nèi)存 RAM讀寫(xiě)數(shù)據(jù)時(shí),一個(gè)周期可以讀寫(xiě)4個(gè)字節(jié)。如果我們把一個(gè)數(shù)據(jù)放在4字節(jié)對(duì)齊的地址上,那么CPU一次就可以把數(shù)據(jù)讀寫(xiě)完畢;如果我們把一個(gè) int型數(shù)據(jù)放在一個(gè)非4字節(jié)對(duì)齊的地址上,那 CPU就要分2次才能把這個(gè)4字節(jié)大小的數(shù)據(jù)讀寫(xiě)完畢。
為了配合計(jì)算機(jī)的硬件設(shè)計(jì),編譯器在編譯程序時(shí),對(duì)于一些基本數(shù)據(jù)類型,比如 int、char、short、float等,會(huì)按照其數(shù)據(jù)類型的大小進(jìn)行地址對(duì)齊,按照這種地址對(duì)齊方式分配的存儲(chǔ)地址,CPU一次就可以讀寫(xiě)完畢。雖然邊界對(duì)齊會(huì)造成一些內(nèi)存空洞,浪費(fèi)一些內(nèi)存單元,但是在硬件上的設(shè)計(jì)卻大大簡(jiǎn)化了。這也是編譯器給我們定義的變量分配地址時(shí),不同類型變量按不同字節(jié)數(shù)地址對(duì)齊的原因。
除了 int、char、short、float這些基本類型數(shù)據(jù),對(duì)于一些復(fù)合類型數(shù)據(jù),也要滿足地址對(duì)齊要求。
7.2結(jié)構(gòu)體的對(duì)齊
結(jié)構(gòu)體作為一種復(fù)合數(shù)據(jù)類型,編譯器在給一個(gè)結(jié)構(gòu)體變量分配存儲(chǔ)空間時(shí),不僅要考慮結(jié)構(gòu)體內(nèi)各個(gè)基本成員的地址對(duì)齊,還要考慮結(jié)構(gòu)體整體的對(duì)齊。為了結(jié)構(gòu)體內(nèi)的成員地址對(duì)齊,編譯器可能會(huì)在結(jié)構(gòu)體內(nèi)填充一些空間;為了結(jié)構(gòu)體整體對(duì)齊,編譯器可能會(huì)在結(jié)構(gòu)體的末尾填充一些空間。
接下來(lái),我們定義一個(gè)結(jié)構(gòu)體,結(jié)構(gòu)體內(nèi)定義 int、char和 short三種成員,并打印結(jié)構(gòu)體的大小和各個(gè)成員的地址。
structdata{
chara;
intb;
shortc;
}
intmain(void)
{
structdatas;
printf("size:%d\n",sizeof(s));
printf("a:%p\n",&s.a);
printf("b:%p\n",&s.b);
printf("c:%p\n",&s.c);
}
程序運(yùn)行結(jié)果如下。
size: 12
&s.a: 0028FF30
&s.b: 0028FF34
&s.c: 0028FF38
我們可以看到,因?yàn)榻Y(jié)構(gòu)體的成員 b需要4字節(jié)對(duì)齊,編譯器在給成員 a分配完空間后,接著會(huì)空出3個(gè)字節(jié),在滿足4字節(jié)對(duì)齊的 0x0028FF34地址處才給成員 b分配存儲(chǔ)空間。接著是 short類型的成員 c占據(jù)2字節(jié)的存儲(chǔ)空間。三個(gè)結(jié)構(gòu)體成員一共占據(jù)4+4+2=10字節(jié)的存儲(chǔ)空間,根據(jù)結(jié)構(gòu)體的對(duì)齊規(guī)則,結(jié)構(gòu)體的整體對(duì)齊要向結(jié)構(gòu)體所有成員中最大對(duì)齊字節(jié)數(shù)或其整數(shù)倍對(duì)齊,或者說(shuō)結(jié)構(gòu)體的整體長(zhǎng)度要為其最大成員字節(jié)數(shù)的整數(shù)倍,如果不是整數(shù)倍要補(bǔ)齊。因?yàn)榻Y(jié)構(gòu)體最大成員 int為4個(gè)字節(jié),或者說(shuō)按4字節(jié)的整數(shù)倍對(duì)齊,所以結(jié)構(gòu)體的長(zhǎng)度要為4的整數(shù)倍,要在結(jié)構(gòu)體的末尾補(bǔ)充2個(gè)字節(jié),所以最后結(jié)構(gòu)體的 size為12個(gè)字節(jié)。
結(jié)構(gòu)體成員中,不同的排放順序,可能也會(huì)導(dǎo)致結(jié)構(gòu)體的整體長(zhǎng)度不一樣,我們修改一下上面的程序。
structdata{
chara;
shortb;
intc;
};
intmain(void)
{
structdatas;
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);
}
程序運(yùn)行結(jié)果如下。
size:8
&s.a:0028FF30
&s.b:0028FF32
&s.c:0028FF34
我們調(diào)整了一些成員順序,你會(huì)發(fā)現(xiàn),char型變量 a和 short型變量b,分配在了結(jié)構(gòu)體的前4個(gè)字節(jié)存儲(chǔ)空間中,而且都滿足各自的地址對(duì)齊,整個(gè)結(jié)構(gòu)體大小是8字節(jié),只造成一個(gè)字節(jié)的內(nèi)存空洞。我們繼續(xù)修改程序,讓 short型的變量 b按4字節(jié)對(duì)齊:
structdata{
chara;
shortb__attribute__((aligned(4)));
intc;
};
程序運(yùn)行結(jié)果如下。
size:12
&s.a:0028FF30
&s.b:0028FF34
&s.c:0028FF38
你會(huì)發(fā)現(xiàn),結(jié)構(gòu)體的大小又重新變?yōu)?2個(gè)字節(jié)。這是因?yàn)?,我們顯式指定 short變量以4字節(jié)地址對(duì)齊,導(dǎo)致變量 a的后面填充了3個(gè)字節(jié)空間。int型變量 c也要4字節(jié)對(duì)齊,所以變量 b的后面也填充了2個(gè)字節(jié),導(dǎo)致整個(gè)結(jié)構(gòu)體的大小為12字節(jié)。
我們不僅可以顯式指定結(jié)構(gòu)體內(nèi)某個(gè)成員的地址對(duì)齊,也可以指定整個(gè)結(jié)構(gòu)體的對(duì)齊方式。
structdata{
chara;
shortb;
intc;
}__attribute__((aligned(16)));
程序運(yùn)行結(jié)果如下。
size:16
&s.a:0028FF30
&s.b:0028FF32
&s.c:0028FF34
在這個(gè)結(jié)構(gòu)體中,各個(gè)成員一共占8個(gè)字節(jié)。通過(guò)前面學(xué)習(xí)我們知道,整個(gè)結(jié)構(gòu)體的對(duì)齊只要是最大成員對(duì)齊字節(jié)數(shù)的整數(shù)倍即可。所以這個(gè)結(jié)構(gòu)體整體就以8字節(jié)對(duì)齊,結(jié)構(gòu)體的整體長(zhǎng)度為8字節(jié)。但是我們?cè)谶@里,顯式指定結(jié)構(gòu)體整體以16字節(jié)對(duì)齊,所以編譯器就會(huì)在這個(gè)結(jié)構(gòu)體的末尾填充8個(gè)字節(jié)以滿足16字節(jié)對(duì)齊的要求,導(dǎo)致結(jié)構(gòu)體的總長(zhǎng)度變?yōu)?6字節(jié)。
7.3思考:編譯器一定會(huì)按照我們指定的大小對(duì)齊嗎?
通過(guò) aligned屬性,我們可以顯式指定一個(gè)變量的對(duì)齊方式,那么,編譯器就一定會(huì)按照我們指定的大小對(duì)齊嗎?非也!
我們通過(guò)這個(gè)屬性聲明,其實(shí)只是建議編譯器按照這種大小地址對(duì)齊,但不能超過(guò)編譯器允許的最大值。一個(gè)編譯器,對(duì)每個(gè)基本數(shù)據(jù)類型,都有默認(rèn)的最大邊界對(duì)齊字節(jié)數(shù)。如果你超過(guò)了,不好意思,我不奉陪,編譯器只能按照它規(guī)定的最大對(duì)齊來(lái)給你的變量分配地址。
charc1=3;
charc2__attribute__((aligned(16)))=4;
intmain(void)
{
printf("c1:%p\n",&c1);
printf("c2:%p\n",&c2);
return0;
}
在這個(gè)程序中,我們指定 char型的變量 c2以16字節(jié)對(duì)齊,然后運(yùn)行結(jié)果為:
c1:00402000c2:00402010
我們可以看到,編譯器給 c2分配的地址就是16字節(jié)地址對(duì)齊的,如果我們繼續(xù)修改 c2變量按32字節(jié)對(duì)齊,你會(huì)發(fā)現(xiàn)程序的運(yùn)行結(jié)果不再會(huì)有變化,編譯器還會(huì)分配一個(gè)16字節(jié)對(duì)齊的地址,因?yàn)橐呀?jīng)超過(guò)編譯器允許的最大值了。
7.4屬性聲明:packed
aligned屬性一般用來(lái)增大變量的地址對(duì)齊,元素之間因?yàn)榈刂穼?duì)齊會(huì)造成一定的內(nèi)存空洞。而 packed屬性則與之相反,用來(lái)減少地址對(duì)齊,用來(lái)指定變量或類型使用最可能小的地址對(duì)齊方式。
structdata{
chara;
shortb__attribute__((packed));
intc__attribute__((packed));
};
intmain(void)
{
structdatas;
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);
}
在這個(gè)程序中,我們將結(jié)構(gòu)體的成員 b和 c使用 packed屬性聲明,就是告訴編譯器,盡量使用最可能小的地址對(duì)齊給它們分配地址,盡可能地減少內(nèi)存空洞。程序的運(yùn)行結(jié)果如下。
size:7
&s.a:0028FF30
&s.b:0028FF31
&s.c:0028FF33
通過(guò)結(jié)果我們看到,結(jié)構(gòu)體內(nèi)各個(gè)成員地址的分配,使用最小1字節(jié)的對(duì)齊方式,導(dǎo)致整個(gè)結(jié)構(gòu)體的大小只有7個(gè)字節(jié)。
這個(gè)特性在底層驅(qū)動(dòng)開(kāi)發(fā)中還是非常有用的。比如,你想定義一個(gè)結(jié)構(gòu)體,封裝一個(gè) IP控制器的各種寄存器。在 ARM芯片中,每一個(gè)控制器的寄存器地址空間一般是連續(xù)存在的。如果考慮數(shù)據(jù)對(duì)齊,結(jié)構(gòu)體內(nèi)有空洞,這樣就跟實(shí)際連續(xù)的寄存器地址不一致了,使用 packed就可以避免這個(gè)問(wèn)題,結(jié)構(gòu)體的每個(gè)成員都緊挨著依次分配存儲(chǔ)地址,這樣就避免了各個(gè)成員元素因地址對(duì)齊而造成的內(nèi)存空洞。
structdata{
chara;
shortb;
intc;
}__attribute__((packed));
我們對(duì)整個(gè)結(jié)構(gòu)體添加 packed屬性,和分別對(duì)每個(gè)成員添加 packed屬性,效果是一樣的。修改結(jié)構(gòu)體后,程序的運(yùn)行結(jié)果跟上面程序運(yùn)行結(jié)果相同——結(jié)構(gòu)體的大小為7,結(jié)構(gòu)體內(nèi)各成員地址相同。
7.5 Linux內(nèi)核中 aligned、packed屬性聲明
在 Linux內(nèi)核中,我們經(jīng)??吹?aligned和 packed一起使用,即對(duì)一個(gè)變量或類型同時(shí)使用 aligned和 packed屬性聲明。這樣做的好處是,既避免了結(jié)構(gòu)體內(nèi)因地址對(duì)齊產(chǎn)生的內(nèi)存空洞,又指定了整個(gè)結(jié)構(gòu)體的對(duì)齊方式。
structdata{
chara;
shortb;
intc;
}__attribute__((packed,aligned(8)));
intmain(void)
{
structdatas;
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);
}
程序運(yùn)行結(jié)果如下。
size:8
&s.a:0028FF30
&s.b:0028FF31
&s.c:0028FF33
在這個(gè)程序中,結(jié)構(gòu)體 data雖然使用 packed屬性聲明,整個(gè)長(zhǎng)度變?yōu)?,但是我們同時(shí)又使用了 aligned(8)指定其按8字節(jié)地址對(duì)齊,所以編譯器要在結(jié)構(gòu)體后面填充1個(gè)字節(jié),這樣整個(gè)結(jié)構(gòu)體的大小就變?yōu)?字節(jié),按8字節(jié)地址對(duì)齊。