2016年1月11日 星期一

Win64 環境下位址算術的陷阱 A+B != A - (-B)

位址算術(address arithmetic/pointer arithmetic)顧名思意就是對某物件的位址進行加減乘除的運算,也就是算術運算子也可以套用在位址變數上進行操作,其目的往往是為了取得物件的部分內容稱為 upcasting,或是從結構中的某一個欄位間接推算其他欄位的頭位址,另外一個常用的目的是為了指向下一個內容物,如圖 1 當一個指向型別 T 的頭位址 p 要跳至下一個物件的頭位址時,只要將頭位址 p 加上指向型別 T 的大小就可以取得下一個物件的頭位址。



現在 OS 已經進入了 64 位元時代,不過因為 x86-64 架構可以相容 32 位元程式,不少程式都還在是 32 位元編寫應用程式 (RING3),不過驅動程式 (RING0) 就一定要用 64 位元架構重寫,主因在於 32 位元環境極度成熟,而且在語言層面上有極強的相容性,在加上 x86 有硬體 AC (Alignment Check) 的能力,因此 x86 架構下 32 位元程式的 C/C++ 編程陷阱最少,幾乎不會碰到,頂多就是要注意無號型態與有號型態在一個算式中可能造成非預期結果的錯誤。不過進入 Win64 時代就不一樣了,寫 64 位元程式並不是把編譯器調整為 X64 配置重新編譯那些原來 "用著 32 位元思維" 寫的原始碼就好了,Win64 有相當多的陷阱,本篇就來講講其中一個可能遇到的陷阱

A+B != A - (-B)

這種陷阱在 32 位元環境中基本上不會碰到,在 Win64 環境中就會遇到,而且程式還常常掛掉
不知道死在哪裡,這樣的情形特別容易發生在 Win64 環境中的指標算術,白話講就是操作指標,把指標當成一個整數去計算,先看看下面錯誤的程式碼 (32/64位元環境結果不同):


char *A = "123456789";
unsigned B = 1;
char *X = A + B;
char *Y = A - (-B);
if(X != Y)
  cout << "Error" << endl;


上面的程式碼我在 32 位元環境沒甚麼好討論,基本上甚麼事情也不會發生,因為最後的結果
X 會等於 Y,用 unsigned 型態的變數操作指標變數在 Win64 下就會有問題了,因為 unsigned 本質上就是 unsigned int,CL X64編譯器的實作中,unsigned int 依然只有佔 4 Bytes,可是在64位元指標變數的處理卻是 8 Bytes,甚麼意思呢?就是 -B 所得到的 -1 只有 FFFFFFFF,並非 64 位元下的 -1 ( FFFFFFFFFFFFFFFF),所以會導致 -B 變成 00000000FFFFFFFF,這個數值並不是 -1,這樣就會導致 64 位元版本產生錯誤的結果,其中 FFFFFFFF 變成 00000000FFFFFFFF是因為 CL X64編譯器會插入一段 cdqe 的指令,將 32 位元整數補滿成 64 位元整數,從這個錯誤就可以知道一個寫程式的基本原理,當寫下每條算式的時候,就要注意左值與右值兩邊變數型態擁有的大小是否有平衡,預先有注意到的話,未來在移植程式到 64 位元的時候就可以減少錯誤發生,型態平衡原理不管是 32 位元 或 64 位元編譯器都會遵守的基本原理,當有不平衡發生的時候,X64 編譯器還是會偷偷插入 cdqe 在機械碼層面維持平衡。上面這類的錯誤在 32 位元也是會發生,例如敝人以前在 VC6 撰寫 RS232 收發程式也有遇過,假如有一個變數 c 是 char 型態,裡面會存放 RS232 接收的字元,這時用 if ( c == '1' ) 時就會有問題,即時 c 的內容為字元 '1' 的時候也不會進入 if 內部,那個比較用的 '1' 要改成 (char) '1' 程式才能正確工作。

那要怎麼把程式改為在 64 位元下能產生正確結果呢?


char *A = "123456789";
__int64 B = 1;
char *X = A + B;
char *Y = A - (-B);
if(X != Y)
  cout << "Error" << endl;


嚴格的使用 __int64 使得 B 被 X64 編譯器作為一個真正的 64 位元有號整數,這樣跟64位元指標做運算就不會有問題了,上面的程式會得到與 32 位元下相同的結果 X 等於 Y
假如考慮到程式要能在 32/64 間移植,執行時期定義程式庫標頭 crtdefs.h 幫我們準備了
ptrdiff_t 型態,用微軟針對儲存指標所定義的型態,就可以寫出具有 32/64 皆相容的程式

改用 ptrdiff_t 則 32/64 皆能得到 X 等於 Y 的正確結果

char *A = "123456789";
ptrdiff_t B = 1;
char *X = A + B;
char *Y = A - (-B);
if(X != Y)
  cout << "Error" << endl;
當然 64 位元程式設計還有不少陷阱有空在講其他可能的情況...

沒有留言:

張貼留言