1. 程式人生 > >位元組對齊詳解-----(二)ARM下的對齊處理

位元組對齊詳解-----(二)ARM下的對齊處理

                                                                                   ARM下的對齊處理
from DUI0067D_ADS1_2_CompLib

3.13 type  qulifiers

有部分摘自ARM編譯器文件對齊部分

一.對齊的使用:
  1.__align(num)
   這個用於修改最高級別物件的位元組邊界。在彙編中使用LDRD或者STRD時就要用到此命令__align(8)進行修飾限制。來保證資料物件是相應對齊。這個修飾物件的命令最大是8個位元組限制,可以讓2位元組的物件進行4位元組對齊,但是不能讓4位元組的物件2位元組對齊。
   __align是儲存類修改,他只修飾最高階型別物件不能用於結構或者函式物件。
  
  2.__packed

  __packed是進行一位元組對齊
      1.不能對packed的物件進行對齊
      2.所有物件的讀寫訪問都進行非對齊訪問
      3.float及包含float的結構聯合及未用__packed的物件將不能位元組對齊
      4.__packed對區域性整形變數無影響
      5.強制由unpacked物件向packed物件轉化是未定義,整形指標可以合法定義為packed。
     __packed int* p;  //__packed int 則沒有意義
      6.對齊或非對齊讀寫訪問帶來問題
     __packed struct STRUCT_TEST
 {
  char a;
  int b;
  char c;
 }  ;    //定義如下結構此時b的起始地址一定是不對齊的
         //在棧中訪問b可能有問題,因為棧上資料肯定是對齊訪問[from CL]
//將下面變數定義成全域性靜態不在棧上
static char* p;
static struct STRUCT_TEST a;
void Main()
{
 __packed int* q;  //此時定義成__packed來修飾當前q指向為非對齊的資料地址下面的訪問則可以

 p = (char*)&a;         
 q = (int*)(p+1);     
 
 *q = 0x87654321;
/*  
得到賦值的彙編指令很清楚
ldr      r5,0x20001590 ; = #0x12345678
[0xe1a00005]   mov      r0,r5
[0xeb0000b0]   bl       __rt_uwrite4  //在此處呼叫一個寫4byte的操作函式
     
[0xe5c10000]   strb     r0,[r1,#0]   //函式進行4次strb操作然後返回保證了資料正確的訪問
[0xe1a02420]   mov      r2,r0,lsr #8
[0xe5c12001]   strb     r2,[r1,#1]
[0xe1a02820]   mov      r2,r0,lsr #16
[0xe5c12002]   strb     r2,[r1,#2]
[0xe1a02c20]   mov      r2,r0,lsr #24
[0xe5c12003]   strb     r2,[r1,#3]
[0xe1a0f00e]   mov      pc,r14
*/

/*
如果q沒有加__packed修飾則彙編出來指令是這樣直接會導致奇地址處訪問失敗
[0xe59f2018]   ldr      r2,0x20001594 ; = #0x87654321
[0xe5812000]   str      r2,[r1,#0]
*/

//這樣可以很清楚的看到非對齊訪問是如何產生錯誤的
//以及如何消除非對齊訪問帶來問題
//也可以看到非對齊訪問和對齊訪問的指令差異導致效率問題
}
 


前言:

《***軟體程式設計規範》中提到:\"在定義結構資料型別時,為了提高系統效率,要注意4位元組對齊原則……\"。本文解釋x86上位元組對齊的機制,其他架構讀者可自行試驗。同時,本文對C/C++的函式呼叫方式進行了討論。

先看下面的例子:

struct A{

 char c1;

 int i;

 short s;

 int j;

}a;

struct B{

 int i;

 int j; 

 short s;

 char c1;

}b;

結構A沒有遵守位元組對齊原則(為了區分,我將它叫做對齊宣告原則),結構B遵守了。我們來看看在x86上會出現什麼結果。先打印出a和b的各個成員的地址。會看到a中,各個成員間的間距是4個位元組。b中,i和j,j和s都間距4個位元組,但是s和c1間距2個位元組。所以:

sizeof(a) = 16

sizeof(b) = 12

為什麼會有這樣的結果呢?這就是x86上位元組對齊的作用。為了加快程式執行的速度,一些體系結構以對齊的方式設計,通常以字長作為對齊邊界。對於一些結構體變數,整個結構要對齊在內部成員變數最大的對齊邊界,如B,整個結構以4為對齊邊界,所以sizeof(b)為12,而不是11。

對於A來講,雖然宣告的時候沒有對齊,但是根據打印出的地址來看,編譯器已經自動為其對齊了,所以每個成員的間距是4。在x86下,宣告A與B唯一的差別,僅在於A多浪費了4個位元組記憶體。(是不是某些特定情況下,B比A執行更快,這個還需要討論。比如緊挨的兩條分別取s和c1的指令)

如果體系結構是不對齊的,A中的成員將會一個挨一個儲存,從而sizeof(a)為11。顯然對齊更浪費了空間。那麼為什麼要使用對齊呢?

體系結構的對齊和不對齊,是在時間和空間上的一個權衡。對齊節省了時間。假設一個體繫結構的字長為w,那麼它同時就假設了在這種體系結構上對寬度為w的資料的處理最頻繁也是最重要的。它的設計也是從優先提高對w位資料操作的效率來考慮的。比如說讀寫時,大多數情況下需要讀寫w位資料,那麼資料通道就會是w位。如果所有的資料訪問都以w位對齊,那麼訪問還可以進一步加快,因為需要傳輸的地址位減少,定址可以加快。大多數體系結構都是按照字長來對齊訪問資料的。不對齊的時候,有的會出錯,比如MIPS上會產生bus error,而x86則會進行多次訪問來拼接得到的結果,從而降低執行效率。

有些體系結構是必須要求對齊的,如sparc,MIPS。它們在硬體的設計上就強制性的要求對齊。不是因為它們作不到對齊的訪問,而是它們認為這樣沒有意義。它們追求的是速度。

上面講了體系結構的對齊。在IA-32上面,sizeof(a)為16,就是對齊的結果。下面我們來看,為什麼變數宣告的時候也要儘量對齊。

我們看到,結構A的宣告並不對齊,但是它的成員地址仍是以4為邊界對齊的(成員間距為4)。這是編譯器的功勞。因為我所用的編譯器gcc,預設是對齊的。而x86可以處理不對齊的資料訪問,所以這樣宣告程式並不會出錯。但是對於其他結構,只能訪問對齊的資料,而編譯器又不小心設定了不對齊的選項,則程式碼就不能執行了。如果按照B的方式宣告,則不管編譯器是否設定了對齊選項,都能夠正確的訪問資料。

目前的開發普遍比較重視效能,所以對齊的問題,有三種不同的處理方法:

1)    採用B的方式宣告

2)    對於邏輯上相關的成員變數希望放在靠近的位置,就寫成A的方式。有一種做法是顯式的插入reserved成員:

         struct A{

           char c1;

           char reserved1[3];

           int i;

           short s;

           char reserved2[2];

           int j;

}a;

3)    隨便怎麼寫,一切交給編譯器自動對齊

程式碼中關於對齊的隱患,很多是隱式的。比如在強制型別轉換的時候。下面舉個例子:

unsigned int ui_1=0x12345678;

unsigned char *p=NULL;

unsigned short *us_1=NULL;   

p=&ui_1;

*p=0x00;

us_1=(unsigned short *)(p+1);

*us_1=0x0000;

      最後兩句程式碼,從奇數邊界去訪問unsigned short型變數,顯然不符合對齊的規定。在x86上,類似的操作只會影響效率,但是在MIPS或者sparc上,可能就是一個bus error(我沒有試)。

      有些人喜歡通過移動指標來操作結構中的成員(比如在linux操作struct sk_buff的成員),但是我們看到,A中(&c1+1) 決不等於&i。不過B中(&s+2)就是 &c1了。所以,我們清楚了結構中成員的存放位置,才能編寫無錯的程式碼。同時切記,不管對於結構,陣列,或者普通的變數,在作強制型別轉換時一定要多看看:)不過為了不那麼累,還是遵守宣告對齊原則吧!(這個原則是說變數儘量宣告在它的對齊邊界上,而且在節省空間的基礎上)   

2.C/C++函式呼叫方式

我們當然早就知道,C/C++中的函式呼叫,都是以值傳遞的方式,而不是引數傳遞。那麼,值傳遞是如何實現的呢?

函式呼叫前的典型彙編碼如下:

push   %eax

call   0x401394 <test__Fc>

add    $0x10,%esp

首先,入棧的是實參的地址。由於被調函式都是對地址進行操作,所以就能夠理解值傳遞的原理和引數是引用時的情況了。

Call ***, 是要呼叫函數了,後面的地址,就是函式的入口地址。Call指令等價於:

   PUSH IP

   JMP ***

首先把當前的執行地址IP壓棧,然後跳轉到函式執行。

執行完後,被調函式要返回,就要執行RET指令。RET等價於POP IP,恢復CALL之前的執行地址。所以一旦使用CALL指令,堆疊指標SP就會自動減2,因為IP的值進棧了。

函式的引數進棧的順序是從右到左,這是C與其它語言如pascal的不同之處。函式呼叫都以以下語句開始:

push   %ebp

mov    %esp,%ebp

首先儲存BP的值,然後將當前的堆疊指標傳遞給BP。那麼現在BP+2就是IP的值(16位register的情況),BP+4放第一個引數的值,BP+6放第二個引數……。函式在結束前,要執行POP BP。

C/C++語言預設的函式呼叫方式,都是由主呼叫函式進行引數壓棧並且恢復堆疊,實參的壓棧順序是從右到左,最後由主調函式進行堆疊恢復。由於主呼叫函式管理堆疊,所以可以實現變參函式。

對於WINAPI和CALLBACK函式,在主呼叫函式中負責壓棧,在被呼叫函式中負責彈出堆疊中的引數,並且負責恢復堆疊。因此不能實現變參函式。

(哪位對編譯原理和編譯器比較瞭解的,可以將這個部分寫完善,謝謝。可以加入編譯時的處理。不然只有等偶繼續學習了)位元組對齊或記憶體對齊

Alignment, Pack and Bit Field . . .

3.關於位元組對齊Alignment 的問題現在寫出來與大家分享與討論歡迎指正。

      1. 為什麼要對齊?

       以32位的CPU為例(16 64位同它),一次可以對一個32位的數進行運算,它的資料匯流排的寬度是32位,它從記憶體中一次可以存取的最大數為32位,這個數叫CPU的字word 長。

    在進行硬體設計時將儲存體組織成32位寬,如每個儲存體的寬度是8位可用四塊儲存體與CPU的32位資料匯流排相連,這也是為什麼以前的 386/486計算機插SIMM30記憶體條(8位)時必須同時插四條的原因。

    請參見下圖:

1       8       16      24      32

-------- ------- ------- --------

| long1 | long1 | long1 | long1 |

-------- ------- ------- --------

|         |          |          | long2 |

-------- ------- ------- --------

| long2 | long2 | long2 |          |

-------- ------- ------- --------

| ....

      當一個long型數,(如圖中long1 )在記憶體中的位置正好與記憶體的字邊界對齊時,CPU存取這個數只需訪問一次記憶體,而當一個long型數(如圖中long2 ),在記憶體中的位置跨越字邊界時,CPU存取這個數就需多次訪問記憶體,如 i960cx訪問這樣的數需讀記憶體三次一個BYTE ,一個short ,一個BYTE ,由CPU的微程式碼執行,對軟體透明.所以在對齊方式下CPU的執行效率明顯快多了這就是要對齊的原因.

      一般在編譯器生成程式碼時,都可以根據各種CPU型別將變數進行對齊,包括結構struct 中的變數,變數與變數之間的空間叫padding.有時為了對齊在一個結構的最後也會填入padding ,通常叫tail padding .但在實際的應用中我們確實有不對齊的要求,如在編通訊程式時幀的結構就不能對齊,否則會帶來錯誤及麻煩,所以各編譯器都提供了不對齊的選項.但由於這是ANSI C中未規定的內容所以各廠家的實現都不一樣.

      下面是我們常用編譯器的實現:

      2. 一般編譯器實現對齊的方法

    由於各廠家的實現不一樣,這裡涉及的內容只使用於Visual C++ 4.x ,Borland C++ 5.0 3.1及pRism x86 1.8.7 (C languange) .其他廠家可能略有不同.

    每種基本資料型別都有它的自然對齊方式Natural Alignment, Align的值與該資料型別的大小相等見下表

     Data Type              sizeof         Natural Align

     (signed/unsigned)

     char                          1                     1

     short                        2                      2

     long                          4                      4

     .

     .

     .

      同時使用者還可以指定一個Align值,使用編譯開關或使用#pragma .

     當用戶指定一個Align值 n 或編譯器的預設時,每種資料型別的實際當前Align值定義如下:

           Actual Align = min ( n, Natual Align ) //公式 1

      如當用戶指定Align值為 2 時,char 的實際Align值仍為1 ,short及long的實際Align值為 2 .

        當用戶指定Align值為 1 時,所有型別的實際Align值都為 1.

      複雜資料型別Complex or Aggregate type,包括 array, struct 及union 的對齊值定義如下:

      struct 結構的Align值等於該結構所有成員的 Actual Align 值中最大的一個Align 值,注意成員的Align值是它的實際Align值

      array 陣列的Align值等於該陣列成員的 Actual Align 值.

      union 聯合的Align值等於該聯合最大成員的 Actual Align 值.

      同時當用戶指定一個Align值時上面的公式 1 同樣起作用,只不過Natural Align應理解為當前的Actual Align.

      那麼編譯器是如何根據一個型別的Align值來分配儲存空間主要是在結構中的空間的呢?

    有如下兩個規律

       1). 一個結構成員的offset等於該成員Actual Align值的整數倍,如果湊不成整數倍就在其前加padding.

       2) .一個結構的大小等於該結構Actual Align值的整數倍,如果湊不成整數倍就在其後加padding ,tail padding

       一個結構的大小在其定義時就已確定不會因為其Actual Align值的改變而改變.

      例如有如下兩個結構定義

#pragma pack(8) // 指定Align為 8

struct STest1

{

        char ch1;

        long lo1;

        char ch2;

} test1;

#pragma pack()

現在 Align of STest1 = 4 , sizeof STest1 = 12 ( 4 * 3 )

test1在記憶體中的排列如下 FF 為 padding

00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --

01 FF FF FF 01 01 01 01 01 FF FF FF

ch1 --       lo1 --         ch2

#pragma pack(2) //指定Align為 2

struct STest2

{

       char ch3;

       STest1 test;

} test2;

#pragma pack()

現在 Align of STest1 = 2, Align of STest2 = 2 ,sizeof STest2 = 14 ( 7 * 2 )

test2在記憶體中的排列如下

00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --

02 FF 01 FF FF FF 01 01 01 01 01 FF FF FF

ch3   ch1 --         lo1 --          ch2

從以上可以看出使用者可以在任何需要的地方定義不同的align值

      3. 不同編譯器實現使用者指定align 值的方法:

    因為是 ANSI C中未規定的內容,所以各廠家的方法都不一樣。一般都提供命令列選項及使用#pragma。

    命令列選項對所有被編譯的檔案都起作用#pragma則是ANSI C特別為實現不同的編譯器及平臺特性而規定的前處理器指令Preprocessor 下面主要講一下#pragma的實現:

       Visual C++ VC使用 #pragma pack( [n] ) ,其中 n 可以是 1, 2, 4, 8, 16, 編譯器在遇到一個#pragma pack(n)後就將 n當作當前的使用者指定align值直到另一個#pragma pack(n) 。當遇到一個不帶 n 的 pack時就恢復以前使用的align值。

      Borland C++ BC使用 #pragma option -an ,在 BC 5.0 的Online Help中沒有發現對#pragma pack的支援但發現在其系統標頭檔案中使用的都是#pragma pack。

      pRism x86 使用 #pragma pack( [n] ) ,但奇怪的是 C 檔案與 C++檔案生成的程式碼不一樣,有待進一步研究。

      gcc960 使用 #pragma pack n 及 #pragma align n 。兩個開關的意義不一樣,並且相互作用比較複雜。但同時使用 #pragma pack 1 及#pragma align 1 可以實現與Visual C++中 #pragma pack(1) 一樣的功能。

     其他編譯器的方法各不相同,可參見手冊。如果要使用不同的編譯器編譯軟體時,就要針對不同的編譯器使用不同的前處理器指令。

       4. 使用 #pragma pack 或其他開關需注意的問題

     1). 為了保證執行速度儘量不使用#pragma pack

     2). 不同的編譯器生成的程式碼極有可能不同一定要檢視相應手冊並做實驗

     3). 需要加pack的地方一定要在定義結構的標頭檔案中加,不要依賴命令列選項。因為如果很多人使用該標頭檔案,並不是每個人都知道應該pack 。特別是為別人開發庫檔案時,如果一個庫函式使用了struct作為其引數,當呼叫者與庫檔案開發者使用不同的pack時,就會造成錯誤,而且該類錯誤很不好查。在VC及BC提供的標頭檔案中除了能正好對齊在四位元組上的結構外都加了pack ,否則我們編的Windows程式哪一個也不會正常執行。

    4). 在 #pragma pack(n)後一定不要include其他標頭檔案,若包含的標頭檔案中改變了align值將產生非預期結果。

VC中提供了一種安全使用pack的方法

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

#pragma pack( push, n)

       將當前的align值壓入編譯器的一個內部堆疊並使用 ,n作為當前的align值,而#pragma pack(pop)則將內部堆疊中的棧頂值作為當前的align值,這樣就保證了巢狀pack時的正確。

    5).不要多人同時定義一個數據結構。在多人合作開發一個軟體模組時,為了保持自己的程式設計風格,每個人都要對同一結構定義一份符合自己風格的資料型別,當兩個人之間需要傳遞該資料結構時,如果兩個人的 pack值不一樣就會產生錯誤,該類錯誤也很難查,所以為了安全起見我們還是捨棄一些自己的風格吧。

      5. 關於位域 Bit Field

      在 ANSI C 中規定位域的型別只能為 signed/unsigned int ,但各廠家都對其進行了擴充套件型別可以是 char, short, long等但其最大長度不能超過int的長度,即32位平臺時為32位,16位平臺時為16位。位域儲存空間的分配也與各編譯器的實現有關,而且與Little Endian(x86,i960),BigEndian(680x0,PowerPc)有關。所以在定義位域時要對不同的編譯器進行不同的支援,如在VC中規定如果兩個連續位域的型別不一樣或位域的長度為零,編譯器將進行對齊。在VC中是這樣,其他編譯器就可能不是這樣。這屬於各廠家不同的實現問題。ANSI

C 中沒有進行規定所以如果涉及到位域問題一定要檢視手冊。

      6. 附例

      以下結果均在VC++4.x ,BC++5.0,3.1 pRism x86 1.8.7(C Language)進行過驗證。其中因為BC++ 3.1 是16位的所以只有pack(1),pack(2)有效。

例中定義瞭如下幾個結構

typedef struct tagSLong

{

char chMem1;

char chMem2;

char chMem3;

unsigned short wMem4;

unsigned long dwMem5;

unsigned short wMem6;

char chMem7;

}SLong;

typedef struct tagSShort

{

char chMem1;

unsigned short wMem2;

char chMem3;

}SShort;

typedef union tagun

{

char uChar;

unsigned short uWord;

}un;

typedef struct tagComplex

{

char chItem1;

SLong struItem2;

unsigned long dwItem3;

char chItem4;

un unItem5;

}Complex;

測試時對每個結構的成員按 1 2 3 ... 依次進行賦值FF 為Padding

下面列出了每個結構的size Align的大小及其空間分配

1. Now the Align(Pack) size is 8

sizeof(SLong) = 16 Alignment of (SLong) = 4

00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --

01 02 03 FF 04 00 FF FF 05 00 00 00 06 00 07 FF

sizeof(SShort) = 6 Alignment of (SShort) = 2

00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --

01 FF 02 00 03 FF

sizeof(Complex) = 28 Alignment of (Complex) = 4

[Notice the alignment of (SLong) = 4 and (un)=2 ]

00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --

01 FF FF FF 01 02 03 FF 04 00 FF FF 05 00 00 00

06 00 07 FF 08 00 00 00 09 FF 0A 00

sizeof(SLong[2]) = 32 Alignment of (SLong[2]) = 4

[Notice the alignment of (SLong) = 4 ]

00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --

01 02 03 FF 04 00 FF FF 05 00 00 00 06 00 07 FF

01 02 03 FF 04 00 FF FF 05 00 00 00 06 00 07 FF

sizeof(un) = 2 Alignment of (un) = 2

00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --

02 00

2. Now the Align(Pack) size is 4

sizeof(SLong) = 16 Alignment of (SLong) = 4

00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --

01 02 03 FF 04 00 FF FF 05 00 00 00 06 00 07 FF

sizeof(SShort) = 6 Alignment of (SShort) = 2

00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --

01 FF 02 00 03 FF

sizeof(Complex) = 28 Alignment of (Complex) = 4

00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --

01 FF FF FF 01 02 03 FF 04 00 FF FF 05 00 00 00

06 00 07 FF 08 00 00 00 09 FF 0A 00

sizeof(SLong[2]) = 32 Alignment of (SLong[2]) = 4

00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --

01 02 03 FF 04 00 FF FF 05 00 00 00 06 00 07 FF

01 02 03 FF 04 00 FF FF 05 00 00 00 06 00 07 FF

sizeof(un) = 2 Alignment of (un) = 2

00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --

02 00

3. Now the Align(Pack) size is 2

sizeof(SLong) = 14 Alignment of (SLong) = 2

00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --

01 02 03 FF 04 00 05 00 00 00 06 00 07 FF

sizeof(SShort) = 6 Alignment of (SShort) = 2

00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --

01 FF 02 00 03 FF

sizeof(Complex) = 24 Alignment of (Complex) = 2

[Notice the alignment of (SLong) = 2 and (un) = 2

00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --

01 FF 01 02 03 FF 04 00 05 00 00 00 06 00 07 FF

08 00 00 00 09 FF 0A 00

sizeof(SLong[2]) = 28 Alignment of (SLong[2]) = 2

00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --

01 02 03 FF 04 00 05 00 00 00 06 00 07 FF 01 02

03 FF 04 00 05 00 00 00 06 00 07 FF

sizeof(un) = 2 Alignment of (un) = 2

00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --

02 00

4. Now the Align(Pack) size is 1

sizeof(SLong) = 12 Alignment of (SLong) = 1

00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --

01 02 03 04 00 05 00 00 00 06 00 07

sizeof(SShort) = 4 Alignment of (SShort) = 1

00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --

01 02 00 03

sizeof(Complex) = 20 Alignment of (Complex) = 1

[Notice the alignment of (SLong) = 1 and (un) = 1]

00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --

01 01 02 03 04 00 05 00 00 00 06 00 07 08 00 00

00 09 0A 00

sizeof(SLong[2]) = 24 Alignment of (SLong[2]) = 1

[Notice the alignment of (SLong) = 1 ]

00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --

01 02 03 04 00 05 00 00 00 06 00 07 01 02 03 04

00 05 00 00 00 06 00 07

sizeof(un) = 2 Alignment of (un) = 1

00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --

02 00

關於pack 使用的幾點建議及需注意的問題

0.由於各種編譯器對pack的實現各不相同建議定義幾個標頭檔案(參照microsoft的做法)

poppack.h, pshpack1.h, pshpack2.h ...

其中poppack.h用於恢復編譯器的預設pack值其大略定義如下

#if _HA_WIN32 //for Visual C++

#pragma pack()

#elif _HA_GNU //for gcc960

#pragma pack

#pragma align 0

#elif ... for any more

pshpack1(n).h用於指定編譯器的pack值為 n , 其大略定義如下

#if _HA_WIN32 //for Visual C++

#pragma pack(1)//n

#elif _HA_GNU //for gcc960

#pragma pack 1//n

#pragma align 1//n

#elif ... //for any more

使用時在需要pack的地方加上 #include \"pshpack1.h\" ,

在需要恢復pack的地方加上 #include \"poppack.h\" .

使用這種標頭檔案的方式有如下幾點好處:

    (1). 在需要pack的標頭檔案中不需要再對不同的編譯器做處理,使得該標頭檔案比較整潔.

    (2). 便於維護.當需要增加對其他編譯器的支援或對現有pack指令進行修改時,只需修改poppack.h等幾個標頭檔案.

1. 為了保證執行速度在沒有必要的地方不要使用#pragma pack 不要只為了節省空間而使用BYTE等型別,其實資料的空間是減少了但程式碼的空間卻變大了,如本來只需一條指令的地方可能需三四條指令,既影響了執行速度又增加了空間,得不償失。如果必須使用BYTE等型別,儘可能將其在結構中排成自然對齊。

2.不同的編譯器生成的程式碼極有可能不同,一定要檢視相應手冊並做實驗。

如對於如下結構定義

struct SLanDest

{

WORD wTag;

MACADDR addr;

};

該結構在VC下是不加padding的但在pRism下就加了padding

3.需要加pack的地方一定要在定義結構的標頭檔案中加,不要依賴命令列選項。因為如果很多人使用該標頭檔案,並不是每個人都知道應該pack 特別是為別人開發庫檔案時如果一個庫函式使用了struct作為其引數,當呼叫者與庫檔案開發者使用不同的pack時,就會造成錯誤,而且該類錯誤很不好查。在VC及BC提供的標頭檔案中除了能正好對齊在四位元組上的結構外都加了pack, 否則我們編的Windows程式哪一個也不會正常執行。

4. 在 #include \"pshpack1.h\"後一定不要include其他標頭檔案,若包含的標頭檔案中改變了align值,如包含了#include \"poppack.h\" 將產生非預期結果。

5.不要多人同時定義一個數據結構,在多人合作開發一個軟體模組時,為了保持自己的程式設計風格,每個人都要對同一結構定義一份符合自己風格的資料型別。當兩個人之間需要傳遞該資料結構時如果兩個人的 pack值不一樣就會產生錯誤,該類錯誤也很難查。所以為了安全起見我們還是捨棄一些自己的風格吧。

6. 何時需要加pack?

        在編寫通訊協議時通訊協議的幀結構對於所有跨CPU的協議都應理解為通訊協議。如郵箱通訊主機與主機通過通訊線路進行通訊等編寫硬體驅動程式時,暫存器的結構這兩個地方都需要加pack1 即使看起來本來就自然對齊的也要加pack 以免不同的編譯器生成的程式碼不一樣。如 2.中的例子對於執行時只與一個CPU有關的結構為了提高執行速度請不要加pack。

4 .VC中位元組對齊問題

      1、 sizeof應用在結構上的情況

   請看下面的結構:

   struct MyStruct

   {

   double dda1;

   char dda;

   int type

   };

   對結構MyStruct採用sizeof會出現什麼結果呢?sizeof(MyStruct)為多少呢?也許你會這樣求:

   sizeof(MyStruct)=sizeof(double)+sizeof(char)+sizeof(int)=13

   但是當在VC中測試上面結構的大小時,你會發現sizeof(MyStruct)為16。你知道為什麼在VC中會得出這樣一個結果嗎?

   其實,這是VC對變數儲存的一個特殊處理。為了提高CPU的儲存速度,VC對一些變數的起始地址做了\"對齊\"處理。在預設情況下,VC規定各成員變數存放的起始地址相對於結構的起始地址的偏移量必須為該變數的型別所佔用的位元組數的倍數。下面列出常用型別的對齊方式(vc6.0,32位系統)。

   型別

   對齊方式(變數存放的起始地址相對於結構的起始地址的偏移量)

   Char    偏移量必須為sizeof(char)即1的倍數

   int        偏移量必須為sizeof(int)即4的倍數

   float     偏移量必須為sizeof(float)即4的倍數

   double 偏移量必須為sizeof(double)即8的倍數

   Short   偏移量必須為sizeof(short)即2的倍數

   各成員變數在存放的時候根據在結構中出現的順序依次申請空間,同時按照上面的對齊方式調整位置,空缺的位元組VC會自動填充。同時VC為了確保結構的大小為結構的位元組邊界數(即該結構中佔用最大空間的型別所佔用的位元組數)的倍數,所以在為最後一個成員變數申請空間後,還會根據需要自動填充空缺的位元組。

   下面用前面的例子來說明VC到底怎麼樣來存放結構的。

   struct MyStruct

   {

   double dda1;

   char dda;

   int type

   };

   為上面的結構分配空間的時候,VC根據成員變量出現的順序和對齊方式,先為第一個成員dda1分配空間,其起始地址跟結構的起始地址相同(剛好偏移量0剛好為sizeof(double)的倍數),該成員變數佔用sizeof(double)=8個位元組;接下來為第二個成員dda分配空間,這時下一個可以分配的地址對於結構的起始地址的偏移量為8,是sizeof(char)的倍數,所以把dda存放在偏移量為8的地方滿足對齊方式,該成員變數佔用 sizeof(char)=1個位元組;接下來為第三個成員type分配空間,這時下一個可以分配的地址對於結構的起始地址的偏移量為9,不是sizeof (int)=4的倍數,為了滿足對齊方式對偏移量的約束問題,VC自動填充3個位元組(這三個位元組沒有放什麼東西),這時下一個可以分配的地址對於結構的起始地址的偏移量為12,剛好是sizeof(int)=4的倍數,所以把type存放在偏移量為12的地方,該成員變數佔用sizeof(int)=4個位元組;這時整個結構的成員變數已經都分配了空間,總的佔用的空間大小為:8+1+3+4=16,剛好為結構的位元組邊界數(即結構中佔用最大空間的型別所佔用的位元組數sizeof(double)=8)的倍數,所以沒有空缺的位元組需要填充。所以整個結構的大小為:sizeof(MyStruct)=8+1+ 3+4=16,其中有3個位元組是VC自動填充的,沒有放任何有意義的東西。

   下面再舉個例子,交換一下上面的MyStruct的成員變數的位置,使它變成下面的情況:

   struct MyStruct

   {

   char dda;

   double dda1;

   int type

   };

   這個結構佔用的空間為多大呢?在VC6.0環境下,可以得到sizeof(MyStruc)為24。結合上面提到的分配空間的一些原則,分析下VC怎麼樣為上面的結構分配空間的。(簡單說明)

   struct MyStruct

   {

    char dda;//偏移量為0,滿足對齊方式,dda佔用1個位元組;

   double dda1;//下一個可用的地址的偏移量為1,不是sizeof(double)=8

                       //的倍數,需要補足7個位元組才能使偏移量變為8(滿足對齊

                       //方式),因此VC自動填充7個位元組,dda1存放在偏移量為8

                       //的地址上,它佔用8個位元組。

   int type;    //下一個可用的地址的偏移量為16,是sizeof(int)=4的倍

                      //數,滿足int的對齊方式,所以不需要VC自動填充,type存

                      //放在偏移量為16的地址上,它佔用4個位元組。

   };//所有成員變數都分配了空間,空間總的大小為1+7+8+4=20,不是結構

       //的節邊界數(即結構中佔用最大空間的型別所佔用的位元組數sizeof

       //(double)=8)的倍數,所以需要填充4個位元組,以滿足結構的大小為

       //sizeof(double)=8的倍數。

   所以該結構總的大小為:sizeof(MyStruc)為1+7+8+4+4=24。其中總的有7+4=11個位元組是VC自動填充的,沒有放任何有意義的東西。

   VC對結構的儲存的特殊處理確實提高CPU儲存變數的速度,但是有時候也帶來了一些麻煩,我們也遮蔽掉變數預設的對齊方式,自己可以設定變數的對齊方式。

   VC 中提供了#pragma pack(n)來設定變數以n位元組對齊方式。n位元組對齊就是說變數存放的起始地址的偏移量有兩種情況:第一、如果n大於等於該變數所佔用的位元組數,那麼偏移量必須滿足預設的對齊方式,第二、如果n小於該變數的型別所佔用的位元組數,那麼偏移量為n的倍數,不用滿足預設的對齊方式。結構的總大小也有個約束條件,分下面兩種情況:如果n大於所有成員變數型別所佔用的位元組數,那麼結構的總大小必須為佔用空間最大的變數佔用的空間數的倍數;    

   否則必須為n的倍數。下面舉例說明其用法。   

   #pragma pack(push) //儲存對齊狀態 

   #pragma pack(4)//設定為4位元組對齊   

   struct test    

   { 

    char m1;   

   double m4;    

    int m3;    

   };    

   #pragma pack(pop)//恢復對齊狀態    

       以上結構的大小為16,下面分析其儲存情況,首先為m1分配空間,其偏移量為0,滿足我們自己設定的對齊方式(4位元組對齊),m1佔用1個位元組。接著開始為 m4分配空間,這時其偏移量為1,需要補足3個位元組,這樣使偏移量滿足為n=4的倍數(因為sizeof(double)大於n),m4佔用8個位元組。接著為m3分配空間,這時其偏移量為12,滿足為4的倍數,m3佔用4個位元組。這時已經為所有成員變數分配了空間,共分配了16個位元組,滿足為n的倍數。如果把上面的#pragma pack(4)改為#pragma pack(16),那麼我們可以得到結構的大小為24。(請讀者自己分析)    

2、 sizeof用法總結    

   在VC中,sizeof有著許多的用法,而且很容易引起一些錯誤。下面根據sizeof後面的引數對sizeof的用法做個總結。   

   A. 引數為資料型別或者為一般變數。例如sizeof(int),sizeof(long)等等。這種情況要注意的是不同系統系統或者不同編譯器得到的結果可能是不同的。例如int型別在16位系統中佔2個位元組,在32位系統中佔4個位元組。

   B. 引數為陣列或指標。下面舉例說明.

   int a[50]; //sizeof(a)=4*50=200; 求陣列所佔的空間大小

   int *a=new int[50];// sizeof(a)=4; a為一個指標,sizeof(a)是求指標

                               //的大小,在32位系統中,當然是佔4個位元組。

   C. 引數為結構或類。Sizeof應用在類和結構的處理情況是相同的。但有兩點需要注意,第一、結構或者類中的靜態成員不對結構或者類的大小產生影響,因為靜態變數的儲存位置與結構或者類的例項地址無關。   第二、沒有成員變數的結構或類的大小為1,因為必須保證結構或類的每一 個例項在記憶體中都有唯一的地址。

   下面舉例說明,

   Class Test{int a;static double c};//sizeof(Test)=4.

   Test *s;//sizeof(s)=4,s為一個指標。

   Class test1{ };//sizeof(test1)=1;

   D. 引數為其他。下面舉例說明。

    int func(char s[5]);

    {

    cout<     //數的引數在傳遞的時候系統處理為一個指標,所

                  //以sizeof(s)實際上為求指標的大小。

    return 1;

   }

   sizeof(func(\"1234\"))=4//因為func的返回型別為int,所以相當於

                                      //求sizeof(int).

   以上為sizeof的基本用法,在實際的使用中要注意分析VC的分配變數的分配策略,這樣的話可以避免一些錯誤