Device Object と Device Stack

こんにちは、K里です。今回は、以前の記事で紹介しました WDM (Windows Driver Model) ドライバのデバイス オブジェクトとデバイス スタックについて説明します。

デバイス オブジェクト
Windows OS は、システムに接続される全てのデバイスをデバイスオブジェクトという情報を用いて制御します。各デバイスには、1 つ以上のデバイス オブジェクトが関連付けられていて、デバイスの操作は、このオブジェクトを対象に機能します。カーネルモードドライバは、デバイスに対して少なくとも 1 つ以上のデバイス オブジェクトを作成することになります。実際のソースコードレベルでは、デバイスオブジェクトは、DEVICE_OBJECT 構造体として wdm.h に定義されています。DEVICE_OBJECT 構造体で定義されている各メンバについては、下記サイトをご覧ください。

DEVICE_OBJECT Structure
https://msdn.microsoft.com/en-us/library/ff543147(VS.85).aspx

また、デバイスオブジェクトは、ドライバに応じて以下のように 3 種類のオブジェクトに分類されます (データ自体は、全て上記 DEVICE_OBJECT 構造体になります)。

Physical Device Object (PDO): バスドライバに対するデバイス オブジェクト
Functional Device Object (FDO): ファンクションドライバに対するデバイス オブジェクト
Filter Device Object (filter DO): フィルタドライバに対するデバイス オブジェクト

なお、例外として、クラスドライバやポートドライバに関連付けられるミニ(クラス/ポート)ドライバでは、デバイスオブジェクトを作成しない場合があります。また、逆に特定デバイスクラスのミニドライバ (NDIS ミニポートドライバや USB ミニポートドライバなど) では、自身がデバイス オブジェクトを作成し、クラスドライバやポートドライバが作成しない場合もあります。

デバイス スタックとドライバ スタック
通常デバイスを制御するためには、複数のドライバが必要となりますが、カーネルは、ドライバの入れ替えを容易にするために、階層構造のフレームワークを提供しています。これにより、あるドライバを別のドライバに置き換えたとしても、対象ドライバの上位、下位に存在するドライバへの影響を軽減することができます。このドライバの階層構造をドライバスタックと呼びます。ドライバが階層構造となるために、ドライバに関連するデバイス オブジェクトもまた階層構造となり、こちらはデバイス スタックと呼びます。同じような名称ですので混同しがちですが、実際の I/O 制御では、デバイス スタックをベースに処理が行われます。ドライバ スタックとデバイス スタックのイメージは、下記サイトの図が参考になると思います。

USB Driver Stack for Windows XP and Later
https://msdn.microsoft.com/en-us/library/ff539311(VS.85).aspx
USB Device Stack for Windows XP and Later
https://msdn.microsoft.com/en-us/library/ff539301(VS.85).aspx

デバイス スタックの例
デバイスオブジェクトとオブジェクトがスタックされるシーケンスを下記サイトの図を基に説明します。

Example WDM Device Stack
https://msdn.microsoft.com/en-us/library/ff544545(VS.85).aspx

① PCI バスに対する PDO と FDO

ルートバスドライバが、内部システムバス上で現在接続されているデバイスを列挙し、各デバイスに対してデバイスオブジェクト (PDO) を作成します。それらの内 1 つが PCI バスに対する PDO となります。カーネル内コンポーネントの PnP マネージャーは、PCI バスのドライバを認識し、対象ドライバがまだロードされていない場合は、ドライバをロードします。その後、PCI ドライバの AddDevice ルーチンにて、PCI バスに対する FDO を作成し、PCI バスの PDO にスタックします。以上の動作で、PCI バスに対するデバイススタックが構築されました。

 

② USB ホストコントローラーに対する PDO と FDO

PnP マネージャーが、PCI ドライバに対して、動作開始要求を通知することで PCI ドライバは、PCI バス上のデバイスを列挙します。例えば、今回の場合、USB ホストコントローラーを検知し、それに対する PDO を作成します。図の白い矢印は、USB ホストコントローラーが、PCI バスの「子デバイス」であることを示しています。PnP マネージャーは、USB ホストコントローラーに対するドライバをロードし、①と同様に FDO を作成し、デバイススタックにアタッチします。ただ、前述の通り FDO を作成するのは、ミニポートドライバとなります。

 

③ USB ハブに対する PDO と FDO

上記 ①、② に続き同様の動作となります。USB ホストコントローラードライバは、USB バス上のデバイスを列挙し、USB ハブを検知し、それに対する PDO を作成します。USB ハブドライバがロードされ、FDO を作成し、デバイス スタックにアタッチします。

 

④ ジョイスティックコントローラーに対する PDO と FDO と 2 つの filter DO

PDO を作成するまではこれまでと同様です。USB ハブにてデバイスを列挙し、ジョイスティックコントローラーに対する PDO を作成します。今回の場合、ジョイスティックコントローラーに関するレジストリ情報内にフィルタドライバ (upper/lower) が設定されていることで、PnP マネージャーは、フィルタドライバをロードすることになります。ロード後、フィルタドライバは、自身の filter DO を作成し、デバイス スタックにアタッチします。PnP マネージャーは、ジョイスティックコントローラーに対するドライバ (クラスドライバとミニクラスドライバ) をロードし、FDO を作成し、デバイススタックにアタッチします。最後にフィルタドライバ (upper) がロードされ、自身の filter DO を作成し、デバイス スタックにアタッチします。

 

デバッガからの確認

最後にデバイスオブジェクトとデバイス スタックについて、WDK サンプルプログラムの Toaster を使ってデバッガ上でどのように処理が行われているかを確認します。今回は、WDM 版 Toaster (WinDDK\7600.16385.1\src\general\toaster\wdm) を使用しています。

 

事前に以下のように break point を設定しておきます。

0: kd> bu busenum!DriverEntry

0: kd> bu busenum!Bus_AddDevice

0: kd> bl

 0 eu 0001 (0001) (busenum!DriverEntry)

 1 eu 0001 (0001) (busenum!Bus_AddDevice)

0: kd> g

 

Toaster のバス ドライバ (BusEnum.sys) をインストールすると、バス ドライバインストール時に、ドライバがロードされ DriverEntry が呼ばれます。

Breakpoint 0 hit

busenum!DriverEntry:

aefdf410 8bff mov edi,edi

 

1: kd> knL

 # ChildEBP RetAddr

00 8eb27698 833f5740 busenum!DriverEntry

01 8eb2787c 833fcfff nt!IopLoadDriver+0x7ed

02 8eb27928 83441ea4 nt!PipCallDriverAddDeviceQueryRoutine+0x34b

03 8eb27960 8344a0f8 nt!RtlpCallQueryRegistryRoutine+0x2cd

04 8eb279cc 833fa120 nt!RtlQueryRegistryValues+0x31d

05 8eb27aa8 833f988e nt!PipCallDriverAddDevice+0x383

06 8eb27ca4 83402681 nt!PipProcessDevNodeTree+0x15d

07 8eb27cd8 83265f56 nt!PiRestartDevice+0x8a

08 8eb27d00 832bff3b nt!PnpDeviceActionWorker+0x1fb

09 8eb27d50 834606d3 nt!ExpWorkerThread+0x10d

0a 8eb27d90 833120f9 nt!PspSystemThreadStartup+0x9e

0b 00000000 00000000 nt!KiThreadStartup+0x19

 

DriverEntry で必要なルーチンが登録され、処理を進めると AddDevice ルーチン (busenum!Bus_AddDevice) が呼ばれます。この時、第 2 引数の PhysicalDeviceObject で、BusEnum.sys の PDO が通知されます。

1: kd> g

Breakpoint 1 hit

busenum!Bus_AddDevice:

aefdaa60 8bff mov edi,edi

 

1: kd> knL

 # ChildEBP RetAddr

00 8eb27970 83266767 busenum!Bus_AddDevice

01 8eb2798c 834017cb nt!PpvUtilCallAddDevice+0x19

02 8eb279d4 833fa302 nt!PnpCallAddDevice+0xb9

03 8eb27aa8 833f988e nt!PipCallDriverAddDevice+0x565

04 8eb27ca4 83402681 nt!PipProcessDevNodeTree+0x15d

05 8eb27cd8 83265f56 nt!PiRestartDevice+0x8a

06 8eb27d00 832bff3b nt!PnpDeviceActionWorker+0x1fb

07 8eb27d50 834606d3 nt!ExpWorkerThread+0x10d

08 8eb27d90 833120f9 nt!PspSystemThreadStartup+0x9e

09 00000000 00000000 nt!KiThreadStartup+0x19

 

1: kd> dv /V PhysicalDeviceObject

8eb2797c @ebp+0x0c PhysicalDeviceObject = 0x87d2b2f8

 

PDO のデータ内容は、以下のようになっています。フラグなどの情報を確認したい場合、!devobj コマンドを使用すると便利です。また、!devstack コマンドで対象デバイスオブジェクトのスタック情報を表示してくれます。この時には、まだ BusEnum に対する FDO が作成されていないのでスタックは PDO のみ表示されます。

1: kd> dt nt!_DEVICE_OBJECT 0x87d2b2f8

   +0x000 Type : 0n3

   +0x002 Size : 0xb8

   +0x004 ReferenceCount : 0n0

   +0x008 DriverObject : 0x859af650 _DRIVER_OBJECT

   +0x00c NextDevice : 0x87026b38 _DEVICE_OBJECT

   +0x010 AttachedDevice : (null)

   +0x014 CurrentIrp : (null)

   +0x018 Timer : (null)

  +0x01c Flags : 0x1040

   +0x020 Characteristics : 0x80

   +0x024 Vpb : (null)

   +0x028 DeviceExtension : (null)

   +0x02c DeviceType : 4

   +0x030 StackSize : 1 ''

   +0x034 Queue : <unnamed-tag>

   +0x05c AlignmentRequirement : 0

   +0x060 DeviceQueue : _KDEVICE_QUEUE

   +0x074 Dpc : _KDPC

   +0x094 ActiveThreadCount : 0

   +0x098 SecurityDescriptor : 0xb695b538 Void

   +0x09c DeviceLock : _KEVENT

   +0x0ac SectorSize : 0

  +0x0ae Spare1 : 0

   +0x0b0 DeviceObjectExtension : 0x87d2b3b0 _DEVOBJ_EXTENSION

   +0x0b4 Reserved : (null)

 

1: kd> !devobj 0x87d2b2f8

Device object (87d2b2f8) is for:

 00000072 \Driver\PnpManager DriverObject 859af650

Current Irp 00000000 RefCount 0 Type 00000004 Flags 00001040

Dacl b695b568 DevExt 00000000 DevObjExt 87d2b3b0 DevNode 85e00008

ExtensionFlags (0x00000810) DOE_START_PENDING

                             Unknown flags 0x00000800

Device queue is not busy.

 

1: kd> !devstack 0x87d2b2f8

  !DevObj !DrvObj !DevExt ObjectName

> 87d2b2f8 \Driver\PnpManager 00000000 00000072 <<<<< PDO

!DevNode 85e00008 :

  DeviceInst is "ROOT\SYSTEM001"

  ServiceName is "busenum"

 

その後、AddDevice ルーチンの内部処理を進めていくと、IoCreateDevice 関数を呼び出して FDO を作成します。作成時には、まだ PDO 0x87258e50 に対してスタックされていないので !devstack コマンドを実施しても単体で表示されます。

1: kd> knL

 # ChildEBP RetAddr

00 8eb27934 aefdaafd nt!IoCreateDevice

01 8eb27970 83266767 busenum!Bus_AddDevice+0x9d

02 8eb2798c 834017cb nt!PpvUtilCallAddDevice+0x19

03 8eb279d4 833fa302 nt!PnpCallAddDevice+0xb9

04 8eb27aa8 833f988e nt!PipCallDriverAddDevice+0x565

05 8eb27ca4 83402681 nt!PipProcessDevNodeTree+0x15d

06 8eb27cd8 83265f56 nt!PiRestartDevice+0x8a

07 8eb27d00 832bff3b nt!PnpDeviceActionWorker+0x1fb

08 8eb27d50 834606d3 nt!ExpWorkerThread+0x10d

09 8eb27d90 833120f9 nt!PspSystemThreadStartup+0x9e

0a 00000000 00000000 nt!KiThreadStartup+0x19

 

1: kd> gu

busenum!Bus_AddDevice+0x9d:

aefdaafd 8945ec mov dword ptr [ebp-14h],eax

 

1: kd> dv /V deviceObject

8eb2796c @ebp-0x04 deviceObject = 0x85d78350

 

1: kd> !devstack 0x85d78350

  !DevObj !DrvObj !DevExt ObjectName

> 85d78350 \Driver\busenum 85d78408 <<<<< FDO

 

更に処理を進めていくことで、IoAttachDeviceToDeviceStack 関数を呼び出して FDO を PDO にスタックします。スタック後 PDO 0x87258e50 と FDO 0x87cc2608 に対して再度 !devstack コマンドを実行すると、以下のように BusEnum.sys のデバイス スタックが作成されたことを確認できます。

1: kd> knL

 # ChildEBP RetAddr

00 8eb27948 aefdac25 nt!IoAttachDeviceToDeviceStack

01 8eb27970 83266767 busenum!Bus_AddDevice+0x1c5

02 8eb2798c 834017cb nt!PpvUtilCallAddDevice+0x19

03 8eb279d4 833fa302 nt!PnpCallAddDevice+0xb9

04 8eb27aa8 833f988e nt!PipCallDriverAddDevice+0x565

05 8eb27ca4 83402681 nt!PipProcessDevNodeTree+0x15d

06 8eb27cd8 83265f56 nt!PiRestartDevice+0x8a

07 8eb27d00 832bff3b nt!PnpDeviceActionWorker+0x1fb

08 8eb27d50 834606d3 nt!ExpWorkerThread+0x10d

09 8eb27d90 833120f9 nt!PspSystemThreadStartup+0x9e

0a 00000000 00000000 nt!KiThreadStartup+0x19

 

1: kd> gu

busenum!Bus_AddDevice+0x1c5:

aefdac25 8b55f0 mov edx,dword ptr [ebp-10h]

 

1: kd> !devstack 0x85d78350

  !DevObj !DrvObj !DevExt ObjectName

> 85d78350 \Driver\busenum 85d78408 <<<<< FDO

  87d2b2f8 \Driver\PnpManager 00000000 00000072 <<<<< PDO

!DevNode 85e00008 :

  DeviceInst is "ROOT\SYSTEM001"

  ServiceName is "busenum"

 

上記処理の流れは、基本的にファンクションドライバやフィルタ ドライバでも同様になります。アプリケーション (Enum.exe など) を用いてToaster バス上にデバイスを追加し、インストールするドライバをファンクションドライバ (toaster.sys) とフィルタドライバ (clsupper.sys, clslower.sys, devupper.sys, devlower.sys) とした時のデバイス スタックは以下のようになります。また、!devnode コマンドで Toaster バスと Toaster デバイスが階層構造になっていることを確認できます。

0: kd> !devnode 0 1 busenum

Dumping IopRootDeviceNode (= 0x859c2518)

DevNode 0x85e00008 for PDO 0x87d2b2f8

  InstancePath is "ROOT\SYSTEM001"

  ServiceName is "busenum"

  State = DeviceNodeStarted (0x308)

  Previous State = DeviceNodeEnumerateCompletion (0x30d)

  DevNode 0x85eed0f0 for PDO 0x87e63790

    InstancePath is "{B85B7C50-6A01-11d2-B841-00C04FAD5171}\MsToaster\1&79f5d87&2&01"

    ServiceName is "toaster"

    State = DeviceNodeStarted (0x308)

    Previous State = DeviceNodeEnumerateCompletion (0x30d)

 

0: kd> !devstack 0x87e63790

  !DevObj !DrvObj !DevExt ObjectName

  87b83020 \Driver\clsupper 87b830d8 <<<<< filter DO

  86ac0888 \Driver\devupper 86ac0940 <<<<< filter DO

  87a9e020 \Driver\toaster 87a9e0d8 <<<<< FDO

  85e78ca0 \Driver\clslower 85e78d58 <<<<< filter DO

  85ef7118 \Driver\devlower 85ef71d0 <<<<< filter DO

> 87e63790 \Driver\busenum 87e63848 00000073 <<<<< PDO

!DevNode 85eed0f0 :

  DeviceInst is "{B85B7C50-6A01-11d2-B841-00C04FAD5171}\MsToaster\1&79f5d87&2&01"

  ServiceName is "toaster"

 

ではまた。