64bit版windowsとVC++による64bitアプリの開発

《 初回公開:2020/05/14 , 最終更新:未 》

VC++の64bitアプリの開発の注意点について調べてみた。

【 目次 】

データモデル

32bit版と64bit版ののプログラミング環境にはデータモデルという概念があって、どのデータモデルを採用するかによってデータ型のビット幅が決まっている。

そして、64bit版Windowsで採用されているデータモデルは「LLP64」

LLP64 IP64 SILP64 ILP64 LP64

  • 64ビット - Wikipedia

    C言語等で書かれたソフトウェアを32ビットアーキテクチャから64ビットアーキテクチャへ変換することの困難さは様々である。 よく言われる問題は、プログラマがしばしば、ポインタとintあるいはlongが同じ大きさだという前提でコードを書いていることである。

  • データ型モデル ‐ 通信用語の基礎知識

32ビットマシンのプログラミング環境

ILP32データモデル
int型、long型、ポインタは全て32ビット幅

Iがint型でLはlong型、そしてPはポインタを指していてILP32とはそのすべてが32bitという事。

64ビットマシンのプログラミング環境

LP64データモデル
int型は32ビットのままだが、long型とポインタは64ビット幅
ILP64データモデル
int型も64ビット幅

LP64モデルを採用している(Solaris、AIX、macOS、z/OS のネイティブコンパイラなど)

LLP64
int型やlong型は32ビットのまま long long型とポインタだけが64ビット幅

同じOSで同じ開発言語でも処理系によってデータモデルが異なる事も。
Visual C++コンパイラはLLP64モデル
MinGW環境のGCCはLLP64

LP64モデルの欠点は、long型の値をint型変数に代入するときにオーバーフローが発生する可能性がある。
一方、ポインタをlong型にキャストすることが可能。
LLP64モデルは、これとは逆、long型の値をint型変数に代入可能であるがポインタをlong型にキャストときにオーバーフローが発生する可能性がある
LP64ではunsigned int(32ビット)同士の乗算の結果をunsigned long(64ビット)に格納することが可能だがLLP64ではunsigned longが32ビットのため出来ない。

64ビット整数型はlong long型とunsigned long long型、 ポインターのビット幅は当然64bit。

VC++ 型 32bit 64bit 違い

固定長整数型

固定長整数型にはint32_tとかuint32_t,int64,__int64とかいろいろあってそれがどの処理系で使えるのかが不明だったので。

C++11以上

C++11以上だとヘッダ <cstdint>

int8_t,uint8_t int16_t,uint16_t int32_t,uint32_t int64_t,uint64_t

などが定義されていて、整数型のビット幅を明示的に示す型を使う事ができる。

VC++のstdint.hでマクロ定義されている型

VC++でも標準型としてstdint.hにtypedefを使ってint8_tなどが定義されていて

VC++の組み込み型

さらにMicrosoft 固有の組み込み型として__int8、__int16、__int32、__int64等が定義されていて、int8_t,int16_t,int32_t,int64_tの替わりに使う事ができる。

  • 基本型 (C++) | Microsoft Docs

    __int8、 __int16、 __int32 、および __int64は、Microsoft 固有のキーワードです。 すべての型がすべてのアーキテクチャで使用できるわけではありません。 ( __int128はサポートされていません)。


もしこれらがいやなら独自のマクロを定義して

Win64プログラムの注意点

32bitアプリから64bitに移行する場合の注意点は

私なりに主要部分を翻訳してみると

Microsoft C ++コンパイラ(MSVC)を使用して、64ビットWindowsオペレーティングシステムで実行するアプリケーションを作成する場合、次の問題に注意する必要があります。

  • intおよびlongは、64ビットWindowsオペレーティングシステムでは32ビット値です。
    64ビットプラットフォーム用にコンパイルする予定のプログラムでは、32ビット変数にポインターを割り当てないように注意する必要があります。
    ポインターは64ビットプラットフォームでは64ビットであり、32ビット変数に割り当てるとポインター値は切り捨てられます。
  • size_t、time_t、ptrdiff_tは、64ビットWindowsオペレーティングシステムでは64ビット値です。
  • time_tは、Visual Studio 2005以前の32ビットWindowsオペレーティングシステムの32ビット値です。
    time_tはデフォルトで64ビット整数になりました。 詳細については、Time Managementを参照してください。

int型の値を使ってa size_tやtime_t valueを処理している事に築くべきです。
数値が32ビットの値より大きくなる可能性があり、データがint型の変数に格納されるときに切り捨てられる可能性があります。

printfのフォーマット指定子%x(16進フォーマット)は、64ビットWindowsオペレーティングシステムでは期待どおりに機能しません。
渡される値の最初の32ビットでだけが処理されます。
%I32xは32bit整数値を16進フォーマットに表示するために使われます。
%I64xは64bit整数値を16進フォーマットに表示するために使われます。
%p (ポインタを16進フォーマット)は64ビットWindowsオペレーティングシステムで期待どおりに機能します。

ポインタをint型やlong型変数に代入しない

64ビットのWindows環境では、新しいデータ型であるLONG_PTRなどを使うようにします。

printf関数のフォーマットに注意する

アライメント(Alignment)に気をつける

変数は8バイト(64bit)置きにに配置されるのでこの事に注意。
でもVC++の場合、構造体のアライメントは32bitとの互換性のためかデフォルトでは4バイト(32bit)置きにに配置される。

他にも

int型とlong型は64bitWindowsが採用しているLLP64モデルでは32bitなので64bitのポインタ型を代入すると上位ビットが失われる。
ポインタ型は整数型に代入するにはint型やlong型では無くint_ptrやLONG_PTRなどを使う

他のプラットフォームへの移植性を考慮するとint型とlong型のビット幅が同じであるとかビット幅を前提としたコードは書かない。

ポインターを整数型に代入するための新しい型

ポインターを整数型に代入する時に Win64ではではポインタは64bit、これを整数型に代入するのに32bit幅であるint型やlong型へ代入すると桁落ちをしてしまう。 64bit長である整数型long long型に代入する事になるのだが、

char c = 'a';
char* c_ptr = &c;
long long ll = (long long)c_ptr;

でも型のビット幅に影響しないプログラムを書くには特定のビット幅を持つlong long型に直接代入するのは不適切。 替わりにポインター型としてintptr_t等の新しい型を使った方が良い。

標準Cのintptr_tとuintptr_t型

標準Cでは新しい型としてポインターを符号付き整数型に代入するためのintptr_tとポインターを符号無し整数型に代入するためのuintptr_tがstdint.hやcstdintで定義されていて。

VC++でもこれらを使う事ができる。

intptr_t i = (intptr_t)c_ptr;
uintptr_t ui = (uintptr_t)c_ptr;

VC++固有のINT_PTR , LONG_PTR 等

  • INT_PTR ‐ 通信用語の基礎知識

    Windowsアプリケーションで、64ビット対応をする場合、intやINTを、INT_PTRに置き換える作業が主となる。
    例えば、ダイアログのコールバック関数の返却値はINT_PTRで定義されることが多いようである。
    ...
    INT_PTRは「ポインター長のint」であり、これを「int長のポインター」と考えることは誤りである。
    ...
    現在の定義では、INT_PTRはLONG_PTRと同じである。
    かつてはINT_PTRはintのtypedef、LONG_PTRはlongのtypedefだったため、このような名前となっているのである。
    BaseTsd.hには、古い環境での定義が今も残されている。

  • LONG_PTR ‐ 通信用語の基礎知識

    Windowsアプリケーションで、64ビット対応をする場合、longやLONGを、LONG_PTRに置き換える作業が主となる。
    例えば、WindowsのAPI関数でよく使われるLPARAM型やLRESULT型は、LONG_PTR型をtypedefしたものである。
    また、SetWindowLong関数の改訂版として登場したSetWindowLongPtr関数は、第3引数がそのままLONG_PTR型で定義されている。
    ...
    名称にPTRとあるためポインター変数のように見えるが、実際にはそうではなく整数型の変数である。従ってLONG_PTRは「ポインター長のlong」であり、これを「long長のポインター」と考えることは誤りである。

Basetsd.hで定義されている新しいデータ型

INT_PTR
ポインター精度(ビット幅)の符号付きint型。
LONG_PTR
ポインター精度の符号付きlong型
UINT_PTR
符号なしINT_PTR
ULONG_PTR
符号なしLONG_PTR
DWORD_PTR
ポインター精度の符号なしlong型。

上記の型はintptr_t型やuintptr_t型と同じくWin64ではすべて同じ型であるlong long型もしくはunsigned long long型を指している。

INT_PTR iptr = (INT_PTR)c_ptr;
LONG_PTR lptr = (LONG_PTR)c_ptr;
DWORD_PTR dptr = (DWORD_PTR)c_ptr;

ならばこれらの型を使わずにintptr_t型やuintptr_t型のみを使えば良いのではないかと思ってしまうが、

過去の遺産となっているWindows API等の膨大な32bitのプログラムを64bitに引き継ぐためには、いままでの32bitのプログラムでポインターをint型に代入していたコードについてはINT_PTR型を、long型に代入していたコードについてはLONG_PTR型に,DWORDに代入していたコードについてはDWORD_PTRに機械的に置き換えるという決まりなのであろう。
unsigned型についても同様に。

size_tとかtime_tとかptrdiff_t

  • size_t - メモリー上のオブジェクトのサイズをバイト数で表します。
  • time_t - 秒数で時間をカウントします。
  • ptrdiff_t - 2 つのポインタの減算結果用の符号付き整数型です。

VC++でも標準タイプとして定義されていて

size_tとptrdiff_tはCRTDEFS.H,time_tはTIME.Hで定義されている。

これらの型がWin32では4byteであったのが8byte長に変わっている事に注意する必要がある。。

printfのformat書式

printfのformat書式にも注意。

%dはint型
%ldはlong型
%I64dはlong long型

  • C/C++標準
    • %lld、%lli、%llo、%llx、%llX (signed)
    • %llo、%llu、%llx、%llX (unsigned)
  • Visual C++系の実装
    • %I64d、%I64i、%I64o、%I64x、%I64X (signed)
    • %I64o、%I64u、%I64x、%I64X (unsigned)

コンパイラによる定義済みマクロ _WIN16 , _WIN32 , _WIN64

  • 64 ビット コンパイラ - Windows drivers | Microsoft Docs
    コンパイラは、プラットフォームを識別するために次のマクロを定義します。
    マクロ 説明
    _WIN64 64 ビットのプラットフォームです。
    _WIN32 32 ビットのプラットフォームです。 この値は、旧バージョンとの互換性のため、64 ビット コンパイラによっても定義されます。
    _WIN16 16 ビットのプラットフォームです。

コンパイラー・オプション/Wp64

64ビットの移植性の問題を検出するコンパイラー・オプション/Wp64は廃止されたようだ。

64ビットの移植性の問題を検出するには、Viva64(PVS-Studio)というツールもあるようだ。

コマンドラインでコンパイル

コマンドラインでVC++をコンパイル(ビルド)する場合、開発者コマンドプロンプトでは32bit版のプログラムとしてコンパイルされる。 64bit版としてコンパイルするには64bit版Windowsではx64 Native Tools Command Prompt for VS 2019を32bit版Windowsではx86_x64 Cross Tools Command Prompt for VS 2019を使えばよい。 これは、「開発者コマンドプロンプトと32bit,64bit開発環境 - 愚鈍人」で述べた通りである。

統合開発環境でのコンパイルと構成マネージャーの設定

ではVisual Studio 2019のIDE(統合開発環境)ではどうであろうか? Visual Studio 2019 IDEで新しいソリューション(プロジェクト)を作成した場合にデフォルトでは構成マネージャーにはx86に設定されていて32bit版のプログラムが生成される。 これを64bit版の開発環境に切り替えるには単純にアクティブリソリューションプラットフォームをx64に切り替えれば良い。

リソリューションプラットフォーム

構成マネージャーを表示させるにはいくつかの方法があるが、メニュー バーで [ビルド] > [構成マネージャー] の順に選択するのも一つの方法である。

構成マネージャー

構成マネージャーでのふるまいが私にはなんとも把握しずらくて、そのふるまいを試行錯誤しながら調べてみた。
アクティブリソリューションプラットフォームをx64に切り替えれば良いと言ったが、
x86からx64に切り替えてもプロジェクトのプロパティはx86の設定がx64の設定に引き継がれる訳では無い。
x86環境でプロジェクトのプロパティを変更していた場合には、同じような環境に設定するにはx64環境で再度プロジェクトのプロパティを変更する必要がある。
例えば、プロジェクトのプロパティで追加のインクルードデレクトリ等を変更していた場合は、x64環境では同様に追加のインクルードデレクトリ等を変更する必要がある。
x86とx64では別々のインクルードデレクトリを参照する可能性があるので、これは考えてみれば当たり前の事なのだが、
また、アクティブリソリューションプラットフォームをx64に切り替えるとリソリューションファイルやプロジェクトのデレクトリの直下にx64というデレクトリが作成されていて64bit用にコンパイル時に生成されたファイルがそれぞれのデレクトリに保存される事になる。

参考までにConsoleApplication1というソリューションの直下にConsoleApplication1というプロジェクトを作成して、これをx86とx64でDebugビルドとReleaseビルドをおこなった場合のデレクトリ構成を示す。

ConsoleApplication1     <- ソリューションデレクトリ
 ├ ConsoleApplication1  <- プロジェクトデレクトリ
 |  ├ Debug            <- x86のDebugビルド中間デレクトリ
 |  ├ Release          <- x86のReleaseビルド中間デレクトリ
 |  └ x64
 |      ├ Debug        <- x64のDebugビルド中間デレクトリ
 |      └ Release      <- x64のReleaseビルド中間デレクトリ
 ├ Debug                <- x86のDebugビルド出力デレクトリ
 ├ Release              <- x86のReleaseビルド出力デレクトリ
 └ x64
     ├ Debug            <- x64のDebugビルド出力デレクトリ
     └ Release          <- x64のDebugビルド出力デレクトリ

デフォルトではプロジェクトデレクトリ直下のDebugとReleaseデレクトリにはそれぞれのビルド時に生成されるobj などの中間ファイルが格納される。
そして、ソリューションデレクトリ直下のDebugとReleaseデレクトリには出力デレクトリとしてそれぞれのビルド時に生成されるexeファイル等のビルド成果物が格納される。

中間デレクトリと出力デレクトリの位置はプロジェクトのプロパティの「全般」より変更できるようだ。

ビルド出力ディレクトリの変更

また、x86やx64以外にも独自の名前の新しいプラットフォームを新規に作成できる。
ここで言うプラットフォームとはコンパイル環境と言った意味になるのかと思う。
各プラットフォーム毎にプロジェクトに対して異なるプロジェクトのプロパティを設定できて別々のコンパイル環境を指定できる。

新しいプラットフォームを作成するためには[アクティブソリューションプラットフォーム] ドロップダウンリストで、[ <新規作成...] を選択する。
[新しいソリューションプラットフォーム] ダイアログボックスが表示される。
上段のテキストボックスには適当な任意のプラットフォームの名前を入力、設定のコピー元には32bitか64bitかでもともと登録されているx86かx64かを選択できる。
新しいプラットフォームを作成するのチェックボックスをチェックするとプラットフォームの名前がデフォルトで表示されるARM以外を指定すると以下のようにメッセージボックスが表示されてエラーになってしまうようである。
このARMというのはArm版Windows用のArmネイティブコードアプリを開発するためのものだろうか?

リューションプラットフォームでx64のプラットフォームを設定しても構成マネージャーでプロジェクトのプラットフォームを個別に指定する事で、プロジェクト毎に32bitビルド環境と64bitビルド環境を指定する事も可能なようである。

これまでの説明は、私が構成マネージャーの設定を適当にいじって試行錯誤した結果であるが、マイクロソフトソフトの公式ドキュメントの説明と一致しない。
私が何か勘違いをしているのだろうか?それともVisual Studio 2019 IDEで設定方法が代わってしまったのだろうか?どうもよくわからない。

ちなみにC#だと
visual studio 構成マネージャ 新規作成は

ページのトップへ戻る