Written in Japanese(UTF-8)
2014.8.30
INASOFT


/トップ/ListView to CSV/ダウンロード/WebHelp/ヘルプトップ

Icon ListView to CSVを作ってみよう!!番外編〜x64を考慮した場合


ListView to CSVの簡単なコマンドライン版を作ってみようという、プログラミング解説講座です。
前回で最終回で、最後の執筆より5年近く経過してしまいましたが、ここでは番外編ということで、x64について考慮した場合について述べたいと思います。

番外編

第4回へ


■64bit版Windowsって? 普通の32bitプログラムを動かすとどうなるの?

私たちはずっと前、16bit から 32bit への変化を体験してきました。具体的には、Windows 3.1やMS-DOS から、Windows 95 や Windows NT への変化です。詳しくは触れませんが、これに伴うプログラミング作法の変化はとても大きなものでした。例えば、int のサイズが 16bit から 32bit へ変化したなど。

そして私たちは今、32bit から 64bit への変化の時代にさしかかっています。Windows XP/Vista/7 には、x86版と呼ばれる32bit版に加え、x64版と呼ばれる64bit版が登場しています。これらのOSのために、どのようなプログラミングスタイルの変化が求められるのでしょうか?

実は、64bit の「あり方」は、1つではありません。開発ソフトの開発メーカーが決めるスタイルが多数あるのです。例えば、先の「intのサイズ」のように変数型のサイズの変化だけを見ても「int=64bit, long=64bit, ポインタ変数=64bit」(ILP64)、「int=32bit, long=64bit, ポインタ変数=64bit」(LP64)、「int=32bit, long=32bit, longlongとポインタ変数=64bit」(LLP64)のように違いがあります。

ここで、64bit版Windowsの開発ソフトではどのタイプが採用されているかというと、LLP64 となっています。つまり、int=32bit, long=32bit, ポインタ型=64bit です。マイクロソフトでは、既存の32bit用に作られたプログラムが、なるべくそのままの姿で64bitへ移植できるよう、最低限の改変だけで済むように考慮した結果、LLP64 になったとしています。

***

さて、普通の32bitプログラムを動かすとどうなるのか、を考える場合に、2通りの考え方があると思います。(1)32bit用に書かれたソースコードを64bit用のコンパイラにかけるとどうなるか? (2)32bit用にビルドされたexeを64bitのWindowsで動かすとどうなるか? です。

まずは(1)から。たいていの場合は、そのままコンパイルが通ってしまいます。ただし、int と ポインタ型でサイズが違いますから、この間で代入をしていたり、両者のサイズが同じであることを前提として書かれているプログラムは、動作不良を起こすことになるでしょう。なお、各種ハンドル(hWnd等)もポインタ型と同サイズと定義されていますから、ハンドルをint等と同列に扱っている場合、問題が起きることがあります。

次に(2)の場合。こちらも、たいていの場合はそのまま動いてしまいます。Windows には 「Windows on Windows 64」(WOW64)という互換の仕組みがあって、これが仲介して正しく動くようになるからです。ただし、次の場合は、注意が必要です。

■ListView to CSVはどうしよう? 1つのソースコードでx86/x64両方へ対応

第3回第4回でも述べておりますとおり、ListView to CSVでは、他プロセスのメモリの読み書きを行っています。これは、重大な問題です。というのも、もしListView to CSVが32bitで、エクスプローラが64bitだったら? ポインタ変数のサイズの違いで、メモリの読み書きがうまくいかなくなります。逆もまた然り。どうしたらよいでしょう?

そこで、ListView to CSVでは、次のような方針を立てて、問題解決としています。

※32bitのListView to CSVであっても、VirtualAllocVlm APIなどを使えば64bitのプロセスのメモリへ正しくアクセスすることは可能なようです。しかし、問題解決を簡易にすることと、64bit用のListView to CSVが存在するんだから、それを使えば良いじゃないかという考えから、32bitのListView to CSVから64bitプロセスへのアクセスは禁じることにしています。

まずは、「32bitのListView to CSVは、〜(中略)〜 64bit OS上では、動かせないようにする。」から実現してみることにしましょう。次の2つの情報が必要であることがわかるかと思います。

前者については、コンパイル時点で知ることができます。64bit 用としてコンパイルが行われる場合、_WIN64 というシンボルが定義されるからです。

後者については、2通りの検出の仕方があります。1つ目は、「自プロセスがWOW64のサポートを受けているかを調べる」。2つ目は、「OS自身がどのアーキテクチャで動いているかを調べる」です。1つめの方法は「IsWow64Process API」を使い、2つ目は「GetNativeSystemInfo API」を使いますが、ここでは1つ目の方法を使うことにしましょう。

さて、ここではサンプルプログラムとして、第4回に登場したサンプルプログラムの main() を使うことにします。太字部分が改修した箇所です。


// --------------------------------------------------------------
// メイン関数
// --------------------------------------------------------------
// 引数:1個の場合→環境内のリストビューのハンドル番号一覧を表示
//    2個の場合→エラー
//    2個以上の場合→1つめのパラメタ(16進数)で指定されたハンドル番号のリストビューを出力
//            2つめのパラメタ(10進数)で、リストビューのカラムの数を指定する
// 戻り値:0=成功  1=失敗
// --------------------------------------------------------------
int main(int argc, char* argv[])
{
// WOW64のサポートを受けて動作しているかの処理は、32bit版のみで必要。64bit版は、WOW64上では絶対に動かない
#ifndef _WIN64
    typedef BOOL (WINAPI* LPFN_ISWOW64PROCESS)(HANDLE hProcess, PBOOL Wow64Process);
    static LPFN_ISWOW64PROCESS IsWow64Process = (LPFN_ISWOW64PROCESS)GetProcAddress(GetModuleHandle("kernel32.dll"), "IsWow64Process");
    BOOL bIsWow64 = FALSE;

    // WOW64のサポートを受けて動作しているか?
    if (IsWow64Process) {
        if (0 == IsWow64Process(GetCurrentProcess(), &bIsWow64)) {
            // エラー処理は省略
            return 1;
        }
        else {
            if (bIsWow64) {
                cerr << "このアプリケーションは32ビットOS用です。64ビットOSでは、x64版をご使用ください。" << endl;
                return 1;
            }
        }
    }
    else {
        // IsWow64Processが存在していないということは、x64が登場していないころのOSということで、32bitである
    }
#endif // _WIN64

    // コマンドラインの解析
    if ( argc < 2 ) {
        // コマンドライン引数が何も指定されていない場合は、
        // リストビューコントロールのハンドル番号一覧を表示。
        showListViewControls();
        return 0;
    }
    else if ( argc < 3 ) {
        // エラー。
        cerr << "リストビューのカラム数を指定してください。" << endl;
        return 1;
    }
    else {
        DWORD dwHandle;
        int   iColumns;

        sscanf(argv[1], "%x", &dwHandle);
        sscanf(argv[2], "%d", &iColumns);
        return (!showListViewItems((HWND)dwHandle, iColumns)); // 各ハンドルは64bitに拡張されているものの、実際には32bit分しか利用されないらしいので、このキャストで許してください
    }
}

■64bitプロセスから32bitプロセスのメモリへアクセス

第3回でも書かれているとおり、他プロセスのリストビューから文字列を得るためには、「他プロセスへのメモリ」へのアクセスが必要です。

ところが、64bitのListView to CSVから32bitのプロセスへのメモリへのアクセスを行うと、どうも正しく情報が取得できません。これはなぜかというと、リストビューへアクセスするために用いている LVITEM の中身に原因があります。

typedef struct tagLVITEMA
{
    UINT mask;
    int iItem;
    int iSubItem;
    UINT state;
    UINT stateMask;
    LPSTR pszText;
    int cchTextMax;
    int iImage;
    LPARAM lParam;
#if (_WIN32_IE >= 0x0300)
    int iIndent;
#endif
    (XPでの拡張情報は省略)
#endif
} LVITEMA, *LPLVITEMA;

ここで赤太字で表したLPSTRは、ポインタ型です。また、LPARAMは「整数値もポインタ型も納めることのできるサイズの型」です。従って、この構造体を32bit向けにコンパイルするのか、64bit向けのコンパイルするのかで、構造体内の各変数の大きさと位置(オフセット)が変わってきてしまうのです。もちろん、構造体全体のサイズも変わってしまいます。

つまり、64bitでコンパイルされたListView to CSVでは上記の構造体のLPSTRとLPARAMを「64bitだ」として扱いますが、32bitでコンパイルされた「アクセスされる側」では、LPSTRとLPARAMを「32bitだ」として扱いますから、ここでズレが生じるというわけです。

では、どうしたらよいか。「アクセスされる側」に合わせ、異なるサイズとなる構造体を2つ準備して挑めばよいことになります。

ここもサンプルプログラムとして、第4回に登場したサンプルプログラムのgetLVTextInOtherProcessを改造してみようかと思います。通常のLVITEMの他に、LVITEMA32という似た構造体を準備します。

(サイズの異なるキャストを行っているため、たくさんのwarningが表示されますが、とりあえず無視することにしましょう)


// --------------------------------------------------------------
// 別プロセスにあるかもしれないリストビューのアイテム文字列を取得
// --------------------------------------------------------------
// 引数 :hListView = リストビューのハンドル
//     iItem     = 取得したい文字列を持つアイテムの番号
//     iSubItem  = 取得したい文字列を持つアイテムのカラム番号
//     szItemText= 取得したい文字列を格納するバッファへのアドレス
// 戻り値:TRUE=成功  FALSE=失敗
// --------------------------------------------------------------
LRESULT getLVTextInOtherProcess(HWND hListView, int iItem, int iSubItem, char *pszItemText)
{
    LVITEM        lvItem;
    LRESULT       lResult = FALSE;

#ifndef _WIN64  // 64bit OSという時点でWindows XP以上であることは確定なので、NT系かどうかを調べる必要はない
    OSVERSIONINFO osvi;

    ZeroMemory(&osvi, sizeof(OSVERSIONINFO));
    osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);
    GetVersionEx( &osvi );
#endif // _WIN64

    ZeroMemory(&lvItem, sizeof(LVITEM));
    lvItem.mask       = LVIF_TEXT;
    lvItem.cchTextMax = MAX_GETITEMTEXT_LENGTH;
    lvItem.iItem      = iItem;
    lvItem.iSubItem   = iSubItem;

#ifndef _WIN64
    if ( osvi.dwPlatformId == VER_PLATFORM_WIN32_NT ) {
#endif // _WIN64
        // ※Windows NT/2000/XP/Vista/7 の場合


       // Windows NT系でしか利用できない関数を、Windows 95系に感知させない形で
       // 呼び出すための工夫
       HMODULE hDLL = GetModuleHandle("kernel32.dll");
       PF_VIRTUALALLOCEX pVirtualAllocEx = (PF_VIRTUALALLOCEX)GetProcAddress(hDLL,"VirtualAllocEx");
       PF_VIRTUALFREEEX  pVirtualFreeEx  = (PF_VIRTUALFREEEX)GetProcAddress(hDLL, "VirtualFreeEx");

       // ※注意:(略)

        DWORD dwProcessId = 0;
        HANDLE hProcess;

        // 対象となるリストビューのハンドルを保持するプロセスのIDを得る
        GetWindowThreadProcessId(hListView, &dwProcessId);
        hProcess = OpenProcess( PROCESS_VM_OPERATION | PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_QUERY_INFORMATION, FALSE, dwProcessId);

        if ( hProcess ) {
#ifdef _WIN64        // 64bitプロセスが32bitプロセスにアクセスする場合の特別処理なので、64bitの場合のみのコード
            typedef BOOL (WINAPI* LPFN_ISWOW64PROCESS)(HANDLE hProcess, PBOOL Wow64Process);
            static LPFN_ISWOW64PROCESS IsWow64Process = (LPFN_ISWOW64PROCESS)GetProcAddress(GetModuleHandle("kernel32.dll"), "IsWow64Process");
            BOOL bIsWow64 = FALSE;

            // 対象プロセスがx86で動いているか? x86ならば、LVITEM 構造体の各メンバのサイズを変えないといけない
            if (IsWow64Process) IsWow64Process(hProcess, &bIsWow64);

            if (bIsWow64) {
typedef struct tagLVITEMA32
{
    UINT mask;
    int  iItem;
    int  iSubItem;
    UINT state;
    UINT stateMask;
    char * __ptr32 pszText;
    int  cchTextMax;
    int  iImage;
    ULONG lParam;
#if (_WIN32_IE >= 0x0300)
    int iIndent;
#endif
} LVITEMA32, *LPLVITEMA32;

                LVITEMA32 lvItem;   // 32bitプロセス向けには、本来のLVITEMを無効化して、こちらのLVITEMA32を使う。
                lvItem.mask       = LVIF_TEXT;
                lvItem.cchTextMax = MAX_GETITEMTEXT_LENGTH;
                lvItem.iItem      = iItem;
                lvItem.iSubItem   = iSubItem;

                // 他プロセス空間内にメモリを確保。
                // 他プロセスのリストビューは、そのリストビューを保持するプロセス内のメモリを
                // 読み書きの対象とするので。
                char   *pvbuf = (char *)pVirtualAllocEx(hProcess, NULL, MAX_GETITEMTEXT_LENGTH+1, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
                LVITEMA32 *pvlvi = (LVITEMA32 *)pVirtualAllocEx(hProcess, NULL, sizeof(LVITEMA32), MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
                // 他プロセス内のバッファへのポインタを指す
                lvItem.pszText = (char * __ptr32)pvbuf;
                // 他プロセス内に準備したLVITEMへ、LVM_GETITEM用のパラメタをコピー
                WriteProcessMemory(hProcess, pvlvi, &lvItem, sizeof(LVITEMA32), NULL);

                // 他プロセス内のポインタを指して、LVM_GETITEMを送る
                lResult = (int)SendMessage(hListView, LVM_GETITEM, 0, (LPARAM)pvlvi);

                if (lResult == TRUE) { // 成功していたら?
                    // 他プロセス内に確保したバッファから、自プロセス内のバッファへ文字列をコピー
                    ReadProcessMemory(hProcess, pvbuf, pszItemText, MAX_GETITEMTEXT_LENGTH, NULL);
                }

                // 他のプロセス内に確保したメモリを解放
                pVirtualFreeEx(hProcess, pvlvi, 0, MEM_RELEASE);
                pVirtualFreeEx(hProcess, pvbuf, 0, MEM_RELEASE);
            }
            else
#endif // _WIN64
            {
                // 他プロセス空間内にメモリを確保。
                // 他プロセスのリストビューは、そのリストビューを保持するプロセス内のメモリを
                // 読み書きの対象とするので。
                char   *pvbuf = (char *)pVirtualAllocEx(hProcess, NULL, MAX_GETITEMTEXT_LENGTH+1, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
                LVITEM *pvlvi = (LVITEM *)pVirtualAllocEx(hProcess, NULL, sizeof(LVITEM), MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
                // 他プロセス内のバッファへのポインタを指す
                lvItem.pszText = pvbuf;
                // 他プロセス内に準備したLVITEMへ、LVM_GETITEM用のパラメタをコピー
                WriteProcessMemory(hProcess, pvlvi, &lvItem, sizeof(LVITEM), NULL);

                // 他プロセス内のポインタを指して、LVM_GETITEMを送る
                lResult = (int)SendMessage(hListView, LVM_GETITEM, 0, (LPARAM)pvlvi);

                if (lResult == TRUE) { // 成功していたら?
                    // 他プロセス内に確保したバッファから、自プロセス内のバッファへ文字列をコピー
                    ReadProcessMemory(hProcess, pvbuf, pszItemText, MAX_GETITEMTEXT_LENGTH, NULL);
                }

                // 他のプロセス内に確保したメモリを解放
                pVirtualFreeEx(hProcess, pvlvi, 0, MEM_RELEASE);
                pVirtualFreeEx(hProcess, pvbuf, 0, MEM_RELEASE);
            }

            // 他プロセスのプロセスハンドルを閉じる
            CloseHandle(hProcess);
        }
#ifndef _WIN64
    }
    else {
        // ※Windows 95/98/Me の場合

        // (中略)
    }
#endif // _WIN64
    return lResult;
}

■おわりに

今回は番外編ということで、筆者の知る64bitの知識を披露してみました。近年は64bitへの関心が高まりつつあるものの、個人プログラムの世界では、まだまだ…と言ったところです。というか、最近ではスクリプトの分野がめざましい進歩を遂げていて、そちらの方が開発者の関心が高いのかもしれません。

なお、ここに記載しているサンプルコードですが、特に筆者に許可を求めなくても、流用してかまいません。ご自由にお使い下さい。

第4回へ


※このページは、ソフトウェアに付属のヘルプファイルをWeb用に再構築したものです。大部分に自動変換を施しているため、一部は正しく変換しきれずに表示の乱れている箇所があるかもしれませんが、ご容赦下さい。また、本ドキュメントはアーカイブドキュメントであり、内容は、右上の作成日付の時点のものとなっております。一部、内容が古くなっている箇所があるかと思いますが、あらかじめご了承下さい。
※このページへは、自由にリンクしていただいてかまいません。

■このページに関するご意見をお待ちしております → フィードバックページ

/トップ/ListView to CSV/ダウンロード/WebHelp/ヘルプトップ