وبسایت شخصی حسن هاشمی

برنامه نویس. ایران. قم :))

آشنایی اجمالی با درایورها در ویندوز

توی این پست سعی می کنم سوالاتی که در مورد درایورها برای بعضی از برنامه نویس هایی که غالباً از زبان های سطح بالاتر استفاده می کنن پیش میاد رو جواب بدم.

قبل از شروع باید بگم که حجم زیادی از مطالبی که اینجا میگم روی میتونید توی کتاب مرجع توسعه درایور برای ویندوز بنام 

Programming the Microsoft Windows Driver Model در اینجا مطالعه کنید.

این کتاب یه مقدار قدیمی هست و از ویندوز 7 به بعد حجم زیادی از درایورها از فضای کرنل به فضای کاربر انتقال پیدا کردن، ولی حقیقتش بهتر ازین کتاب توی این مبحث من ندیدم. می تونید با این استارت بزنید و بقیه رو توی سایت مستندات مایکروسافت دنبال کنید.

خب پس بذارید اول درایور رو اینجوری تعریف کنیم:

درایور یعنی یه سری تابع که کرنل اونا رو invoke می کنه تا بتونه با سخت افزاری که خودش نمیشناسه ارتباط برقرار کنه. سناریویی رو در نظر بگیرید که کرنل بخواد 100 بایت دیتا روی شبکه ارسال کنه اما هیچ دیدی نسبت به جزئیات پیاده سازی و نحوه ارتباط با کارت شبکه رو نداره. چرا؟ چون سیستم عامل رو شرکت x نوشته و کارت شبکه تولید شده شرکت y هست.  در این حالت کرنل دستور رو از طریق درایور به کارت شبکه ارسال می کنه.


حالا قبل از اینکه جلوتر بریم بذارید مطمئن بشیم که  شما با مفهوم User-Mode و Kernel-Mode مشکلی ندارین. 

User-Mode حالتی از اجرای کد هست که غیر قابل اعتماد فرض میشه و عملاً دسترسی محدودی داره. برنامه هایی که تحت این حالت اجرا میشن ابدا به RAM و بقیه قسمت های سخت افزاری دسترسی مستقیم ندارن و تعداد زیادی از دستورات CPU هم براشون باز نیست. مثل دستورات تغییر بردار سرویس دهنده های وقفه یا Interrupt Handler Vector و ... .

در صورتی که کد توی این حالت بخواد دستوراتی رو اجرا کنه که اجازه نداره، CPU خطایی رو تولید می کنه که بعداً توسط سیستم عامل هندل میشه و غالباً خطاهایی اینجوری از نوع AccessViolation هست که منجر به kill شدن Process میشه که کد تحت اون اجرا شده.


Kernel-Mode حالتی از اجرای کد هست که دسترسی کامل به تمامی امکانات سیستمی اعم از پردازنده و بقیه سخت افزارها داره. و قاعدتا kernel خودش در این حالت اجرا میشه. 

مکانیزم همکاری سیستم عامل و CPU در این میان از طریق CPU Protection Ring هست، که اونم خودش شامل یه سری لایه امنیتی روی CPU هست که توسط سازنده های CPU تولید میشه. هر لایه از CPU دارای محدودیت های خاص هست.


هر موقع که برنامه ای نیاز به ارتباط با سخت افزار داشته باشه از طریق یه system call اول حالت به kernel-mode تغییر می کنه و بعد ازون ادامه کار از طریق درایور اجرا میشه. 

یه نمونه از پیاده سازی حرکت بین حالت هایX86  رو توی C براتون میذارم:

void __cstd changeMode(IRPS*) {
__asm {
cli
mov ax, x01
push [eax], dword YOUR_RING
push dword, YOUR_RING_ESP // CHANGE THE STACK POINTER
// باید وضعیت به اصطلاح پشته رو عوض کنید :))
// چه ترجمه تابلویی هست، نمیدونم چجوری به این ترجمه رسیدن
push dword, YOUR_RING_CS
dword [esp], 0x200
push dword YOURRING_EIP
iret
}
}

اگه دنبال یه پیاده سازی درست و حسابی هستین می  تونید سورس کد لینوکس رو ببینید. اینجا

ولی خب نکته ش توی دستور x86 iret هست که می تونید مستنداتش رو یه سرچ کوچیک بزنید، فت وفراوون موجود هست.


خب حالا برگردیم سر بحث اصلی:

از نظر نحوه اجرای درایورها، ما 2 جور درایور داریم: 1- user mode driver   و 2- kernel mode driver 


User-Mode Driver :

دلیل وجود درایورهای User-Mode جلوگیری از ترکیدن کل سیستم هست، :) یعنی اگه خطایی توی کد درایور توی حالت user پیش بیاد، تنها اتفاقی که میفته unload شدن همون درایور خاص و یا اون process خاص هست. (درایورهای حالت کاربر توی یه process جدا اجرا میشن) ولی اگه درایور در حالت کرنل منجر به خطا بشه باعث بوجود اومدن اون صفحه آبی  Blue screen of death میشه. و خیلی شانس بیارید سیستم رو ریستارت می کنه.


Kernel-Mode Driver:

درایورهایی که توی فضای کرنل لود و اجرا میشن و در نتیجه دسترسی کلی به تمامی امکانات سخت افزای اعم از دیوایس ها و حافظه دارن. (و نوشتنشون هم جدا خیلی دردسرداره چون دیباگ کردنش مرد کهن میخواد). سرعت اجرای این درایورها بیشتر هست چون نیازی به چک هایی که برای درایورهای سطح کاربر هستن ندارن.



بخشی از ویندوز که درایورها رو مدیریت می کنه WDF یا Windows Driver Framework گفته میشه و در کل مجموعه یه سری component هستن که باهم ارتباط دارن و این ارتباطشون اجرای درایور و ارتباط با سخت افزار رو محیا می کنه و شامل بخش های اصلی زیر هستن:


توی این دیاگرام Driver یه struct هست که نمایانگر یه درایور فیزیکی هست (کدی که شما می نویسید) و اونو با یه structure به نام DRIVER_OBJECT که توسط IO System با مقادیر پیش فرض ساخته میشه. و به کد درایور پاس داده میشه و درواقع میشه گفت وظیفه اصلیش شناسوندن بخش های مختلف Driver به کرنل هست و ساختارش به این شکل هست که توی فایل Wdm.h وجود داره: (Wdm.h بخشی از wdk هست )

پارامترهای اصلیش رو زیر توضیح میدم و بقیه چیزا رو همینجوری کامنت میزنم براتون:

typedef struct _DRIVER_OBJECT {
  PDEVICE_OBJECT     DeviceObject; // توضیحات در پایین  
PDRIVER_EXTENSION  DriverExtension; // توضیحات در پایین 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, *PDRIVER_OBJECT;


1- PDEVICE_OBJECT:

یه اشاره گر هست به لیست دیوایس هایی که مربوط به درایور هستن، خود PDEVICE_OBJECT خودش یه linked-list هست. این struct کلی عضو دیگه هم داره که اطلاعات مربوط به دیوایس رو encapsulate می کنه. و هر آبجکت از این struct هم توسط تابع IoCreateDevice ساخته میشن. و در نتیجه نمونه سازی ازین struct به عهده خود درایور هست.

2-  PDRIVER_EXTENSION:

یه اشاره گر به PDRIVER_EXTENSION هست که در واقع حاوی یه اشاره گر به تابع AddDevice هست، این تابع توسط PNP که همون سرویس شناسایی خودکار درایورها (Plug and play) هست invoke میشه. و یه دیوایس رو به لیست دیوایس های مربوط به درایور اضافه می کنه.


3- PDRIVER_STARTIO:

که یه اشاره گر هست به تابعی که عملیات IO رو شروع می کنه، عملیاتی IO یی هم که توسط سیستم درخواست شده توسط یه struct دیگه بنام IRP به این تابع پاس داده میشه که در ادامه توضیح میدم.


4- PDRIVER_DISPATCH: 

یکی از مهم ترین پارامترها هست، و یه آرایه از یه سری function pointer هست که اعمال درایور رو تعیین می کنه. هر عضو ازین آرایه به ازای کدی که توی IRP هست یه تابع ست می کنه. این پارامتر باید توی Entry Point مقدار دهی بشه.


IRP:

یه struct هست که نمایانگر یه درخواست برای انجام عملیات io هست. برای حالا فقط تعریف این struct رو براتون میذارم، بعدا انشالله سر فرصت میام براتون توضیحات و کامنت میزنم:

typedef struct _IRP {
PMDL            MdlAddress;
ULONG           Flags;
union {
struct _IRP  *MasterIrp;
PVOID       SystemBuffer;
} AssociatedIrp;
KPROCESSOR_MODE RequestorMode;
IO_STATUS_BLOCK IoStatus;
BOOLEAN         Cancel;
BOOLEAN         PendingReturned;
PVOID           UserBuffer;
KIRQL           CancelIrql;
PDRIVER_CANCEL  CancelRoutine;
union {
struct {
union {
KDEVICE_QUEUE_ENTRY DeviceQueueEntry;
struct {
PVOID DriverContext[4];
};
LIST_ENTRY ListEntry;
};
PETHREAD   Thread;
} Overlay;
} IRP, *PIRP;
} Tail;

فقط باید به کدهای مربوط به IRP توجه داشته باشید. این کدها نقطه اصلی تشخیص نوع عملیات برای درایورها هستن:

IRP_MJ_CLEANUP

IRP_MJ_CLOSE

IRP_MJ_CREATE

IRP_MJ_DEVICE_CONTROL

IRP_MJ_FILE_SYSTEM_CONTROL

IRP_MJ_FLUSH_BUFFERS

IRP_MJ_INTERNAL_DEVICE_CONTROL

IRP_MJ_PNP

IRP_MJ_POWER

IRP_MJ_QUERY_INFORMATION

IRP_MJ_READ

IRP_MJ_SET_INFORMATION

IRP_MJ_SHUTDOWN

IRP_MJ_SYSTEM_CONTROL

IRP_MJ_WRITE

Loading