1. 程式人生 > >將32位程式碼向64位平臺移植的注意事項

將32位程式碼向64位平臺移植的注意事項

隨著低成本64位平臺的來臨,加上記憶體和硬碟價格的不斷下跌,無疑為32位程式向64位硬體的移植又加了一把勁,那些科學運算、資料庫、消耗大量記憶體或密集浮點運算的程式也搭上了這一順風車。在本文中,主要討論向64位平臺移植現有32位程式碼時,應注意的一些細小問題。

  新近的64位平臺在二進位制上與32位應用程式相容,這意味著可以非常簡單地移植現有的程式。許多目前在32位平臺上執行良好的程式也許不必移植,除非程式有以下要求:

  ·需要多於4GB的記憶體。

  ·使用的檔案大小常大於2GB。

  ·密集浮點運算,需要利用64位架構的優勢。

  ·能從64位平臺的優化數學庫中受益。

  否則,只需簡單地重新編譯一下,就已經足夠了。大多數編寫良好的程式不費吹灰之力就可移植到64位平臺之上,在此假定你的程式編寫良好,並熟悉本文將要討論的問題。 

  ILP32和LP64資料模型


  32位環境涉及"ILP32"資料模型,是因為C資料型別為32位的int、long、指標。而64位環境使用不同的資料模型,此時的long和指標已為64位,故稱作"LP64"資料模型。

  現今所有64位的類Unix平臺均使用LP64資料模型,而64位Windows使用LLP64資料模型,除了指標是64位,其他基本型別都沒有變。我們在此主要探討ILP32到LP64的移植問題,表1顯示了ILP32與LP64資料模型的差異。


  向64位移植程式碼時的所有問題差不多都可以總結出一個簡單的規律:千萬不要認為int、long、指標的長度一樣。任何違反這條規律的程式碼,當執行在LP64資料模型下時,都會出現不同的問題,而且很難找出原因所在。例1中有許多違反這條規律的地方,其在移植到64位平臺上時都需要重寫。

  例1:

1 int *myfunc(int i)
2 {
3  return(&i);
4 }
5
6 int main(void)
7 {
8  int myint;
9  long mylong;
10 int *myptr;
11
12  char *name = (char * ) getlogin();
13
14  printf("Enter a number %s: ", name);
15  (void) scanf("%d", &mylong);
16  myint = mylong;
17  myptr = myfunc(mylong);
18  printf("mylong: %d pointer: %x \n", mylong, myptr);
19  myint = (int)mylong;
20  exit(0);
21
22 }

  第一步是要求編譯器捕捉到移植時的問題,因所用編譯器的不同,選項可能也有所不同,但對IBM XL編譯器系列,可用的選項有-qwarn64 -qinfo=pro,為了得到64位可執行檔案,可使用選項-q64(如果使用GCC,選項應為-m64,表2中列出了其他可用的GCC選項)。圖1是編譯例1中程式碼時的情況。

缺少原型的截斷

  如果一個函式被呼叫時沒有指定函式原型,返回值將是32位的int。不使用原型的程式碼可能會發生意料之外的資料截斷,由此導致一個分割錯誤。編譯器捕捉到了例1中第12行的這個錯誤。

  char *name = (char *) getlogin();

  編譯器假定函式返回一個int值,並截短結果指標。這行程式碼在ILP32資料模型下工作正常,因為此時的int和指標是同樣長度,換到LP64模型中,就不一定正確了,甚至於型別轉換都不能避免這個錯誤,因為getlogin()在返回之後已經被截斷了。

  要修正這個問題,需包括標頭檔案<unistd.h>,其中有getlogin()的函式原型。

  格式指定符

  如果對64位long、指標使用了32位格式指定符,將導致程式錯誤。編譯器捕捉到了例1中第15行的這個錯誤。

(void) scanf("%d", &mylong);

  注意,scanf將向變數mylong中插入一個32位的值,而剩下的4位元組就不管了。要修正這個問題,請在scanf中使用%ld指定符。

  第18行也演示了在printf中的一個類似的問題:

  printf("mylong: %d pointer: %x \n", mylong, myptr);

  要修正此處的錯誤,mylong應使用%ld,對myptr使用 %p而不是%x。

  賦值截斷

  有關編譯器發現賦值截斷的一個例子在第16行中:

  myint = mylong;

  這在ILP32模型下不會有任何問題,因為此時的int、long都是32位,而在LP64中,當把mylong賦值給myint時,如果數值大於32位整數的最大值時,數值將被截短。

  被截斷的引數

  編譯器發現的下一個錯誤在第17行中,雖然myfunc函式只接受一個int引數,但呼叫時卻用了一個long,引數在傳遞時會悄無聲息地被截斷。

  轉換截斷

  轉換截斷髮生在把long轉換成int時,比如說例1中的第19行:

myint = (int) mylong;

  導致轉換截斷的原因是int與long非同樣長度。這些型別的轉換通常在程式碼中以如下形式出現:

int length = (int) strlen(str);

  strlen返回size_t(它在LP64中是unsigned long),當賦值給一個int時,截斷是必然發生的。而通常,截斷只會在str的長度大於2GB時才會發生,這種情況在程式中一般不會出現。雖然如此,也應該儘量使用適當的多型型別(如size_t、uintptr_t等等),而不要去管它最下面的基型別是什麼。

  一些其他的細小問題

  編譯器可捕捉到移植方面的各種問題,但不能總指望編譯器為你找出一切錯誤。

  那些以十六進位制或二進位制表示的常量,通常都是32位的。例如,無符號32位常量0xFFFFFFFF通常用來測試是否為-1:

#define INVALID_POINTER_VALUE 0xFFFFFFFF

  然而,在64位系統中,這個值不是-1,而是4294967295;在64位系統中,-1正確的值應為0xFFFFFFFFFFFFFFFF。要避免這個問題,在宣告常量時,使用const,並且帶上signed或unsigned。

const signed int INVALID_POINTER_VALUE = 0xFFFFFFFF;

  這行程式碼將會在32位和64位系統上都執行正常。

  其他有關於對常量硬編碼的問題,都是基於對ILP32資料模型的不當認識,如下:

int **p; p = (int**)malloc(4 * NO_ELEMENTS);

  這行程式碼假定指標的長度為4位元組,而這在LP64中是不正確的,此時是8位元組。正確的方法應使用sizeof():

int **p; p = (int**)malloc( sizeof(*p) * NO_ELEMENTS);

  注意對sizeof()的不正確用法,例如:

sizeof(int) = = sizeof(int *);

  這在LP64中是錯誤的。

  符號擴充套件

  要避免有符號數與無符號數的算術運算。在把int與long數值作對比時,此時產生的資料提升在LP64和ILP32中是有差異的。因為是符號位擴充套件,所以這個問題很難被發現,只有保證兩端的運算元均為signed或均為unsigned,才能從根本上防止此問題的發生。

  例2:

long k;
int i = -2;
unsigned int j = 1;
k = i + j;

printf("Answer: %ld\n", k);

  你無法期望例2中的答案是-1,然而,當你在LP64環境中編譯此程式時,答案會是4294967295。原因在於表示式(i+j)是一個unsigned int表示式,但把它賦值給k時,符號位沒有被擴充套件。要解決這個問題,兩端的運算元只要均為signed或均為unsigned就可。像如下所示:

k = i + (int) j
聯合體問題(Union)

  當聯合本中混有不同長度的資料型別時,可能會導致問題。如例3是一個常見的開原始碼包,可在ILP32卻不可在LP64環境下執行。程式碼假定長度為2的unsigned short陣列,佔用了與long同樣的空間,可這在LP64平臺上卻不正確。

  例3:

typedef struct {
 unsigned short bom;
 unsigned short cnt;
 union {
  unsigned long bytes;
  unsigned short len[2];
 } size;
} _ucheader_t;

  要在LP64上執行,程式碼中的unsigned long應改為unsigned int。要在所有程式碼中仔細檢查聯合體,以確認所有的資料成員在LP64中都為同等長度。

  位元組序問題(Endian)

  因64位平臺的差異,在移植32位程式時,可能會失敗,原因可歸咎於機器上位元組序的不同。Intel、IBM PC等CISC晶片使用的是Little-endian,而Apple之類的RISC晶片使用的是Big-endian;小尾位元組序(Little-endian)通常會隱藏移植過程中的截斷bug。

  例4:

long k;
int *ptr;

int main(void)
{
 k = 2 ;
 ptr = &k;
 printf("k has the value %ld, value pointed to by ptr is %ld\n", k, *ptr);
 return 0;
}

  例4是一個有此問題的明顯例子,一個宣告指向int的指標,卻不經意間指向了long。在ILP32上,這段程式碼打印出2,因為int與long長度一樣。但到了LP64上,因為int與long的長度不一,而導致指標被截斷。不管怎麼說,在小尾位元組序的系統中,程式碼依舊會給出k的正確答案2,但在大尾位元組序(Big-endian)系統中,k的值卻是0。



  表3說明了為什麼在不同的位元組序系統中,會因截斷問題而產生不同的答案。在小尾位元組序中,被截斷的高位地址中全為0,所以答案仍為2;而在大尾位元組序中,被截斷的高位地址中包含值2,這樣就導致結果為0,所以在兩種情況下,截斷都是一種bug。但要意識到,小尾位元組序會隱藏小數值的截斷錯誤,而這個錯誤只有在移植到大尾位元組序系統上時才可能被發現。
移植到64位平臺之後的效能降低

  當代碼移植到64位平臺之後,也許發現效能實際上降低了。原因與在LP64中的指標長度和資料大小有關,並由此引發的快取命中率降低、資料結構膨脹、資料對齊等問題。 

  由於64位環境中指標所佔用的位元組更大,致使原來執行良好的32位程式碼出現不同程度的快取問題,具體表現為執行效率降低。可使用工具來分析快取命中率的變化,以確認效能降低是否由此引起。

  在遷移到LP64之後,資料結構的大小可能會改變,此時程式可能會需要更多的記憶體和磁碟空間。例如,圖2中的結構在ILP32中只需要16位元組,但在LP64中,卻需要32位元組,整整增長了100%。這緣於此時的long已是64位,編譯器為了對齊需要而加入了額外的填充資料。


  通過改變結構中資料排列的先後順序,能將此問題所帶來的影響降到最小,並能減少所需的儲存空間。如果把兩個32位int值放在一起,會因為少了填充資料,儲存空間也隨之減少,現在儲存整個結構只需要24位元組。

  在重排資料結構之前,在根據資料使用的頻度仔細衡量,以免因降低快取命中率而帶來效能上的損失。

  如何生成64位程式碼

  在一些情況中,32位和64位程式在原始碼級別的介面上很難區分。不少標頭檔案中,都是通過一些測試巨集來區分它們,不幸的是,這些特定的巨集依賴於特定的平臺、特定的編譯器或特定的編譯器版本。舉例來說,GCC 3.4或之後的版本都定義了__LP64__,以便為所有的64位平臺 通過選項-m64編譯產生64位程式碼。然而,GCC 3.4之前的版本卻是特定於平臺和作業系統的。 

  也許你的編譯器使用了不同於__LP64__的巨集,例如IBM XL的編譯器當用-q64編譯程式時,使用了__64bit__巨集,而另一些平臺使用_LP64,具體情況可用__WORDSIZE來測試一下。請檢視相關編譯器文件,以便找出最適合的巨集。例5可適用於多種平臺和編譯器:

  例5:

#if defined (__LP64__) || defined (__64BIT__) || defined (_LP64) || (__WORDSIZE == 64)
printf("I am LP64\n");
#else
printf("I am ILP32 \n");
#endif

  共享資料

  在移植到64位平臺時的一個典型問題是,如何在32位和64位程式之間讀取和共享資料。例如一個32位程式可能把結構體作為二進位制檔案儲存在磁碟上,現在你要在64位程式碼中讀取這些檔案,很可能會因LP64環境中結構大小的不同而導致問題。

  對那些必須同時執行在32位和64位平臺上的新程式而言,建議不要使用可能會因LP64和ILP32而改變長度的資料型別(如long),如果實在要用,可使用標頭檔案<inttypes.h>中的定寬整數,這樣不管是通過檔案還是網路,都可在32位和64位的二進位制層面共享資料。

  例6:

#include <stdio.h>
#include <inttypes.h>

struct on_disk
{
 /* ILP32|LP64共享時,這個應該使用int32_t */
 long foo;
};
int main()
{
 FILE *file;
 struct on_disk data;
 #ifdef WRITE
  file=fopen("test","w");
  data.foo = 65535;
  fwrite(&data, sizeof(struct on_disk), 1, file);
 #else
  file = fopen("test","r");
  fread(&data, sizeof(struct on_disk), 1, file);
  printf("data: %ld\n", data.foo);
 #endif
 fclose(file);
}

  來看一下例6,在理想的情況下,這個程式在32位和64位平臺上都可正常執行,並且可以讀取對方的資料。但實際上卻不行,因為long在ILP32和LP64之中長度會變化。結構on_disk裡的變數foo應該宣告為int32_t,這個定寬型別可保證在當前ILP32或移植到的LP64資料模型下,都生成相同大小的資料。

  混合Fortran和C的問題

  許多科學運算程式從C/C++中呼叫Fortran的功能,Fortran從它本身來說並不存在移植到64位平臺的問題,因為Fortran的資料型別有明確的位元大小。然而,如果混合Fortran和C語言,問題就來了,如下:例7中C語言程式呼叫例8中Fortran語言的子例程。

  例7:

void FOO(long *l);
main ()
{
 long l = 5000;
 FOO(&l);
}

  例8:

subroutine foo( i )
integer i
write(*,*) 'In Fortran'
write(*,*) i
return
end subroutine foo

  例9:

% gcc -m64 -c cfoo.c
% /opt/absoft/bin/f90 -m64 cfoo.o foo.f90 -o out
% ./out
In Fortran
0

  當連結這兩個檔案後,程式將打印出變數i的值為"5000"。而在LP64中,程式打印出"0",因為在LP64模式下,子例程foo通過地址傳遞一個64位的引數,而實際上,Fortran子例程想要的是一個32位的引數。如果要改正這個錯誤,在宣告Fortran子例程變數i時,把它宣告為INTEGER*8,此時和C語言中的long為一樣長度。

  結論

  64位平臺是解決大型複雜科學及商業問題的希望,大多數編寫良好的程式可輕鬆地移植到新平臺上,但要注意ILP32和LP64資料模型的差異,以保證有一個平滑的移植過程。