آموزش درایور نویسی - قسمت دوم - ارتباط با سطح کاربر
در قسمت قبل طریقه ساخت یک درایور و لود کردن آن را یاد گرفتیم، و همچنین با ابزارهای ماند OSRLoader و DbgView آشنا شدیم. در این آموزش هم قصد داریم ببینیم چگونه میتوان درایوری نوشت که بتواند با یک برنامه سطح کاربر ارتباط برقرار کند در واقع اطلاعاتی به درایور بفرسیم و دریافت کنیم.
مقدمه
در ابتدا باید بگویم مثالی که برای این آموزش انتخاب کرده ام یکی از مثالهای بسته WDK به نام ioctl است که در مسیر زیر قرار گرفته.
[WDK path]\7600.16385.1\src\general\ioctl\
اینجا دو پوشه به نامهای wdm و kmdf وجود دارد. این دو در واقع معماری های متفاوت درایور نویسی در ویندوز است. معماری دیگری هم قبلا وجود داشت به نام VxD که بر می گردن به زمانی که ویندوزهای نسخه 95-98 وجود داشتند ولی الان دیگه استفاده نمی شود. آموزش هایی که من فعلا قرار می دهم بر پایه wdm است و فعلا با kmdf کاری نداریم. در این نمونه سورس روش های مختلفی که می توان میان سطح کاربر و سطح کرنل ارتباط برقرار کرد را نشان داده است. من قسمت هایی از این سورس را حذف کرده ام نسخه تغییر یافته را از این لینک می توانید بگیرید و ما در ادامه در مورد مفاهیمی مانند IOCTL, IRP, Driver Object, Device Name, Symbolic Link آشنا می شویم.
Driver Object چیست؟
هر درایوری که در سیستم لود شده باشد از دید سیستم عامل یک Driver Object دیده می شود در واقع در سیستم عامل ویندوز خیلی چیز ها مثل فایل (File)، پوشه (Directory)، فرایند (Process)، نخ (Thread) و ... را به صورت Object می بیند. هر کدام از این آبجکتها برای خود ساختار مشخصی دارند. برای آبجکت درایور در ویندوز ما ساختاری به نام _DRIVER_OBJECT داریم که در پایین محتویات آن را می بینید. من این محتویات را از فایل wdm.h داخل بسته WDK برداشته ام
typedef struct _DRIVER_OBJECT {
CSHORT Type;
CSHORT Size;
PDEVICE_OBJECT DeviceObject;
ULONG Flags;
PVOID DriverStart;
ULONG DriverSize;
PVOID DriverSection;
PDRIVER_EXTENSION DriverExtension;
UNICODE_STRING DriverName;
PUNICODE_STRING HardwareDatabase;
PFAST_IO_DISPATCH FastIoDispatch;
PDRIVER_INITIALIZE DriverInit;
PDRIVER_STARTIO DriverStartIo;
PDRIVER_UNLOAD DriverUnload;
PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1];
} DRIVER_OBJECT;
شما در آموزش اول با DriverUnload اشنا شدید که تابعی تعریف کردیم که در هنگام Unload شدن درایور توسط سیستم عامل صدا زده می شود. از این مقادیر یکسری از اسمشان تا حدودی مشخص است که چه هستند. باقی هم فعلا چون به کار ما نمیاید کاری با آنها نداریم. چیزی که بیشتر مورد توجه ما است MajorFunction است.
MajorFunction چیست؟
اول با استفاده از مفاهیمی که از زبان C می دانیم قسمتی که مربوط به MajorFunciton است را شرح می دهیم. اینجا ما یک آرایه از اشارگرهایی که از نوع DRIVER_DISPATCH هستند داریم و طول آرایه ما IRP_MJ_MAXIMUM_FUNCTION + 1 می باشد. مقادیر DRIVER_DISPATCH و IRP_MJ_MAXIMUM_FUNCTION را هم از فایل wdm.h بدست می آوریم
DRIVER_DISPATCH در واقع تعریف یک تابع است به این صورت
DRIVER_DISPATCH (
_In_ struct _DEVICE_OBJECT *DeviceObject,
_Inout_ struct _IRP *Irp
);
typedef DRIVER_DISPATCH *PDRIVER_DISPATCH;
و IRP_MJ_MAXIMUM_FUNCTION که مقداری برابر 0x1b دارد که به صورت هگزادسیمال آمده که در واقع در مبنای دسیمال عدد ۲۷ می شود
#define IRP_MJ_MAXIMUM_FUNCTION 0x1b
این به معنی است که این آرایه می تواند ۲۸ (۲۷+۱) تابع از نوع DRIVER_DISPATCH را در خود داشته باشد.
DriverObject->MajorFunction[0] = Function0
DriverObject->MajorFunction[1] = Function1
DriverObject->MajorFunction[2] = Function2
DriverObject->MajorFunction[3] = Function3
...
DriverObject->MajorFunction[27] = Function27
از آنجایی که این توابع برای هر ایندکس از این آرایه قرار است کار مشخصی انجام دهد ایندکس های ۰-۲۷ خود به صورت یک نام قابل فهم تعریف شده اند.
#define IRP_MJ_CREATE 0x00
#define IRP_MJ_CREATE_NAMED_PIPE 0x01
#define IRP_MJ_CLOSE 0x02
#define IRP_MJ_READ 0x03
#define IRP_MJ_WRITE 0x04
...
...
#define IRP_MJ_PNP 0x1b
در نتیجه اگر ما بخواهیم از این تعاریف استفاده کنیم به این شکل استفاده میکنیم
DriverObject->MajorFunction[IRP_MJ_CREATE] = Function0
DriverObject->MajorFunction[IRP_MJ_CREATE_NAMED_PIPE] = Function1
DriverObject->MajorFunction[IRP_MJ_CLOSE] = Function2
DriverObject->MajorFunction[IRP_MJ_READ] = Function3
...
DriverObject->MajorFunction[IRP_MJ_PNP] = Function27
هر درایور برای اینکه ارتباط با دنیای بیرون داشته باشد که این ارتباط می تواند از طرف درایورهای دیگر یا از طرف برنامه های سطح کاربر باش. ملزم است آرایه MajorFunciton را مقدار دهی کند. بسته به نوع و کارکرد درایور شما مقداردهی این آرایه متفاوت خواهد بود. راهنمایی که همراه WDK نصب می شود برای هر نوع درایور مشخص کرده که مقدار دهی چه Major Funciton هایی واجب و کدام اختیاری است.
اگر دقت کنید همه این تعاریف با عبارت IRP شروع می شوند. IRP مختصر شده I/O Request Packet است. در مورد IRP در آموزش های بعدی توضیح میدهم وفتی به Filter Driver برسیم. ولی در همین حد بگویم که تمام درخواست هایی که به درایور میرسد به بصورت یک ساختار IRP در می آید.
برنامه های سطح کاربر با فراخوانی یکسری API ها می توانند درخواست خود را به درایور بفرستند. و تابع مربوط به درخواست در درایور اجرا شود در پایین لیست API و در مقابل در خواستی که تولید می شود آورده ام.
CreateFile ==> IRP_MJ_CREATE
ReadFile ==> IPR_MJ_READ
WriteFile ==> IRP_MJ_WRITE
CloseFile ==> IRP_MJ_CLOSE
DeviceIoControl ==> IRP_MJ_DEVICE_CONTROL
در استفاده از CreateFile برای اینکه با درایورمان مرتبط شود از نامی مشخص استفاده می کنیم در مورد این نام بعد در همین آموزش توضیح داده ام. باقی توابع از هندلی که از CreateFile به دست آمده استفاده میکنند. حالا در ادامه نمونه سورسی که آماده کرده ام بررسی میکنیم تا بیشتر با این موضوع آشنا بشویم.
توضیح در مورد این توابع و پارامترهای آنها را به عهده خودتان می گذارم با اینکه به جز DeviceIoControl که در ادامه در مورد آن توضیح می دهم باقی از نامشان می شود حدس زد کارشان چیست ولی اگر برایتان نامفهوم است در موردشان جستجو کنید.
بررسی Major Function در سورس کد sioctl.c
نمونه سورسی که ما استفاده کرده ایم بخشی که Major Funciton ها را مقدار میدهد به این صورت است.
DriverObject->MajorFunction[IRP_MJ_CREATE] = SioctlCreateClose;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = SioctlCreateClose;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = SioctlDeviceControl
;در این درایور سه درخواست کنترل می شود که دو تا از این درخواست ها توسط یک تابع بررسی می شود. قبلا گفتم که یکسری درخواست ها هستند که کنترل برخی درخواست ها برایشان اجباری است. در واقع وقتی سیستم عامل درخواستی که مربوط به درایور ما می شود. برای ما میفرستد ( یا در واقع Major Function مربوط به آن درخواست را صدا می زند) انتظار دارد. به این درخواست پاسخی از طرف درایور ما به سیستم عامل داده شود.
تابع SioctlCreateClose وظیفه کنترل درخواست های IRP_MH_CREATE و IRP_MJ_CLOSE را دارد. کد مربوط به این تابع را در زیر می بینید. اگر دقت کنید تابع از نوع DRIVER_DISPATCH است که بالاتر دیدیم.
NTSTATUS
SioctlCreateClose(
PDEVICE_OBJECT DeviceObject,
PIRP Irp
)
{
UNREFERENCED_PARAMETER(DeviceObject);
PAGED_CODE();
Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
IoCompleteRequest( Irp, IO_NO_INCREMENT );
return STATUS_SUCCESS;
}
اگر به کد دقت کنید می بینید تقریبا کاری انجام نمی دهد. در واقع به خاطر اجباری بودن پاسخ به این درخواست مجبوریم به این صورت عمل کنیم. به صورت کلی این کد به سیستم عامل می گوید که
اولا:درخواست مربوطه توسط درایور بررسی شد و خطایی رخ نداد، با این کد
Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0
;دوما: در این کد می گوییم این درخواست در این مرحله به اتمام رسیده و به سیستم عامل نیز با این تابع اعلام میکنیم. شاید الان این کد زیاد معنی نداشته باشد. ولی به هر حال این کار را باید در مورد درخواست هایی که کاری با آنها نداریم ولی باید پاسخی در قبال دریافت آنها به سیستم عامل بفرسیم انجام دهیم. من در آموزش دیگری که در مورد IRP است این مباحث را توضیح می دهم.
IoCompleteRequest( Irp, IO_NO_INCREMENT );
حالا میرسیم به تابع SioctlDeviceControl که می شود گفت مهمترین قسمت از کد این درایور است. در این تابع درخواستی که از سمت کاربر می آید بررسی می شود و متناسب با در خواست کاری را انجام می دهد. بخشی که تصمیم می گیرد بر اساس دستور سطح کاربر چکار باید کند به این صورت تعریف شده است.
switch ( irpSp->Parameters.DeviceIoControl.IoControlCode )
{
case IOCTL_SIOCTL_METHOD_BUFFERED:
// کد مربوط به این دستور
break;
case IOCTL_SIOCTL_METHOD_NEITHER:
// کد مربوط به این دستور
break;
case IOCTL_SIOCTL_METHOD_IN_DIRECT:
// کد مربوط به این دستور
break;
case IOCTL_SIOCTL_METHOD_OUT_DIRECT:
// کد مربوط به این دستور
break;
default:
}
مقدارهایی که برای هر دستور case مشخص شده فرمان هایی است که از طرف کاربر می آید این نام گذاری را توسط خودمان انجام می شود قابل تغییر هستند. به جای هر کدام از مقادیر IOCTL_SIOCTL_METHOD_BUFFERED یا IOCTL_SIOCTL_METHOD_NEITHER و ... هر نامی را می توان تعریف کزد.
از این چهار مورد من فقط یکی را در این آموزش آورده ام، چون آوردن باقی هم حجم و هم پیچیپگی این آموزش را زیاد می کند. در مورد این دستورات که به آنها IOCTL و چگونه تولید می شوند بخشی جدا اختصاص داده ام.
دسترسی به بافر داده
از سه طریق می توانیم به بافر حاوی داده ارسالی دسترسی داشده باشیم این سه روش به قرار زیر است
- روش Buffered I/O : در این روش سیستم عامل "حافظه سیستمی" به اندازه حافظه ی داده ای که می خواهید به درایور بفرستید ایجاد میکند و داده ما را در آن قرار می دهد. و بالعکس در موقع دریافت داده سیستم عامل داده را از "حافظه سیستمی" به حافظه کاربر انتقال می دهد
- روش Direct I/O : در این روش سیستم عامل آدرس سطح کاربر را قفل می کند و بعد از این آدرس حافظه اصطلاحا یک MDL (Memory Descritor List) می سازد و آن را به درایور می فرستد.
- روش نه Buffered I/O و نه Direct I/O : در این حالت آدرس مجازی که ما در سطح کاربر به داده خود اختصاص داده ایم را عینا در سطح کرنل دریافت می کنیم
این نمونه کد بخاطر اینکه همه حالت ها را پشتیبانی می کرد به صورت مشخص تعریف نکرده از کدام روش برای دسترسی به حافظه داده استفاده کند. ولی اگر بخواهیم مشخصا تعیین کنیم از کدام روش قرار است استفاده کنیم باید این مقدار را تغییر دهید
pDeviceObject->Flagsبررسی بخش دسترسی به حافظه داده از کد sioctl.c مربوط به درایور
برای دسترسی به حافظه داده ما سه کار باید انجام دهیم
اول) اندازه حافظه ورودی یا خروجی را می توانیم به صورت زیر بدست آوریم. از این لینک اطلاعات بیشتر در مورد ساختار IO_STACK_LOCATION را دریافت کنید.
irpSp = IoGetCurrentIrpStackLocation( Irp );
inBufLength = irpSp->Parameters.DeviceIoControl.InputBufferLength;
outBufLength = irpSp->Parameters.DeviceIoControl.OutputBufferLength;
دوم) گرفتن حافظه داده مان که به این صورت بدست می آید. از این لینک اطلاعات بیشتر در مورد ساختار IRP
inBuf = Irp->AssociatedIrp.SystemBuffer;
outBuf = Irp->AssociatedIrp.SystemBuffer;
سوم) گرفتن کد IOCTL که از سمت برنامه کاربر رسیده و در دستور switch-case استفاده شده
irpSp->Parameters.DeviceIoControl.IoControlCode
NT Device Name و DOS Device Name
تا الان در مورد اینکه چگونه با تعریف Major Funciton ها درخواست هایی که به درایور ما می آیند را کنترل کنیم و همچنین مختصر توضیحی هم در رابطه با روش های موجود برای دسترسی به حافظه داده ی درخواستیمان داده ایم. حالا مساله ی دیگری که باید با آن آشنا باشید Device Name است. در ویندوز برای دسترسی به یکسری چیزها یا دستگاه ها از API های مربوط به فایل استفاده می شود مثل Files, Hard Disk, Serial Port, Parallel Port, ... و اگر قبلا با C/C++ و API های ویندوز کد سیستمی نوشته باشید احتمالا می دانید که با تابعی مثل CreateFile اول یک هندل از دستگاه مربوطه می گرفیتم بعد مثلا با توابعی مثل ReadFile یا WriteFile داده می خواندیم و می نوشتیم در هنگام هندل گرفتن با تابع CreateFile هم از نام هایی مانند \\.\PhysicalDrive0, \\.\PhysicalDrive1, \\.\COM1, ... استفاده می کردیم. حالا حتما می گویید این دستگاه ها چکار به درایور دارد. خوب در واقع کسی(برنامه سطح کربر و درایور های سطح کرنل دیگر) دسترسی مستقیم به درایور شما ندارند و برای اینکه دیگران بتوانند به شما درخواستی بفرستند اصطلاحا باید یک Device Name بسازید این Device Name خود دو نوع است NT Device Name که این نام فقط قابل دسترسی برای دیگر درایورها و کدهای سطح کرنل است و برنامه های سطح کربر دسترسی به این نام ندارن.د این نام ها با عبارت \Device\ شروع می شوند. دیگری DOS Device Name است که این نام امکان دسترسی برنامه های سطح کاربر را مهیا می کند که این نامها هم با عبارت \DosDevices\ شروع می شوند. کاری که شما باید انجام بدهید این است که اول NT Device Name را با تابع IoCreateDevice بسازید بعد باید یک رشته تعریف کنید که حاوی نام DOS شما می شود و در نهایت این دو با هم توسط تابعی به نام IoCreateSymbolicLink متصل می شوند. این کار حتما باید انجام شود یعنی باید نام NT شما ایجاد شده باشد و چون برنامه سطح کاربر دسترسی به این نام ندارد نیاز است نامی تعریف کنیم که در سطح کاربر قابل دسترسی باشد.این کدی است که این نام ها را ایجاد میکند. نام های Symbolic Link با عبارت \\.\ شروع می شوند که مثال هایی برای این نوع بالاتر زده ام
#define NT_DEVICE_NAME L"\\Device\\SIOCTL"
#define DOS_DEVICE_NAME L"\\DosDevices\\IoctlTest"
...
...
RtlInitUnicodeString( &ntUnicodeString, NT_DEVICE_NAME );
ntStatus = IoCreateDevice(
DriverObject,
0,
&ntUnicodeString,
FILE_DEVICE_UNKNOWN,
FILE_DEVICE_SECURE_OPEN,
FALSE,
&deviceObject );
...
...
RtlInitUnicodeString( &ntWin32NameString, DOS_DEVICE_NAME );
ntStatus = IoCreateSymbolicLink(
&ntWin32NameString, &ntUnicodeString );
اکثر توابع کرنلی از رشته های نوع UNICODE_STRING استفاده میکنند. این رشته ساختاری است با تعریف زیر. در نتیجه رشته خود را با استفاده از تابع RtlInitUnicodeString تبدیل به UNICODE_STRING می کنیم.
typedef struct _LSA_UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING, *PUNICODE_STRING;
در مورد پارامترهای تابع IoCreateDevice
۱) DriverObject: این مقدار را از پارامتری که در DriverEntry است می توانید بگیرید
۲) DeviceExtensionSize: یک مقدار عددی است که به اندازه آن برایتان فضایی تخصیصی داده می شود و از این فضا می شود به عنوان نگهداری وضعیت های داخلی درایور استفاده کرد. ما چون نیاز نداریم صفر قرار می دهیم
۳) DeviceName: همان نام NT که بالاتر به آن اشاره کردیم که باید از نوع UNICODE_STRING باشد
۴) DeviceType: بسته به کاری که می خواهیم انجام دهیم می توانیم از نوع های تعریف شده برای device خود استفاده کنیم. این مقادیر را در این لینک می توانید ببینید. چون device ما با هیچ نوع خاصی از سخت افزار ارتباطی ندارد مقدار FILE_DEVICE_UNKNOWN را قرار می دهیم
۵) DeviceCharacteristics: یکسری خصوصیات دیگری که درایور ما می تواند داشته باشد
۶) Exclusive: این پارامتر را FALSE قرار می دهیم
۷) DeviceObject: اینجا device ایجاد شده بر می گردد
توضیحات بیشتر تابع IoCreateDevice را در این لینک ببینید.
ابزاری به نام WinObj وجود دارد که Device Name و Symbolic Name های تعریف شده در سیستم را نمایش می دهد. مثلا در شکل زیر من بعد از اینکه درایور را با OSR Loader لود کردم اطلاعاتی که در شکل آمده اضافه شده اند.
بررسی تابع DriverUnload
در این قسمت وقتی قرار است درایور unload شود باید یکسری تغییراتی که در سیستم داده ایم را به حالت قبل بر گردانیم در واقع Device و Symbolic Link ایجاد شده باید حذف گردد ما این کار را به این دو دستور انجام می دهیم
IoDeleteSymbolicLink( &uniWin32NameString );IoDeleteDevice( deviceObject );
بررسی برنامه سطح کاربر با سورس فایل testapp.c
بخش سطح کاربر به صورت کلی سه کار انجام می دهد.
اول) نصب درایور که ما در آموزش اول این کار را با ابزار OSR Loader انجام می داده ایم. ولی اینبار بد نیست ببینید بدون استفاده از ابزارهای جانبی و از طریق کدنویسی چگونه می شود این کار را انجام داد. API های استفاده شده مربوط به کار با سرویسها در ویندوز می باشد که به غیر از ایجاد سرویس از آنها برای لود درایور هم می توان استفاده کرد. بررسی توابع را به عهده ی خودتان می گذارم چون هدفم از این آموزش ها بیشتر اشنایی به مباحث کرنلی است. به هر حال
دوم) گرفتن هندل با استفاده از CreateFile که قبلا گفتم نامی که به عنوان پارامتر به این تابع می دهیم Symbolic Link ایجاد شده برای device ما است. به این صورت
if ((hDevice = CreateFile( "\\\\.\\IoctlTest",
GENERIC_READ | GENERIC_WRITE,
0,
NULL,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL)) == INVALID_HANDLE_VALUE) {
...
...
}
سوم) آماده کردن بافر خود و فرستادن دستورات IOCTL به درایور. برای فرستان یک دستور IOCTL به یک device از تابع DeviceIoControl باید استفاده کنیم. به غیر از اینکه IOCTL هایی که خودمان تعریف کرده ایم تا به device درایور خود بفرستیم. در ویندوز تعدادی IOCTL های عمومی نیز وجود دارد که کارهای خوب و بد زیادی می توان با آنها انجام داد. (به هر حال)
ما به این صورت از تابع استفاده کرده ایم
bRc = DeviceIoControl ( hDevice,
(DWORD) IOCTL_SIOCTL_METHOD_BUFFERED,
&InputBuffer,
(DWORD) strlen ( InputBuffer )+1,
&OutputBuffer,
sizeof( OutputBuffer),
&bytesReturned,
NULL
);
توضیح پارامترها:
۱) hDevice: هندل خروجی که از CreateFile می گیرید را اینجا باید قرار دهید
۲) dwIoControlCode: کد IOCTL ما که پایین تر توضیح داده ام چگونه این کد تولید می شود
۳) lpInBuffer: بافری است که ما می خواهیم به درایور بفرستیم این بافر می تواند هر فرمتی مثلا یک رشته یا struct باشد
۴) nInBufferSize: طول بافر ورویدی
۵) lpOutBuffer: بافری که درایور قرار است اطلاعاتی در آن قرار دهد
۶) nOutBufferSize: طول بافر خروجی
۷) lpBytesReturned: اندازه اطلاعاتی که در بافر خروجی ریخته می شود. در واقع اندازه واقعی ممکن از کو چکتر از اندازه بافر باشد که این پارامتر مشخص می کند
۸) lpOverlapped: این پارامتر برای ما مهم نیست.
توضیحات بیشتر اینجا
ساختار IOCTL چیست؟
تا الان باید بدانید ioctl چیست. یک کد که در واقع یک عدد است و ما در سمت سطح کاربر می خواهیم بسازیم و از طریق آن به درایور خود بگوییم فلان کار را انجام بدهد. ساختاری که این کد ۳۲ بیتی دارد در شکل زیر نمایش داده شده است.
ioctl را با این ماکرو ایجاد می کنند. که IOCTL_Device_Function نامی اختیاری است.
#define IOCTL_Device_Function CTL_CODE(DeviceType, Function, Method, Access)
توضیح پارامترها
۱) DeviceType: از آنجایی که درایور ما برای سخت افزار خاصی نیست کاری با مقدارهای تعریف شده نداریم. مقادیر که می دهیم باید مقداری بیشتر از 0x8000 داشته باشد چون مقادیر زیر 0x8000 برای ماکروسافت رزرو شده اند
۲) FunctionCode: این مقدار در واقع عملیاتی است که قرار است اتفاق بیافتد خود ما مقدار آن را مشخص میکنم و این عدد باید بیشتر از 0x800 باشد چون مقادیر کمتر رزرو شده اند
۳) TransferType: نوع دسترسی به حافظه داده که قبلتر در مورد آن صحبت کردیم و شامل این مقادیر می تواند باشد
METHOD_BUFFERED
METHOD_IN_DIRECT
METHOD_OUT_DIRECT
METHOD_NEITHER
۴) RequiredAccess: میزان دسترسی که در هنگام هندل گرفتن از device باید وارد کنیم ما در درایور خود همه دسترسی ها را گرفتیم یعنی هم خواندن و هم نوشتن
با استفاده از این کد ioctl را تعریف کرده ایم
#define IOCTL_SIOCTL_METHOD_BUFFERED \
CTL_CODE( SIOCTL_TYPE, 0x902, METHOD_BUFFERED, FILE_ANY_ACCESS )
پایان