1. 程式人生 > >unix/linux "資料的對齊" "指標的對齊"

unix/linux "資料的對齊" "指標的對齊"

 "資料的的對齊"
    以下內容節選自《Intel Architecture 32 Manual》。
    字,雙字,和四字在自然邊界上不需要在記憶體中對齊。(對字,雙字,和四字來說,自然邊界分別是偶數地址,可以被4整除的地址,和可以被8整除的地址。)
    無論如何,為了提高程式的效能,資料結構(尤其是棧)應該儘可能地在自然邊界上對齊。原因在於,為了訪問未對齊的記憶體,處理器需要作兩次記憶體訪問;然而,對齊的記憶體訪問僅需要一次訪問。
    一個字或雙字運算元跨越了4位元組邊界,或者一個四字運算元跨越了8位元組邊界,被認為是未對齊的,從而需要兩次匯流排週期來訪問記憶體。一個字起始地址是奇
    數但卻沒有跨越字邊界被認為是對齊的,能夠在一個匯流排週期中被訪問。某些操作雙四字的指令需要記憶體運算元在自然邊界上對齊。如果運算元沒有對齊,
    這些指令將會產生一個通用保護異常(#GP)。雙四字的自然邊界是能夠被16整除的地址。其他的操作雙四字的指令允許未對齊的訪問(不會產生通用保護異常),
    然而,需要額外的記憶體匯流排週期來訪問記憶體中未對齊的資料。
a) 以下在hp64機上驗證出資料對齊的結果

   char  ci=1  ci的地址是1 的倍數
   short si=1  si的地址是2 的倍數
   int   ii=1  ii的地址是4 的倍數
   long  li=1  li的地址是8 的倍數
b) 以下是一個記憶體不對齊的例子:
    unsigned char  opc_2[]={0x40};

    op1_1=(unsigned short *)opc_2;    可以給op1_1進行任何地址引用,如果不對齊,在引用值時發生錯誤致命COREDUMP.
    printf("op1_1=%p/n",*op1_1);     
 如果地址剛好是2的倍數,在以上能過執行,否則COREDUMP(在引用時產生printf).

說明:char型的地址因為是1的倍數,所以可能是2的倍數,4的倍數.....他是一個隨機的.
    如果剛好符合程式,如上轉化為

 
“指標的對齊”問題。

CPU一般要求指標的值(記憶體地址)要與它的指向型別資料的尺寸相匹配。例如,2個位元組的資料型別被訪問的地址值為 2 的倍數,
4個位元組的資料型別(如 int)被訪問的地址值是 4 的倍數,等等。一個位元組的資料型別(如 char 型)對其訪問地址無限制(因為是 1 的倍數)。

在Intel處理器上,指標對齊這個問題不是致命的,至多佔用CPU更多的時間進行指標轉換,從而帶來效能的下降;
但是對於其它型別的處理器來說就是致命的了:如果訪問的指標不對齊,會帶來執行錯誤。

當指標從一種型別的指標轉換到另外一種型別的指標的時候,就存在著產生非對齊指標的可能性。不過,正如原文中所說的,
“一個對齊要求較高的指標型別S轉換成一個對齊要求較低的指標型別D是安全的”,這種轉換不會產生非對齊指標;但是,
如果是一個對齊要求較低的指標型別D轉換成一個對齊要求較高的指標型別S,就有可能產生非對齊指標,這種轉換就是不安全。

<<c語言參考手冊>>中6.1.3節中提到,把一個對齊要求較高的指標型別S轉換成一個對齊要求較低的指標型別D是安全的,
安全指的是型別D在用於讀取與儲存D型別物件時可以得到預期效果,後面轉換回原指標型別時能夠恢復原指標.

舉例說明:
1) 較高的指標型別到較低的指標型別轉換
 在unix上,每個int型資料佔4個位元組,在hp系統上,例如,十六進位制表示的整數 0x1a2b3c4d 在記憶體中是這樣存放的:

(高儲存地址)
 Base Address +0 1a 
 Base Address +1 2b
 Base Address +2 3c
 Base Address +3 4d
 (低儲存地址)

 如果有這樣的程式:
 程式碼:
   int a = 0x1a2b3c4d;
   int *p = &a;
   char *q = (char *)p;
   printf("%p/n", *q);  //執行結果:000000000000001a
  
   p=NULL;
   p = (int*)q;
   printf("%p/n", *p);  //執行結果:000000001a2b3c4d
 

 則其中的指標 p 和 q 的值就是 Base Address。q 是char型指標(重要),所以 *q 的結果得到 0x1a不就是我們期待的嗎?
 在這種情況下,不會得到 0x1a 以外的值,所以是也可以說是“安全”的。
 *****重要:搞清楚指標操作受指標“基型別”而不是指標“所指向的物件型別”支配 就沒問題了****

 無論對於自己的程式還是系統來說。程式後面兩句說明,從 q 指標能夠恢復原來的 p 指標,從結果來看也能得到我們預期的值。

1) 較低的指標型別到較高的指標型別轉換
把一個對齊要求較低的指標型別D轉換成一個對齊要求較高的指標型別S是不安全的,得不到預期值。例如,char* 到 int*的轉換:
程式碼:
  char c = 0x1a;
  char* p = &c;
  int* q = (int*)p;

  printf("%p/n", *q);  //執行結果:000000001a000000
  p=NULL;
  p = (char*)q;
  printf("%p/n", *p);  //執行結果:000000000000001a
 

你無法預測(預期)打印出的 *q 的值是什麼,因為我們除了知道整數的一個低位位元組(0x1a)之外,對於這個位元組後面的其餘三個位元組位一無所知,
其值是不確定的。因此,這樣的指標轉換就不是安全的(更嚴重的情況是用 *q 寫資料,會破壞掉 0x1a 後面的三個位元組的資料,給程式帶來錯誤隱患),
其結果也是不能預測的。通過 q 恢復原來的指標 p 沒有問題。

另外說明:
  對於char型陣列可以自然對其
   例如: char[9],地址是8的倍數   可以把它的值賦給LONG型資料。
          char[5],地址是4的倍數
          char[3],地址是2的倍數