翻訳元サイト
20 issues of porting C++ code on the 64-bit platform
http://www.viva64.com/en/a/0004/#ID0EMEBK
概要
C++コードを32ビットから64ビット環境へ移植する際に発生し得る問題について述べる。間違ったコードとそれを正す方法を示す。また、これらの問題を診断するコード解析手法についも述べる。
序論
本稿では、32ビットから64ビット環境へ移植手順について議論する。本稿はC++プログラマ向けであるが、別プラットフォームでアプリケーションを移植する全ての人に有用な情報となるかもしれな。著者は64ビット環境移植の専門家と64ビット移植に特化したコード解析ツールViva64ツールの開発者である。
64ビット移植をする際には、他の数千以上もあるプログラムの間違いではないの新しいタイプのエラーを理解するべきである。これらは全ての開発者が経験する不可避の問題である。この記事ではそのような問題に対する準備と対策方法を示す。プロミングだけでなく全ての新しい技術は、いくつかの制限や問題を抱えている。64ビットソフトウェア開発も状況は同じである。われわれは64ビットソフトウェアがIT開発の次のステップだと知っているが、実際はほとんどのプログラマは64ビットプログラム開発に関わっておらず、これらの問題に触れる機会はない。
64ビットアーキテクチャの利点を長々と話すつもりはない。このテーマに関してはたくさんの文献があるので参照されたい。この記事の目的は、64ビットアプリケーションの開発者が直面する問題の考察し、以下を学ぶことにある。
- 64ビットシステムで起こる典型的な問題
- それらの理由と対応するコード例
- エラーの修正方法
- 64ビットプログラムの間違いを探す方法
この知識によって読者は
- 32/64ビットシステムの違いを理解できる
- 64ビットプログラムを開発する際に間違いを避けられる
- デバッグやテストにかける時間を減らせることで、32ビットから64ビットへの移植をすばやく行える
- 移植にかかる時間をより正確に見積もることができる
また、この記事の中には多くのサンプルコードが含まれているため、より深く理解するためにぜひ試していただきたい。これは読者にとって64ビットの世界の入り口となるはずだ。まずは、以降の説明の手始めにいくつかの型を思い出してほしい。
整数型とメモリサイズ型
型名 | 型のサイズ
(32ビット) | 型のサイズ
(64ビット) | 説明 |
ptrdiff_t | 32 | 64 | ポインタの引き算時に使用する符号あり整数型。この型はメモリサイズを保持するのに利用するまた、関数の戻り値の型としてサイズやエラー時に-1を返す。 |
size_t | 32 | 64 | 符号なし整数型で、sizeof()等、オブジェクトのサイズや数を保持するのに使われる。 |
intptr_t, uintptr_t, SIZE_T, SSIZE_T, INT_PTR, DWORD_PTR, etc | 32 | 64 | ポインタの値を保持する整数型。 |
time_t | 32 | 64 | 秒単位の時間。 |
この記事では、これらの型を
”メモリサイズ型”と呼ぶことにする。この用語はポインタを保持し、プラットフォームが32から64ビットへ変わることによってサイズが変化する全ての型を意味する。たとえばメモリサイズ型として、size_t、ptrdiff_t、全てのポインタ、INT_PTR、DWORD_PTR等がある。
32/64ビットデータモデル
| ILP32
(Win*32, UNIX*32, Linux*32) | LP64
(UNIX*64, Linux*64) | LLP64
(Win*64) | 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 |
size_t | 32 | 64 | 64 | 64 |
ポインタ | 32 | 64 | 64 | 64 |
1. ワーニングの抑制
どんなソフトウェアの書籍においても、ワーニングレベルをできるだけ高く設定されていることが推奨されている。プロジェクトをこなしてきたプログラマは経験的に、そのソフトウェアに必要な品質を判断することができる。
しかし、64ビット化においては、重大なバグを見逃すことになる可能性があるので、その考えは改め全てのプロジェクトのワーニングレベルを最高に引き上げるべき。そうしないなら、以下のようなミスを見逃すことになる。
unsigned char *array[50];
unsigned char size = sizeof(array);
// 32bit環境:sizeof(array) = 200
// unsigned charの最大値は255なので400を代入すると変数がオーバーフローする。
// 64bit環境:sizeof(array) = 400
2. 可変個の引数をとる関数
64ビット環境でNGな例
const char *invalidFormat = "%u";
size_t value = SIZE_MAX;
// 64ビット環境ではsize_tは8Byteの符号なし整数型なのでこのコードはNG
printf(invalidFormat, value);
このようなバグは64ビット化だけではない。C++言語の本質的な危険性からきている。この問題を
避けるための方法として、そのような関数の使用は避け、より安全な関数を使うことである。例えば、printfの代わりにcout、sprintfの代わりにboost::formatやstd::stringstreamである。
もし、printfやscanf系の関数を利用したい場合は、以下のマクロを使うとよい。
size_t u;
scanf("%u", &u); // Win32
scanf("%Iu", &u); // Win64
scanf("%lu", &u); // Linux64
3. マジックナンバー
質の低いコードはマジックナンバーを含んでいる。マジックナンバーはその存在自体が危険であり、アドレス演算、オブジェクトのサイズ、ビット演算等に使われていた場合、64ビット化移植時が困難になる。
以下は64ビット化移植に影響する基本的なマジックナンバーである。
値 | 説明 |
4 | ポインタのバイト数 |
32 | ポインタのビット数 |
0x7fffffff | 32ビット符号付き整数の最大値、また32ビット型の最上ビットのマスク値として使われる |
0x80000000 | 32ビット符号付き整数の最小値、また32ビット型の最上ビットのマスク値として使われる |
0xffffffff | 32ビット変数の最大値、またはエラー値-1として使われる |
マジックナンバーは、sizeof()演算子や
、のマクロで置き換えるべきである。
64ビット環境でNGな例
// 1) intptr_tの動的領域確保にマジックナンバーを使用している。
// intptr_tは32ビット環境では4byteだが64ビット環境ではは8byte
size_t ArraySize = N * 4;
intptr_t *Array = (intptr_t *)malloc(ArraySize);
// 2) 64ビット環境ではsize_tは8byte
size_t values[ArraySize];
memset(values, ArraySize * 4, 0);
64ビット環境でOKな例
// 1)
size_t ArraySize = N * sizeof(intptr_t);
intptr_t *Array = (intptr_t *)malloc(ArraySize);
// 2)
size_t values[ArraySize];
memset(values, ArraySize * sizeof(size_t), 0);
4. int型数値のdouble型変数への格納
32ビット環境では、intが4byteでdoubleが8Byteのためint値をdouble型変数に格納しても問題なかった。64ビット環境では、以下の図のとおりsize_t等の変数が8byteになるため変数がオーバーフローする可能性がある。
// 1) 32ビット これはOK
size_t val1 = SIZE_MAX; // size_tはunsigned intと同じで4バイト
double val2 = UINT_MAX;
// 2) 64ビット これはNG
size_t val1 = SIZE_MAX; // size_tは4バイトとなる
double val2 = UINT_MAX; // オーバーフロー
5. ビットシフト
64ビット環境のことをあまり考えずにビットシフトをすると、以下のような問題に陥ることがある。
64ビット環境でNGな例
ptrdiff_t SetBitN(ptrdiff_t value, unsigned int bitNum)
{
// 1はint型リテラルなので、4Byteの範囲しか値を取ることができないため、
// 8Byte変数のビットシフトができない可能性がある
ptrdiff_t mask = 1 << bitNum;
return value | mask;
}
ここで何が起きているかというと、
以下の図のように32Bit変数に1の値が入っている場合、32回ビットシフトすると桁があふれて値が0となってしまう。一方、64Bit変数は32回以上ビットシフトしても値があふれることはない。
64ビット環境でOKな例
// 数値1のptrdiff_t変数を生成する
ptrdiff_t mask = ptrdiff_t(1) << bitNum;
ptrdiff_t mask = CONST3264(1) << bitNum;
6. ポインタアドレスの保存
32ビット環境では、intとポインタのサイズがともに4Byteであるため、ポインタをintにキャストしてアドレス演算をすることができた。64ビット環境では、メモリサイズ型のuintptr_t型を使うと別アーキテクチャへ移植できて良い。
64ビット環境でNGな例
1) char *p;
p = (char *)((int)p & PAGEOFFSET); // ポインタをintにキャストしてはいけない
// mallocの戻り値(void *)をDWORD(unsigned int)にキャストしてはいけない
2) DWORD tmp = (DWORD)malloc(ArraySize);
64ビット環境でOKな例
1) char *p;
p = (char *)((intptr_t)p & PAGEOFFSET);
2) DWORD_PTR tmp = (DWORD_PTR)malloc(ArraySize);
64ビット環境でも少ないメモリしか利用していない時は再現しないので、非常に発見が困難なバグになり得る。上記NG例はメモリ使用量が4GBを超えた時点で、動作は未定義となる。
8. 配列のキャスト
64ビット環境では、配列のキャストが危険な場合とそうでない場合がある。
int array[] = {1, 2, 3, 4};
enum ENumbers {ZERO, ONE, TWO, THREE, FOUR};
// 安全なキャスト
ENumbers *enumPtr = (ENumbers *)(array);
cout << enumPtr[1] << " ";
// 危険なキャスト
size_t *sizePtr = (size_t *)(array);
cout << sizePtr[1] << endl;
// 32ビットプログラムでの出力結果: 2 2
// 64ビットプログラムでの出力結果: 2 17179869187
ここで何が起こっているというと、以下の図のように64ビット環境でint、enum型は4Byteだがsize_t型は8Byteのためである。
9. メモリサイズ型を引数に持つvirtualなメンバ関数
virtualなメンバ関数をもつ多くなクラスがある場合、不注意でメモリサイズ型を別の型と混同して使ってしまうことがある。32ビット環境はこれでも問題ない。例えば、基底クラスのvirtualなメンバ関数にsize_tを使い、派生クラスにはunsignedを使ったとしよう。64ビット環境では問題が生じてしまう。
しかし
class CWinApp {
...
virtual void WinHelp(DWORD_PTR dwData, UINT nCmd);
};
class CSampleApp : public CWinApp {
...
virtual void WinHelp(DWORD dwData, UINT nCmd);
};
ここでアプリケーション開発のライフサイクルを考えてみよう。例えば、Microsoft Visual C++ 6.0を使ったCWinAppクラスのWinHelp関数を以下のようにプロトタイプ宣言したとする。
virtual void WinHelp(DWORD dwData, UINT nCmd = HELP_CONTEXT);
派生クラスのCSampleAppでこの関数をオーバーライドすることに特に問題はない。その後、このプロジェクトはMicrosoft Visual C++ 2005にポーティングしたとする。