公司 OS 要全面升級至 64 Bit 環境,其中包含了超過 20 個機器群組,一時間無法做完,只好先將部分機器升上 64 Bit ,其它舊的機器就保留為 32 Bit ,這代表有些 32 Bit 機器會傳資料給 64 Bit,而問題就來了, 32 Bit 的變數長度跟 64 Bit 是不同的,例如我在 32 Bit 中寫入一個 size_t 4 Bytes 長度的數字, 再把這個變數傳給 64 Bit Server ,這時 Server 會試著去讀取一個 8 Bytes 長度的數字,結果取得的資料就跟 32 Bit 傳給來的值不同。
基本變數長度
以下是各種變數型態的長度
Variable | 32 Bit (bytes) | 64 Bit (bytes) |
---|---|---|
short | 2 | 2 |
float | 4 | 4 |
int | 4 | 4 |
double | 8 | 8 |
long | 4 | 8 |
long long | 8 | 8 |
size_t | 4 | 8 |
為了 32/64 Bit 相容性,第一件事就是將 int 修改成 int32_t ,雖然 32/64 Bit 的環境中宣告的 int 長度是一致的,不過為了容易了解,我還是將 int 改成 int32_t ,再將 size_t 與 long 改成 int64_t。
int32_t 與 int64_t 代表強迫指定 int 的長度 , int32_t 的長度為 32 Bit, 4 Bytes , int64_t 的長度為 64 Bit , 8 Bytes 。
修改完後,兩個境環的變數長度就一致了,而且這次的修改 ,不用重新編譯 64 Bit 的環境,因為 size_t 改成 int64_t ,對 64 Bit 的環境來說,變數的長度是一樣的,所以就算不重新編譯也不會有問題。
- #include <stdio.h>
- #include <inttypes.h>
- int => int32_t a;
- size_t => int64_t b;
Struct 長度
在解決了 integer 長度之後,本來想說就搞定下班了,但是沒想到,程式還是有問題, client 傳給 server 的資料還是有錯, server 仍然出現 segmentation fault ,追查了一陣子,發現 struct 的長度有點問題, client 傳出來的 struct 長度為 12 Bytes ,而 server 要接收的 struct 長度為 16 Bytes,兩個環境對 struct 的長度定義不同。
程式中 struct 的結構如下:
- struct resHeader {
- static int code;
- int status;
- unsigned long long length;
- };
這個 struct 中有一個 static int ,這種靜態變數是不會增加 struct 的長度, static 在程序中只能存在一個,每個 struct resHeader 都會取存到同一個 static 數值,所以 struct resHeader 中只有 int status 與 unsigned long long length 會影響到 resHeader 的長度, int 的長度在 32/64 Bit 環境中都是 4 bytes ,而 long long 的長度在 32/64 Bit 環境中也都是 8 bytes, 兩個總合起來,struct resHeader 的長度為 12 bytes。
但是我實際測試的結果, struct resHeader 在 64 Bit 環境中的長度是 16 bytes,下面這段是我的測試程式。
- #include <stdio.h>
- struct resHeader {
- static int code;
- int status;
- unsigned long long length;
- };
- int main() {
- printf("size = %d", sizeof(resHeader));
- }
- // g++ size.cc && ./a.out
- // size = 16
追查了很久之後,才知道原來 64 Bit 編譯器,為了優化 Performance ,每次讀取 memory 的時候,一定要讀一個固定 bytes 的倍數,也就是一次讀取 8 bytes, 16 bytes, 24 bytes,所以 struct 的長度也會被自動優化成某一個值的倍數。
首先 Compiler 會先在這個 struct resHeader 中找一個需要最多 memory 的基本變數,在我的例子中 long long 會最大的變數值,它需要的長度為 8 bytes ,所以這個 struct 中的所有變數後面都會被自動補上 Null 湊成 8 bytes 的倍數, 例如 status 為 int 需要 4 個 bytes ,後面會被接上 4 個 Null,再來 length 為 long long 需要 8 bytes, 8 bytes 就是 8 的倍數,所以不用補 Null ,最後在 64-Bit 環境中 ,這個 structure 就會變成 16 bytes。
- struct resHeader {
- static int code; // static
- int status; // Memory 位置 0 - 3
- char pad[4]; // 自動補上 4 個 null, Memory 位置 4 - 7
- unsigned long long length; // Memory 位置 8 - 15
- // Total 16 bytes
- };
那麼 32-bit 的環境中這個 struct memory address 會是怎樣的呢? 同上的邏輯,Compiler 先找到一個最大的變數 length(8 bytes) ,但是 32-bit 的環境中一次讀取的最大 memory 為 4 bytes (32 bits) , 8 bytes 已經超過這個值,所以 Compiler 會將最大的 memory 讀取自動調整為 4 bytes ,代表每個變數只要是 4 的倍數即可。
- struct resHeader {
- static int code; // static
- int status; // Memory 位置 0 - 3 , 剛好為 4 的倍數,不用補 Null
- unsigned long long length; // Memory 位置 4 - 11
- // Total 12 bytes
- };
32-Bit 環境的 struct 長度是 12 bytes ,而 64-Bit 的環境被自動補上了 4 bytes 的 null 形成 16 bytes,這個問題就是造成 32/64 Bit 環境不相容的原因。
解決方法也不難,上述兩個情形的最大公倍數為 8 ,所以我們只要把 struct 的長度弄成 8 bytes 的倍數,然後將 C/C++ 重新 compile ,分別裝到 client/server 的機器就搞定收工囉。
- struct resHeader {
- static int code;
- int64_t status;
- int64_t length;
- };
No Downtime
以上的解法,還有個小問題, 因為 server 目前已經有兩組機器,一組是 32 Bit ,一組是 64 Bit 如果要將 client 機器裝上新版相容 64 Bit 的程式,那麼代表 64 Bit 的 server 也要一併裝上新版的程式,而且還要同時 restart 伺服器,但是我們公司的機器數可不是普通的多,一時半刻是無法將全部的機器安裝完畢的,再加上公司平常 Release 都是追求 No Downtime 的目標在做,所以再來要想想是否能夠單獨安裝新的 package 到 client 的機器,而且不影響線上的運作。
第一步我先來測試舊的 struct resHeader 在 64 Bit 環境中,struct resHeader memory 儲存的格式是什麼, g++ old_struct.cc && ./a.out
。
- #include <stdio.h>
- #include <inttypes.h>
- struct resHeader {
- static int code;
- int status;
- unsigned long long length;
- };
- int main() {
- struct resHeader k = {2,4};
- unsigned char *p = (unsigned char*) &k;
- for (int i = 0; i < sizeof(struct resHeader); i++) {
- printf("%02x ", p[i]);
- }
- return 1;
- }
將 memory 印出來之後如下,前 4 個 bytes 存的是 status,接著會有 4 個 bytes 的 null 值 ,後 8 個 bytes 存的是 length,剛好加起來是 16 bytes 。
02 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00
這個格式看起來跟我們改的 struct resHeader 是一致的, 來測試一下新改的 struct 在 32 Bit 環境下的執行結果, g++ -m32 new_struct.cc && ./a.out
。
- #include <stdio.h>
- #include <inttypes.h>
- struct resHeader {
- static int code;
- int64_t status;
- int64_t length;
- };
- int main() {
- struct resHeader k = {2,4};
- unsigned char *p = (unsigned char*) &k;
- for (int i = 0; i < sizeof(struct resHeader); i++) {
- printf("%02x ", p[i]);
- }
- return 1;
- }
執行結果如下,memory 存的方式跟舊程式在 64 Bit 上執行的結果一樣,這代表了我只要將新程式安裝到 client 的機器上, client (32 Bit) 與 server (64 Bit) 的機器就能相容了。
02 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00
最後實際在 Beta 環境上測試成功,終於可以下班了...
參考資料
- Data structure alignment : https://en.wikipedia.org/wiki/Data_structure_alignment
- structure padding : http://www.catb.org/esr/structure-packing/