potisanのプログラミングメモ

趣味のプログラマーがプログラミング関係で気になったことや調べたことをいつでも忘れられるようにメモするブログです。はてなブログ無料版なので記事の上の方はたぶん広告です。記事中にも広告挿入されるみたいです。

C# 5.0 DeviceIoControl関数でボリュームの物理ドライブを取得する

目的とサンプルコード

Win APIを用いてデバイス名からボリュームデバイスのハンドルを開いたり、DeviceIoControl関数を用いてボリューム(論理ボリューム)の所属する物理ドライブを取得するサンプルコードです。

DiskExtent構造体やGetDiskExtentsメソッドでパディングを強引に追加していますが(sizeof(UInt32) * 2/*Padding*/等)、元の構造体(VOLUME_DISK_EXTENTS.aspx)やDISK_EXTENT.aspx))が8バイト毎にパッキングされていることに由来しています。

VolumeDeviceIoControl.cs

using System;
using System.ComponentModel;
using System.IO;
using System.Runtime.InteropServices;
using System.Security;
using System.Security.Permissions;
using Microsoft.Win32.SafeHandles;

namespace Katabamisan.Windows.DeviceIoControl
{
    [SecurityPermission(SecurityAction.LinkDemand, UnmanagedCode = true)]
    public sealed class VolumeDeviceIoControl : IDisposable
    {
        [SuppressUnmanagedCodeSecurity]
        private static class NativeMethods
        {
            [DllImport("kernel32.dll", SetLastError = true,
                CallingConvention = CallingConvention.StdCall,
                CharSet = CharSet.Auto)]
            public static extern SafeFileHandle CreateFile(
                 [MarshalAs(UnmanagedType.LPTStr)] string filename,
                 [MarshalAs(UnmanagedType.U4)] FileAccess access,
                 [MarshalAs(UnmanagedType.U4)] FileShare share,
                 IntPtr securityAttributes,
                 [MarshalAs(UnmanagedType.U4)] FileMode creationDisposition,
                 [MarshalAs(UnmanagedType.U4)] FileAttributes flagsAndAttributes,
                 IntPtr templateFile);

            [DllImport("kernel32.dll", SetLastError = true, CallingConvention = CallingConvention.StdCall)]
            [return: MarshalAs(UnmanagedType.Bool)]
            public static extern bool DeviceIoControl(
                SafeFileHandle hDevice,
                UInt32 dwIoControlCode,
                IntPtr lpInBuffer,
                UInt32 nInBufferSize,
                IntPtr lpOutBuffer,
                UInt32 nOutBufferSize,
                [Out]out UInt32 lpBytesReturned,
                IntPtr lpOverlapped);
            [DllImport("kernel32.dll", SetLastError = true, CallingConvention = CallingConvention.StdCall)]
            [return: MarshalAs(UnmanagedType.Bool)]
            public static extern bool DeviceIoControl(
                SafeFileHandle hDevice,
                UInt32 dwIoControlCode,
                IntPtr lpInBuffer,
                UInt32 nInBufferSize,
                [MarshalAs(UnmanagedType.LPArray)] byte[] lpOutBuffer,
                UInt32 nOutBufferSize,
                [Out]out UInt32 lpBytesReturned,
                IntPtr lpOverlapped);
        }

        private const int ERROR_MORE_DATA = 234;
        private const UInt32 IOCTL_VOLUME_DISK_EXTENTS = 0x00560000;

        private SafeFileHandle handle;

        public VolumeDeviceIoControl(char driveLetter)
        {
            OpenVolumeDevice("\\\\.\\" + driveLetter + ":");
        }

        public VolumeDeviceIoControl(string devicePath)
        {
            OpenVolumeDevice(devicePath);
        }

        private void OpenVolumeDevice(string devicePath)
        {
            handle = NativeMethods.CreateFile(
                devicePath,
                0,
                FileShare.ReadWrite,
                IntPtr.Zero,
                FileMode.Open,
                0,
                IntPtr.Zero);
            if (handle.IsInvalid)
                throw new Exception(
                    "ボリュームデバイスのハンドルが開けませんでした。",
                    new Win32Exception());
        }

        ~VolumeDeviceIoControl()
        {
            Dispose();
        }

        public SafeFileHandle Handle { get { return handle; } }

        public void Dispose()
        {
            if (!handle.IsClosed)
                handle.Dispose();
            GC.SuppressFinalize(this);
        }

        public void ThrowExceptionWhenDisposed()
        {
            if (handle.IsClosed)
                throw new ObjectDisposedException(GetType().FullName);
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct DiskExtent
        {
            public UInt32 DiskNumber;
            public Int64 StartingOffset;
            public Int64 ExtentLength;
            public DiskExtent(byte[] value, int startIndex)
            {
                DiskNumber = BitConverter.ToUInt32(value, startIndex);
                startIndex += sizeof(Int32) * 2/*padding*/;
                StartingOffset = BitConverter.ToInt64(value, startIndex);
                startIndex += sizeof(Int64) ;
                ExtentLength = BitConverter.ToInt64(value, startIndex);
            }
        }

        public DiskExtent[] GetDiskExtents()
        {
            ThrowExceptionWhenDisposed();

            byte[] buffer = new byte[sizeof(UInt32) * 2/*Padding*/ + Marshal.SizeOf(typeof(DiskExtent))];
            uint returnedBytes;
            UInt32 numberOfDiskExtents = 0;
            if (!NativeMethods.DeviceIoControl(
                handle,
                IOCTL_VOLUME_DISK_EXTENTS,
                IntPtr.Zero, 0,
                buffer, checked((uint)buffer.Length),
                out returnedBytes,
                IntPtr.Zero))
            {
                if (Marshal.GetLastWin32Error() != ERROR_MORE_DATA)
                {
                    throw new Win32Exception();
                }
                else
                {
                    numberOfDiskExtents = BitConverter.ToUInt32(buffer, 0);
                    buffer = new byte[sizeof(UInt32) * 2 +
                        numberOfDiskExtents * Marshal.SizeOf(typeof(DiskExtent))];
                    if (!NativeMethods.DeviceIoControl(
                        handle,
                        IOCTL_VOLUME_DISK_EXTENTS,
                        IntPtr.Zero, 0,
                        buffer, checked((uint)buffer.Length),
                        out returnedBytes,
                        IntPtr.Zero))
                    {
                        throw new Win32Exception();
                    }
                }
            }
            else
            {
                numberOfDiskExtents = BitConverter.ToUInt32(buffer, 0);
            }
            int position = sizeof(UInt32) * 2/*Padding*/;
            DiskExtent[] diskExtents = new DiskExtent[numberOfDiskExtents];
            for (UInt32 i = 0; i < numberOfDiskExtents; i++)
            {
                diskExtents[i] = new DiskExtent(buffer, position);
                position += Marshal.SizeOf(typeof(DiskExtent));
            }
            return diskExtents;
        }
    }
}

サンプルコード

以下のコードではバイト数を文字列に変換する関数を利用していることに注意して下さい。ソースコードC#でWin APIを使ってバイトの単位を変換するにあります。

using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using Katabamisan.Windows.DeviceIoControl;
using Katabamisan.Windows.ByteFormat;

namespace VolumeDeviceIoControl1
{
    static class Program
    {
        static void Main()
        {
            StringBuilder builder = new StringBuilder();
            foreach (char driveLetter in DriveInfo.GetDrives().Select(driveInfo => driveInfo.Name[0]))
            {
                using (VolumeDeviceIoControl volumeDeviceIoControl = new VolumeDeviceIoControl(driveLetter))
                {
                    builder.AppendLine(driveLetter + ":");
                    try
                    {
                        foreach (VolumeDeviceIoControl.DiskExtent diskExtent in volumeDeviceIoControl.GetDiskExtents())
                        {
                            builder.AppendLine(" DiskNumber: " + diskExtent.DiskNumber);
                            builder.AppendLine("  StartingOffset: " + diskExtent.StartingOffset.FormatByteSize());
                            builder.AppendLine("  ExtentLength: " + diskExtent.ExtentLength.FormatByteSize());
                            builder.AppendLine();
                        }
                    }
                    catch (Exception ex)
                    {
                        builder.AppendLine(" <ERROR>" + ex.Message);
                        builder.AppendLine();
                    }
                }
            }
            MessageBox.Show(builder.ToString());
        }
    }
}

実行例は以下のようになります。確認環境ではOSを含むハードディスク(DiskNumber: 0)がC、G、Sの3つのパーテーションに分けてあります(正確には先頭にシステム領域を含む名無しパーテーションも存在しているのでC:ドライブのSutartingOffset≠0です)。また、E:はCD/DVDドライブなのでエラーが発生しており、U:はUSBです。

ボリュームレター(論理ドライブ名)が異なるC、G、SはDiskNumberを共通しているので同じ物理ディスク上に存在していることが分かります。

2021/3/10:この記事は別のブログで投稿した記事を移動したものです。