• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>

            eryar

            PipeCAD - Plant Piping Design Software.
            RvmTranslator - Translate AVEVA RVM to OBJ, glTF, etc.
            posts - 603, comments - 590, trackbacks - 0, articles - 0

            C語(yǔ)言字節(jié)對(duì)齊問(wèn)題詳解

               

            引言

                 考慮下面的結(jié)構(gòu)體定義:

            1 typedef struct{
            2     char  c1;
            3     short s; 
            4     char  c2; 
            5     int   i;
            6 }T_FOO;

                 假設(shè)這個(gè)結(jié)構(gòu)體的成員在內(nèi)存中是緊湊排列的,且c1的起始地址是0,則s的地址就是1,c2的地址是3,i的地址是4。

                 現(xiàn)在,我們編寫(xiě)一個(gè)簡(jiǎn)單的程序:

            1 int main(void){  
            2     T_FOO a; 
            3     printf("c1 -> %d, s -> %d, c2 -> %d, i -> %d\n", 
            4           (unsigned int)(void*)&a.c1 - (unsigned int)(void*)&a,
            5           (unsigned int)(void*)&a.s  - (unsigned int)(void*)&a, 
            6           (unsigned int)(void*)&a.c2 - (unsigned int)(void*)&a, 
            7           (unsigned int)(void*)&a.i  - (unsigned int)(void*)&a); 
            8     return 0;
            9 }

                 運(yùn)行后輸出: 

            1 c1 -> 0, s -> 2, c2 -> 4, i -> 8

                 為什么會(huì)這樣?這就是字節(jié)對(duì)齊導(dǎo)致的問(wèn)題。

                 本文在參考諸多資料的基礎(chǔ)上,詳細(xì)介紹常見(jiàn)的字節(jié)對(duì)齊問(wèn)題。因成文較早,資料來(lái)源大多已不可考,敬請(qǐng)諒解。

             

             

            一  什么是字節(jié)對(duì)齊

                 現(xiàn)代計(jì)算機(jī)中,內(nèi)存空間按照字節(jié)劃分,理論上可以從任何起始地址訪問(wèn)任意類型的變量。但實(shí)際中在訪問(wèn)特定類型變量時(shí)經(jīng)常在特定的內(nèi)存地址訪問(wèn),這就需要各種類型數(shù)據(jù)按照一定的規(guī)則在空間上排列,而不是順序一個(gè)接一個(gè)地存放,這就是對(duì)齊。

             

            二  對(duì)齊的原因和作用

                 不同硬件平臺(tái)對(duì)存儲(chǔ)空間的處理上存在很大的不同。某些平臺(tái)對(duì)特定類型的數(shù)據(jù)只能從特定地址開(kāi)始存取,而不允許其在內(nèi)存中任意存放。例如Motorola 68000 處理器不允許16位的字存放在奇地址,否則會(huì)觸發(fā)異常,因此在這種架構(gòu)下編程必須保證字節(jié)對(duì)齊。

                 但最常見(jiàn)的情況是,如果不按照平臺(tái)要求對(duì)數(shù)據(jù)存放進(jìn)行對(duì)齊,會(huì)帶來(lái)存取效率上的損失。比如32位的Intel處理器通過(guò)總線訪問(wèn)(包括讀和寫(xiě))內(nèi)存數(shù)據(jù)。每個(gè)總線周期從偶地址開(kāi)始訪問(wèn)32位內(nèi)存數(shù)據(jù),內(nèi)存數(shù)據(jù)以字節(jié)為單位存放。如果一個(gè)32位的數(shù)據(jù)沒(méi)有存放在4字節(jié)整除的內(nèi)存地址處,那么處理器就需要2個(gè)總線周期對(duì)其進(jìn)行訪問(wèn),顯然訪問(wèn)效率下降很多。

                 因此,通過(guò)合理的內(nèi)存對(duì)齊可以提高訪問(wèn)效率。為使CPU能夠?qū)?shù)據(jù)進(jìn)行快速訪問(wèn),數(shù)據(jù)的起始地址應(yīng)具有“對(duì)齊”特性。比如4字節(jié)數(shù)據(jù)的起始地址應(yīng)位于4字節(jié)邊界上,即起始地址能夠被4整除。

                 此外,合理利用字節(jié)對(duì)齊還可以有效地節(jié)省存儲(chǔ)空間。但要注意,在32位機(jī)中使用1字節(jié)或2字節(jié)對(duì)齊,反而會(huì)降低變量訪問(wèn)速度。因此需要考慮處理器類型。還應(yīng)考慮編譯器的類型。在VC/C++和GNU GCC中都是默認(rèn)是4字節(jié)對(duì)齊。

               

            三  對(duì)齊的分類和準(zhǔn)則

                 主要基于Intel X86架構(gòu)介紹結(jié)構(gòu)體對(duì)齊和棧內(nèi)存對(duì)齊,位域本質(zhì)上為結(jié)構(gòu)體類型。

                 對(duì)于Intel X86平臺(tái),每次分配內(nèi)存應(yīng)該是從4的整數(shù)倍地址開(kāi)始分配,無(wú)論是對(duì)結(jié)構(gòu)體變量還是簡(jiǎn)單類型的變量。

            3.1 結(jié)構(gòu)體對(duì)齊

                 在C語(yǔ)言中,結(jié)構(gòu)體是種復(fù)合數(shù)據(jù)類型,其構(gòu)成元素既可以是基本數(shù)據(jù)類型(如int、long、float等)的變量,也可以是一些復(fù)合數(shù)據(jù)類型(如數(shù)組、結(jié)構(gòu)體、聯(lián)合等)的數(shù)據(jù)單元。編譯器為結(jié)構(gòu)體的每個(gè)成員按照其自然邊界(alignment)分配空間。各成員按照它們被聲明的順序在內(nèi)存中順序存儲(chǔ),第一個(gè)成員的地址和整個(gè)結(jié)構(gòu)的地址相同。

                 字節(jié)對(duì)齊的問(wèn)題主要就是針對(duì)結(jié)構(gòu)體。

            3.1.1 簡(jiǎn)單示例

                 先看個(gè)簡(jiǎn)單的例子(32位,X86處理器,GCC編譯器):

                【例1】設(shè)結(jié)構(gòu)體如下定義:

             1 struct A{
             2     int    a;
             3     char   b;
             4     short  c;
             5 };
             6 struct B{
             7     char   b;
             8     int    a;
             9     short  c;
            10 };

                 已知32位機(jī)器上各數(shù)據(jù)類型的長(zhǎng)度為:char為1字節(jié)、short為2字節(jié)、int為4字節(jié)、long為4字節(jié)、float為4字節(jié)、double為8字節(jié)。那么上面兩個(gè)結(jié)構(gòu)體大小如何呢?

                 結(jié)果是:sizeof(strcut A)值為8;sizeof(struct B)的值卻是12。 

                 結(jié)構(gòu)體A中包含一個(gè)4字節(jié)的int數(shù)據(jù),一個(gè)1字節(jié)char數(shù)據(jù)和一個(gè)2字節(jié)short數(shù)據(jù);B也一樣。按理說(shuō)A和B大小應(yīng)該都是7字節(jié)。之所以出現(xiàn)上述結(jié)果,就是因?yàn)榫幾g器要對(duì)數(shù)據(jù)成員在空間上進(jìn)行對(duì)齊。

            3.1.2 對(duì)齊準(zhǔn)則

                 先來(lái)看四個(gè)重要的基本概念:

                 1) 數(shù)據(jù)類型自身的對(duì)齊值:char型數(shù)據(jù)自身對(duì)齊值為1字節(jié),short型數(shù)據(jù)為2字節(jié),int/float型為4字節(jié),double型為8字節(jié)。

                 2) 結(jié)構(gòu)體或類的自身對(duì)齊值:其成員中自身對(duì)齊值最大的那個(gè)值。

                 3) 指定對(duì)齊值:#pragma pack (value)時(shí)的指定對(duì)齊值value。

                 4) 數(shù)據(jù)成員、結(jié)構(gòu)體和類的有效對(duì)齊值:自身對(duì)齊值和指定對(duì)齊值中較小者,即有效對(duì)齊值=min{自身對(duì)齊值,當(dāng)前指定的pack值}。

                 基于上面這些值,就可以方便地討論具體數(shù)據(jù)結(jié)構(gòu)的成員和其自身的對(duì)齊方式。

                 其中,有效對(duì)齊值N是最終用來(lái)決定數(shù)據(jù)存放地址方式的值。有效對(duì)齊N表示“對(duì)齊在N上”,即該數(shù)據(jù)的“存放起始地址%N=0”。而數(shù)據(jù)結(jié)構(gòu)中的數(shù)據(jù)變量都是按定義的先后順序存放。第一個(gè)數(shù)據(jù)變量的起始地址就是數(shù)據(jù)結(jié)構(gòu)的起始地址。結(jié)構(gòu)體的成員變量要對(duì)齊存放,結(jié)構(gòu)體本身也要根據(jù)自身的有效對(duì)齊值圓整(即結(jié)構(gòu)體成員變量占用總長(zhǎng)度為結(jié)構(gòu)體有效對(duì)齊值的整數(shù)倍)。

                 以此分析3.1.1節(jié)中的結(jié)構(gòu)體B:

                 假設(shè)B從地址空間0x0000開(kāi)始存放,且指定對(duì)齊值默認(rèn)為4(4字節(jié)對(duì)齊)。成員變量b的自身對(duì)齊值是1,比默認(rèn)指定對(duì)齊值4小,所以其有效對(duì)齊值為1,其存放地址0x0000符合0x0000%1=0。成員變量a自身對(duì)齊值為4,所以有效對(duì)齊值也為4,只能存放在起始地址為0x0004~0x0007四個(gè)連續(xù)的字節(jié)空間中,符合0x0004%4=0且緊靠第一個(gè)變量。變量c自身對(duì)齊值為 2,所以有效對(duì)齊值也是2,可存放在0x0008~0x0009兩個(gè)字節(jié)空間中,符合0x0008%2=0。所以從0x0000~0x0009存放的都是B內(nèi)容。

                 再看數(shù)據(jù)結(jié)構(gòu)B的自身對(duì)齊值為其變量中最大對(duì)齊值(這里是b)所以就是4,所以結(jié)構(gòu)體的有效對(duì)齊值也是4。根據(jù)結(jié)構(gòu)體圓整的要求, 0x0000~0x0009=10字節(jié),(10+2)%4=0。所以0x0000A~0x000B也為結(jié)構(gòu)體B所占用。故B從0x0000到0x000B 共有12個(gè)字節(jié),sizeof(struct B)=12。

                 之所以編譯器在后面補(bǔ)充2個(gè)字節(jié),是為了實(shí)現(xiàn)結(jié)構(gòu)數(shù)組的存取效率。試想如果定義一個(gè)結(jié)構(gòu)B的數(shù)組,那么第一個(gè)結(jié)構(gòu)起始地址是0沒(méi)有問(wèn)題,但是第二個(gè)結(jié)構(gòu)呢?按照數(shù)組的定義,數(shù)組中所有元素都緊挨著。如果我們不把結(jié)構(gòu)體大小補(bǔ)充為4的整數(shù)倍,那么下一個(gè)結(jié)構(gòu)的起始地址將是0x0000A,這顯然不能滿足結(jié)構(gòu)的地址對(duì)齊。因此要把結(jié)構(gòu)體補(bǔ)充成有效對(duì)齊大小的整數(shù)倍。其實(shí)對(duì)于char/short/int/float/double等已有類型的自身對(duì)齊值也是基于數(shù)組考慮的,只是因?yàn)檫@些類型的長(zhǎng)度已知,所以他們的自身對(duì)齊值也就已知。 

                 上面的概念非常便于理解,不過(guò)個(gè)人還是更喜歡下面的對(duì)齊準(zhǔn)則。

                 結(jié)構(gòu)體字節(jié)對(duì)齊的細(xì)節(jié)和具體編譯器實(shí)現(xiàn)相關(guān),但一般而言滿足三個(gè)準(zhǔn)則:

                 1) 結(jié)構(gòu)體變量的首地址能夠被其最寬基本類型成員的大小所整除;

                 2) 結(jié)構(gòu)體每個(gè)成員相對(duì)結(jié)構(gòu)體首地址的偏移量(offset)都是成員大小的整數(shù)倍,如有需要編譯器會(huì)在成員之間加上填充字節(jié)(internal adding);

                 3) 結(jié)構(gòu)體的總大小為結(jié)構(gòu)體最寬基本類型成員大小的整數(shù)倍,如有需要編譯器會(huì)在最末一個(gè)成員之后加上填充字節(jié){trailing padding}。

                 對(duì)于以上規(guī)則的說(shuō)明如下:

                 第一條:編譯器在給結(jié)構(gòu)體開(kāi)辟空間時(shí),首先找到結(jié)構(gòu)體中最寬的基本數(shù)據(jù)類型,然后尋找內(nèi)存地址能被該基本數(shù)據(jù)類型所整除的位置,作為結(jié)構(gòu)體的首地址。將這個(gè)最寬的基本數(shù)據(jù)類型的大小作為上面介紹的對(duì)齊模數(shù)。

                 第二條:為結(jié)構(gòu)體的一個(gè)成員開(kāi)辟空間之前,編譯器首先檢查預(yù)開(kāi)辟空間的首地址相對(duì)于結(jié)構(gòu)體首地址的偏移是否是本成員大小的整數(shù)倍,若是,則存放本成員,反之,則在本成員和上一個(gè)成員之間填充一定的字節(jié),以達(dá)到整數(shù)倍的要求,也就是將預(yù)開(kāi)辟空間的首地址后移幾個(gè)字節(jié)。

                 第三條:結(jié)構(gòu)體總大小是包括填充字節(jié),最后一個(gè)成員滿足上面兩條以外,還必須滿足第三條,否則就必須在最后填充幾個(gè)字節(jié)以達(dá)到本條要求。

                【例2】假設(shè)4字節(jié)對(duì)齊,以下程序的輸出結(jié)果是多少?

             1 /* OFFSET宏定義可取得指定結(jié)構(gòu)體某成員在結(jié)構(gòu)體內(nèi)部的偏移 */
             2 #define OFFSET(st, field)     (size_t)&(((st*)0)->field)
             3 typedef struct{
             4     char  a;
             5     short b;
             6     char  c;
             7     int   d;
             8     char  e[3];
             9 }T_Test;
            10 
            11 int main(void){  
            12     printf("Size = %d\n  a-%d, b-%d, c-%d, d-%d\n  e[0]-%d, e[1]-%d, e[2]-%d\n",
            13            sizeof(T_Test), OFFSET(T_Test, a), OFFSET(T_Test, b),
            14            OFFSET(T_Test, c), OFFSET(T_Test, d), OFFSET(T_Test, e[0]),
            15            OFFSET(T_Test, e[1]),OFFSET(T_Test, e[2]));
            16     return 0;
            17 }

                 執(zhí)行后輸出如下:

            1 Size = 16
            2   a-0, b-2, c-4, d-8
            3   e[0]-12, e[1]-13, e[2]-14

                 下面來(lái)具體分析:

                 首先char a占用1個(gè)字節(jié),沒(méi)問(wèn)題。

                 short b本身占用2個(gè)字節(jié),根據(jù)上面準(zhǔn)則2,需要在b和a之間填充1個(gè)字節(jié)。

                 char c占用1個(gè)字節(jié),沒(méi)問(wèn)題。

                 int d本身占用4個(gè)字節(jié),根據(jù)準(zhǔn)則2,需要在d和c之間填充3個(gè)字節(jié)。

                 char e[3];本身占用3個(gè)字節(jié),根據(jù)原則3,需要在其后補(bǔ)充1個(gè)字節(jié)。

                 因此,sizeof(T_Test) = 1 + 1 + 2 + 1 + 3 + 4 + 3 + 1 = 16字節(jié)。

            3.1.3 對(duì)齊的隱患

            3.1.3.1 數(shù)據(jù)類型轉(zhuǎn)換

                 代碼中關(guān)于對(duì)齊的隱患,很多是隱式的。例如,在強(qiáng)制類型轉(zhuǎn)換的時(shí)候:

             1 int main(void){  
             2     unsigned int i = 0x12345678;
             3         
             4     unsigned char *p = (unsigned char *)&i;
             5     *p = 0x00;
             6     unsigned short *p1 = (unsigned short *)(p+1);
             7     *p1 = 0x0000;
             8 
             9     return 0;
            10 }

                 最后兩句代碼,從奇數(shù)邊界去訪問(wèn)unsigned short型變量,顯然不符合對(duì)齊的規(guī)定。在X86上,類似的操作只會(huì)影響效率;但在MIPS或者SPARC上可能導(dǎo)致error,因?yàn)樗鼈円蟊仨氉止?jié)對(duì)齊。

                 又如對(duì)于3.1.1節(jié)的結(jié)構(gòu)體struct B,定義如下函數(shù):

            1 void Func(struct B *p){
            2     //Code
            3 }

                 在函數(shù)體內(nèi)如果直接訪問(wèn)p->a,則很可能會(huì)異常。因?yàn)镸IPS認(rèn)為a是int,其地址應(yīng)該是4的倍數(shù),但p->a的地址很可能不是4的倍數(shù)。

                 如果p的地址不在對(duì)齊邊界上就可能出問(wèn)題,比如p來(lái)自一個(gè)跨CPU的數(shù)據(jù)包(多種數(shù)據(jù)類型的數(shù)據(jù)被按順序放置在一個(gè)數(shù)據(jù)包中傳輸),或p是經(jīng)過(guò)指針移位算出來(lái)的。因此要特別注意跨CPU數(shù)據(jù)的接口函數(shù)對(duì)接口輸入數(shù)據(jù)的處理,以及指針移位再?gòu)?qiáng)制轉(zhuǎn)換為結(jié)構(gòu)指針進(jìn)行訪問(wèn)時(shí)的安全性。 

                 解決方式如下:

                 1) 定義一個(gè)此結(jié)構(gòu)的局部變量,用memmove方式將數(shù)據(jù)拷貝進(jìn)來(lái)。

            1 void Func(struct B *p){
            2     struct B tData;
            3     memmove(&tData, p, sizeof(struct B));
            4     //此后可安全訪問(wèn)tData.a,因?yàn)榫幾g器已將tData分配在正確的起始地址上
            5 }

                 注意:如果能確定p的起始地址沒(méi)問(wèn)題,則不需要這么處理;如果不能確定(比如跨CPU輸入數(shù)據(jù)、或指針移位運(yùn)算出來(lái)的數(shù)據(jù)要特別小心),則需要這樣處理。

                 2) 用#pragma pack (1)將STRUCT_T定義為1字節(jié)對(duì)齊方式。

            3.1.3.2 處理器間數(shù)據(jù)通信

                 處理器間通過(guò)消息(對(duì)于C/C++而言就是結(jié)構(gòu)體)進(jìn)行通信時(shí),需要注意字節(jié)對(duì)齊以及字節(jié)序的問(wèn)題。

                 大多數(shù)編譯器提供內(nèi)存對(duì)其的選項(xiàng)供用戶使用。這樣用戶可以根據(jù)處理器的情況選擇不同的字節(jié)對(duì)齊方式。例如C/C++編譯器提供的#pragma pack(n) n=1,2,4等,讓編譯器在生成目標(biāo)文件時(shí),使內(nèi)存數(shù)據(jù)按照指定的方式排布在1,2,4等字節(jié)整除的內(nèi)存地址處。

                 然而在不同編譯平臺(tái)或處理器上,字節(jié)對(duì)齊會(huì)造成消息結(jié)構(gòu)長(zhǎng)度的變化。編譯器為了使字節(jié)對(duì)齊可能會(huì)對(duì)消息結(jié)構(gòu)體進(jìn)行填充,不同編譯平臺(tái)可能填充為不同的形式,大大增加處理器間數(shù)據(jù)通信的風(fēng)險(xiǎn)。 

                 下面以32位處理器為例,提出一種內(nèi)存對(duì)齊方法以解決上述問(wèn)題。

                 對(duì)于本地使用的數(shù)據(jù)結(jié)構(gòu),為提高內(nèi)存訪問(wèn)效率,采用四字節(jié)對(duì)齊方式;同時(shí)為了減少內(nèi)存的開(kāi)銷,合理安排結(jié)構(gòu)體成員的位置,減少四字節(jié)對(duì)齊導(dǎo)致的成員之間的空隙,降低內(nèi)存開(kāi)銷。

                 對(duì)于處理器之間的數(shù)據(jù)結(jié)構(gòu),需要保證消息長(zhǎng)度不會(huì)因不同編譯平臺(tái)或處理器而導(dǎo)致消息結(jié)構(gòu)體長(zhǎng)度發(fā)生變化,使用一字節(jié)對(duì)齊方式對(duì)消息結(jié)構(gòu)進(jìn)行緊縮;為保證處理器之間的消息數(shù)據(jù)結(jié)構(gòu)的內(nèi)存訪問(wèn)效率,采用字節(jié)填充的方式自己對(duì)消息中成員進(jìn)行四字節(jié)對(duì)齊。

                 數(shù)據(jù)結(jié)構(gòu)的成員位置要兼顧成員之間的關(guān)系、數(shù)據(jù)訪問(wèn)效率和空間利用率。順序安排原則是:四字節(jié)的放在最前面,兩字節(jié)的緊接最后一個(gè)四字節(jié)成員,一字節(jié)緊接最后一個(gè)兩字節(jié)成員,填充字節(jié)放在最后。

                 舉例如下:

            1 typedef struct tag_T_MSG{
            2     long  ParaA;
            3     long  ParaB;
            4     short ParaC;
            5     char  ParaD;
            6     char  Pad;   //填充字節(jié)
            7 }T_MSG;

            3.1.3.3 排查對(duì)齊問(wèn)題

                 如果出現(xiàn)對(duì)齊或者賦值問(wèn)題可查看:

                 1) 編譯器的字節(jié)序大小端設(shè)置;

                 2) 處理器架構(gòu)本身是否支持非對(duì)齊訪問(wèn);

                 3) 如果支持看設(shè)置對(duì)齊與否,如果沒(méi)有則看訪問(wèn)時(shí)需要加某些特殊的修飾來(lái)標(biāo)志其特殊訪問(wèn)操作。 

            3.1.4 更改對(duì)齊方式

                 主要是更改C編譯器的缺省字節(jié)對(duì)齊方式。   

                 在缺省情況下,C編譯器為每一個(gè)變量或是數(shù)據(jù)單元按其自然對(duì)界條件分配空間。一般地,可以通過(guò)下面的方法來(lái)改變?nèi)笔〉膶?duì)界條件:

            • 使用偽指令#pragma pack(n):C編譯器將按照n個(gè)字節(jié)對(duì)齊;
            • 使用偽指令#pragma pack(): 取消自定義字節(jié)對(duì)齊方式。

                 另外,還有如下的一種方式(GCC特有語(yǔ)法):

            • __attribute((aligned (n))): 讓所作用的結(jié)構(gòu)成員對(duì)齊在n字節(jié)自然邊界上。如果結(jié)構(gòu)體中有成員的長(zhǎng)度大于n,則按照最大成員的長(zhǎng)度來(lái)對(duì)齊。
            • __attribute__ ((packed)): 取消結(jié)構(gòu)在編譯過(guò)程中的優(yōu)化對(duì)齊,按照實(shí)際占用字節(jié)數(shù)進(jìn)行對(duì)齊。

                【注】__attribute__機(jī)制是GCC的一大特色,可以設(shè)置函數(shù)屬性(Function Attribute)、變量屬性(Variable Attribute)和類型屬性(Type Attribute)。詳細(xì)介紹請(qǐng)參考:

                 http://www.unixwiz.net/techtips/gnu-c-attributes.html

                 下面具體針對(duì)MS VC/C++ 6.0編譯器介紹下如何修改編譯器默認(rèn)對(duì)齊值。

                 1) VC/C++ IDE環(huán)境中,可在[Project]|[Settings],C/C++選項(xiàng)卡Category的Code Generation選項(xiàng)的Struct Member Alignment中修改,默認(rèn)是8字節(jié)。

             

                 VC/C++中的編譯選項(xiàng)有/Zp[1|2|4|8|16],/Zpn表示以n字節(jié)邊界對(duì)齊。n字節(jié)邊界對(duì)齊是指一個(gè)成員的地址必須安排在成員的尺寸的整數(shù)倍地址上或者是n的整數(shù)倍地址上,取它們中的最小值。亦即:min(sizeof(member), n)

                 實(shí)際上,1字節(jié)邊界對(duì)齊也就表示結(jié)構(gòu)成員之間沒(méi)有空洞。

                 /Zpn選項(xiàng)應(yīng)用于整個(gè)工程,影響所有參與編譯的結(jié)構(gòu)體。在Struct member alignment中可選擇不同的對(duì)齊值來(lái)改變編譯選項(xiàng)。

                 2) 在編碼時(shí),可用#pragma pack動(dòng)態(tài)修改對(duì)齊值。具體語(yǔ)法說(shuō)明見(jiàn)附錄5.3節(jié)。

                 自定義對(duì)齊值后要用#pragma pack()來(lái)還原,否則會(huì)對(duì)后面的結(jié)構(gòu)造成影響。 

                【例3】分析如下結(jié)構(gòu)體C:

            1 #pragma pack(2)  //指定按2字節(jié)對(duì)齊
            2 struct C{
            3     char  b;
            4     int   a;
            5     short c;
            6 };
            7 #pragma pack()   //取消指定對(duì)齊,恢復(fù)缺省對(duì)齊

                 變量b自身對(duì)齊值為1,指定對(duì)齊值為2,所以有效對(duì)齊值為1,假設(shè)C從0x0000開(kāi)始,則b存放在0x0000,符合0x0000%1= 0;變量a自身對(duì)齊值為4,指定對(duì)齊值為2,所以有效對(duì)齊值為2,順序存放在0x0002~0x0005四個(gè)連續(xù)字節(jié)中,符合0x0002%2=0。變量c的自身對(duì)齊值為2,所以有效對(duì)齊值為2,順序存放在0x0006~0x0007中,符合 0x0006%2=0。所以從0x0000到0x00007共八字節(jié)存放的是C的變量。C的自身對(duì)齊值為4,所以其有效對(duì)齊值為2。又8%2=0,C只占用0x0000~0x0007的八個(gè)字節(jié)。所以sizeof(struct C) = 8。

                 注意,結(jié)構(gòu)體對(duì)齊到的字節(jié)數(shù)并非完全取決于當(dāng)前指定的pack值,如下:

            1 #pragma pack(8)
            2 struct D{
            3     char  b;
            4     short a;
            5     char  c;
            6 };
            7 #pragma pack()

                 雖然#pragma pack(8),但依然按照兩字節(jié)對(duì)齊,所以sizeof(struct D)的值為6。因?yàn)椋?span style="color: #800080">對(duì)齊到的字節(jié)數(shù) = min{當(dāng)前指定的pack值,最大成員大?。?。

                 另外,GNU GCC編譯器中按1字節(jié)對(duì)齊可寫(xiě)為以下形式:

            1 #define GNUC_PACKED __attribute__((packed))
            2 struct C{
            3     char  b;
            4     int   a;
            5     short c;
            6 }GNUC_PACKED;

                 此時(shí)sizeof(struct C)的值為7。

            3.2 棧內(nèi)存對(duì)齊

                 在VC/C++中,棧的對(duì)齊方式不受結(jié)構(gòu)體成員對(duì)齊選項(xiàng)的影響??偸潜3謱?duì)齊且對(duì)齊在4字節(jié)邊界上。

                【例4】

             1 #pragma pack(push, 1)  //后面可改為1, 2, 4, 8
             2 struct StrtE{
             3     char m1;
             4     long m2;
             5 };
             6 #pragma pack(pop)
             7 
             8 int main(void){  
             9     char a;
            10     short b;
            11     int c;
            12     double d[2];
            13     struct StrtE s;
            14         
            15     printf("a    address:   %p\n", &a);
            16     printf("b    address:   %p\n", &b);
            17     printf("c    address:   %p\n", &c);
            18     printf("d[0] address:   %p\n", &(d[0]));
            19     printf("d[1] address:   %p\n", &(d[1]));
            20     printf("s    address:   %p\n", &s);
            21     printf("s.m2 address:   %p\n", &(s.m2));
            22     return 0;
            23 }

                 結(jié)果如下:

            1 a    address:   0xbfc4cfff
            2 b    address:   0xbfc4cffc
            3 c    address:   0xbfc4cff8
            4 d[0] address:   0xbfc4cfe8
            5 d[1] address:   0xbfc4cff0
            6 s    address:   0xbfc4cfe3
            7 s.m2 address:   0xbfc4cfe4

                 可以看出都是對(duì)齊到4字節(jié)。并且前面的char和short并沒(méi)有被湊在一起(成4字節(jié)),這和結(jié)構(gòu)體內(nèi)的處理是不同的。

                 至于為什么輸出的地址值是變小的,這是因?yàn)樵撈脚_(tái)下的棧是倒著“生長(zhǎng)”的。

            3.3 位域?qū)R

            3.3.1 位域定義

                 有些信息在存儲(chǔ)時(shí),并不需要占用一個(gè)完整的字節(jié),而只需占幾個(gè)或一個(gè)二進(jìn)制位。例如在存放一個(gè)開(kāi)關(guān)量時(shí),只有0和1兩種狀態(tài),用一位二進(jìn)位即可。為了節(jié)省存儲(chǔ)空間和處理簡(jiǎn)便,C語(yǔ)言提供了一種數(shù)據(jù)結(jié)構(gòu),稱為“位域”或“位段”。

                 位域是一種特殊的結(jié)構(gòu)成員或聯(lián)合成員(即只能用在結(jié)構(gòu)或聯(lián)合中),用于指定該成員在內(nèi)存存儲(chǔ)時(shí)所占用的位數(shù),從而在機(jī)器內(nèi)更緊湊地表示數(shù)據(jù)。每個(gè)位域有一個(gè)域名,允許在程序中按域名操作對(duì)應(yīng)的位。這樣就可用一個(gè)字節(jié)的二進(jìn)制位域來(lái)表示幾個(gè)不同的對(duì)象。

                 位域定義與結(jié)構(gòu)定義類似,其形式為:

            struct 位域結(jié)構(gòu)名

                   { 位域列表 };

                 其中位域列表的形式為:

            類型說(shuō)明符位域名:位域長(zhǎng)度

                 位域的使用和結(jié)構(gòu)成員的使用相同,其一般形式為:

            位域變量名.位域名

                 位域允許用各種格式輸出。

                 位域在本質(zhì)上就是一種結(jié)構(gòu)類型,不過(guò)其成員是按二進(jìn)位分配的。位域變量的說(shuō)明與結(jié)構(gòu)變量說(shuō)明的方式相同,可先定義后說(shuō)明、同時(shí)定義說(shuō)明或直接說(shuō)明。      

                 位域的使用主要為下面兩種情況:

                 1) 當(dāng)機(jī)器可用內(nèi)存空間較少而使用位域可大量節(jié)省內(nèi)存時(shí)。如把結(jié)構(gòu)作為大數(shù)組的元素時(shí)。

                 2) 當(dāng)需要把一結(jié)構(gòu)體或聯(lián)合映射成某預(yù)定的組織結(jié)構(gòu)時(shí)。如需要訪問(wèn)字節(jié)內(nèi)的特定位時(shí)。

            3.3.2 對(duì)齊準(zhǔn)則

                 位域成員不能單獨(dú)被取sizeof值。下面主要討論含有位域的結(jié)構(gòu)體的sizeof。 

                 C99規(guī)定int、unsigned int和bool可以作為位域類型,但編譯器幾乎都對(duì)此作了擴(kuò)展,允許其它類型的存在。位域作為嵌入式系統(tǒng)中非常常見(jiàn)的一種編程工具,優(yōu)點(diǎn)在于壓縮程序的存儲(chǔ)空間。

                 其對(duì)齊規(guī)則大致為:

                 1) 如果相鄰位域字段的類型相同,且其位寬之和小于類型的sizeof大小,則后面的字段將緊鄰前一個(gè)字段存儲(chǔ),直到不能容納為止;

                 2) 如果相鄰位域字段的類型相同,但其位寬之和大于類型的sizeof大小,則后面的字段將從新的存儲(chǔ)單元開(kāi)始,其偏移量為其類型大小的整數(shù)倍;

                 3) 如果相鄰的位域字段的類型不同,則各編譯器的具體實(shí)現(xiàn)有差異,VC6采取不壓縮方式,Dev-C++和GCC采取壓縮方式;

                 4) 如果位域字段之間穿插著非位域字段,則不進(jìn)行壓縮;

                 5) 整個(gè)結(jié)構(gòu)體的總大小為最寬基本類型成員大小的整數(shù)倍,而位域則按照其最寬類型字節(jié)數(shù)對(duì)齊。

                【例5】

            1 struct BitField{
            2     char element1  : 1;
            3     char element2  : 4;
            4     char element3  : 5;
            5 };

                 位域類型為char,第1個(gè)字節(jié)僅能容納下element1和element2,所以element1和element2被壓縮到第1個(gè)字節(jié)中,而element3只能從下一個(gè)字節(jié)開(kāi)始。因此sizeof(BitField)的結(jié)果為2。

                【例6】

            1 struct BitField1{
            2     char element1   : 1;
            3     short element2  : 5;
            4     char element3   : 7;
            5 };

                 由于相鄰位域類型不同,在VC6中其sizeof為6,在Dev-C++中為2。

                【例7】

            1 struct BitField2{
            2     char element1  : 3;
            3     char element2  ;
            4     char element3  : 5;
            5 };

                 非位域字段穿插在其中,不會(huì)產(chǎn)生壓縮,在VC6和Dev-C++中得到的大小均為3。

                【例8】

            1 struct StructBitField{
            2     int element1   : 1;
            3     int element2   : 5;
            4     int element3   : 29;
            5     int element4   : 6;
            6     char element5  :2;
            7     char stelement;  //在含位域的結(jié)構(gòu)或聯(lián)合中也可同時(shí)說(shuō)明普通成員
            8 };

                 位域中最寬類型int的字節(jié)數(shù)為4,因此結(jié)構(gòu)體按4字節(jié)對(duì)齊,在VC6中其sizeof為16。

            3.3.3 注意事項(xiàng)

                 關(guān)于位域操作有幾點(diǎn)需要注意:

                 1) 位域的地址不能訪問(wèn),因此不允許將&運(yùn)算符用于位域。不能使用指向位域的指針也不能使用位域的數(shù)組(數(shù)組是種特殊指針)。

                 例如,scanf函數(shù)無(wú)法直接向位域中存儲(chǔ)數(shù)據(jù):

            1 int main(void){  
            2     struct BitField1 tBit;
            3     scanf("%d", &tBit.element2); //error: cannot take address of bit-field 'element2'
            4     return 0;
            5 }

                 可用scanf函數(shù)將輸入讀入到一個(gè)普通的整型變量中,然后再賦值給tBit.element2。

                 2) 位域不能作為函數(shù)返回的結(jié)果。

                 3) 位域以定義的類型為單位,且位域的長(zhǎng)度不能夠超過(guò)所定義類型的長(zhǎng)度。例如定義int a:33是不允許的。

                 4) 位域可以不指定位域名,但不能訪問(wèn)無(wú)名的位域。

                 位域可以無(wú)位域名,只用作填充或調(diào)整位置,占位大小取決于該類型。例如,char :0表示整個(gè)位域向后推一個(gè)字節(jié),即該無(wú)名位域后的下一個(gè)位域從下一個(gè)字節(jié)開(kāi)始存放,同理short :0和int :0分別表示整個(gè)位域向后推兩個(gè)和四個(gè)字節(jié)。

                 當(dāng)空位域的長(zhǎng)度為具體數(shù)值N時(shí)(如int :2),該變量?jī)H用來(lái)占位N位。

                【例9】

            1 struct BitField3{
            2     char element1  : 3;
            3     char  :6;
            4     char element3  : 5;
            5 };

                 結(jié)構(gòu)體大小為3。因?yàn)閑lement1占3位,后面要保留6位而char為8位,所以保留的6位只能放到第2個(gè)字節(jié)。同樣element3只能放到第3字節(jié)。

            1 struct BitField4{
            2     char element1  : 3;
            3     char  :0;
            4     char element3  : 5;
            5 };

                 長(zhǎng)度為0的位域告訴編譯器將下一個(gè)位域放在一個(gè)存儲(chǔ)單元的起始位置。如上,編譯器會(huì)給成員element1分配3位,接著跳過(guò)余下的4位到下一個(gè)存儲(chǔ)單元,然后給成員element3分配5位。故上面的結(jié)構(gòu)體大小為2。

                 5) 位域的表示范圍。

            • 位域的賦值不能超過(guò)其可以表示的范圍;
            • 位域的類型決定該編碼能表示的值的結(jié)果。

                 對(duì)于第二點(diǎn),若位域?yàn)閡nsigned類型,則直接轉(zhuǎn)化為正數(shù);若非unsigned類型,則先判斷最高位是否為1,若為1表示補(bǔ)碼,則對(duì)其除符號(hào)位外的所有位取反再加一得到最后的結(jié)果數(shù)據(jù)(原碼)。如:

            1 unsigned int p:3 = 111;   //p表示7
            2 int p:3 = 111;            //p 表示-1,對(duì)除符號(hào)位之外的所有位取反再加一

                 6) 帶位域的結(jié)構(gòu)在內(nèi)存中各個(gè)位域的存儲(chǔ)方式取決于編譯器,既可從左到右也可從右到左存儲(chǔ)。

                【例10】在VC6下執(zhí)行下面的代碼:

            int main(void){  
                union{
                    int i;
                    struct{
                        char a : 1;
                        char b : 1;
                        char c : 2;
                    }bits;
                }num;
                printf("Input an integer for i(0~15): ");
                scanf("%d", &num.i);
                printf("i = %d, cba = %d %d %d\n", num.i, num.bits.c, num.bits.b, num.bits.a); 
                return 0;
            }

                 輸入i值為11,則輸出為i = 11, cba = -2 -1 -1。

                 Intel x86處理器按小字節(jié)序存儲(chǔ)數(shù)據(jù),所以bits中的位域在內(nèi)存中放置順序?yàn)閏cba。當(dāng)num.i置為11時(shí),bits的最低有效位(即位域a)的值為1,a、b、c按低地址到高地址分別存儲(chǔ)為10、1、1(二進(jìn)制)。

                 但為什么最后的打印結(jié)果是a=-1而不是1?

                 因?yàn)槲挥騛定義的類型signed char是有符號(hào)數(shù),所以盡管a只有1位,仍要進(jìn)行符號(hào)擴(kuò)展。1做為補(bǔ)碼存在,對(duì)應(yīng)原碼-1。

                 如果將a、b、c的類型定義為unsigned char,即可得到cba = 2 1 1。1011即為11的二進(jìn)制數(shù)。

                 注:C語(yǔ)言中,不同的成員使用共同的存儲(chǔ)區(qū)域的數(shù)據(jù)構(gòu)造類型稱為聯(lián)合(或共用體)。聯(lián)合占用空間的大小取決于類型長(zhǎng)度最大的成員。聯(lián)合在定義、說(shuō)明和使用形式上與結(jié)構(gòu)體相似。 

                 7) 位域的實(shí)現(xiàn)會(huì)因編譯器的不同而不同,使用位域會(huì)影響程序可移植性。因此除非必要否則最好不要使用位域。

                 8) 盡管使用位域可以節(jié)省內(nèi)存空間,但卻增加了處理時(shí)間。當(dāng)訪問(wèn)各個(gè)位域成員時(shí),需要把位域從它所在的字中分解出來(lái)或反過(guò)來(lái)把一值壓縮存到位域所在的字位中。

             

            四  總結(jié)

                 讓我們回到引言部分的問(wèn)題。

                 缺省情況下,C/C++編譯器默認(rèn)將結(jié)構(gòu)、棧中的成員數(shù)據(jù)進(jìn)行內(nèi)存對(duì)齊。因此,引言程序輸出就變成"c1 -> 0, s -> 2, c2 -> 4, i -> 8"。

                 編譯器將未對(duì)齊的成員向后移,將每一個(gè)都成員對(duì)齊到自然邊界上,從而也導(dǎo)致整個(gè)結(jié)構(gòu)的尺寸變大。盡管會(huì)犧牲一點(diǎn)空間(成員之間有空洞),但提高了性能。

                 也正是這個(gè)原因,引言例子中sizeof(T_ FOO)為12,而不是8。 

                 總結(jié)說(shuō)來(lái),就是

            在結(jié)構(gòu)體中,綜合考慮變量本身和指定的對(duì)齊值;

            在棧上,不考慮變量本身的大小,統(tǒng)一對(duì)齊到4字節(jié)。

             

            五  附錄

            5.1 字節(jié)序與網(wǎng)絡(luò)序

            5.1.1 字節(jié)序

                 字節(jié)序,顧名思義就是字節(jié)的高低位存放順序。

                 對(duì)于單字節(jié),大部分處理器以相同的順序處理比特位,因此單字節(jié)的存放和傳輸方式一般相同。

                 對(duì)于多字節(jié)數(shù)據(jù),如整型(32位機(jī)中一般占4字節(jié)),在不同的處理器的存放方式主要有兩種(以內(nèi)存中0x0A0B0C0D的存放方式為例)。

                 1) 大字節(jié)序(Big-Endian,又稱大端序或大尾序)

                 在計(jì)算機(jī)中,存儲(chǔ)介質(zhì)以下面方式存儲(chǔ)整數(shù)0x0A0B0C0D則稱為大字節(jié)序:

            數(shù)據(jù)以8bit為單位

            低地址方向

            0x0A

            0x0B

            0x0C

            0x0D

            高地址方向

            數(shù)據(jù)以16bit為單位

            低地址方向

            0x0A0B

            0x0C0D

            高地址方向

                 其中,最高有效位(MSB,Most Significant Byte)0x0A存儲(chǔ)在最低的內(nèi)存地址處。下個(gè)字節(jié)0x0B存在后面的地址處。同時(shí),最高的16bit單元0x0A0B存儲(chǔ)在低位。

                 簡(jiǎn)而言之,大字節(jié)序就是“高字節(jié)存入低地址,低字節(jié)存入高地址”。

                 這里講個(gè)詞源典故:“endian”一詞來(lái)源于喬納森·斯威夫特的小說(shuō)《格列佛游記》。小說(shuō)中,小人國(guó)為水煮蛋該從大的一端(Big-End)剝開(kāi)還是小的一端(Little-End)剝開(kāi)而爭(zhēng)論,爭(zhēng)論的雙方分別被稱為Big-endians和Little-endians。

                 1980年,Danny Cohen在其著名的論文"On Holy Wars and a Plea for Peace"中為平息一場(chǎng)關(guān)于字節(jié)該以什么樣的順序傳送的爭(zhēng)論而引用了該詞。

                 借用上面的典故,想象一下要把熟雞蛋旋轉(zhuǎn)著穩(wěn)立起來(lái),大頭(高字節(jié))肯定在下面(低地址)^_^

                 2) 小字節(jié)序(Little-Endian,又稱小端序或小尾序)

                 在計(jì)算機(jī)中,存儲(chǔ)介質(zhì)以下面方式存儲(chǔ)整數(shù)0x0A0B0C0D則稱為小字節(jié)序:

            數(shù)據(jù)以8bit為單位

            高地址方向

            0x0A

            0x0B

            0x0C

            0x0D

            低地址方向

            數(shù)據(jù)以16bit為單位

            高地址方向

            0x0A0B

            0x0C0D

            低地址方向

                 其中,最低有效位(LSB,Least Significant Byte)0x0D存儲(chǔ)在最低的內(nèi)存地址處。后面字節(jié)依次存在后面的地址處。同時(shí),最低的16bit單元0x0A0B存儲(chǔ)在低位。

                 可見(jiàn),小字節(jié)序就是“高字節(jié)存入高地址,低字節(jié)存入低地址”。 

                 C語(yǔ)言中的位域結(jié)構(gòu)也要遵循比特序(類似字節(jié)序)。例如:

            1 struct bitfield{
            2     unsigned char a: 2;
            3     unsigned char b: 6;
            4 }

                 該位域結(jié)構(gòu)占1個(gè)字節(jié),假設(shè)賦值a = 0x01和b=0x02,則大字節(jié)機(jī)器上該字節(jié)為(01)(000010),小字節(jié)機(jī)器上該字節(jié)為(000010)(01)。因此在編寫(xiě)可移植代碼時(shí),需要加條件編譯。

                 注意,在包含位域的C結(jié)構(gòu)中,若位域A在位域B之前定義,則位域A所占用的內(nèi)存空間地址低于位域B所占用的內(nèi)存空間。

                 對(duì)上述問(wèn)題,詳細(xì)的講解可參考http://www.linuxjournal.com/article/6788

                 另見(jiàn)以下聯(lián)合體,在小字節(jié)機(jī)器上若low=0x01,high=0x02,則hex=0x21:

             1 int main(void){
             2     union{
             3         unsigned char hex;
             4         struct{
             5             unsigned char low  : 4;
             6             unsigned char high : 4;
             7         };
             8     }convert;
             9     convert.low = 0x01;
            10     convert.high = 0x02;
            11     printf("hex = 0x%0x\n", convert.hex);
            12     return 0;
            13 }

            5.1.2 網(wǎng)絡(luò)序

                 網(wǎng)絡(luò)傳輸一般采用大字節(jié)序,也稱為網(wǎng)絡(luò)字節(jié)序或網(wǎng)絡(luò)序。IP協(xié)議中定義大字節(jié)序?yàn)榫W(wǎng)絡(luò)字節(jié)序。

                 對(duì)于可移植的代碼來(lái)說(shuō),將接收的網(wǎng)絡(luò)數(shù)據(jù)轉(zhuǎn)換成主機(jī)的字節(jié)序是必須的,一般會(huì)有成對(duì)的函數(shù)用于把網(wǎng)絡(luò)數(shù)據(jù)轉(zhuǎn)換成相應(yīng)的主機(jī)字節(jié)序或反之(若主機(jī)字節(jié)序與網(wǎng)絡(luò)字節(jié)序相同,通常將函數(shù)定義為空宏)。

                 伯克利socket API定義了一組轉(zhuǎn)換函數(shù),用于16和32位整數(shù)在網(wǎng)絡(luò)序和主機(jī)字節(jié)序之間的轉(zhuǎn)換。Htonl、htons用于主機(jī)序轉(zhuǎn)換到網(wǎng)絡(luò)序;ntohl、ntohs用于網(wǎng)絡(luò)序轉(zhuǎn)換到本機(jī)序。

                 注意:在大小字節(jié)序轉(zhuǎn)換時(shí),必須考慮待轉(zhuǎn)換數(shù)據(jù)的長(zhǎng)度(如5.1.1節(jié)的數(shù)據(jù)單元)。另外對(duì)于單字符或小于單字符的幾個(gè)bit數(shù)據(jù),是不必轉(zhuǎn)換的,因?yàn)樵跈C(jī)器存儲(chǔ)和網(wǎng)絡(luò)發(fā)送的一個(gè)字符內(nèi)的bit位存儲(chǔ)順序是一致的。

            5.1.3 位序

                 用于描述串行設(shè)備的傳輸順序。一般硬件傳輸采用小字節(jié)序(先傳低位),但I(xiàn)2C協(xié)議采用大字節(jié)序。網(wǎng)絡(luò)協(xié)議中只有數(shù)據(jù)鏈路層的底端會(huì)涉及到。

            5.1.4 處理器字節(jié)序

                 不同處理器體系的字節(jié)序如下所示:

            • X86、MOS Technology 6502、Z80、VAX、PDP-11等處理器為L(zhǎng)ittle endian;
            • Motorola 6800、Motorola 68000、PowerPC 970、System/370、SPARC(除V9外)等處理器為Big endian;
            • ARM、PowerPC (除PowerPC 970外)、DEC Alpha,SPARC V9,MIPS,PA-RISC and IA64等的字節(jié)序是可配置的。

            5.1.5 字節(jié)序編程

                 請(qǐng)看下面的語(yǔ)句:

            1 printf("%c\n", *((short*)"AB") >> 8);

                 在大字節(jié)序下輸出為'A',小字節(jié)序下輸出為'B'。

                 下面的代碼可用來(lái)判斷本地機(jī)器字節(jié)序:

             1 //字節(jié)序枚舉類型
             2 typedef enum{
             3     ENDIAN_LITTLE = (INT8U)0X00,
             4     ENDIAN_BIG    = (INT8U)0X01
             5 }E_ENDIAN_TYPE;
             6 
             7 E_ENDIAN_TYPE GetEndianType(VOID)
             8 {
             9     INT32U dwData = 0x12345678;
            10     
            11     if(0x78 == *((INT8U*)&dwData))
            12         return ENDIAN_LITTLE;
            13     else
            14         return ENDIAN_BIG;
            15 }
            16 
            17 //Start of GetEndianTypeTest//
            18 #include <endian.h>
            19 VOID GetEndianTypeTest(VOID)
            20 {
            21 #if _BYTE_ORDER == _LITTLE_ENDIAN
            22     printf("[%s]<Test Case> Result: %s, EndianType = %s!\n", __FUNCTION__, 
            23            (ENDIAN_LITTLE != GetEndianType()) ? "ERROR" : "OK", "Little");
            24 #elif _BYTE_ORDER == _BIG_ENDIAN
            25     printf("[%s]<Test Case> Result: %s, EndianType = %s!\n", __FUNCTION__, 
            26            (ENDIAN_BIG != GetEndianType()) ? "ERROR" : "OK", "Big");
            27 #endif
            28 }
            29 //End of GetEndianTypeTest//

                 在字節(jié)序不同的平臺(tái)間的交換數(shù)據(jù)時(shí),必須進(jìn)行轉(zhuǎn)換。比如對(duì)于int類型,大字節(jié)序?qū)懭胛募?/p>

            1 int i = 100;
            2 write(fd, &i, sizeof(int));

                 小字節(jié)序讀出后:

             1 int i;
             2 read(fd, &i, sizeof(int));
             3 char buf[sizeof(int)];
             4 memcpy(buf, &i, sizeof(int));
             5 for(i = 0; i < sizeof(int); i++)
             6 {
             7     int v = buf[sizeof(int) - i - 1];
             8     buf[sizeof(int) - 1] =  buf[i];
             9     buf[i] = v;
            10 }
            11 memcpy(&i, buf, sizeof(int));

                 上面僅僅是個(gè)例子。在不同平臺(tái)間即使不存在字節(jié)序的問(wèn)題,也盡量不要直接傳遞二進(jìn)制數(shù)據(jù)。作為可選的方式就是使用文本來(lái)交換數(shù)據(jù),這樣至少可以避免字節(jié)序的問(wèn)題。

                 很多的加密算法為了追求速度,都會(huì)采取字符串和數(shù)字之間的轉(zhuǎn)換,在計(jì)算完畢后,必須注意字節(jié)序的問(wèn)題,在某些實(shí)現(xiàn)中可以見(jiàn)到使用預(yù)編譯的方式來(lái)完成,這樣很不方便,如果使用前面的語(yǔ)句來(lái)判斷,就可以自動(dòng)適應(yīng)。 

                 字節(jié)序問(wèn)題不僅影響異種平臺(tái)間傳遞數(shù)據(jù),還影響諸如讀寫(xiě)一些特殊格式文件之類程序的可移植性。此時(shí)使用預(yù)編譯的方式來(lái)完成也是一個(gè)好辦法。

            5.2 對(duì)齊時(shí)的填充字節(jié)

                 代碼如下:

             1 struct A{ 
             2     char  c; 
             3     int   i; 
             4     short s;
             5 };
             6 int main(void){  
             7     struct A a; 
             8     a.c = 1; a.i = 2; a.s = 3;
             9     printf("sizeof(A)=%d\n", sizeof(struct A));
            10     return 0;
            11 }

                 執(zhí)行后輸出為sizeof(A)=12。

                 VC6.0環(huán)境中,在main函數(shù)打印語(yǔ)句前設(shè)置斷點(diǎn),執(zhí)行到斷點(diǎn)處時(shí)根據(jù)結(jié)構(gòu)體a的地址查看變量存儲(chǔ)如下:

                 可見(jiàn)填充字節(jié)為0xCC,即int3中斷。 

            5.3 pragma pack語(yǔ)法說(shuō)明

            #pragma  pack(n)

            #pragma pack(push, 1)

            #pragma pack(pop)

                 1) #pragma pack(n)

                 該指令指定結(jié)構(gòu)和聯(lián)合成員的緊湊對(duì)齊。而一個(gè)完整的轉(zhuǎn)換單元的結(jié)構(gòu)和聯(lián)合的緊湊對(duì)齊由/ Z p選項(xiàng)設(shè)置。緊湊對(duì)齊用pack編譯指示在數(shù)據(jù)說(shuō)明層設(shè)置。該編譯指示在其出現(xiàn)后的第一個(gè)結(jié)構(gòu)或者聯(lián)合說(shuō)明處生效。該編譯指示對(duì)定義無(wú)效。

                 當(dāng)使用#pragma pack (n) 時(shí),n 為1、2、4、8 或1 6 。第一個(gè)結(jié)構(gòu)成員后的每個(gè)結(jié)構(gòu)成員都被存儲(chǔ)在更小的成員類型或n字節(jié)界限內(nèi)。如果使用無(wú)參量的#pragma pack,結(jié)構(gòu)成員被緊湊為以/ Z p指定的值。該缺省/ Z p緊湊值為/ Z p 8。

                 2. 編譯器也支持以下增強(qiáng)型語(yǔ)法:

                 #pragma  pack( [ [ { push | pop } , ] [identifier, ] ] [ n] )

                 若不同的組件使用pack編譯指示指定不同的緊湊對(duì)齊, 這個(gè)語(yǔ)法允許你把程序組件組合為一個(gè)單獨(dú)的轉(zhuǎn)換單元。

                 帶push參量的pack編譯指示的每次出現(xiàn)將當(dāng)前的緊湊對(duì)齊存儲(chǔ)到一個(gè)內(nèi)部編譯器堆棧中。編譯指示的參量表從左到右讀取。如果使用push,則當(dāng)前緊湊值被存儲(chǔ)起來(lái);如果給出一個(gè)n值,該值將成為新的緊湊值。若指定一個(gè)標(biāo)識(shí)符,即選定一個(gè)名稱,則該標(biāo)識(shí)符將和這個(gè)新的的緊湊值聯(lián)系起來(lái)。

                 帶一個(gè)pop參量的pack編譯指示的每次出現(xiàn)都會(huì)檢索內(nèi)部編譯器堆棧頂?shù)闹担⑹乖撝禐樾碌木o湊對(duì)齊值。如果使用pop參量且內(nèi)部編譯器堆棧是空的,則緊湊值為命令行給定的值,并將產(chǎn)生一個(gè)警告信息。若使用pop且指定一個(gè)n值,該值將成為新的緊湊值。

                 若使用pop且指定一個(gè)標(biāo)識(shí)符,所有存儲(chǔ)在堆棧中的值將從棧中刪除,直到找到一個(gè)匹配的標(biāo)識(shí)符。這個(gè)與標(biāo)識(shí)符相關(guān)的緊湊值也從棧中移出,并且這個(gè)僅在標(biāo)識(shí)符入棧之前存在的緊湊值成為新的緊湊值。如果未找到匹配的標(biāo)識(shí)符, 將使用命令行設(shè)置的緊湊值,并且將產(chǎn)生一個(gè)一級(jí)警告。缺省緊湊對(duì)齊為8。

                 pack編譯指示的新的增強(qiáng)功能讓你在編寫(xiě)頭文件時(shí),確保在遇到該頭文件的前后的緊湊值是一樣的。

            5.4 Intel關(guān)于內(nèi)存對(duì)齊的說(shuō)明

                 以下內(nèi)容節(jié)選自《Intel Architecture 32 Manual》。

                 字、雙字和四字在自然邊界上不需要在內(nèi)存中對(duì)齊。(對(duì)于字、雙字和四字來(lái)說(shuō),自然邊界分別是偶數(shù)地址,可以被4整除的地址,和可以被8整除的地址。)

                 無(wú)論如何,為了提高程序的性能,數(shù)據(jù)結(jié)構(gòu)(尤其是棧)應(yīng)該盡可能地在自然邊界上對(duì)齊。原因在于,為了訪問(wèn)未對(duì)齊的內(nèi)存,處理器需要作兩次內(nèi)存訪問(wèn);然而,對(duì)齊的內(nèi)存訪問(wèn)僅需要一次訪問(wèn)。

                 一個(gè)字或雙字操作數(shù)跨越了4字節(jié)邊界,或者一個(gè)四字操作數(shù)跨越了8字節(jié)邊界,被認(rèn)為是未對(duì)齊的,從而需要兩次總線周期來(lái)訪問(wèn)內(nèi)存。一個(gè)字起始地址是奇數(shù)但卻沒(méi)有跨越字邊界被認(rèn)為是對(duì)齊的,能夠在一個(gè)總線周期中被訪問(wèn)。

                 某些操作雙四字的指令需要內(nèi)存操作數(shù)在自然邊界上對(duì)齊。如果操作數(shù)沒(méi)有對(duì)齊,這些指令將會(huì)產(chǎn)生一個(gè)通用保護(hù)異常(#GP)。雙四字的自然邊界是能夠被16 整除的地址。其他操作雙四字的指令允許未對(duì)齊的訪問(wèn)(不會(huì)產(chǎn)生通用保護(hù)異常),然而,需要額外的內(nèi)存總線周期來(lái)訪問(wèn)內(nèi)存中未對(duì)齊的數(shù)據(jù)。

            5.5 不同架構(gòu)處理器的對(duì)齊要求

                 RISC指令集處理器(MIPS/ARM):這種處理器的設(shè)計(jì)以效率為先,要求所訪問(wèn)的多字節(jié)數(shù)據(jù)(short/int/ long)的地址必須是為此數(shù)據(jù)大小的倍數(shù),如short數(shù)據(jù)地址應(yīng)為2的倍數(shù),long數(shù)據(jù)地址應(yīng)為4的倍數(shù),也就是說(shuō)是對(duì)齊的。

                 CISC指令集處理器(X86):沒(méi)有上述限制。 

                  對(duì)齊處理策略

                 訪問(wèn)非對(duì)齊多字節(jié)數(shù)據(jù)時(shí)(pack數(shù)據(jù)),編譯器會(huì)將指令拆成多條(因?yàn)榉菍?duì)齊多字節(jié)數(shù)據(jù)可能跨越地址對(duì)齊邊界),保證每條指令都從正確的起始地址上獲取數(shù)據(jù),但也因此效率比較低。

                 訪問(wèn)對(duì)齊數(shù)據(jù)時(shí)則只用一條指令獲取數(shù)據(jù),因此對(duì)齊數(shù)據(jù)必須確保其起始地址是在對(duì)齊邊界上。如果不是在對(duì)齊的邊界,對(duì)X86 CPU是安全的,但對(duì)MIPS/ARM這種RISC CPU會(huì)出現(xiàn)“總線訪問(wèn)異常”。

                 為什么X86是安全的呢?

                 X86 CPU是如何進(jìn)行數(shù)據(jù)對(duì)齊的。X86  CPU的EFLAGS寄存器中包含一個(gè)特殊的位標(biāo)志,稱為AC(對(duì)齊檢查的英文縮寫(xiě))標(biāo)志。按照默認(rèn)設(shè)置,當(dāng)CPU首次加電時(shí),該標(biāo)志被設(shè)置為0。當(dāng)該標(biāo)志是0時(shí),CPU能夠自動(dòng)執(zhí)行它應(yīng)該執(zhí)行的操作,以便成功地訪問(wèn)未對(duì)齊的數(shù)據(jù)值。然而,如果該標(biāo)志被設(shè)置為1,每當(dāng)系統(tǒng)試圖訪問(wèn)未對(duì)齊的數(shù)據(jù)時(shí),CPU就會(huì)發(fā)出一個(gè)INT 17H中斷。X86的Windows 2000和Windows   98版本從來(lái)不改變這個(gè)CPU標(biāo)志位。因此,當(dāng)應(yīng)用程序在X86處理器上運(yùn)行時(shí),你根本看不到應(yīng)用程序中出現(xiàn)數(shù)據(jù)未對(duì)齊的異常條件。

                 為什么MIPS/ARM不安全呢?

                 因?yàn)镸IPS/ARM  CPU不能自動(dòng)處理對(duì)未對(duì)齊數(shù)據(jù)的訪問(wèn)。當(dāng)未對(duì)齊的數(shù)據(jù)訪問(wèn)發(fā)生時(shí),CPU就會(huì)將這一情況通知操作系統(tǒng)。這時(shí),操作系統(tǒng)將會(huì)確定它是否應(yīng)該引發(fā)一個(gè)數(shù)據(jù)未對(duì)齊異常條件,對(duì)vxworks是會(huì)觸發(fā)這個(gè)異常的。

            5.6 ARM下的對(duì)齊處理

                 有部分摘自ARM編譯器文檔對(duì)齊部分。   

                 對(duì)齊的使用:

                 1) __align(num)

                 用于修改最高級(jí)別對(duì)象的字節(jié)邊界。在匯編中使用LDRD或STRD時(shí)就要用到此命令__align(8)進(jìn)行修飾限制。來(lái)保證數(shù)據(jù)對(duì)象是相應(yīng)對(duì)齊。

                 這個(gè)修飾對(duì)象的命令最大是8個(gè)字節(jié)限制,可以讓2字節(jié)的對(duì)象進(jìn)行4字節(jié)對(duì)齊,但不能讓4字節(jié)的對(duì)象2字節(jié)對(duì)齊。

                 __align是存儲(chǔ)類修改,只修飾最高級(jí)類型對(duì)象,不能用于結(jié)構(gòu)或者函數(shù)對(duì)象。   

                 2) __packed

                 進(jìn)行一字節(jié)對(duì)齊。需注意:

            • 不能對(duì)packed的對(duì)象進(jìn)行對(duì)齊;
            • 所有對(duì)象的讀寫(xiě)訪問(wèn)都進(jìn)行非對(duì)齊訪問(wèn);
            • float及包含float的結(jié)構(gòu)聯(lián)合及未用__packed的對(duì)象將不能字節(jié)對(duì)齊;
            • __packed對(duì)局部整型變量無(wú)影響。
            • 強(qiáng)制由unpacked對(duì)象向packed對(duì)象轉(zhuǎn)化時(shí)未定義。整型指針可以合法定義為packed,如__packed int* p(__packed int 則沒(méi)有意義)

                 對(duì)齊或非對(duì)齊讀寫(xiě)訪問(wèn)可能存在的問(wèn)題:

             1 //定義如下結(jié)構(gòu),b的起始地址不對(duì)齊。在棧中訪問(wèn)b可能有問(wèn)題,因?yàn)闂I蠑?shù)據(jù)對(duì)齊訪問(wèn)
             2 __packed struct STRUCT_TEST{
             3     char a;
             4     int  b;
             5     char c;
             6 };
             7 //將下面的變量定義成全局靜態(tài)(不在棧上)
             8 static char *p;
             9 static struct STRUCT_TEST a;
            10 void Main(){
            11     __packed int *q; //定義成__packed來(lái)修飾當(dāng)前q指向?yàn)榉菍?duì)齊的數(shù)據(jù)地址下面的訪問(wèn)則可以
            12     
            13     p = (char*)&a; 
            14     q = (int*)(p + 1); 
            15     *q = 0x87654321;
            16     /* 得到賦值的匯編指令很清楚
            17     ldr      r5,0x20001590 ; = #0x12345678
            18     [0xe1a00005]   mov     r0,r5
            19     [0xeb0000b0]   bl      __rt_uwrite4  //在此處調(diào)用一個(gè)寫(xiě)4字節(jié)的操作函數(shù)
            20         
            21     [0xe5c10000]   strb    r0,[r1,#0]    //函數(shù)進(jìn)行4次strb操作然后返回,正確訪問(wèn)數(shù)據(jù)
            22     [0xe1a02420]   mov     r2,r0,lsr #8
            23     [0xe5c12001]   strb    r2,[r1,#1]
            24     [0xe1a02820]   mov     r2,r0,lsr #16
            25     [0xe5c12002]   strb    r2,[r1,#2]
            26     [0xe1a02c20]   mov     r2,r0,lsr #24
            27     [0xe5c12003]   strb    r2,[r1,#3]
            28     [0xe1a0f00e]   mov     pc,r14
            29     
            30     若q未加__packed修飾則匯編出來(lái)指令如下(會(huì)導(dǎo)致奇地址處訪問(wèn)失敗):
            31     [0xe59f2018]   ldr      r2,0x20001594 ; = #0x87654321
            32     [0xe5812000]   str     r2,[r1,#0]
            33     */
            34     //這樣很清楚地看到非對(duì)齊訪問(wèn)如何產(chǎn)生錯(cuò)誤,以及如何消除非對(duì)齊訪問(wèn)帶來(lái)的問(wèn)題
            35     //也可看到非對(duì)齊訪問(wèn)和對(duì)齊訪問(wèn)的指令差異會(huì)導(dǎo)致效率問(wèn)題
            36 }

            5.7 《The C Book》之位域篇

                 While we're on the subject of structures, we might as well look at bitfields. They can only be declared inside a structure or a union, and allow you to specify some very small objects of a given number of bits in length. Their usefulness is limited and they aren't seen in many programs, but we'll deal with them anyway. This example should help to make things clear:

            1 struct{
            2     unsigned field1 :4; //field 4 bits wide
            3     unsigned        :3; //unnamed 3 bit field(allow for padding)
            4     signed field2   :1; //one-bit field(can only be 0 or -1 in two's complement)
            5     unsigned        :0; //align next field on a storage unit
            6     unsigned field3 :6;
            7 }full_of_fields;

                 Each field is accessed and manipulated as if it were an ordinary member of a structure. The keywords signed and unsigned mean what you would expect, except that it is interesting to note that a 1-bit signed field on a two's complement machine can only take the values 0 or -1. The declarations are permitted to include the const and volatile qualifiers.

                 The main use of bitfields is either to allow tight packing of data or to be able to specify the fields within some externally produced data files. C gives no guarantee of the ordering of fields within machine words, so if you do use them for the latter reason, you program will not only be non-portable, it will be compiler-dependent too. The Standard says that fields are packed into ‘storage units’, which are typically machine words. The packing order, and whether or not a bitfield may cross a storage unit boundary, are implementation defined. To force alignment to a storage unit boundary, a zero width field is used before the one that you want to have aligned.

                 Be careful using them. It can require a surprising amount of run-time code to manipulate these things and you can end up using more space than they save.

                 Bit fields do not have addresses—you can't have pointers to them or arrays of them.

            5.8 C語(yǔ)言字節(jié)相關(guān)面試題

            5.8.1 Intel/微軟C語(yǔ)言面試題

                 請(qǐng)看下面的問(wèn)題:

             1 #pragma pack(8)
             2 struct s1{
             3     short a;
             4     long  b;
             5 };
             6 struct s2{
             7     char c;
             8     s1   d;
             9     long long e;  //VC6.0下可能要用__int64代替雙long
            10 };
            11 #pragma pack()

                 問(wèn):1. sizeof(s2) = ? 2. s2的s1中的a后面空了幾個(gè)字節(jié)接著是b?

                【分析】

                 成員對(duì)齊有一個(gè)重要的條件,即每個(gè)成員分別按自己的方式對(duì)齊。

                 也就是說(shuō)上面雖然指定了按8字節(jié)對(duì)齊,但并不是所有的成員都是以8字節(jié)對(duì)齊。其對(duì)齊的規(guī)則是:每個(gè)成員按其類型的對(duì)齊參數(shù)(通常是這個(gè)類型的大小)和指定對(duì)齊參數(shù)(這里是8字節(jié))中較小的一個(gè)對(duì)齊,并且結(jié)構(gòu)的長(zhǎng)度必須為所用過(guò)的所有對(duì)齊參數(shù)的整數(shù)倍,不夠就補(bǔ)空字節(jié)。

                 s1中成員a是1字節(jié),默認(rèn)按1字節(jié)對(duì)齊,而指定對(duì)齊參數(shù)為8,兩值中取1,即a按1字節(jié)對(duì)齊;成員b是4個(gè)字節(jié),默認(rèn)按4字節(jié)對(duì)齊,這時(shí)就按4字節(jié)對(duì)齊,所以sizeof(s1)應(yīng)該為8;

                 s2中c和s1中a一樣,按1字節(jié)對(duì)齊。而d 是個(gè)8字節(jié)結(jié)構(gòu)體,其默認(rèn)對(duì)齊方式就是所有成員使用的對(duì)齊參數(shù)中最大的一個(gè),s1的就是4。所以,成員d按4字節(jié)對(duì)齊。成員e是8個(gè)字節(jié),默認(rèn)按8字節(jié)對(duì)齊,和指定的一樣,所以它對(duì)到8字節(jié)的邊界上。這時(shí),已經(jīng)使用了12個(gè)字節(jié),所以又添加4個(gè)字節(jié)的空,從第16個(gè)字節(jié)開(kāi)始放置成員e。此時(shí)長(zhǎng)度為24,并可被8(成員e按8字節(jié)對(duì)齊)整除。這樣,一共使用了24個(gè)字節(jié)。 

                 各個(gè)變量在內(nèi)存中的布局為:

                 c***aa**

                 bbbb****

                 dddddddd     ——這種“矩陣寫(xiě)法”很方便看出結(jié)構(gòu)體實(shí)際大小

                 因此,sizeof(S2)結(jié)果為24,a后面空了2個(gè)字節(jié)接著是b。   

                 這里有三點(diǎn)很重要:

                 1) 每個(gè)成員分別按自己的方式對(duì)齊,并能最小化長(zhǎng)度;

                 2) 復(fù)雜類型(如結(jié)構(gòu))的默認(rèn)對(duì)齊方式是其最長(zhǎng)的成員的對(duì)齊方式,這樣在成員是復(fù)雜類型時(shí)可以最小化長(zhǎng)度;

                 3) 對(duì)齊后的長(zhǎng)度必須是成員中最大對(duì)齊參數(shù)的整數(shù)倍,這樣在處理數(shù)組時(shí)可保證每一項(xiàng)都邊界對(duì)齊。

                 還要注意,“空結(jié)構(gòu)體”(不含數(shù)據(jù)成員)的大小為1,而不是0。試想如果不占空間的話,一個(gè)空結(jié)構(gòu)體變量如何取地址、兩個(gè)不同的空結(jié)構(gòu)體變量又如何得以區(qū)分呢?

            5.8.2 上海網(wǎng)宿科技面試題

                 假設(shè)硬件平臺(tái)是intel x86(little endian),以下程序輸出什么:

             1 //假設(shè)硬件平臺(tái)是intel x86(little endian)
             2 typedef unsigned int uint32_t; 
             3 void inet_ntoa(uint32_t in){
             4     char  b[18];
             5     register  char  *p;
             6     p = (char *)&in;
             7 #define UC(b) (((int)b)&0xff) //byte轉(zhuǎn)換為無(wú)符號(hào)int型
             8     sprintf(b, "%d.%d.%d.%d\n", UC(p[0]), UC(p[1]), UC(p[2]), UC(p[3]));
             9     printf(b);
            10 }
            11 int main(void){  
            12     inet_ntoa(0x12345678);
            13     inet_ntoa(0x87654321);
            14     return 0;
            15 }

                 先看如下程序:

            1 int main(void){  
            2     int a = 0x12345678;
            3     char *p = (char *)&a;
            4     char str[20];
            5     sprintf(str,"%d.%d.%d.%d\n", p[0], p[1], p[2], p[3]);
            6     printf(str);
            7     return 0;
            8 }

                 按照小字節(jié)序的規(guī)則,變量a在計(jì)算機(jī)中存儲(chǔ)方式為:

            高地址方向

            0x12

            0x34

            0x56

            0x78

            低地址方向

            p[3]

            p[2]

            p[1]

            p[0]

                 注意,p并不是指向0x12345678的開(kāi)頭0x12,而是指向0x78。p[0]到p[1]的操作是&p[0]+1,因此p[1]地址比p[0]地址大。輸出結(jié)果為120.86.52.18。

                 反過(guò)來(lái)的話,令int a = 0x87654321,則輸出結(jié)果為33.67.101.-121。

                 為什么有負(fù)值呢?因?yàn)橄到y(tǒng)默認(rèn)的char是有符號(hào)的,本來(lái)是0x87也就是135,大于127因此就減去256得到-121。

                 想要得到正值的話只需將char *p = (char *)&a改為unsigned char *p = (unsigned char *)&a即可。

                 綜上不難得出,網(wǎng)宿面試題的答案為120.86.52.18和33.67.101.135。 

             

            久久99精品免费一区二区| 日日狠狠久久偷偷色综合免费| 无码人妻精品一区二区三区久久| 日韩av无码久久精品免费| 久久99国产精品久久久 | 青青青青久久精品国产h久久精品五福影院1421 | 久久久久亚洲av成人无码电影| 99久久这里只精品国产免费| 无码精品久久久久久人妻中字| 久久久久九国产精品| 人妻无码αv中文字幕久久琪琪布| 99久久精品九九亚洲精品| 欧美黑人又粗又大久久久| 久久久国产一区二区三区| 99久久超碰中文字幕伊人 | 久久夜色精品国产噜噜麻豆 | 久久久久久国产精品美女| 久久国产美女免费观看精品| 国产99精品久久| 久久久久久夜精品精品免费啦| 日本精品久久久久久久久免费| 久久天天躁狠狠躁夜夜avapp| 久久精品国产精品亚洲| 四虎国产精品免费久久5151| 久久国产精品无码一区二区三区| 久久夜色精品国产噜噜亚洲a | 国产色综合久久无码有码| 久久这里只有精品首页| 久久性生大片免费观看性| 91精品国产高清久久久久久91| 国产精品久久久久天天影视| 丁香五月网久久综合| 亚洲国产成人久久综合一| 亚洲国产精品久久66| 精品久久久久久无码人妻热| 国产毛片久久久久久国产毛片| 亚洲国产精品婷婷久久| 国产一区二区三精品久久久无广告| 久久久九九有精品国产| 亚洲国产精品久久久久婷婷老年| 精品人妻伦九区久久AAA片69|