碰到了一個問題,在32位機器上運轉正常的程式在64位機器上會出錯。所以找到了這篇關於移植的文章好好看了看。
原帖地址: http://queniao.blog.51cto.com/10636/126564
其實還有一個問題,就是在虛擬機器上運轉正常的程式在普通的多核機器上運轉會出錯,而且還是隨機出錯。其實原因在於在虛擬機器上的多線程並發程式很難做到真正的並發,以後的開發應該注意到這一點。
隨著 64 位元體繫結構的普及,針對 64 位元系統準備好您的 Linux 軟體已經變得比以前更為重要。在本文中,您將學習如何在進行語句聲明、賦值、位移、類型轉換、字串格式化以及更多操作時,防止出現可移植性缺陷。
Linux 是可以使用 64 位元處理器的跨平台作業系統之一,現在 64
位的系統在伺服器和案頭端都已經非常常見了。很多開發人員現在都面臨著需要將自己的應用程式從 32 位環境移植到 64 位元環境中。隨著
Intel Itanium 和其他 64 位元處理器的引入,使軟體針對 64 位元環境做好準備變得日益重要了。 與 UNIX 和其他類 UNIX 作業系統一樣,Linux 使用了 LP64 標準,其中指標和長整數都是 64
位的,而普通的整數則依然是 32 位的。儘管有些進階語言並不會受到這種類型大小不同的影響,但是另外一些語言(例如 C
語言)卻的確會受到這種影響。 將應用程式從 32 位系統移植到 64
位系統上的工作可能會非常簡單,也可能會非常困難,這取決於這些應用程式是如何編寫和維護的。很多瑣碎的問題都可能導致產生問題,即使在一個編寫得非常好
的高度可移植的應用程式中也是如此,因此本文將對這些問題進行歸納總結,並給出解決這些問題的一些方法建議。 64 位元的優點
32
位平台有很多限制,這些限制正在阻礙大型應用程式(例如資料庫)開發人員的工作進展,尤其對那些希望充分利用電腦硬體優點的開發人員來說更是如此。科學
計算通常要依賴於浮點計算,而有些應用程式(例如金融計算)則需要一個比較狹窄的數字範圍,但是卻要求更高的精度,其精度高於浮點數所提供的精度。64
位元學運算提供了這種更高精度的定點數學計算,同時還提供了足夠的數字範圍。現在在電腦業界中有很多關於 32
位地址空間所表示的地址空間的討論。32 位指標只能定址 4GB
的虛擬位址空間。我們可以克服這種限制,但是應用程式開發就變得非常複雜了,其效能也會顯著降低。 在語言實現方面,目前的 C 語言標準要求 “long long” 資料類型至少是 64 位元的。然而,其實現可能會將其定義為更大。 另外一個需要改進的地方是日期。在 Linux 中,日期是使用 32 位整數來表示的,該值所表示的是從 1970 年 1 月 1
日至今所經過的秒數。這在 2038 年就會失效。但是在 64 位元的系統中,日期是使用有符號的 64 位元整數表示的,這可以極大地擴充其可用範圍。
總之,64 位元具有以下優點:
- 64 位元的應用程式可以直接存取 4EB 的虛擬記憶體,Intel Itanium 處理器提供了連續的線性地址空間。
- 64 位元的 Linux 允許檔案大小最大達到 4 EB(2 的 63 次冪),其重要的優點之一就是可以處理對大型資料庫的訪問。
Linux 64 位元體繫結構
不幸的是,C 程式設計語言並沒有提供一種機制來添加新的基礎資料型別 (Elementary Data Type)。因此,提供 64 位元的定址和整數運算能力必須要修改現有資料類型的綁定或映射,或者向 C 語言中添加新的資料類型。
表 1. 32 位和 64 位元資料模型
|
ILP32 |
LP64 |
LLP64 |
ILP64 |
char |
8 |
8 |
8 |
8 |
short |
16 |
16 |
16 |
16 |
int |
32 |
32 |
32 |
64 |
long |
32 |
64 |
32 |
64 |
long long |
64 |
64 |
64 |
64 |
指標 |
32 |
64 |
64 |
64 |
這 3 個 64 位元模型(LP64、LLP64 和 ILP64)之間的區別在於非浮點數據類型。當一個或多個 C 資料類型的寬度從一種模型變換成另外一種模型時,應用程式可能會受到很多方面的影響。這些影響主要可以分為兩類:
- 資料對象的大小
。編譯器按照常態範圍對資料類型進行對齊;換而言之,32 位的資料類型在 64 位元系統上要按照 32
位邊界進行對齊,而 64 位元的資料類型在 64 位元系統上則要按照 64 位元邊界進行對齊。這意味著諸如結構或聯合之類的資料對象的大小在 32
位和 64 位元系統上是不同的。
- 基礎資料型別 (Elementary Data Type)的大小
。通常關於基礎資料型別 (Elementary Data Type)之間關係的假設在 64 位元資料模型上都已經無效了。依賴於這些關係的應用程式在 64 位元平台上編譯也會失敗。例如,sizeof (int) = sizeof (long) = sizeof (pointer)
的假設對於 ILP32 資料模型有效,但是對於其他資料模型就無效了。
總之,編譯器要按照常態範圍對資料類型進行對齊,這意味著編譯器會進行 “填充”,從而強制進行這種方式的對齊,就像是在 C 結構和聯合中所做的一樣。結構或聯合的成員是根據最寬的成員進行對齊的。清單 1 對這個結構進行瞭解釋。
清單 1. C 結構
struct test { int i1; double d; int i2; long l; }
|
表 2 給出了這個結構中每個成員的大小,以及這個結構在 32 位系統和 64 位元系統上的大小。
表 2. 結構和結構成員的大小
結構成員 |
在 32 位系統上的大小 |
在 64 位元系統上的大小 |
struct test { |
|
|
int i1; |
32 位 |
32 位 |
|
|
32 位填充 |
double d; |
64 位元 |
64 位元 |
int i2; |
32 位 |
32 位 |
|
|
32 位填充 |
long l; |
32 位 |
64 位元 |
}; |
結構大小為 20 位元組 |
結構大小為 32 位元組 |
注意,在一個 32 位的系統上,編譯器可能並沒有對變數
d
進行對齊,儘管它是一個 64 位元的對象,這是因為硬體會將其當作兩個 32 位的對象進行處理。然而,64 位元的系統會對
d
和
l
都進行對齊,這樣會添加兩個 4 位元組的填充。
從 32 位系統移植到 64 位元系統
本節介紹如何解決一些常見的問題:
- 聲明
- 運算式
- 賦值
- 數字常數
- Endianism
- 類型定義
- 位移
- 字串格式化
- 函數參數
聲明
要想讓您的代碼在 32 位和 64 位元系統上都可以工作,請注意以下有關聲明的用法:
- 根據需要適當地使用 “L” 或 “U” 來聲明整型常量。
- 確保使用不帶正負號的整數來防止符號擴充的問題。
- 如果有些變數在這兩個平台上都需要是 32 位的,請將其類型定義為 int。
- 如果有些變數在 32 位系統上是 32 位的,在 64 位元系統上是 64 位元的,請將其類型定義為 long。
- 為了對齊和效能的需要,請將數字變數聲明為 int 或 long 類型。不要試圖使用 char 或 short 類型來儲存位元組。
- 將字元指標和字元位元組聲明為無符號類型的,這樣可以防止 8 位字元的符號擴充問題。
運算式
在 C/C++ 中,運算式是基於結合律、操作符的優先順序和一組數學計算規則的。要想讓運算式在 32 位和 64 位元系統上都可以正確工作,請注意以下規則:
- 兩個有符號整數相加的結果是一個有符號整數。
- int 和 long 類型的兩個數相加,結果是一個 long 類型的數。
- 如果一個運算元是不帶正負號的整數,另外一個運算元是有符號整數,那麼運算式的結果就是不帶正負號的整數。
- int 和 doubule 類型的兩個數相加,結果是一個 double 類型的數。此處 int 類型的數在執行加法運算之前轉換成 double 類型。
賦值
由於指標、int 和 long 在 64 位元系統上大小不再相同了,因此根據這些變數是如何賦值和在應用程式中使用的,可能會出現問題。下面是有關賦值的一些技巧:
數字常量
16 進位的常量通常都用作掩碼或特殊位的值。如果一個沒有尾碼的 16 進位的常量是 32 位的,並且其高位被置位了,那麼它就可以作為無符號整型進行定義。 例如,常數 OxFFFFFFFFL 是一個有符號的 long 類型。在 32 位系統上,這會將所有位都置位(每位全為 1),但是在 64 位元系統上,只有低 32 位被置位了,結果是這個值是 0x00000000FFFFFFFF。 如果我們希望所有位全部置位,那麼一種可移植的方法是定義一個有符號的常數,其值為 -1。這會將所有位全部置位,因為它採用了二進位補碼演算法。
可能產生的另外一個問題是最高位的設定。在 32 位系統上,我們使用的是常量 0x80000000。但是可移植性更好的方法是使用一個位移運算式:
1L << ((sizeof(long) * 8) - 1);
|
Endianism
Endianism 是指用來儲存資料的方法,它定義了整數和浮點數據類型中是如何對位元組進行定址的。 Little-endian 是將低位位元組儲存在記憶體的低地址中,將高位位元組儲存在記憶體的高地址中。 Big-endian 是將高位位元組儲存在記憶體的低地址中,將低位位元組儲存在記憶體的高地址中。 表 3 給出了一個 64 位元長整數的布局樣本。
表 3. 64 位元 long int 類型的布局
|
低地址 |
|
|
|
|
|
|
高地址 |
Little endian |
Byte 0 |
Byte 1 |
Byte 2 |
Byte 3 |
Byte 4 |
Byte 5 |
Byte 6 |
Byte 7 |
Big endian |
Byte 7 |
Byte 6 |
Byte 5 |
Byte 4 |
Byte 3 |
Byte 2 |
Byte 1 |
Byte 0 |
例如,32 位的字 0x12345678 在 big endian 機器上的布局如下:
表 4. 0x12345678 在 big-endian 系統上的布局
記憶體位移量 |
0 |
1 |
2 |
3 |
記憶體內容 |
0x12 |
0x34 |
0x56 |
0x78 |
如果將 0x12345678 當作兩個半字來看待,分別是 0x1234 和 0x5678,那麼就會看到在 big endian 機器上是下面的情況:
表 5. 0x12345678 在 big-endian 系統上當作兩個半字來看待的情況
記憶體位移量 |
0 |
2 |
記憶體內容 |
0x1234 |
0x5678 |
然而,在 little endian 機器上,字 0x12345678 的布局如下所示:
表 6. 0x12345678 在 little-endian 系統上的布局
記憶體位移量 |
0 |
1 |
2 |
3 |
記憶體內容 |
0x78 |
0x56 |
0x34 |
0x12 |
類似地,兩個半字 0x1234 和 0x5678 如下所示:
表 7. 0x12345678 在 little-endian 系統上作為兩個半字看到的情況
記憶體位移量 |
0 |
2 |
記憶體內容 |
0x3412 |
0x7856 |
下面這個例子解釋了 big endian 和 little endian 機器上位元組順序之間的區別。 下面的 C 程式在一台 big endian 機器上進行編譯和運行時會列印 “Big endian”,在一台 little endian 機器上進行編譯和運行時會列印 “Little endian”。
清單 2. big endian 與 little endian
#include <stdio.h> main () { int i = 0x12345678; if (*(char *)&i == 0x12) printf ("Big endian/n"); else if (*(char *)&i == 0x78) printf ("Little endian/n"); }
|
Endianism 在以下情況中非常重要:
在 C 和 C++ 中有位域來協助處理 endian 的問題。我建議使用位域,而不要使用掩碼域或 16 進位的常量。有幾個函數可以用來將 16 位和 32 位元據從 “主機位元組順序” 轉換成 “網路位元組順序”。例如,htonl (3)
、ntohl (3)
用來轉換 32 位整數。類似地,htons (3)
、ntohs (3)
用來轉換 16 位整數。然而,對於 64 位元整數來說,並沒有標準的函數集。但是在 big endian 和 little endian 系統上,Linux 都提供了下面的幾個宏:
- bswap_16
- bswap_32
- bswap_64
類型定義
建議您不要使用 C/C++ 中那些在 64 位元系統上會改變大小的資料類型來編寫應用程式,而是使用一些類型定義或宏來顯式地說明變數中所包含的資料的大小和類型。有些定義可以使代碼的可移植性更好。
ptrdiff_t
:
這是一個有符號整型,是兩個指標相減後的結果。
size_t
:
這是一個無符號整型,是執行 sizeof
操作的結果。這在向一些函數(例如 malloc (3)
)傳遞參數時使用,也可以從一些函數(比如 fred (2)
)中返回。
int32_t
、uint32_t
等:
定義具有預定義寬度的整型。
intptr_t
和 uintptr_t
:
定義整數型別,任何有效指標都可以轉換成這個類型。
例 1: 在下面這條語句中,在對
bufferSize
進行賦值時,從
sizeof
返回的 64 位元值被截斷成了 32 位。
int bufferSize = (int) sizeof (something);
解決方案是使用
size_t
對傳回值進行類型轉換,並將其賦給聲明為
size_t
類型的 bufferSize,如下所示:
size_t bufferSize = (size_t) sizeof (something);
例 2: 在 32 位系統上,int 和 long 大小相同。由於這一點,有些開發人員會交換使用這兩種類型。這可能會導致指標被賦值給 int 類型,或者反之。但是在 64 位元的系統上,將指標賦值給 int 類型會導致截斷高 32 位的值。 解決方案是將指標作為指標類型或為此而定義的特殊類型進行儲存,例如
intptr_t
和
uintptr_t
。
位移
無類型的整數常量就是 (unsigned) int 類型的。這可能會導致在位移時出現被截斷的問題。 例如,在下面的代碼中,
a
的最大值可以是 31。這是因為
1 << a
是 int 類型的。
long t = 1 << a;
要在 64 位元系統上進行位移,應該使用
1L
,如下所示:
long t = 1L << a;
字串格式化
函數
printf (3)
及其相關函數都可能成為問題的根源。例如,在 32 位系統上,使用
%d
來列印 int 或 long 類型的值都可以,但是在 64 位元平台上,這會導致將 long 類型的值截斷成低 32 位的值。對於 long 類型的變數來說,正確的用法是
%ld
。 類似地,當一個小整數(char、short、int)被傳遞給
printf (3)
時,它會擴充成 64 位元的,符號會適當地進行擴充。在下面的例子中,
printf (3)
假設指標是 32 位的。
char *ptr = &something;
printf (%x/n", ptr);
上面的代碼在 64 位元系統上會失敗,它只會顯示低 4 位元組的內容。 這個問題的解決方案是使用
%p
,如下所示;這在 32 位和 64 位元系統上都可以很好地工作:
char *ptr = &something;
printf (%p/n", ptr);
函數參數
在向函數傳遞參數時需要記住幾件事情:
- 在參數的資料類型是由函數原型定義的情況中,參數應該根據標準規則轉換成這種類型。
- 在參數類型沒有指定的情況中,參數會被轉換成更大的類型。
- 在 64 位元系統上,整型被轉換成 64 位元的整型值,單精確度的浮點類型被轉換成雙精確度的浮點類型。
- 如果傳回值沒有指定,那麼函數的預設傳回值是 int 類型的。
在將有符號整型和無符號整型的和作為 long 類型傳遞時就會出現問題。考慮下面的情況:
清單 3. 將有符號整型和無符號整型的和作為 long 類型傳遞
long function (long l); int main () { int i = -2; unsigned k = 1U; long n = function (i + k); }
|
上面這段代碼在 64 位元系統上會失敗,因為運算式
(i + k)
是一個無符號的 32 位運算式,在將其轉換成 long 類型時,符號並沒有得到擴充。解決方案是將一個運算元強制轉換成 64 位元的類型。 在基於寄存器的系統上還有一個問題:系統採用寄存器而不是堆棧來向函數傳遞參數。考慮下面的例子:
float f = 1.25;
printf ("The hex value of %f is %x", f, f);
在基於堆棧的系統中,這會列印對應的 16 進位值。但是在基於寄存器的系統中,這個 16 進位的值會從一個整數寄存器中讀取,而不是從浮點寄存器中讀取。 解決方案是將浮點變數的地址強制轉換成一個指向整數型別的指標,如下所示:
printf ("The hex value of %f is %x", f, *(int *)&f);