電脳昔話・2

第三勢力:ほらふきクラブ

 WindowsXP/2000用のHDDユーティリティの作り方について書く。
 おそらく甲州画報を読んでいるほとんどの人にはなんのメリットも興味も無いだろうが、穴埋め記事なので一回ほどは容赦して欲しい。
 まずWindows環境からHDDにセクタ単位でアクセスする必要がある。MS-DOSやWindows95/98/meならばDISK BIOSを呼び出すことで、WindowsXP/2000の場合にはCreateFile()で物理ドライブをオープンすることでアクセスを行なう。
 で、物理ドライブのオープン処理は、たとえば次のようになる。

HANDLE OpenDevice(int DevNum)
{
    HANDLE hDrive;
    CHAR szDevName[32];
    wsprintf(szDevName,"\\\\.\\PhysicalDrive%d",DevNum);
    hDrive = CreateFile(
        szDevName,
        GENERIC_READ|GENERIC_WRITE,
        FILE_SHARE_READ|FILE_SHARE_WRITE,
        NULL,
        OPEN_EXISTING,
        FILE_ATTRIBUTE_NORMAL, NULL);
    if(hDrive == INVALID_HANDLE_VALUE) {
        hDrive = NULL;
    }
    return hDrive;
}

 この関数の引数DevNumは物理ドライブに割り振られた番号。通常はCドライブが0で、接続されたドライブ順に割り振られている。
 これを順にオープンに失敗するまで繰り返してもいいし、また接続されたドライブはレジストリの  HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Disk\Enum
に記述されている。(ここの記述でIDE、SCSI、USB などの接続区別も可能)
 この関数で得られたデバイスハンドルを使用して通常のファイルと同様にReadFile()、WriteFile()、SetFilePointer()を呼び出し、物理デバイスの任意の場所にアクセスできる。ただしアクセスはバッファリングされないので必ずセクタ単位(HDDの場合は512byte)で行なう必要がある。(当然関数の呼び出しはセクタ単位ではなくバイト単位)
 次の関数は任意のセクタからデータを読み出し、書き込みする処理の例。(書き込みは Read が Write に変わるだけなので省略する)

BOOL ReadSector(HANDLE hDrive,ULONG sector, ULONG len, LPBYTE buff)
{
    LARGE_INTEGER point;
    point.QuadPart = sector * 512;
    DWORD ret;
    ret = SetFilePointer(hDrive,
        point.LowPart, &point.HighPart,
        FILE_BEGIN);
    if((ret==INVALID_SET_FILE_POINTER) && GetLastError() != NO_ERROR) {
        return FALSE;
    }
    DWORD retLen;
    BOOL Result;
    ULONG length = len * 512;
    Result = ReadFile(hDrive,buff, length, &retLen, NULL);
    return Result && (retLen == length);
}

 もちろんSeek処理とRead/Write処理をひとつの関数にまとめる必要はない。このあたりは設計の問題にすぎない。
 ディスクの情報を取得するには、オープンした物理ドライブにIOCTL_DISK_GET_DRIVE_GEOMETRY をパラメータとしてDeviceIoControl()を呼び出す。しかしこの処理は128GBを越える容量のドライブでは正しい値を返さない。
 またIOCTL_DISK_GET_DRIVE_GEOMETRY_EXを使用すると正しい容量を取得できるが、WindowsXP以降でないと動作しない。
 このままではWindows 2000で128GBを越えるドライブを使用した場合すべて128GBと認識されてしまう。しかしWindows 2000で正しく動作するプログラムがある以上はなんらかの方法があるのは間違いない。(S.M.A.R.T関連の機能を使用するれば良いと考えられるが、いま手元に資料がないし実験もしてないので今回は省略。MicroSoftのサイトにサンプルプログラムがある)
 次にIOCTL_DISK_GET_DRIVE_LAYOUTを使用するとパーティション情報が取得できる。こちらは128GB以上でも正しい値を返すので、Windows 2000の環境で128GBよりも大きなパーティションを確保したドライブの場合そのままではドライブ容量よりもパーティション容量の方が大きいなどといった矛盾した情報が取得されることになる。
 IOCTL_DISK_SET_DRIVE_LAYOUTでパーティション情報の設定、IOCTL_DISK_DELETE_DRIVE_LAYOUTで削除、IOCTL_DISK_CREATE_DISKでドライブ情報の作成などDeviceIoControl()を使用してパーティション関係の処理を行なうことができる。
 また、HDDの先頭セクタはMBRと呼ばれ、このMBRを直接読み書きすればDeviceIoControl()を使用せずにパーティション情報を取得・操作することができる。
 簡単な構造なので直接パーティションテーブルを書き換えた方が簡単なのだが、DeviceIoControl()の処理とMBRの直接操作とを混在するとWindowsXPでは相互に処理が反映されない現象が発生する。そこで、

BOOL DriveUpdate()
{
    if(OsVer == OS_WINXP) {
        DWORD RetLen;
        return DeviceIoControl(hDrive,
            IOCTL_DISK_UPDATE_PROPERTIES,NULL, 0, NULL, 0, &RetLen, NULL);
    }
    else
        return TRUE;
}

などとした処理を呼び出す必要がある。(この処理はWindows 2000ではエラーになるので、先頭でOSをチェックする必要がある)
 IOCTL_DISK_SET_DRIVE_LAYOUTで取得できるパーティションのSignatureは、MBRの先頭から440、441バイトにリトルエンディアンで書き込まれている。ここはブートストラップローダの領域なのでWindows XP/2000以外で作成した場合どうなっているかは調べていない。
 ただこの個所が0の場合、ディスクの管理ツールを起動すると「新しいデバイスを見つけました」とメッセージが表示されドライブ作成メニューが起動するのでなんらかの値が設定されているのだろう。
 HDDを調べてみると、パーティションの先頭は直前のパーティションから、なぜか63セクタ空けて確保されている。直後からパーティションを作成みても特に問題なく動作しているようなのではっきりとはわからないが、このあたりはIBMが仕様を決めているのかもしれない。(複数のOSなどで使用するのでなければ、パーティションにしてもMBRにしても使用する環境で独自に決めれば良いだけだし)
 ディスクユーティリティに必要な処理はいろいろあるので個々に頑張るとして、ファイルシステムがFAT/FAT32なら処理も簡単だけどNTFSとなると仕様の解析も含めてかなり面倒なことになる。(NTFSの仕様って公開されてなかったかも、少しずつ拡張されているようだし)
物理ドライブへの直接アクセスはAdministrator権限を持ったユーザで使用する必要がある。このあたりはOSの判別と一緒にプログラムの先頭で判定し警告することになる。逆にドライブの検索は、最近ではUSBなどで起動中に挿抜されるので、適時行なう必要がある。
 あと、連続した多数のセクタにアクセスする場合に1セクタづつ読み書きするとパフォーマンスが悪いのでなるべくまとめて読み書きするほうが良い。ただし実際にどれだけの単位で処理されるかはDisk Driverしだいになる。(しかも元々IDE HDDは256セクタ以上は同時に処理できない。LBA48対応に拡張されたときに、仕様が65536セクタまでに拡張された)




●第三勢力の頁に戻る