新聞中心

EEPW首頁 > 嵌入式系統(tǒng) > 牛人業(yè)話 > 字節(jié)那些事兒

字節(jié)那些事兒

作者: 時間:2016-07-29 來源:網(wǎng)絡(luò) 收藏

  7、 如何控制對齊

本文引用地址:http://m.butianyuan.cn/article/201607/294782.htm

  控制程序的對齊行為是一個與編譯器相關(guān)的工作。以下編譯指示( directive )被許多編譯器認可:

  #pragma pack(n)

  #pragma pack()

  任何處于這兩個編譯指示語句之間的數(shù)據(jù)結(jié)構(gòu),將采用 n 的數(shù)據(jù)對齊方式。 n 是一個可以指定的數(shù)字,取值范圍請參閱所使用編譯器的文檔,通常都會取值為 2 的冪?,F(xiàn)代編譯器在對程序進行編譯時,處于效率方面的考慮,會對數(shù)據(jù)結(jié)構(gòu)的內(nèi)存布局使用一個默認的字節(jié)對齊值,這個值一般都可以在命令行上顯式指定。如果要在一個頭文件 / 源文件中對特定的部分指定對齊屬性,則需要上述的編譯指示。結(jié)束指示的寫法在某些編譯器或者平臺下需要寫成:

  #pragma pack(pop)

  我們用一個例子來看一下這兩個指示的實際效用,看它究竟是如何影響數(shù)據(jù)的內(nèi)存排列的。假定我們有如下的數(shù)據(jù)結(jié)構(gòu)定義:

  struct S1

  {

  int i;

  char c;

  short s;

  };

  struct S2

  {

  char c;

  int i;

  short s;

  };

  這兩個結(jié)構(gòu)的成員看起來是一樣的,只不過換了一下順序而已。我們使用 sizeof() 操作符來測量各自占用多少字節(jié)(除非特別指出,均在 32 位平臺上,并認為 int 占用 4 字節(jié), char 占用 1 字節(jié), short 占用 2 字節(jié))。答案似乎不可思議, sizeof(S1) 的結(jié)果是 8 ,而 sizeof(S2) 卻是 12 。差異是怎么來的呢?原因就在于編譯器缺省的字節(jié)對齊設(shè)定在發(fā)生作用。

  這里需要引入以下概念和規(guī)則:

  概念及規(guī)則一,原生數(shù)據(jù)類型自身對齊值。原生數(shù)據(jù)類型即是 C/C++ 直接支持的數(shù)據(jù)類型,也可以稱為內(nèi)建(built in )數(shù)據(jù)類型。它們的自身對齊值分別為: char 為 1 , short int 為 2 , int 、 float 、 double 等為 4 ,不受符號位(即正負)的影響。

  概念及規(guī)則二,用戶數(shù)據(jù)類型自身對齊值。用戶數(shù)據(jù)類型即由程序員定義的類、結(jié)構(gòu)、聯(lián)合等,也叫抽象數(shù)據(jù)類型( ADT )。它們的自身對齊值等同于為其成員的對齊值中的最大值。

  概念及規(guī)則三,用戶指定對齊值。程序員在編譯器命令行上的指定值,或者在 pragma pack 編譯指示中指定的值,對最終數(shù)據(jù)的影響取就近原則(顯然 pragma pack 指示會覆蓋命令行的指定)。

  概念及規(guī)則四,有效對齊值。取數(shù)據(jù)類型的自身對齊值與用戶指定對齊值中的較小值。此值一旦決出,則會影響到數(shù)據(jù)在內(nèi)存中的布局。一個有效對齊值為 n ,表示以下事實:相關(guān)數(shù)據(jù)在內(nèi)存中存放時,其起始地址的值必須可以被 n 整除 。

  根據(jù)以上四條,可以很圓滿地解釋 S1 和 S2 的大小不同這一現(xiàn)狀。由于沒有使用 pragma pack 指示,那么編譯器(在我的測試環(huán)境下)會采用缺省的對齊值 4 。假設(shè) S1 或者 S2 的實例將從地址 0x0000 處開始。

  在 S1 中,第一個成員 i 的自身對齊值為 4 ,指定對齊值(盡管是缺省的)也是 4 ,同時 0x0000 這一地址符合被 4 整除的要求,因此, i 將占據(jù) 0x0000 到 0x0003 的四個字節(jié),下一個可用地址值為 0x0004 ;接下來的成員c 的數(shù)據(jù)類型為 char ,自身對齊值為 1 ,指定對齊值為 4 ,取較小者仍然是 1 , 0x0004 符合被 1 整除的要求,因此 c 將占據(jù) 0x0004 處的一個字節(jié),下一個可用地址值為 0x0005 ;最后的一個成員 s 數(shù)據(jù)類型為 short ,自身對齊值為 2 ,指定對齊值為 4 ,有效對齊值取 2 ,但是地址 0x0005 不能符合被 2 整除的要求,因此編譯器作相應(yīng)調(diào)整,向后移動到最近的滿足要求的地址處,即 0x0006 , s 將占用 0x0006 和 0x0007 處的兩個字節(jié),由此導(dǎo)致S1 的大小為 8 。

  在地址 0x0005 處的一個字節(jié),習(xí)慣上稱之為填充數(shù)據(jù)( padding )。

  同理可以輕易推出 S2 結(jié)構(gòu)的大小確實是 12 。是這樣嗎?不是的。實際動手的結(jié)果應(yīng)該是 10 。那么 12 應(yīng)該作何解釋?

  我們來設(shè)想一個場景,程序員用 new 或者 malloc 分配一個 S2 的數(shù)組。不用多,假定有兩個元素,而地址0x0000 處正好有空閑的內(nèi)存可以滿足這一內(nèi)存分配請求。我們都知道,在 C/C++ 語言中,數(shù)組的元素是緊鄰排放的。也就是說,后一個元素的起始地址應(yīng)該正好等于前一個元素的起始地址,并加上元素的大小。我們來檢視一下S2 的情況,它的元素大小為 10 ,它的有效對齊值是 4 (請參閱概念及規(guī)則二),這表示任何一個 S2 結(jié)構(gòu)的起始地址都應(yīng)該位于 4 的整數(shù)倍處?,F(xiàn)實的情況是,第一個元素的起始地址是 0x0000 ,第二個元素的起始地址變成了0x000A ,而后者的數(shù)值不能滿足被 4 整除的要求。正是為了解決這一情況,編譯器為 S2 結(jié)構(gòu)在結(jié)尾處也增加了兩個字節(jié)的填充,從而滿足各個條件的限定。

  pragma pack 指示非常有效,使用也比較普遍,但是對于 平臺,它有一些力所不及的地方,我們再來看一個例子。仍然用 S2 ,這一次,我們強制把它的字節(jié)對齊設(shè)定為 1 ,并同時定義了 S2 的一個全局變量 s2 。也即:

  #pragma pack(1)

  struct S2

  {

  char c;

  int i;

  short s;

  } s2;

  #pragma pop()

  然后,在某處具有如下的數(shù)據(jù)訪問:

  int i = s2.i;

  這條看上去稀松平常的語句很可能不能如所希望的那樣執(zhí)行。因為對于 i 的訪問其前提應(yīng)該是 i 的起始地址是 4的倍數(shù)(注意,這個不是對齊規(guī)則的約束結(jié)果,而是 CPU 的數(shù)據(jù)訪問規(guī)則的約束結(jié)果),但強行指定的 1 字節(jié)對齊則導(dǎo)致 i 的起始地址是一個奇數(shù)。

  RVCT 編譯器為此做了特別的努力,引入了 __packed 關(guān)鍵字。此關(guān)鍵字應(yīng)用到用戶定義數(shù)據(jù)結(jié)構(gòu)上會導(dǎo)致該結(jié)構(gòu)的內(nèi)存布局取得與 pragma pack(1) 等同的效果,但是,更進一步地,編譯器會把對該結(jié)構(gòu)中成員的訪問作適當(dāng)?shù)奶幚?,發(fā)現(xiàn)不對齊的訪問則會翻譯為調(diào)用適當(dāng)?shù)谋WC數(shù)據(jù)正確性的函數(shù)。此關(guān)鍵字也可以應(yīng)用到指針上,以保證經(jīng)由指針對目標(biāo)對象的訪問也采用保守方式。可以預(yù)料到的是,此關(guān)鍵字的使用會降低代碼執(zhí)行的效率,所以需要慎用,一個很典型的使用場景是移植其他平臺的代碼時。以下是一些使用了此關(guān)鍵字的定義示例:

  typedef __packed struct

  {

  char x; // 所有成員都會被 __packed 修飾

  int y;

  } X; // 5 字節(jié)的結(jié)構(gòu),自身對齊值 = 1

  int f(X* p)

  {

  return p->y; // 執(zhí)行一個非對齊的讀取操作

  }

  typedef struct

  {

  short x;

  char y;

  __packed int z; // 僅 __pack 本成員,此用法僅適用于整型

  char a;

  } Y; // 8 字節(jié)結(jié)構(gòu),自身對齊值 = 2(請思考原因)

  int g(Y* p)

  {

  return p->z + p->x; // 僅對 z 執(zhí)行非對齊讀取操作

  }

  需要注意的是, GCCE 編譯器沒有實現(xiàn)類似的努力,它有一個和對齊有關(guān)的關(guān)鍵字: __attribute__ (packed)),該關(guān)鍵字的功效與 pragma pack(1) 類似。

  8、 思考 / 練習(xí)題

  a) 位( bit )在字節(jié)中的排列,應(yīng)該也有類似字節(jié)序那樣的問題,為什么沒有?

  b) 自己寫幾個結(jié)構(gòu),根據(jù)規(guī)則推斷其大小,然后寫代碼驗證

  c) 請查閱 RVCT 的相關(guān)文檔,學(xué)習(xí) __align 關(guān)鍵字的含義和用法

  d) 了解微軟公司針對 Windows Mobile 平臺的編譯器是否也具有幫助程序員自動解決對其訪問的機制

  9、 參考資料

  a) 《編程卓越之道》,第一卷

  b) 《 RealView Compilation Tools - Compiler and Libraries Guide 》

  c) Information Center

  d) http://blog.csdn.net/xhfwr/archive/2006/07/23/963793.aspx


上一頁 1 2 下一頁

關(guān)鍵詞: 字節(jié) ARM

評論


相關(guān)推薦

技術(shù)專區(qū)

關(guān)閉