1. 程式人生 > >記憶體地址對齊及大小端

記憶體地址對齊及大小端

我們常常看到“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不允許將字或者長字儲存到記憶體中的任意地址. 比如Motorola 68000不允許將16位的字儲存到奇數地址中, 將一個16位的字寫到奇數地址將引發異常.

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

  • 單個位元組(char)能對齊到任意地址
  • 2位元組(short)以2位元組邊界對齊
  • 4位元組(int, long)以4位元組邊界對齊
不同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可就麻煩了.

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

#include <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 在記憶體中的表達為: 0x 00 000001 . (這裡假設int為4位元組). 用char可以擷取一個位元組. LSB為0x01, 若它出現在c的低地址, 則為小端.

method_2

#include <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 = 0x01 02 ;
 
  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的機器進行通訊時, 就有必要區分了