1. 程式人生 > >大小端及記憶體對齊問題細議

大小端及記憶體對齊問題細議

    在接觸PowerPC開發時,難免會碰到大小端轉換的問題,PowerPC系統核心是大端的,而像DMA、DSP、PCIE、FPGA都是小端的,所以有必要把它們詳細記錄一下。我們常常看到“alignment", "endian"之類的字眼, 但很少有C語言教材提到這些概念。實際上它們是與處理器與記憶體介面,編譯器型別密切相關的。考慮這樣一個例子:兩個異構的CPU進行通訊, 定義了這樣一個結果來傳遞訊息:

struct Message
{
     short opcode;
     char subfield;
     long message_length;
     char version;
     short destination_processor;
}message;

    用這樣一個結構來傳遞訊息貌似非常方便, 但也引發了這樣一個問題: 若這兩種不同的CPU對該結構的定義不一樣, 兩者就會對訊息有不同的理解。有可能導致二義性。 會引發二義性的有這兩個方面:

1.記憶體地址對齊

2.大小端定義

    本文先介紹記憶體地址對齊和大小端的概念,再回頭來看這個例子就會豁然開朗了。

    記憶體地址對齊,英文名為" Byte Alignment"。大部分16位和32位的CPU不允許將字或者長字儲存到記憶體中的任意地址。 比如MPC 83xx系列就不允許將16位的字儲存到奇數地址中,將一個16位的字寫到奇數地址將引發異常。

    實際上, 對於c中的位元組組織, 有這樣的對齊規則:   

1) 結構體變數的首地址能夠被其最寬基本型別成員的大小所整除;

2) 結構體每個成員相對於結構首地址的偏移量(offset)都是成員大小的整數倍,如有需要編譯器會在成員之間加上填充位元組(internal adding);

3) 結構體的總大小為結構體最寬基本型別成員大小的整數倍,如有需要編譯器會在最末一個成員之後加上填充位元組(trailing padding)。

    不同CPU的對其規則可能不同, 請參考手冊。為什麼會有上述的限制呢? 理解了記憶體組織, 就會清楚了CPU通過地址匯流排來存取記憶體中的資料,32位的CPU的地址匯流排寬度既為32位置, 標為A[0:31]。在一個匯流排週期內,CPU從記憶體讀/寫32位。 但是CPU只能在能夠被4整除的地址進行記憶體訪問,這是因為32位CPU不使用地址匯流排的A1和A2(比如ARM,它的A[0:1]用於位元組選擇, 用於邏輯控制, 而不和儲存器相連,儲存器連線到A[2:31])。訪問記憶體的最小單位是位元組(byte), A0和A1不使用, 那麼對於地址來說, 最低兩位是無效的,所以它只能識別能被4整除的地址了。 在4位元組中,通過A0和A1確定某一個位元組。

    再看看剛才的message結構, 你想想它佔了多少位元組? 別想當然的以為是10個位元組。 實際上它佔了12個位元組。可以用sizeof(message)看到。 對於結構體,編譯器會針對起中的元素新增"pad"以滿足位元組對齊規則。所以,message會被編譯器改為下面的形式:

struct Message
{
     short opcode;
     char subfield;
     char pad1;     // Pad to start the long word at a 4 
// byte boundary
     long message_length;
     char version;
     char pad2;     // Pad to start a short at a 2 byte boundary
     short destination_processor;
     char pad3[4];  // Pad to align the complete structure to a 16 
     // byte boundary
};

所以,如果不同的編譯器採用不同的對齊規則, 對傳遞message可就麻煩了。

    再看下大端(Big Endian)與小端(Little Endian),Byte Endian是指位元組在記憶體中的組織,所以也稱它為Byte Ordering。對於資料中跨越多個位元組的物件, 我們必須為它建立這樣的約定:

(1) 它的地址是多少?

(2) 它的位元組在記憶體中是如何組織的?

    針對第一個問題,有這樣的解釋:對於跨越多個位元組的物件,一般它所佔的位元組都是連續的, 它的地址等於它所佔位元組最低地址。(連結串列可能是個例外,但連結串列的地址可看作連結串列頭的地址)。

    比如: int x, 它的地址為0x100。 那麼它佔據了記憶體中的Ox100, 0x101, 0x102, 0x103這四個位元組。

    上面只是記憶體位元組組織的一種情況: 多位元組物件在記憶體中的組織有一般有兩種約定。 考慮一個W位的整數。 它的各位表達如下:

 [Xw-1, Xw-2, ... , X1, X0]

它的MSB (Most Significant Byte, 最高有效位元組)為[Xw-1, Xw-2, ... Xw-8]; LSB (Least Significant Byte, 最低有效位元組)為 [X7,X6,..., X0]。 其餘的位元組位於MSB, LSB之間。LSB和MSB誰位於記憶體的最低地址, 即誰代表該物件的地址。這也就引出了大端(Big Endian)與小端(Little Endian)的問題。

    如果LSB在MSB前面, 既LSB是低地址, 則該機器是小端; 反之則是大端。 DEC (Digital Equipment Corporation, 現在是Compaq公司的一部分)和Intel的機器一般採用小端。 IBM, Motorola, Sun的機器一般採用大端。

    當然, 這不代表所有情況。 有的CPU即能工作於小端, 又能工作於大端, 比如ARM, PowerPC,Alpha。具體情形參考處理器手冊。

    舉個例子來說名大小端: 比如一個int x, 地址為0x100, 它的值為0x1234567。 則它所佔據的0x100, 0x101,0x102, 0x103地址組織為低位在前,0x01234567的MSB為0x01, LSB為0x67。0x01在低地址(或理解為"MSB出現在LSB前面,因為這裡討論的地址都是遞增的), 則為大端; 0x67在低地址則為小端。

    認清這樣一個事實: C中的資料型別都是從記憶體的低地址向高地址擴充套件,取址運算"&"都是取低地址。

    下面是兩個測試Bit Endian的小程式:

      method_1

      #i nclude <stdio.h>
      int main(int argc, char *argv[])
      {
          int c = 1;
          if ((*(char *)&c) == 1) 
          {
              printf("little endian\n");
          }
          else
              printf("big endian");
          return 0;
      }

    int c 在記憶體中的表達為: 0x00000001。 (這裡假設int為4位元組)。 用char可以擷取一個位元組。 LSB為0x01,若它出現在c的低地址, 則為小端。

 method_2

      #i nclude <stdio.h>
      int main(void)
      {
          /* Each component to a union type is allocated storage at the
              beginning of the union */
           
          union 
          {
             short n;
             char c[sizeof(short)];
          }un;
          un.n = 0x0102;

          if ((un.c[0] == 1 && un.c[1] == 2))
              printf("big endian\n");
          else if ((un.c[0] == 2 && un.c[1] == 1))
              printf("little endian\n");
          else
              printf("error!\n");
          return 0;
      }

union中元素的起始地址都是相同的——位於聯合的開始。 用char來擷取感興趣的位元組。

區分大端與小端有什麼用呢? 如果兩個不同Endian的機器進行通訊時, 就有必要區分了。