Device Stack چیست؟
امروز می خواهم در مورد Device Stack مطلبی بنویسم. من خودم زمانی که با این مفهوم آشنا شدم برایم این مفهوم کمی گنگ بود. تا اینکه بعد ها روی این موضوع بیشتر کار کردم و جزئیات بیشتری از چگونه نگهداری این ساختار در ویندوز و اینکه یک IRP چه مسیری را طی میکند تا به مقصد بررسد را متوجه شدم. در این پست سعی کرده ام این مسائل را شرح دهم. از آنجایی که در نوشتن مطالب طولانی تنبل هستم تصمیم گرفتم پست های کوتاه تری بگذارم. این آموزش به دو بخش تقسیم شده یک بخش نگاه سطح بالا که بدون وارد شدن به جزئیات به موضوع پرداخته ام و بخش دوم نگاه سطح پایین که نشان داده ام چگونه با استفاده از Windbg ساختار مربوط به Deivce Stack را بیرون بکشیم.
بخش اول - نگاه سطح بالا به Device Stack
ما اصطلاحی داریم به نام Plug and Play (PnP) Device Tree یا به اختصار Device Tree در واقع هر Device یک گره از این درخت می شود. برای هر کدام از این گره ها ما یک Device Stack داریم. Device Stack طبق تعریف مایکروسافت "مجموعه مرتبی از Device Object ها به همراه درایور مرتبط به آن را Device Stack می گویند". جلوتر که در مورد ساختار این درخت توضیح داده ام این جمله برایتان بهتر معنی پیدا می کند. دلیل وجود چنین درختی برای این است که سیستم عامل بداند درخواست به یک Device باید از چه Device های دیگری بگذرد. الان در تصویر پایین می توانید بهتر تصویر گره هایی از device ها را در یک Device Stack فرضی را ببینید.
طبق تصویر بالا می بینید که PCI Bus به تعداد زیادی device اشاره می کند. درایورهای Bus یکی از انواع تقسیم بندی درایور در سیستم عامل ویندوز می باشد. به طور کلی به device این نوع درایور PDO (Physical Device Object)
می گویند. به غیر از Bus ما دو نوع درایور دیگر هم داریم به نام Function Driver و Filter Driver. این طبقه بندی ای است که ماکروسافت تعریف کرده و اینطور که من متوجه شدم این تعریف برای وقتی است که یک device در device stack قرار گرفته باشد. اگر نباشد جزو هیچ کدام از این نوع ها قرار نمی گیرد (اگر این موضوع صحت ندارد خوشحال می شوم در کامنت اطلاع بدهید). به هر حال درایورهای Bus مستقیم با سخت افزار ارتباط دارند و در صورت متصل شدن دستگاه جدید از طریق PnP Manger از وجود device جدید مطلع می شوند. در هنگام بالا آمادن سیستم عامل نیز PnP Manager از درایورهای Bus می خواهد تمام device های متصل به خود را شناسایی کنند. در این هنگام درایور PCI شروع به ایجاد گره device های متصل به خود می کند.
قبل از اینکه ادامه بدهم یک نکته را جا داره اشاره کنم، من در آموزش درایور نویسی دوم در مورد اینکه چگونه device بسازیم توضیح داده بودم. این کار را با استفاده از تابع IoCreateDevice انجام می دادیم. برای افرادی که تازه کار درایور نویسی را شروع کرده اند ممکن است استفاده از Device و Driver که من دائم به آن اشاره می کنم گیج کننده باشد. چرا یک جا می گوییم Device یک جا Driver. اصلا این دو چطور به هم مرتبط هستند؟ من در آموزش دوم گفته بودم که برای ارتباط با یک درایور دیگر باید از device ای که توسط آن درایور ایجاد شده استفاده کنیم. یک درایور میتواند چندین device برای خود ایجاد کند. device هم میتواند دارای نام (مانند نمونه ای که در آموزش دوم درایور نویسی آوردم) باشد یا بدون نام باشد. وقتی هم از Device Stack و ارسال درخواست به یک device صحبت می کنیم در واقع می گوییم که این درخواست از درایور های مرتبط با آن device ها گذشته است.
لازم است توضیحی هم در مورد ساختار پشته (stack) بدهم. همانطور که بالاتر گفتم داخل یک stack همیشه یک درایور از نوع Bus وجود دارد. این device همیشه در پایین stack قرار می گیرد به خاطر اینکه در نهایت باید درخواست دریافت شده را به سخت افزار مربوطه بفرستد. قبل از درایور Bus دو نوع درایور دیگر می توانند در stack قرار بگیرند. این دو نوع درایور عبارتند از Function Driver و Filter Driver.
Funciton Driver Object یا FDO عملیات اصلی در آن stack توسط این درایور انجام می شود. مثلا کار با دیسک، شبکه، کیبورد، ماوس کلا هر دستگاه فیزیکی همه برای خود یک درایور Function دارند که مسٔول پردازش های اصلی درخواست های وارد شده است. هر stack برای خود حتما باید یک درایور Function داشته باشد.
Filter Driver Object یا FiDO این نوع درایورها دو نوع Upper Filter Driver هستند یکی که قبل یا بالای Function Driver قرار می گیرد و دیگری Lower Filter Driver که بعد از Function Driver یا پایین Function Driver داخل یک stack قرار می گیرد. درایور Bus هم می تواند بالای خود Filter Driver داشته باشد. Filter Driver می تواند به هر تعدادی باشد یا اصلا نباشد. این نوع درایورها برای این وجود دارند که اگر درخواستی لازم است قبل از رسیدن به Function Driver یا بعد از آن پردازشی رویش انجام شود با استفاده این درایور انجام شود. مثلا می توانیم با یک Upper Filter Driver روی درایور Disk تمام درخواست هایی که به این درایور فرستاده می شوند را یک جا LOG بگیریم (یک چیزی مثل IDS ها) یا مثلا اطلاعات آن را رمز گذاری کنیم (مثل BitLocker).
تصویر پایین سه stack را نمایش می دهد. در دو تا از stack ها (که در بالا قرار گرفته اند) pci.sys به عنوان درایور Bus قرار گرفته. stack سمت چپ دارای یک Upper Filter Driver است (AfterThought.sys) که می تواند همه درخواست ها را قبل از اینکه به Function Driver برسد را دریافت و حتی تغییر رهد. stack سمت راست دارای یک Lower Filter Driver می باشد که همه درخواست ها را بعد از پردازش توسط Function Driver را می بینید و باز در صورت نیاز تغییر دهد که بالاتر هم اشاره کردیم. و در stack پایینی pci.sys به عنوان Function Driver و acpi.sys به عنوان Bus Driver قرار گرفته اند و هیچ Filter Driver ای نیز وجود ندارد.
بخش دوم - نگاه سطح پایین با استفاده از ابزار Windbg
من این بخش را جدا کردم برای افرادی که علاقه مندن بیشتر با ساختار Device Stack آشنا بشوند. توضیحاتی که بالا آورده ام را دیگر اینجا تکرار نمی کنم. یکسری دستورات جدید خواهم گفت که به کمک آنها بتوانیم اطلاعات مختلفی را از طریق Windbg بدست بیاوریم.
دستور !object
قبلا در آموزش دوم درایور نویسی در مورد Object توضیح داده ام در سیستم عامل ویندوز خیلی مفاهیم مثل File, Process, Driver, Device و چیزهای دیگر به صورت یک Object دیده می شوند. با این دستور می توان از object ها اطلاعات بدست آورد. بگذارید با چند مثال کار با این دستور را یاد بگیریم.
Object ها یک ساختار پوشه مانندی دارند که ریشه این ساختار پوشه ای است که با کاراکتر backslash یا "\" تعریف شده. برای مثال بخواهیم هر چیز داخل این پوشه را ببینیم این دستور را وارد میکنیم
lkd> !object \
Object: e1001108 Type: (825ed260) Directory
ObjectHeader: e10010f0 (old version)
HandleCount: 0 PointerCount: 40
Directory Object: 00000000 Name: \
130 symbolic links snapped through this directory
Hash Address Type Name
---- ------- ---- ----
00 e100d748 Directory ArcName
82224030 Device Ntfs
01 e15f1860 Port SeLsaCommandPort
02 81fc7368 Device FatCdrom
03 e1011490 Key \REGISTRY
05 e15e28d8 Port ThemeApiPort
06 e170e970 Port XactSrvLpcPort
09 e1615860 Directory NLS
10 e1008688 SymbolicLink DosDevices
...
...
32 82338a20 WaitablePort NLAPrivatePort
e1008540 Directory Callback
33 8220c0c0 Event SeLsaInitEvent
82494ae8 Event UniqueSessionIdEvent
35 e1917748 Directory KnownDlls
اگر یادتان باشد در همان آموزش دوم یک ابزار به نام WnObj هم معرفی کردم که این اطلاعات را نمیاش می داد. منتها اینجا شما آدرس object ها را هم دارید
مثلا دوم: اگر بخواهیم لیست درایورهایی که در سیستم load شده اند را ببینیم با دستور زیر این کار را انجام می دهیم که همراه خروجی آمده است. به ساختار پوشه ای هم دقت کنید.
lkd> !object \Driver
Object: e1022968 Type: (825ed260) Directory
ObjectHeader: e1022950 (old version)
HandleCount: 0 PointerCount: 86
Directory Object: e1001108 Name: Driver
Hash Address Type Name
---- ------- ---- ----
00 82437818 Driver Beep
823c0218 Driver NDIS
823c0a40 Driver KSecDD
01 8243ada8 Driver Mouclass
8209c110 Driver Raspti
82065ad0 Driver es1371
02 824c98e8 Driver vmx_svga
03 824c7e88 Driver Fips
824caec8 Driver Kbdclass
04 823c9f38 Driver VgaSave
82085ae8 Driver NDProxy
...
...
34 82397978 Driver AFD
82011740 Driver Ndisuio
35 82031168 Driver hidusb
8243a2a0 Driver Parport
36 820653e0 Driver i8042prt
824ca238 Driver CmBatt
82086c70 Driver IntelIde
مثال های دیگر، خروجی این مثال ها را خودتان امتحان کنید
گرفتن object های device روی سیستم
!object \Device
گرفتن اطلاعات از object درایور NDIS
!object \Driver\NDIS
گرفتن اطلاعات از آدرس یک object که این آدرس را از دستورات بالا می توان بدست آورد مثلا آنجا که object های مربوط به درایورها را نمایش دادیم
!object e102296
دستور !drvobj
با این دستور اطلاعات بیشتری در مورد object یک درایور می توانیم بدست می آوریم. بالاتر گفتم که یک درایور می تواند بیش از یک device برای خود ایجاد کند. این دستور لیست device های یک درایور را نمایش می دهد.
به عنوان مثال لیست device های درایور PCI همانطور که مشاهده می کنید تعداد زیادی device object توسط این درایور ایجاد شده است و دلیلش این است که PCI یک درایور از نوع Bus است و تقریبا تمام درخواست های دیگر درایورها همه در نهایت به این درایور در stack می رسند.
lkd> !drvobj PCI
Driver object (825c4500) is for:
\Driver\PCI
Driver Extension List: (id , addr)
Device Object list:
825e3378 8245ae50 823e80d0 8252f900
8252fac8 8252fc90 8252fe58 8252f020
825303a8 82530570 82530738 82530900
82530ac8 82530c90 82530e58 82530020
824983a8 82498570 82498738 82498900
82498ac8 82498c90 82498e58 82498020
8235d3a8 8235d570 8235d738 8235d900
8235dac8 8235dc90 8235de58 8235d020
82096178 82096340 82096508 820966d0
82096af8 825e49c0 825e4cf8 825e5628
825e5960 825e5c98 8253b2f0 8253b628
8252b9c0 8252bcf8 820a1578 820a18b0
820a1a90 825ad600 825ad7e0 823e7c70
823e7e50 825adcf8 825e6738 825e6a70
8252b378 824f88b0 824f8be8 825e5380
825ac220 825ac030 825ae370 825aecf8
823c2330 8245a518 825ae830 8253be50
824a9e50 823e7688 824f8620 8242f1f0
825ab2e8 825af338 825e62f8 825ab7b8
825b30c0 8245aba8
دستور !devstack
با این دستور میتوانیم stack مربوط به یک device را نمایش بدهیم. مثلا من یکی از Device Object هایی که در دستور قبل بدست آمد را به این دستور می دهم تا stack آن استخراج شود
lkd> !devstack 825e3378
!DevObj !DrvObj !DevExt ObjectName
82493790 \Driver\es1371 82493848 00000078
8245aa88 \Driver\ACPI 825cc008 00000066
> 825e3378 \Driver\PCI 825e3430 NTPNP_PCI0042
!DevNode 8257e1c8 :
DeviceInst is "PCI\VEN_1274&DEV_1371&SUBSYS_13711274&REV_02\4&47b7341&0&1088"
ServiceName is "es1371"
در خروجی بالا می بینیم که دو درایور در بالای درایور PCI ما قرار کرفته. یکی ACPI (مربوط به مدیریت پاور) و دیگری e1371 (مربوط به کارت صدا)
استخراج اطلاعات با دستور dt
من در بالا دستورات سطح بالاتر windbg را گفتم در این حالت ما مستقیم با ساختارهای داخلی سیستم عامل درگیر نیستیم ولی یک موقع هایی شاید لازم داشته باشیم با این ساختارها درگیر شویم. در آموزشی که در مورد windbg نوشته بودم توضیح دادم چگونه ساختارهای _DRIVER_OBJECT
و _DEVICE_OBJECT
را استخراج کنیم.
ساختار _DRIVER_OBJECT
یک عضو به نام DeviceObject دارد. من اطلاعات مربوط به درایور Disk را اینجا آوردم.
lkd> dt _DRIVER_OBJECT 8242e940
nt!_DRIVER_OBJECT
+0x000 Type : 4
+0x002 Size : 168
+0x004 DeviceObject : 0x822259f0 _DEVICE_OBJECT
+0x008 Flags : 0x12
+0x00c DriverStart : 0xf86ca000
+0x010 DriverSize : 0x8e00
+0x014 DriverSection : 0x825ed928
+0x018 DriverExtension : 0x8242e9e8 _DRIVER_EXTENSION
+0x01c DriverName : _UNICODE_STRING "\Driver\Disk"
+0x024 HardwareDatabase : 0x80671860 _UNICODE_STRING "\REGISTRY\MACHINE\HARDWARE\DESCRIPTION\SYSTEM"
+0x028 FastIoDispatch : (null)
+0x02c DriverInit : 0xf86d18ad long +fffffffff86d18ad
+0x030 DriverStartIo : (null)
+0x034 DriverUnload : 0xf86e14b4 void +fffffffff86e14b4
+0x038 MajorFunction : [28] 0xf86e0bb0 long +fffffffff86e0bb0
این یکی از device های ساخته شده توسط درایور Disk است. حالا اطلاعات این device را ببینیم
lkd> dt _DEVICE_OBJECT 0x822259f0
nt!_DEVICE_OBJECT
+0x000 Type : 3
+0x002 Size : 0x368
+0x004 ReferenceCount : 0
+0x008 DriverObject : 0x8242e940 _DRIVER_OBJECT
+0x00c NextDevice : 0x823c1ab8 _DEVICE_OBJECT
+0x010 AttachedDevice : (null)
+0x014 CurrentIrp : (null)
+0x018 Timer : (null)
+0x01c Flags : 0xd0
+0x020 Characteristics : 0x100
+0x024 Vpb : 0x8245a248 _VPB
+0x028 DeviceExtension : 0x82225aa8
+0x02c DeviceType : 7
+0x030 StackSize : 4 ''
+0x034 Queue : __unnamed
+0x05c AlignmentRequirement : 0
+0x060 DeviceQueue : _KDEVICE_QUEUE
+0x074 Dpc : _KDPC
+0x094 ActiveThreadCount : 0
+0x098 SecurityDescriptor : 0xe10178d8
+0x09c DeviceLock : _KEVENT
+0x0ac SectorSize : 0x200
+0x0ae Spare1 : 0
+0x0b0 DeviceObjectExtension : 0x82225d58 _DEVOBJ_EXTENSION
+0x0b4 Reserved : (null)
از این عضوهای داده NextDevice, AttachedDevice و DeviceObjectExtension به کار ما می آیند. اگر درایور دارای device های دیگری باشد NextDevice باید دارای مقدار غیر null باشد که در مثال ما این گونه است. اگر device دارای device هایی در بالای stack خود باشد این device ها از طریق عضو AttachedDevice مشخص می شود. مثلا این device هیچ device ای در بالای stack خود ندارد. لازم به ذکره ما نام درایور مربوط به device را از طریق استخراج اطلاعات عضو DriverObject می توانیم بدست آوریم
هر Device Object یک عضو به نام DeviceObjectExtension نیز دارد که نام ساختار آن _DEVOBJ_EXTENSION
است. که از طریق آن می توانیم به device های پایین stack دسترسی پیدا کنیم.
lkd> dt _DEVOBJ_EXTENSION 0x82225d58
nt!_DEVOBJ_EXTENSION
+0x000 Type : 13
+0x002 Size : 0
+0x004 DeviceObject : 0x822259f0 _DEVICE_OBJECT
+0x008 PowerFlags : 0
+0x00c Dope : 0x8242f900 _DEVICE_OBJECT_POWER_EXTENSION
+0x010 ExtensionFlags : 0
+0x014 DeviceNode : (null)
+0x018 AttachedTo : (null)
+0x01c StartIoCount : 0
+0x020 StartIoKey : 0
+0x024 StartIoFlags : 0
+0x028 Vpb : (null)
که در این ساختار عضو AttachedTo اشاره به device هایی دارد که در پایین device stack ما قرار گرفته. که در این مثال ما هیچ device stack ای در پایین نداریم.
همین کار اگر با دستور !devstack
انجام دهیم همان نتیجه ملاحظه می شود
lkd> !devstack 0x822259f0
!DevObj !DrvObj !DevExt ObjectName
> 822259f0 \Driver\Disk 82225aa8 DP(1)0x7000-0x3bf2ca000+1
حالا همین کار را می توانید با device دیگر که با NextDevice مشخص شده انجام دهید می بینید که برای آن device ما stack داریم.
lkd> !devstack 0x823c1ab8
!DevObj !DrvObj !DevExt ObjectName
82352930 \Driver\PartMgr 823529e8
> 823c1ab8 \Driver\Disk 823c1b70 DR0
8242ea38 \Driver\VMSCSI 8242eaf0 VMSCSI1Port2Path0Target0Lun0
!DevNode 8209e8e8 :
DeviceInst is "SCSI\Disk&Ven_VMware_&Prod_VMware_Virtual_S&Rev_1.0\4&5fcaafc&0&000"
ServiceName is "disk"
همانطور که میبینید در بالا stack ما درایور PartMgr و در پایین stack درایور VMSCSI را داریم.