Home برمجة سي/سي++ أساسيات البرمجه في نظام الويندوز
أساسيات البرمجه في نظام الويندوز

أساسيات البرمجه في نظام الويندوز

264
0

بســم الله الـرحمــن الرحيــم

[starlist][/starlist]

فهم البنية التحتية لأنظمه الويندوز مطلب ضروري لأي مبرمج يريد بناء برامج تعمل بكفائه عالية على نظام ويندوز ، وفي هذه السلسله من الدروس سوف نتناول أساسيات البرمجه تحت نظام الويندوز والتي تعطيك الأساس الصحيح لانشاء واستخدام الProcess/Thread وMemory Management والتعامل مع ال DLL وال SEH .

سوف تبدأ المقالة بشرح كيف يعمل نظام ويندوز ومن ثم نقوم بعرض مراحل تطور النظام وميزاته الأساسية، وكيف تتعامل مع الأخطاء بداخل برنامجك في دوال الويندوز Win32 Function باستخدام مفهوم ال  Error-Code ، وسوف سيتم استعراض دوال الويندوز للتعامل مع الASCII وال Unicode حتى تكتب برامج Unicode بالسي++ وأمنة بدون مشاكل Buffer overflow ، وسوف ستتعرف اخيراً على ال Kernel Objects وكيف يمكن أن تتشاركها في أكثر من Process أو Thread ، وسيتم طرح مثال على الاستفاده منها من خلال منع البرنامج لأن يعمل أكثر من مرة في الويندوز باستخدام ال Mutex.

هذه المقالة تعتبر حجر الأساس لمن يريد البدء في مجال برمجة الويندوز باستخدام سي++ لكي يعمل برامج Low Level مثلاً برامج الحماية أو الفايرول أو برامج التحكم في النظام وغيرها من ال System Software.

1.1. نظرة على أنظمة الويندوز:
أنظمه الويندوز تقسم الى قسمين أساسين الأول وهو Windows NT (ويشمل Windows Server 2003,2000,XP) والثاني وهو Window 9x (ويشمل Windows ME,98,95,16 bit versions). صمم الNT من البداية ليكون 32 بت ويدعم الذاكرة الظاهرية Virtual Memory ويدعم تعدد المسارات Multithreading وتعددية المعالجات Multiprocessors مما جعله مناسب لأغلب البرامج والأجهزه الحديثة .

كل من هذين النظامين متوافقين مع Win32 API لكتابة برامج تعمل على النظامين ومنذ 2001 توقف مايكروسوفت عن دعم 9x وأصبحت تتعامل مع ال NT-Based System . وأحد أفضل نسخ الNT التي أنتجتها مايكروسوفت هي Windows XP والتي سيكون عليها محور الحديث في هذا الفصل والقادم ولكن بما أن جميع الأنظمه مايكروسوفت الحديثه هي نسخ NT فالمفاهيم التي سندرسها الأن تنطبق عليها .

تميزت أنظمه Windows NT بأنها نسخ 32 بت 32-bit Architecture وحالياً وبدءً من أخر أصدار لويندوز 7 فلن تكون هناك نسخ 32 بت بعد ذلك، وكل النسخ سوف تكون 64 بت. حاليا تتوفر نسخ NT لل64 بت . ميزة أخرى وهي دعم الVirtual Memory وسوف نتحدث عنها فيما بعد . أيضا يتميز النظام بأنه يعمل على أكثر من منصة بعكس الإصدارات الأولى لويندوز حيث يمكن عمل اعادة ترجمته في المنصة لكي يعمل عليها. والتعامل مع الهاردوير يتم عبر طبقة Hardware abstract Layer والتي تعزل النظام من الهارديور وتسمح بنقل النظام لأي هارديور جديد.

أيضا يتميز النظام بأنه يدعم أسلوب الPreemptive في جدولة العمليات بالإضافة الى دعم الMultithreading .وبالرغم من أن أنظمه ويندوز القديمه كانت تدعم تعدد المسارات الا أنها كانت تستخدم أسلوب non-preemptive في بعض المكونات المكتوبة من نوع 16 بت مثل GDI و USER . ويتميز كيرنل NT بدعم أكثر من معالج وهذا يعني أنه مناسب لبيئات العمل التي تتطلب أداء عالي مثل Data-Center Server أو التطبيقات التي تستهلك المعالج.

وبعكس أنظمة ويندوز القديمة فNT بنى من الأساس لكي يكون أكثر أماناً Security فكل كائنا في النظام له محدد وصول Access Control List والذي تحدد المستخدم الذي يتعامل مع هذا الObject وحتى نظام الملفات NTFS يدعم ACL لكل ملف ويدعم التشفير لكل ملف أو القرص بالكامل. اضافة الى أن نظام NT متوافق Compatible مع التطبيقات القديمه ويستطيع تشغيل بعض تطبيقات ويندوز 16 بت أو تطبيقات الدوز وذلك من خلال تشغيها في بيئة تخيلية تكون معزولة عن النظام حتى لا تؤثر فيه. أخيرا فإن نظام NT مصمم من الأساس لكي يكون متعدد المنصات ولكي يعمل على أكثر من معمارية مثل IA-32,DE,ALPHA لكن النسخ الأخيره تدعم فقط IA-32 ولكنها تدعم المنصات المتوافقه مع انتل مثل AMD64.

1.2. التعامل مع الأخطاء Error Handling:
قبل التعرف على خصائص نظام الويندوز يجب فهم كيف تتعامل الدوال التي نقوم باستدعائها Win32 Function مع الأخطاء. فعندما يتم استدعاء دالة يتم التحقق من المعاملات المرسلة واذا كانت صحيحه القيم فيتم تنفيذ الدالة والا في حال لم تكن صحيحه أو لم تعمل لأي سبب من الأسباب فإن الدالة سوف ترجع قيمه تدل على الخطأ Error-Code . والجدول التالي يوضح القيم الراجعه في أغلب دوال الويندوز:

الشكل ‏1 يبين القيم الراجعه من أغلب دوال الويندوز
الشكل ‏1 يبين القيم الراجعه من أغلب دوال الويندوز

وعندما يحصل خطأ في الدالة تقوم الدالة باستخدام Thread Local Storage بوضع رقم الخطأ في الThread الذي قام باستدعاء الدالة وحصل بها الخطأ وبهذه الألية سوف يتم السماح لأكثر من Thread بالعمل دون أن يأثر رقم الخطأ الموجود في أحد الThread عن بقية الأرقام الموجوده في الThreads الأخرى في العملية.

عند حدوث الخطأ فأن الدالة ترجع رقم الخطأ Error Code ويمكن معرفة هذا الرقم من خلال الدالة GetLastError والتي لها التصريح التالي:

الشكل ‏2 يبين تصريح الدالة GetLastError
الشكل ‏2 يبين تصريح الدالة GetLastError

وترجع الدالة أخر خطأ حصل في الThread ، وبعد الحصول على رقم هذا الخطأ فمن المفيد تحويل هذا الرقم الى رسالة نصية توضح سبب الخطأ بشكل أكبر ، وتوجد جميع رسائل الخطأ والثوابت التي ترجعها الدالة GetLastError في ملف الرأس winerror.h والشكل 3 يبين مقطع صغير من ملف الرأس والذي يتكون من حوالى 3000 سطر في المترجم GCC بينما في الPSDK لمايكرسوفت فيبلغ حوالى 40000 سطر! والسبب في ذلك أن ملف الرأس في تطبيق مايكروسوفت يحتوي على الكثير من التعليقات بالإضافة الID للرسالة وماكرو مشابه للID .

الشكل ‏3 يبين التعاريف في الملف winerror.h
الشكل ‏3 يبين التعاريف في الملف winerror.h

وتوجد أداه تأتي مع الVisual Studio تسمى Error Lookup تستطيع من خلالها تحويل رقم الخطأ الى النص المقابل لرقم الخطأ والموجود في الشكل السابق باسم MessageText وهذا البرنامج يستخدم الدالة FormatMessage لتحويل رقم الخطأ الى الرسالة MessageText وللدالة FormatMessage التصريح التالي:

الشكل ‏4 يبين تصريح الدالة FormatMessageW
الشكل ‏4 يبين تصريح الدالة FormatMessageW

هذه الدالة من الدوال المفيده لاظهار النصوص للمستخدم حيث أنها تتعامل مع أكثر من لغه وتستقبل الدالة معرف اللغه Language ID كمعامل للدالة وترجع النص المناسب، وفيما يلي مثال لأداه command line تستخدم هذه الدالة وتعمل مثل عمل برنامج Error Lookup.

الشكل 5 يبين مثال على برنامج لمعرفه الخطأ Error Lookup
الشكل 5 يبين مثال على برنامج لمعرفه الخطأ Error Lookup

ويتم ارسال رقم الخطأ من خلال الargument للبرنامج كما في الشكل التالي:

الشكل 6 يبين مخرج البرنامج السابق
الشكل 6 يبين مخرج البرنامج السابق

 

1.3. Working with Characters and Strings:
بما أن نظام الويندوز يعد من أكثر الأنظمه انتشاراً حول العالم ويدعم أغلب اللغات المعروفة فيجب على المبرمجين كتابة برامج تدعم تلك اللغات لكي ينتشر التطبيق بشكل كبير في International Market. في السابق كان المبرمجين يكتبوا التطبيق باللغه الإنجليزية ثم يتم طرح دعم اللغات المختلفه مع الإصدارات التالية للبرنامج. وبالإستفادة من دعم النظام لأغلب اللغات فيمكن من البدايه كتابة البرنامج وجعله يدعم تلك اللغات المختلفة.

ومن المواضيع المهمه للغايه عند التعامل مع العمليات على النصوص هو تجنب الثغرات Buffer Overrun والتي تكون عادة عند التعامل مع النصوص. وأولت مايكروسوفت إهتماماً لهذا الأمر وقدمت مجموعه جديده من الدوال الأمنه والتي عند استخدامها ستحمي التطبيق من هذه الثغرات كما سيتبين ذلك لاحقاً.

المشكلة الحقيقية في موضوع الLocalization هو التعامل مع أبجديات مختلفة Character Sets ففي لغه السي نجد أن حجم المتغير من النوع char يبلغ واحد بايت (8 بت) وهو كافي لتمثيل جميع الحروف اللاتينية ولكن بعض اللغات مثل اليابانية تحتوي على الكثير من الأحرف في الأبجدية وهنا لن يكفى بايت واحد لمثيل كل هذه الحروف. ولحل هذه المشكلة تم تطوير أبجدية Double-Byte Character Sets )DBCS) وهي تمثل الحرف ببايت وحد أو 2 بايت. ففي اللغه اليابانية Kanji اذا كان الحرف الأول يقع في المدى من 0x81 الى 0x9F أو من المدى 0xE0 الى 0xFC فيجب النظر للبايت التالي لمعرفه الحرف (حيث يكون مكون من بايتين). والتعامل مع هذه النظام DBCS كان غير مريح للمبرمجين حيث بعض الأحرف تكون بحجم واحد بايت والأخرى بحجم 2 بايت ومن هنا كانت ولادة النظام اليونيكود Unicode (في عام 1988 بواسطة Apple و Xerox وبعدها انضمت بقية الشركات الكبرى في دعم هذا المقياس وتطويره مثل Oracle, IBM, Microsoft وغيرهم).

ونظام الويندوز يدعم اليونيكود سواء في داول الويندوز Win32 API أو في دوال مكتبة السي القياسية C Run-Time Library. ويستخدم النظام الترميز UTF-16 (وهو اختصار لUnicode Transformation Format) وهذا النظام UTF-16 يرمز كل حرف من خلال بايتين (16 بت) وهو كافي لمتثيل أغلب لغات العالم وبالتالي يسهل التعامل مع النصوص وحساب طولها عند العمل بهذا الترميز.

هناك ترميزات مختلفة من الUnicode منها الUTF-8 وهو يرمز الحرف الواحد بواحد أو 2 أو 3 أو 4 بايت! فالحروف التي هي أصغر من 0x0080 ترمز بواحد بايت (مثل أحرف اللغه الإنجليزي) والأحرف من 0x0080 الى 0x07FF ترمز ب2 بايت (مثل اللغات الأوروبية ولغات الشرق الأوسط Middle East) والأحرف من 0x0080 وما فوق ترمز ب3 بايت (مثل لغات الEast Asian) . وبعض الرموز ترمز من خلال 4 بايت. وبشكل عام نظام الUTF-8 استخدم كثيرا في السابق ولكنه أقل كفائه منUTF-16 في حال تم التعامل مع أحرف أكبر من 0x0080.

ترميز أخر وهو الUTF-32 وهو يرمز الحرف الواحد ب4 بايت وهو بالطبع يتعامل مع أي لغه ولكنه غير كفئ لأنه يستهلك الذاكرة (فلكل حرف 4 بايت) وبالتالي لا يستخدم هذه الترميز خصوصا في عمليات الحفظ Saving أو الإرسال عبر الشبكة بل يستخدم فقط من داخل البرنامج.

تقسم الينوكود القيم الى مناطق وكل منها تتعامل مع أبجدية مختلفة ، الجدول التالي يبين بعضا من المناطق والأبجديات المستخدمه فيها:

الشكل ‏7 يبين الأبجديات المختلفة في ترميز UTF-16
الشكل ‏7 يبين الأبجديات المختلفة في ترميز UTF-16

ANSI and Unicode Character and String Data Type 1.3.1 :
كل من تعامل مع لغة السي يعلم أن حجم المتغير من نوع الحرفي char هو 1 بايت وفي حال كتب الحروف داخل علامتي تنصيص Literal String فيقوم المترجم بتحويل كل حرف الى 1 بايت.

win32image_8

ولإستخدام الUTF-16 يتم استخدام النوع wchar_t ومترجمات مايكروسفت القديمه لا تدعم هذا النوع فعند استخدامه يتم كتابة المفتاح \Zc:wchar_t وهذا المفتاح يكون موجود تلقائياً في خيارات الترجمة عند عمل مشروع جديد في Visual Studio.

ويعرف النوع wchar_t على أنه unsigned short كما يوضحه الشكل التالي:

الشكل 8 يوضح تعريف المتغير wchar_t
الشكل 8 يوضح تعريف المتغير wchar_t

الكود التالي يبين استخدام الينوكود في النصوص string والأحرف character باستخدم wchar_t :

الشكل 9 يبين مثال للتعامل مع الANSI and Unicode Characters
الشكل 9 يبين مثال للتعامل مع الANSI and Unicode Characters

استخدام الحرف الكبير L قبل أي Literal String يخبر المترجم بأن هذا النص يجب ترجمته على أنه Unicode وعندما يضع المترجم النص في قسم البيانات Data Section فهو يرمز كل حرف باستخدام UTF-16.

وقامت مايكروسوفت بتعريف أنواع متغيرات بأسماء مختلفه عن أنواع المتغيرات الموجودة في لغه السي لكي تفصل التعامل مع Win32 API قليلا عن الأنواع في لغه السي ، فمثلا في ملف الرأس WinNT.h نجد التعاريف التالية:

الشكل 10 يبين تعاريف الأنواع الأساسية في ملف WinNt.h
الشكل 10 يبين تعاريف الأنواع الأساسية في ملف WinNt.h

ونجد أيضا للWide Char هذه الأنواع :

الشكل ‏11 يبين تعاريف الأنواع الأساسية والتي تتعامل بالUnicode
الشكل ‏11 يبين تعاريف الأنواع الأساسية والتي تتعامل بالUnicode

بالنسبة __nullterminated فهي header annotation تصف كيف سيستخدم النوع سواء في المعاملات أو في القيم الراجعه وهي غير مهمه في أغلب الأحوال.

عند البرمجه بWin32 API يفضل استخدام هذه الأنواع بقدر الإمكان للتوافق أولا مع توثيق الMSDN وهذا يسهل قرائه الكود وصيانته. ومن الأفضل دائما كتابة كود عام بحيث يمكن أن يتترجم الى ANSI أو الى Unicode بدون أي تغيير. فلو نظرنا الى الملف WinNT.h مره أخرى سنجد أنه اذا كان يوجد تعريف للUnicode فسيتم تحويل Expand أي اسم TCHAR الى WCHAR (والذي هو في النهايه wchar_t) . واذا لم يكن هناك تعريف سيكون النوع TCHAR هو char عادي.

الشكل ‏12 يبين تحديد النوع على حسب وجود تعريف للUnicode
الشكل ‏12 يبين تحديد النوع على حسب وجود تعريف للUnicode

وبالتالي عند كتابة الكود التالي سنجد أن السطر الأول قد يكون حجمه 1 بايت أو 2 بايت وذلك بالإعتماد على هل الثابت UNICODE معرف في الكود أم لأ. ونفس الأمر بالنسبة للسطر التالي.

win32image_13

1.3.2. ANSI and Unicode Functions in Widows
منذ صدور Windows NT وكل النسخ التي تليه بنيت بدعم كامل للينوكود، وكل الدوال سواء كانت لإنشاء النوافذ أو عرض ومعالجه النصوص تتطلب نصوص بنظام يونيكود Unicode string. وفي حال قمت باستدعاء أحد تلك الدوال بنص ANSI (1 بايت لكل حرف) فسوف تقوم الدالة أولا بتحويل النص الى Unicode وبعدها تكمل العمل وعند ارجاع القيمه الراجعه وكنت تستقبلها في نص ANSI فسوف تقوم الدالة بعملية تحويل النص من اليونيكود الى الANSI وترجعه الى الدالة المستدعية. وكل من هذه التحويلات لا يعلم عنها المستخدم ولكن لها استهلاك overhead سواء في الوقت المستغرق لعملية التحويل أو الذاكرة التي تتطلبها عملية التحويل.

وأغلب دوال الويندوز والتي تتطلب string كمعامل يكون لها نسختين الأولى للتعامل مع الANSI String وتنتهي هذه الدوال بA والنسخ الثانية للتعامل مع الUnicode وتنتهي هذه الدوال بW (نسبة لWide Character) . مثال الدالة CreateWindowEx هي ليست دالة في الحقيقة بل ماكروا سيتم عمل تحويل expand له الى النسخه ANSI (CreateWindowExA) في حال كان لا يوجد تعريف للUNICODE أما اذا كان هناك تعريف للUNICODE سيتم تحويل الماكروا (قبل البدء بالترجمه بواسطة الPreprocessor ) الى النسخه CreateWindowExW .

الكود التالي من ملف الرأس WinUser.h وهو يحتوي على تصاريح الدالتين CreateWindow(Ex) ويبين كيفية الexpand في حال كان هناك معرف لليونيكود:

الشكل ‏13 يبين اختلاف الدالة على حسب تعريف الماكروا UNICODE
الشكل ‏13 يبين اختلاف الدالة على حسب تعريف الماكروا UNICODE

في نظام Windows Vista نجد دوال الANSI والتي تتنهي بA هي مجرد طبقة للتحويل حيث تقوم بتحويل النص من ANSI الى Unicode ثم تقوم باستدعاء النسخه Unicode من الدالة وترسل لها النص وعندما تعود الدالة الUnicode الى النسخه ANSI تقوم الأخيره بتحويل النص الANSI وترجعه للتطبيق. وبهذه الطريقة سوف تجعل التطبيق أكثر استهلاكا للذاكرة وأكثر بطئا في العمل. لذلك استخدام الUnicode يجعل التطبيق أكثر كفائه.

في حال كنت تطور ملف dll يمكنك ان تزود ملف الDll بدالتين للتصدير الأول تدعم ANSI والأخرى Unicode وتقوم الأولى بعملية التحويل لليونيكود واستدعاء نسخه الUnicode كما في نظام vista بالضبط.

بعض الدوال في Win32 API مثل OpenFile و WinExec ما زالت تعمل كنوع من التوافق مع التطبيقات القديمه Backward Compability وهي تستقبل ANSI String فقط. وهذه الدوال يجب تجنبها واستخدام CreateProcess بدلا من WinExec واستخدام CreateFile بدلا من OpenFile. وعلى أيه حال هذه الدوال القديمه تستدعي تلك الدوال الجديدة من الداخل ولكن المشكلة في أنها لا تستقبل Unicode. وحاليا بدأت مايكروسوفت بعمل دوال تستقبل Unicode فقط مثلا الدالة ReadDirectoryChangeW.

عند ترجمه ملف الresource (والذي يحتوي على قالب الDialog والصور والنصوص في واجهه التطبيق) باستخدام مترجم الresource فسوف يقوم المترجم بتحويل النصوص الى Unicode مباشره.

1.3.3. ANSI and Unicode Functions in C Run-Time Library:
كما في دوال نظام الويندوز Win32 API نجد أن مكتبة لغه السي القياسية تحتوي على دوال تتعامل مع الANSI ودوال تتعامل مع الUnicode ولكن تفرق مكتبة السي عن دوال الويندوز في أن نسخ الANSI لا تستدعي نسخ الUnicode بل تقوم بالعمل بمفردها وبنفس الأمر دوال الUnicode تقوم بكل العمل ولا تستدعي نسخه الANSI .

مثلا الدالة strlen وهي نسخه ANSI و ترجع طول النص ANSI string أما الدالة wcslen في نسخه Unicode وترجع طول الUnicode string وكل من هذه الدالتين مصرحات في ملف الرأس String.h . ولكتابة برنامج يترجم عام يترجم الى ANSI أو الى Unicode يجب أستخدام الماكروا _tcslen وهو موجود في ملف الرأس TChar.h والذي يتم عمل expand له الى wcslen في حال كان هناك تعريف لل _UNCODE والا سيتم تحويله الى strlen .

1.3.4. Secure String Functions in C Run-Time Library:
الدوال التي تتعامل مع النصوص يجب أن تكتب بعنايه وخاصه تلك الدوال التي تقوم بنقل أو نسخ النصوص من مكان لأخر ، ففي حال كان المكان الذي سننقل اليه النص destination Buffer ليس كبيرا كفاية لإستقبال النص سوف تحدث مشكلة بالذاكرة Memory Corruption. المثال التالي يوضح ذلك :

win32image_14

المشكلة في دالة _tcscpy بنوعيها (strcpy و wcscpy) وأيضا أغلب دوال التعامل مع النصوص في أنها لا تستقبل معامل يحدد لنا طول الDestination Buffer وبالتالي فإن الدالة لن تعلم بأن هناك مشكلة ولن تظهر أي رسالة تدل على خطأ وهكذا لن يعلم المستخدم بأن هناك مشكلة في الذاكرة. ومن الأفضل أن لا تعمل الدالة على أن تعمل وتحدث مشكلة قد تؤدي لحدوث مشاكل أخرى فيما بعد.

استغلت الMalware هذا النوع من الأخطاء بشكل كبير في الماضي ، لذلك قدمت مايكروسوفت مجموعه جديدة من الدوال الأمنه تقوم بنفس وظائف تلك الدوال ولكن مع اضافة التحقق من حجم الBuffer وهل هو مساوي للSource String . وهذه الدوال مصرحه عنها في ملف الرأس StrSafe.h . وقامت مايكروسوفت بتعديل مكتبة MFC و ATL وجعلها تتعامل مع هذه الدوال الأمنه وبالتالى اعادة بناء Rebuilding تطبيق MFC قديم بأحد الإصدارات الجديدة كافي للتخلص من مشاكل التعامل مع الدوال غير الأمنه.

وعند تضمين ملف الرأس StrSafe.h في البرنامج (والذي يحتوي على تضمين ملف String.h ) وأستخدام دوال السي غير الأمنه مثل _tcscpy فسوف تخرج أخطاء عند الترجمة . كما يوضح ذلك المثال التالي :

الشكل 14 يبين مثال لإستخدام الدوال غير الأمنه
الشكل 14 يبين مثال لإستخدام الدوال غير الأمنه

اذا قمنا بالنظر الى ملف StrSafe.h سنجد أن هناك أسطر توضح انه اذا تم استخدام أي من تلك الدوال القديمه سيخرج لنا الخطأ المناسب والذي يحدد لنا الدالة الأمنه (من ملف StrSafe.h) المقابلة لها :

الشكل ‏15 يبين الدالة الأمنه المقابلة للدالة غير الأمنه في الملف StrSafe.h
الشكل ‏15 يبين الدالة الأمنه المقابلة للدالة غير الأمنه في الملف StrSafe.h

وفي مترجمات مايكروسوفت الجديده وحتى ولم نصرح عن StrSafe.h سنجد بأن هناك تحذير ينبهنا الى عدم استخدام تلك الدوال ، ويفضل التعامل مع تلك التحذيرات بجدية واستبدال الدوال غير الأمنه بنظيراتها الأمنه.

الشكل 16 يبين التحذير الذي يخرج عند استدعاء الدوال غير الأمنه
الشكل 16 يبين التحذير الذي يخرج عند استدعاء الدوال غير الأمنه

ولكل دالة في دوال مكتبة السي مثل _tcscpy و _tcscat ما يقابلها في الدوال الجديدة وله نفس الإسم ولكن ينتهي بعلامه _s وكل من هذه الدوال الجديدة اضافت معامل يوضح عدد الحروف في الBuffer والتي تستقبل النص. التصريح التالي للدالة الأمنه _tcscpy_s ونلاحظ أن هناك معامل واحد زياده عن الدالة الأصليه _tcscpy .

التصريح للنسخه ANSI :

الشكل ‏ 17 يبين تصريح الدالة strcpy_s (النسخه ANSI ) .
الشكل ‏ 17 يبين تصريح الدالة strcpy_s (النسخه ANSI ) .

وهذا التصريح للنسخه Unicode :

الشكل ‏18 يبين تصريح الدالة strcpy_s (النسخه UNICODE ) .
الشكل ‏18 يبين تصريح الدالة strcpy_s (النسخه UNICODE ) .

لاحظ أن هذه الدوال تستقبل عدد الأحرف وليس عدد البايتات (والتي تخرجها الدالة sizeof ) ويتم استخراج عدد الأحرف بقسم حجم المصفوفه على حجم العنصر الأول منها أو باستخدام الماكروا _countof الموجود في stdlib.h (لا يوجد في GCC) .

الشكل ‏ 19 يبين تعريف الماكروا _countof في مترجم مايكروسوفت
الشكل ‏ 19 يبين تعريف الماكروا _countof في مترجم مايكروسوفت

كل الدوال الأمنه والتي تنتهي بعلامه (_s) تتحقق من جميع المعاملات قبل أن تبدأ بالعمل ، حيث تقوم بالتأكد من المؤشر (أول معامل) صحيح ولا يؤشر لNULL ويتأكد من أن المعامل الثاني (عدد الأحرف) هو عدد صحيح integer ، وتتأكد من المعامل الثالث بأنها صحيحه وأن الBuffer حجمه كبير كفاية لنقل المعامل الثالث اليه.

وفي حال حدثت مشكلة أثناء أحدى هذه الفحوصات فتضع الدالة خطأ errno وترجع في كلا الأحوال errno_t وهي قيمه تدل على النجاح (S_OK) أو الفشل (أي قيمه أخرى مثل EINVAL ومعناها أن المؤشر يؤشر لNULL ، بقية الأخطاء توجد في ملف الرأس errno.h ). وفي الحقيقة هذه الدالة عند الخطأ لا ترجع قيمه وإنما بدلا من ذلك (عند العمل في طور الdebug) تقوم باخراج رسالة للمستخدم عن المشكلة ويتم انهاء التطبيق . أما في طور النسخه النهائية Release فيتم انهاء التطبيق مباشره بلا أي رسالة . المثال التالي ينقل نص أكبر من الBuffer باستخدام الدالة الأمنه وبما أن التطبيق يعمل في Debug mode فسوف تخرج الرسالة التاليه وهيUnfriendly بكل تأكيد.

الشكل ‏ 20 يبين اكتشاف خطأ BOF عند استخدام الداول الأمنه
الشكل ‏ 20 يبين اكتشاف خطأ BOF عند استخدام الداول الأمنه

وعند تنفيذ المثال التالي:

الشكل ‏21 يبين مثال على استخدام الدوال الأمنه
الشكل ‏21 يبين مثال على استخدام الدوال الأمنه

سوف يحصل نفس الخطأ السابق ويتم انهاء التطبيق ، وسوف تكون قيمه szBuffer بعد الدالة هي NULL والسبب أن هذه الدوال الأمنه والتي تنتهي ب _s لا تقوم بعمل تخزين النتيجة المناسبة وترك الباقي بل تضع NULL في حال الخطأ.

وأضافت مايكروسوفت للC Run-Time Library العديد من الدوال الجديدة والتي تعطي تحكم أكثر عن التعامل مع النصوص مثلا التحكم بالحروف التي ستملاء الBuffer عند حدوث الoverrun وكيف يطبق الTruncation .

من هذه الدوال StringCchCopy)Ex) و StringCchCat)Ex) و StringCchPrintf)Ex) وغيرها الكثير من الدوال وهي مصرحه في ملف الرأس StrSafe.h . الشكل التالي يبين تعريف الدالة StringCchCopy(نسخه الANSI):

الشكل ‏22 يبين تعريف الدالة StringCchCopyA
الشكل ‏22 يبين تعريف الدالة StringCchCopyA

لاحظ الأحرف Cch في هذه الدوال والتي هي اختصار ل Count of Character ويتم الحصول عليها باستخدام _countof .أيضا هناك دوال لها Cb مثل StringCbCopy و StringCbCat وغيرها والتي تعني Count of Byte ويتم الحصول عليها بواسطة sizeof. كل من هذه الدوال ترجع HRESULT والجدول التالي يبين القيم الراجعه:

الشكل ‏23 يبين القيم الراجعه من الدوال StringC**
الشكل ‏23 يبين القيم الراجعه من الدوال StringC**

وبعكس الدوال الأمنه والتي تنتهي ب _s فهذه الدوال تقوم بقطع القيم الزائده Truncation عندما يكون الBuffer صغيرا وتنقل ما تستطيع نقله بالإضافة الى علامه ال\0 وتستطيع معرفه هذا الوضع عندما تكون القيمه الراجعه هي INFUICEINT_BUFFER . وفي حال طبقنا المثال السابق (نقل الأرقام 0123456789 الى Buffer) باستخدام الدالة StringCchCopy فسوف تكون محتوى الBuffer هو 012345678 .

الشكل 24 يبين استخدام الدالة StringCchCopy
الشكل 24 يبين استخدام الدالة StringCchCopy

لاحظ أن خاصية الTruncation قد تكون مطلوبه في أحدى الأحيان وأحيانا غير ذلك لذلك على المبرمج استخدام هذه الدوال عندما يريد هذه الخاصية. بعض الدوال تنتهي ب (Ex) ولها المعاملات والقيم الراجعه التالية:

الشكل ‏ 25 بين المعاملات المرسلة والقيم الراجعه للدوال الأمنه
الشكل ‏ 25 بين المعاملات المرسلة والقيم الراجعه للدوال الأمنه

1.3.5. Windows String Functions:
أيضا قدم نظام ويندوز العديد من الدوال لمعالجة النصوص ولكن البعض منها لا يفضل استخدامه لأنها لا تحتوي على تحقق من حجم الBuffer وتعتبر ملغية Deprecated خاصه دوال الكيرنل lstrcpy و lstrcat . أيضا قدم ويندوز العديد من دوال معالجة النصوص المفيده في ملف الشل ShlwApi.h .

بالنسبة لمقارنه النصوص فتوجد الدالتين CompareString(Ex) و CompareStringOrdinal ، وتوجد الدالتين في ملف WinNls.h وتصريح الدالة الأولى CompareString كما يلي (نسخه الANSI ) :

الشكل 26 يبين تصريح الدالة CompareStringA
الشكل 26 يبين تصريح الدالة CompareStringA

لاحظ أن هذه الدالة تستقبل معامل يحدد معرف اللغه ويمكن الحصول عليه باستخدام الدالة GetThreadLocale وترجع هذ الدالة قيمه LCID.والمعامل الثاني يحدد الFlags المستخدم والجدول التالي يوضح هذه الFlags

الشكل ‏27 يبين الFlag للدالة CompareStringA
الشكل ‏27 يبين الFlag للدالة CompareStringA

أما بقية المعاملات فهي لتحديد النصين وعدد الأحرف لهذين النصين ، واذا تم ارسال قيمه سالبة لعدد الأحرف (سواء ccCount1 أو cchCount2) ستقوم الدالة بحساب طول النص بنفسها.

أما الدالة الثانية وهي لعمل مقارنه ولكن بدون النظر للLocale كما في الدالة الأولى والتي تستقبله كمعامل. وتصريح الدالة CompareStringOrdinal كالتالي:

الشكل 28 يبين تصريح الدالة CompareStringOrdinal
الشكل 28 يبين تصريح الدالة CompareStringOrdinal

وتقوم بعمل مقارنه بدون النظر للLocale وبالتالي فهي أسرع في العمل ويفضل استخدامها عند مقارنه النصوص العادي(مثل المسارات Path و قيم الRegistry وغيرها) .

وترجع هذين الدالتين قيمه مختلفه عن القيم الناتجة من دوال المقارنه في لغه السي حيث ترجع هذه الدوال القيمه 0 في حال الفشل والقيمه 1 ( CSTR_LESS_THAN) في حال كان النص الأول lpString1 أقل من الثاني، وترجع 3 (CSTR_GREATER_THAN) في حال كان النص الأول lpString1 أكبر من الثاني ،وترجع 2 (CSTR_EQUAL) في حال التساوي.

ولجعل ناتج هذه الدوال مشابه لدوال المقارنه في لغه السي يمكن (في حال نجاح الدالة) أن نطرح من القيمه الناتجه 2 وبالتالي يكون الناتج 0 في حال التساوي و1 في حال النص الأول أكبر و-1 في حال كان النص الأول أصغر.

1.3.6. لماذا يجب أستخدام اليونيكود Why You Should Use Unicode:
يفضل عند تطوير التطبيقات استخدام ال Unicode لأنه يجعل عملية الLocalization سهله وبالتالي يكون هناك ملف exe أو dll واحد يدعم كل اللغات. بالإضافة الى أن استخدام الUnicode يجعل التطبيق أكثر كفائه ويستهلك ذاكرة أقل. وأيضا يكون متوافق مع مخرجات مترجم الResource بالإضافة الى العمل على .NET و COM والتي تدعمان الUnicode بالكامل.
ولذلك يفضل تحويل التطبيقات وجعلها جاهزه للUnicode وحتى لو لم تكن هناك نية لاستخدام الUnicode في الوقت الحالي ، ويمكن أتباع ما يلي:

• استخدام أنواع البيانات العامه Generic Data Type مثل TCHAR و PTSTR.
• استخدام الماكروا _T أو TEXT لأي نصوص Literal String في البرنامج .
• الدوال التي تستقبل عدد الأحرف نرسل لها _countof أما التي تستقبل عدد البايتات نرسل لها sizeof . وعند إراده حجز مساحة وكان لدينا عدد الأحرف فيجب حجز مساحة تساوي عدد الأحرف مضروبا في عدد بايتات الحرف الواحد ، كما يوضح الشكل التالي:

الشكل ‏ 29 يحدد كيف يمكن حجز مساحة بطول النص بطريقة صحيحه
الشكل ‏ 29 يحدد كيف يمكن حجز مساحة بطول النص بطريقة صحيحه

• تجاهل استخدام عائلة printf بالكامل وخصوصا التي تستخدم %s و %S للتحويل من الANSI الى الUnicode والعكس ، ويتم استخدام الدوال MultiByteToWideChar و WideCharToMultiByte .

• دائما يتم تحديد المعرفين UNICODE و _UNICODE (الأثنين معا أو بدونهم).
• عند معالجة النصوص يجب استخدام الدوال الأمنه والتي تنتهي ب _s واذا كان المبرمج يريد قطع النتيجة فيستخدم دوال StringCch* .
• لا تستخدم دوال الكيرنل lstrcpy , lstrcat .

 

1.4. كائنات النظام Kernel Objects:
كائنات الكيرنل هي عباره عن بنية بيانات في الذاكرة ينشئها الكيرنل وهو الوحيد الذي يستطيع التعامل معها بشكل مباشر, وتتكون هذه البنية من عده حقول تحتوي على معلومات حول الكائن ، وبعض الحقول تكون موجوده في كل كائنات الكيرنل المختلفه (مثل عداد الإستخدام Usage Count و الSecurity Descriptor) . والأغلب في الحقول تكون متعلقة بالكائن نفسه مثلا الكائن من نوع Process Object يكون له معرف ID وله Exit Code وله أولويه Priority وأما الكائن من نوع File Object يكون له إزاحه Byte Offset وله طريقة للفتح Open Mode وللتشارك Share Mode. وهكذا ..

هناك العديد من كائنات الكيرنل Kernel Objects منها File و Event و Pip و Process و Thread و Semaphore و FileMapping وغيرها الكثير .

ولأن هذه الكائنات هي عباره عن بني بيانات ينشئها ويتعامل معها الكيرنل فيستحيل على التطبيقات العادية الوصول لتلك البني في الذاكرة وتعديل محتوياتها بشكل مباشر. واذا أرادت التطبيقات التعامل مع هذه البنى فهي تستخدم دوال معينه تستطيع التعامل مع تلك البني باستخدام مقبض Handle للKernel Object. فعند انشاء أي Kernel Object فسوف تعود الدالة بمقبض Handle لذلك الكائن ويمكن أن يستخدم هذا المقبض أي Thread داخل العملية .

هذه المقابض (طولها 32 بت في أنظمه 32 ) هي متعلقة بالعملية Process-Relative بمعنى في حال أرسلت هذا المقبض لThread في عملية أخرى (باستخدام أي من طرق التواصل بين التطبيقات IPC) فإن ذلك الThread لن يصل لذلك المقبض بل سيصل ربما لمقبض أخر تماما أو سوف تفشل العملية ، وسوف نوضح هذه العملية بشيء من التفصيل بعد التعرف على جدول المقابض في كل عملية Process Handle Table.

1.4.1. Usage Count:
كائنات الكيرنل يمتلكها الكيرنل وليست العملية بمعنى اذا قامت العملية باستدعاء دالة لإنشاء Kernel Object وأنتهت هذه العملية فقد يمكن أن لا يقوم النظام بحذف هذا الكائن من الذاكرة Destroy في حال كانت هناك عملية أخرى تستخدم هذا الكائن. وبالتالي لن يحذف destroy الكيرنل هذا الكائن حتى تقوم العملية التالية بالتوقف عن استخدام هذه الكائن. باختصار شديد وهي النقطة المهمه هو أن الKernel Object يمكن أن يتواجد حتى بعد انتهاء العملية التي قامت بانشائه.

ويعلم الكيرنل عدد العمليات التي تستخدم هذا الكائن من خلال الحقل Usage Count وهو يوجد في أي Kernel Object . فعندما يقوم الكيرنل بانشاء Object يتم وضع الUsage Count لها الكائن بالقيمه 1 وعندما تقوم عملية اخرى باستخدام هذا الكائن يزداد الUsage Count بواحد وهكذا. وعند تنتهي العملية يتم طرح واحد من الUsage Count من جميع الكائنات التي انشئأتها العملية. وأي كائن له Usage Count مساوي لصفر يقوم الكيرنل مباشره بتحريره من الذاكرة. وهكذا لن يبقى أي كائن في الذاكرة لا تستخدمه أي عملية.

1.4.2. Security:
يتم حمايه كائنات الكيرنل من خلال الـ security descriptor وهو يصف من الذي يملك الكائن (العملية التي أنشأته) وما هي المجموعه groups والمستخدمين Users المخولين للوصول لهذا الكائن واستخدامه وما الذين لا يحق لهم الوصول.

وكل الدوال التي تنشئ Kernel Object لها معامل وهو SECURITY_ATTRIBUTE مثلا الدالة التالية لانشاء ملف CreateFile سنجد أن هناك معامل للحماية وهكذا مع جميع الدوال التي تنشئ كائنات الكيرنل:

الشكل ‏30 يبين تصريح الدالة CreateFileA
الشكل ‏30 يبين تصريح الدالة CreateFileA

في أغلب التطبيقات نرسل NULL لهذا المعامل وبالتالي يتم انشاء الكائن بالحماية الموجوده في العملية الحالية. واذا أردنا عمل حماية يجب عمل كائن من البنية SECURITY_ATTRIBUTES وتهيئتها ثم ارسال العنوان كمعامل لأي داله من دوال انشاء الKernel Object. ويتكون SECURITY_ATTRIBUTES من الحقول التالية:

الشكل ‏31 يبين تعريف الSECURITY_ATTRIBUTES
الشكل ‏31 يبين تعريف الSECURITY_ATTRIBUTES

هناك حقل واحد من هذه الحقول متعلق بالحماية وهو lpSecurityDescriptor واذا كنا نريد تحديد الوصول للكائن فيجب انشاء SECURITY_ATTRIBUTES واعطاء قيمة lpSecurityDescriptor . بالنسبة للحقل الأخير bInheritHandle فهو غير متعلق بالحماية وسنتحدث عنه بعد قليل عند الحديث عن مشاركة المقابض بين العمليات.

وعند الوصول لأي كائن في الكيرنل Kernel Object (وصول وليس انشاء) فيجب تحديد العملية التي نريد أن نجريها على هذا الكائن عن الوصول اليه. فمثلا اذا كنا نريد الوصول لكائن من نوع FileMapping للقرائه سوف نستدعي دالة الوصول Open :

الشكل 32 يبين استدعاء دالة تنشئ Kernel Object
الشكل 32 يبين استدعاء دالة تنشئ Kernel Object

المعامل الأول لهذه الدالة يحدد نوع العملية (القرائه في هذه الحالة) التي نريد أن نجريها على الكائن ، وتقوم الدالة OpenFileMapping أولا بفحص الحماية قبل أن تعيد المقبض للكائن الموجود. فاذا كان المستخدم الذي استدعى هذه الدالة ضمن مجموعه المستخدمين المخولين للوصول لهذا الكائن فسوف تعيد الدالة المقبض والا سوف تعيد NULL وعند معرفه الخطأ باستخدام GetLastError سيخرج (5) وهو ERROR_ACCESS_DENIED . على العموم أغلب التطبيقات لا تستخدم الحماية Security كثيرا.

يمكن للتطبيق التعامل مع أنواع كائنات أخرى بخلاف كائنات الكيرنل مثل User Object أو GDI Object والتي تستطيع انشاء والتعامل مع النوافذ والفأره والخطوط وغيرها. لمعرفة هل الكائن الحالي هو Kernel Object أم لا يكون ذلك عن طريق النظر الى الدالة التي قامت بانشائه فاذا وجدت معامل SECURITY_ATTRIBUTES سوف يكون الكائن هو كيرنل والا سوف يكون خلاف ذلك.

1.4.3. Process Kernel Object Handle Table:
عندما يتم انشاء عملية يتم انشاء Handle Table لهذه العملية (جدول للمقابض) ويستخدم هذا الجدول لحفظ معلومات كائنات الكيرنل (كائنات الكيرنل فقط Kernel Object وليس بقية الكائنات مثل كائنات الرسم والمستخدم User and GDI Objects). طريقة بناء هذه الجدول وكيفية ادارته غير موثقة Undocumented وبالتالي المعلومات التي ستوضح أدناه هي ليست معلومات تفصيليه حول طريقة العمل ولكنها تعطي فكره عن المفهوم بشكل عام حيث هذا المفهوم ضروري لمبرمج نظام الويندوز.

الشكل التالي يبين كيف يبدوا شكل جدول المقابض Handle Table وكما يتضح فهو مصفوفه من بنى البيانات Array of Data Structure وكل خانه فيها تتكون من مؤشر للكائن و Access Mask و Flags .

الشكل ‏33 يبين تركيب جدول المقابض في العملية
الشكل ‏33 يبين تركيب جدول المقابض في العملية

وعند انشاء العملية يكون هذا الجدول فارغاً ويتم التعامل معه عندما يقوم الThread داخل العملية بانشاء أي Kernel Object (عن طريق استدعاء أي دالة من دوال انشاء Kernel Object ) حيث يقوم الكيرنل بحجز منطقة في الذاكرة للكائن ويقوم بتهيئتها. ثم بعد ذلك يقوم الكيرنل بالبحث في جدول المقابض عن مكان فارغ وبما أن الجدول حاليا فارغا (كما في الشكل السابق) يقوم الكيرنل بايجاد الموقع الأول 1 ويقوم باستخدامه . وهكذا تكون قيمه مؤشر الكائن (العمود الثاني) هي عنوان الذاكرة التي يوجد بها الكائن ، وستكون الAccess Mask هي Full Access وسيتم وضع الFlag ببعض القيم سنوضحها بعد قليل.

من الدوال التي تنشئ Kernel Object : CreateThread , CreateProcess CreateFile, CreateSemaphore وغيرها الكثير . وكل من هذه الدوال تنشئ Kernel Object وترجع Handle متعلق بالعملية Process-Relative . وهذا المقبض يمكن ان يستخدمه أي Thread داخل العملية. قيمه هذا المقبض يجب أن تقسم على 4 (أو اجراء عمليتين ازاحه لليمين Two Right Bit-Shift) . للحصول على رقم الموقع index في جدول المقابض للعملية. وبالتالي عند عمل debug للتطبيق وفحص قيمه هذا المقبض Handle سنجد أنها قيم صغيره 4 و 8 وهكذا .

وعندما يتم استدعاء دالة تستقبل Kernel Object كمعامل سوف يتم ارسال القيمه العاده من الدوال (المقبض) انشاء كائن الكيرنل Create* ومن داخل الدالة سوف تقوم بالنظر الى جدول المقابض للحصول على عنوان الكائن في الذاكرة حتى تقوم بمعالجته. وفي حال تم ارسال مقبض خاطئ سوف تعود الدالة بخطأ وتكون قيمه GetLastError هي ERROR_INVALID_HANDLE .

أما في حال تم استدعاء دالة لانشاء Kernel Object ولم تعمل الدالة بشكل صحيح سوف ترجع 0 (NULL) ولذلك يكون دائما قيمه أول مقبض تبدأ من 4. وهناك بعض الدوال ترجع -1 (INVALID_HANDLE_VALUE) عند الخطأ مثل الدالة CreateFile لذلك يجب النظر الى التوثيق Documentation والتأكد من القيمه الراجعه.

الشكل 34 يبين استخدام خاطي للقيم الراجعه.
الشكل 34 يبين استخدام خاطي للقيم الراجعه.

1.4.4. Closing Kernel Object:
بغض النظر عن كيفية انشاء الكائن سوف يتم انهاء التعامل معه من خلال الدالة CloseHandle وتقوم الدالة بفحص هل المقبض المرسل صحيح أم لا واذا كان كذلك يتم الذهاب لموقع الكائن في الذاكرة من خلال العنوان الموجود في جدول المقابض ويقوم بطرح واحد من الUsage Count للكائن واذا وصلت قيمه الUsage Count للكائن صفر سيقوم الكائن بحذف الكائن وتحريره من الذاكرة .

أما اذا تم ارسال مقبض خاطئ للدالة فسوف يحدث شيء من اثنين ، الأول أن الدالة سوف ترجع خطأ FALSE ويكون الخطا عند استدعاء GetLastError هو ERROR_INVALID_HANDLE ، أما الإحتمال الثاني هو أن يتم عمل throws للexception (0xC0000008) اذا كانت العملية تعمل في طور التنقيح.

وقبل أن تعود الدالة CloseHandle تقوم بحذف المدخل لهذا الكائن من جدول المقابض للعملية. وبالتالى يكون المقبض غير صحيح ويجب عدم استخدامه . (سوف يتم حذف المدخل سواء تم حذف الكائن من الذاكرة أو اذا لم يتم حذفه –في حال وجود عملية اخرى تستخدم هذا الكائن-).

ومن الأفضل بعد استدعاء الدالة CloseHandle هو وضع القيمه NULL في المقبض ، لأنه اذا تم استخدامه بعد ذلك سوف يقوم النظام باصدار خطأ لأن المقبض غير صحيح حيث لا وجود لمدخل لهذا المقبض في جدول المقابض. أو الأسوأ وهو أن يتم استخدام هذا المدخل بواسطه Kernel Object جديد وبالتالي يتم الوصول لمقبض أخر وقد يكون من نوع مختلف وبالتالي حدوث مشاكل غير متوقعه في البرنامج.

وفي حال لم يتم عمل CloseHandle سوف يحدث Leak (تسرب للذاكرة) اثناء عمل العملية ولكن عندما تنتهي يتم انهاء وتحرير كل المصادر حيث يقوم النظام بالبحث في جدول المقابض للعملية واذا وجد مدخل (كائن لم يتم انهاء التعامل معه) يقوم النظام بطرح واحد من الUsage Count وحذف المدخل من الجدول. واذا وصل الUsage Count لهذه المقبض الى صفر يقوم النظام بحذف الكائن من الذاكرة.

بالتالي يمكن أن يحدث Leak للKernel Object أثناء عمل العملية ولكن عند انتهاءها يضمن النظام تحرير كل شيء.والحديث ينطبق على بقية أنواع الكائنات الأخرى مثل GDI and User Object.

وهناك طريقة لكشف وجود Kernel Object Leak وعن عن طريق مراقبة الHandle للعملية فاذا كان الرقم يتصاعد فهذا يعني أن هناك Leak وأن هناك العملية لم تنهي العمل من الكائنات بعد الإنتهاء منها.

الشكل ‏ 35 يبين وجود Leak في برنامج حيث يتصاعد عدد الHandle بشكل كبير
الشكل ‏ 35 يبين وجود Leak في برنامج حيث يتصاعد عدد الHandle بشكل كبير

1.4.5. تشارك كائنات الكيرنل بين العمليات:
تحتاج المسارات Threads في العمليات عادة للتشارك في كائنات الكيرنل والتي أنشأتها عمليات أخرى وقد يكون ذلك لأسباب مختلفة منها عند اراده تشارك البيانات في عمليتين مختلفتين في النظام (يتم ذلك باستخدام FileMapping) ، أو اراده تشارك البيانات بين عمليتين مختلفتين في نظامين مختلفين يرتبطان بشكبة (ويتم ذلك من خلال MailSlots أو named pipes)، أو عند اراده استخدام الsynchronization لجعل عملية تنبة notify العملية الأخرى عند حدوث حدث معين (يتم من خلال Mutex أو Semaphore).

وبما أن أي مقبض الكائن متعلق بالعملية Process-Relative فإن العملية ليست سهله ، وكما ذكرنا سابقا أن مايكروسوفت جعلت المقبض متعلق بالعملية وذلك لجعل النظام أكثر متانه Robustness حتى لا تجعل أي عملية تستخدم هذا الكائن بكل سهوله ، والسبب الثاني هو الحماية security حيث أن أي Kernel Object له حماية يحددها من ينشئ هذا الكائن ، وأي عملية تريد أن تستخدم هذا الكائن يجب أن يكون لها الصلاحية لذلك .

سوف نتناول الأن 3 طرق لكي تجعل العمليات تتشارك في الKernel Object وهي :

  1. Object Handle Inheritance
  2. Naming Objects
  3. Duplicating Object Handle

A. الطريقة الأول لتشارك كائنات الكيرنل Object Handle Inheritance:
يتم استخدام هذه الطريقة عندما تكون للعمليات علاقه parent-Child بمعنى أن العملية الأولى تقوم بانشاء العملية الثانية Spawn Process وهنا يمكن السماح للعملية الإبن بالوصول الى كائنات العملية الأب. ولعمل ذلك يجب اتباع الخطوات التالية.

أولا عندما تقوم العملية الأب بانشاء Kernel Object تحدد هذه العملية بأن المقبض قابل للوراثه Inheritable Handle وذلك من خلال عمل نسخه من التركيب SECURITY_ATTRIBUTES واسناد القيمه TRUE للعضو bInheritHandle . كما يبين المقطع التالي.

الشكل 36 يبين جعل مقبض الكائن قابل للوراثه
الشكل 36 يبين جعل مقبض الكائن قابل للوراثه

نعود الى جدول المقابض والى العمود Flags والذي يحدد هل المقبض قابل للوراثه (1) كما في الشكل السابق ، أو أنه غير قابل للوراثه (0) وذلك في حال انشاء الKernel Object وتمرير القيمه NULL للمعامل SECURITY_ATTRIBUTES .

لنفرض أن جدول المقابض كان بهذا الشكل:

الشكل ‏ 37 يبين جدول المقابض وهناك مقبضين أحدهما قابل للوراثه (3) والأخر غير ذلك (1)
الشكل ‏ 37 يبين جدول المقابض وهناك مقبضين أحدهما قابل للوراثه (3) والأخر غير ذلك (1)

الخطوه الثانيه عند استخدام هذه الطريقة (Object Handle Inheritance) هي انشاء العملية الإبن Spawn Child Process ويتم ذلك من خلال الدالة CreateProcess.

الشكل 38 يبين تصريح الدالة CreateProcess
الشكل 38 يبين تصريح الدالة CreateProcess

هذه الدالة تنشئ عملية وليست مهمه حاليا في سياق الحديث ، ما يهمنا الأن هو المعامل bInheritHandle وعاده يتم ارسال القيمه FALSE وهكذا يتم اخبار النظام بإن العملية الإبن Child Process لن ترث أي مقبض قابل للوراثة في العملية الأب. وعند ارسال القيمه TRUE فإن العملية الأبن سوف ترث أي مقبض قابل للوراثه في العملية الأب. وحين ارسال القيمه TRUE سيقوم النظام بعد انشاء مساحة العنواين للعملية الإبن وقبل أن تبدأ بالعمل بنسخ مداخل (نقصد بها السطر بالكامل ) جميع المقابض القابلة للوراثه في جدول المقابض في العملية الأب الى جدول المقابض في العملية الإبن ويضعها في نفس المواقع بالضبط وهذه معلومة مهمه. وبالإضافة الى نسخ مداخل المقابض في الجدول يقوم الكيرنل بزيادة الUsage Count للكائن لأن هناك عملية جديده تستخدم هذا الكائن ولكي يتم حذف هذا الكائن من الذاكرة يجب أن تقوم العمليتين باستدعاء CloseHandle أو أن يتم انهاء العمليتين (وكما ذكرنا سابقا ان عند انتهاء العملية سيقوم النظام بتحرير كل المصادر المستخدمه بواسطة العملية). ولا يشترط أن تنهي أي عملية استخدام الكائن قبل العملية الثانيه ويمكن لأي عملية سواء الأب أو الإبن أن تنهي التعامل مع الكائن وهذا لن يؤثر على تعامل العملية الأخرى مع الكائن.

الشكل التالي نجد جدول المقابض للعملية الإبن قبل أن تبدأ بالعمل وسوف نجد أنه تم نسخ مدخل المقبض القابل للوراثه في العملية الأب (وهو في الموقع 3) وتم نسخه الى نفس الموقع في العملية الإبن (3) كما نلاحظ أن عنوان الكائن في الذاكرة و الAccess Mask والFlags هي نفسها كما في العملية الأب.

الشكل 39 يبين جدول المقابض للعمليه الإبن
الشكل 39 يبين جدول المقابض للعمليه الإبن

وهذا يعني أنه في حال قامت العملية الإبن بانشاء عملية جديدة spawn process ومررت القيمه TRUE للمعامل bInheritHandle في الدالة CreateProcess فسوف يتم انشاء عملية ترث المقبض بنفس العنوان والAccess Mask والFlags. (لاحظ أن وراثه المقابض تكون في لحظه انشاء العملية الإبن وسوف ترث المقابض القابله للوراثه الحالية، وفي حال بعد ذلك قامت العملية الأب بانشاء Kernel Object ومقبضه قابل للوراثه فإن العملية الإبن لن ترث ذلك المقبض) .

B. الطريقة الثانية لتشارك كائنات الكيرنل Named Object :
الطريقة الثانيه لتشارك كائنات الكيرنل وهي عن طريق تسمية الكائنات Naming Objects، فأغلب كائنات الكيرنل يمكن تسميتها وقت انشائها ، على سبيل المثال كل الدوال التالية تستقبل معامل يحدد إسم كائن الكيرنلCreateMutex , CreateEvent , CreateSemaphore CreateFileMapping ,CreateJobObject .

الشكل 40 يبين أحد الدوال التي تنشئ Kernel Object
الشكل 40 يبين أحد الدوال التي تنشئ Kernel Object

كل هذه الدوال تستقبل معامل pszName وهو يحدد لنا اسم الكائن ، وعندما نرسل NULL لهذا المعامل فيتم انشاء كائن من غير اسم Anonymous Kernel Object وفي تلك الحالة اذا أردت مشاركة الكائن للعمليات الأخرى فيجب استخدام طريقة وراثه المقابض التي وضحناها قبل قليل أو أستخدام الدالة DuplicateHandle كما سيتبين عند الحديث حول الطريقة الثالثه للتشارك كائنات الكيرنل بين العمليات .

أما في حال ارسال اسم للكائن عند انشائه (ويجب أن يكون طوله بحدود MAX_PATH) وهنا سيتم وضع الكائن مع بقية كائنات الكيرنل المنشئة مسبقا في مساحة أسماء واحده Single Namespace وفي حال كان هناك كائن كيرنل له نفس الإسم (وحتى اذا لم يكن من نفس النوع فلن يتم انشاء الكائن الجديد) .

المثال التالي يوضح أنه لم يتم انشاء Semaphore وسترجع الدالة NULL لأن هناك كائن كيرنل له نفس الإسم AnyName وسوف تكون قيمه الناتج من الدالة GetLastError هي القيمه 6 (ERROR_INVALID_HANDLE) !

الشكل 41 يبين انشاء كائنان في الكيرنل بنفس الإسم
الشكل 41 يبين انشاء كائنان في الكيرنل بنفس الإسم

تعرفنا في الأعلى حول انشاء كائن في الكيرنل له اسم Naming Kernel Object ، ولكي يتم مشاركة الكائنات بين العمليات ستقوم العملية الأولى A بالإستدعاء للدالة وستقوم بانشاء كائن في الكيرنل وتعطي له الإسم MyMutex :

وعندما تبدأ العملية الثانية بالعمل (لا يشترط أن تكون أبن للعملية A وهذا ما يميز طريقة الNaming Object على طريقة Handle Inheritance) سوف يستدعي الدالة التالية :

قامت العملية الثانية بتسمية الكائن بنفس الإسم وسوف يقوم النظام في البداية بالتحقق من وجود كائن بنفس الإسم ولأنه يوجد هذا الكائن سوف يقوم النظام بالتحقق من نوع هذا الكائن وبما أنها من نفس النوع سوف يقوم النظام بالتحقق من الحماية Security وهل العملية الثانية لها صلاحيات الوصول وفي حال كان كذلك يقوم النظام بتهيئة مدخل جديد في جدول المقابض للعملية B ويكون المؤشر للكائن الموجود . أما اذا لم يكن الكائن من نفس النوع أو العملية ليست لها صلاحيات للوصول سوف تفشل الدالة وترجع NULL .

عادة الدوال التي تنشئ كائنات في الكيرنل (مثلا CreateMutex ) ترجع مقبض له كامل الصلاحيات Full Access وفي حال أردنا تحديد الصلاحيات لهذا المقبض فيتم استدعاء الدالة الإضافية والتي تنتهي بEx (مثل CreateMutexEx) .

نعود للمثال السابق ولنفرض أن الدالة عملت بنجاح وهنا بالطبع لن يتم انشاء كائن جديد ولكن كل ما في الأمر أن العملية B تكون حصلت على مقبض للكائن (المقبض كالعاده متعلق بالعملية Process-Relative ) وقد يكون قيمه هذا المقبض مختلفة ولكنه في النهايه يؤشر للكائن في الذاكرة . ولأنه تم الحصول على مقبض للكائن سوف يتم زياده الUsage Count للكائن بواحد ولن يتم حذف الكائن من الذاكرة الا في حال قامت العمليتان A و B باغلاق المقبض CloseHandle .

طريقة أخرى لتشارك الكائنات من خلال الأسماء وهو باستدعاء الدوال Open* وليس من خلال الدوال Create* ، ومن هذه الدوال : OpenMutex, OpenEvent OpenSemaphore , OpenJobObject . ولهذه الدوال نفس المعاملات للدوال التي تقوم بانشاء الكائن .

ولكن لن تستطيع ارسال القيمه NULL لإسم الكائن عند استدعاء دوال الOpen* ، ويجب أن يتم ارسال اسم للكائن وستقوم هذه الدوال بالبحث في مساحة الأسماء Namespace عن كائن له هذا الإسم واذا لم تم ايجاده سوف ترجع الدالة NULL وترجع دالة الخطأ GetLastError القيمه 2 وهي (ERROR_FILE_NOT_FOUND ) . واذا تم ايجاد كائن له نفس الإسم ولكنه من نوع مختلف سيتم ارجاع NULL وسوف ترجع دالة الخطأ GetLastError القيمه 2 وهي ERROR_INVALID_HANDLE. أما في حال كان من نفس النوع وسوف يفحص النظام الصلاحية (من خلال المعامل dwDesiredAccess) وهل يسمح لها وفي تلك الحالة سوف يتم الحصول على المقبض ويدخل في جدول المقابض وتزداد Usage Count للكائن بواحد. وفي حال تم ارسال القيمه TRUE للمعامل bInheritHandle في دوال Open* سوف يكون المقبض قابل للوراثه.

اذا الفرق الرئيسي بين دوال الCreate* والOpen* هي أن دوال الCreate* تنشئ الكائن اذا لم يكن موجود بينما تفشل دوال Open* في تلك الحالة.

ولم تضع مايكروسوفت طريقة أو أي نصائح لتسمية تلك الكائن وقد تحصل أحيانا مشكلة في حال قام شخص بكتابة برنامج وأنشئ كائنات بالإسم X وقام شخص أخر في برنامج أخر وأنشئ كائنات بنفس الإسم ، والحل الأفضل هو انشاء GUID واستخدام الإسم لهذا الGUID في اسم الكائنات. وهناك حل أخر عن طريق وضع الكائن في مساحة أخرى بخلاف المساحة العامة وسوف نتطرق لها بعد قليل.

الكائنات التي تسمى Named Object عاده تستخدم لكي تمنع تشغيل أكثر من نسخه واحده من التطبيق ولعمل ذلك فقط يتم استدعاء أي دالة من دوال Create* في الدالة الرئيسية وقم بتسمية الكائن بأي اسم ، وعندما ترجع الدالة يتم النظر لGetLastError فاذا كانت قيمته هي ERROR_ALREADY_EXISTS فهذا يعني أن هناك نسخه من التطبيق تعمل وبالتالي نقوم باغلاق النسخه الحالية ، المثال التالي يبين ذلك .

الشكل ‏42 يبين كود للتحقق من وجود نسخه أخرى تعمل من التطبيق
الشكل ‏42 يبين كود للتحقق من وجود نسخه أخرى تعمل من التطبيق

C. الطريقة الثالثة لتشارك كائنات الكيرنل DuplicateHandle:
الطريقة الثالثة لتشارك الكائنات بين العمليات وهو باستخدام الدالة DuplicateHandle ولها التصريح التالي.

الشكل 43 يبين تصريح الدالة DuplicateHandle
الشكل 43 يبين تصريح الدالة DuplicateHandle

وتقوم هذه الدالة بنسخ المدخل للمقبض من جدول المقابض في العملية الأولى الى جدول المقابض في العملية الثانية . والمعامل الأول والثالث في الدالة هما مقبضان يجب أن يكونا متعلقين بالعملية Process-Relative التي سوف تستدعي الدالة DuplicateHandle ويجب أن يكونا من نوع Process والا فلن تعمل الدالة .

المعامل الثاني وهو مقبض Handle من أي نوع من كائنات الكيرنل وهو غير متعلق بالعملية التي سوف تستدعي الدالة DuplicateHandle ويجب أن يكون متعلق بالعملية التي مقبضها هو المعامل الأول للدالة DuplicateHandle ، أما المعامل الرابع فهو مؤشر للمقبض والذي سيم نسخ قيمه المقبض في العملية hTargetProcessHandle اليه .أما أخر ثلاثه معاملات فهي لتحديد قيمه الAccess Mask والFlag وأخيرا المعامل dwOptions فهو يمكن أن بالقيمة 0 أو أحد الFlags التالية DUPLICATE_SAME_ACCESS و DUPLICATE_CLOSE_SOURCE .وعند تحديد الFlag الأول سوف يتم نسخ الAccess Mask كما هو من المقبض في العملية الأولى الى العملية الثانية وسوف يتم حينها تجاهل المعامل الخامس في الدالة DuplicateHandle ، أما الFlag الثاني فيعني أن العملية سوف تغلق المقبض بعد النسخ وبالتالى لن يتأثر الUsage Count عند استخدام هذا الFlag (لأنه سيطرح منه واحد وسوف يزيد بواحد).

نأخذ مثال وليكن لدينا العملية S وهي العملية الأولى source process والتي تملك بعضا من كائنات الكيرنل وليكن لدينا العملية T وهي العملية الثانية والتي تريد الوصول لذلك الكائن Target Process وليكن لدينا العملية C وهي العملية التي ستقوم باستدعاء الدالة DuplicateHandle لكي تقوم بالنسخ.

الشكل التالي يبين جدول المقابض للعملية C ويحتوي على مقبض للعملية الأولى S ومقبض للعملية الثانية T .

الشكل 44 يبين Process C Handle Table
الشكل 44 يبين Process C Handle Table

والشكل التالي يبين جدول المقابض للعملية S ويحتوي على مقبض واحد لكائن في الكيرنل :

الشكل 45 يبين Process S Handle Table
الشكل 45 يبين Process S Handle Table

الشكل التالي بين جدول المقابض للعملية T قبل أن تستدعي العملية C الدالة DuplicateHandle ويحتوي جدول المقابض للعملية T على مقبض لكائن في الكيرنل.

الشكل ‏46 يبين Process T Handle Table
الشكل ‏46 يبين Process T Handle Table

بعد ذلك عندما تقوم العملية C باستدعاء الدالة DuplicateHandle بالكود التالي سوف يكون شكل جدول المقابض للعملية T كما يوضحه الشكل التالي.

الشكل 47 يبين Process T Handle Table
الشكل 47 يبين Process T Handle Table

وكما يوضح الشكل السابق فإن مدخل المقبض الثاني في العملية S تم نسخه كمدخل أول في جدول المقابض في العملية T ، وقامت الدالة DuplicateHandle بوضع القيمه 1 في المتغير hObj وهو موقع Index المدخل في العملية T . ولأنه تم استخدام الFlag (DUPLICATE_SAME_ACCESS) فسوف يكون الAccess Mask لهذا المقبض في العملية T مشابه للAccess Mask في مدخل المقبض في العملية S وهكذا لن يتم النظر الى المعامل dwDesiredAccess. وأخيراً سنجد أن الFlag المختص بالوراثه (العمود الأخير في الشكل السابق) للمدخل الجديد أصبح 1 والسبب أننا أرسلنا القيمه True كمعامل للدالة DuplicateHandle وقمنا بتحديد أن ذلك المقبض قابل للوراثه .

لكن بعد استدعاء الدالة DuplicateHandle لن تعرف العملية أن الكائن أصبح متاح للإستخدام ولذلك يجب أن تقوم العملية C باخبار Notify العملية T بعد استدعاء الدالة DuplicateHandle وذلك باستخدام أي طريقة من طرق التواصل بين العمليات Interprocess Communication (IPC) .

ما قمنا بعمله الأن باستخدام الدالة DuplicateHandle من خلال 3 عمليات حتى نقوم بعملية تشارك الكائن ولكن في الغالب لا تستخدم هذه الطريقة بسبب أن العملية الثالثه C قد لا تعرف رقم المقبض للكائن في العملية الأولى S .

لكن الدالة DuplicateHandle دالة مرنه ويمكن أن نستخدمها فقط بين عمليتين كما سنوضح ذلك الأن ، ولنفرض أن لدينا العملية S تمتلك كائن في الكيرنل وتريد أن تسمح للعملية T الوصول لهذا الكائن وسوف يتم استدعاء الدالة DuplicateHandle بالطريقة التي يوضحها الشكل 6-48.

استدعاء الدالة GetCurrentProcess سوف يرجع مقبض للعملية الحالية ، وبعد استدعاء الدالة DuplicateHandle سوف يكون المتغير hObjInProcessT هو مقبض متعلق بالعملية T ويؤشر لنفس الكائن الذي يؤشر اليه hObjInProcessS . ويجب على العملية S عدم اغلاق المقبض المتعلق بالعملية T كما يوضح الكود التالي ذلك.

الشكل ‏ 48 يبين طريقة تمرير المقبض للعملية T بعد استخدام الدالة DuplicateHandle
الشكل ‏ 48 يبين طريقة تمرير المقبض للعملية T بعد استخدام الدالة DuplicateHandle

الى هنا نصل لنهاية الجزء الأول ..
المره القادمة سنبدأ بالغوص في Memory Management أو الProcess/Thread

 

المرجع الأساسي:
Windows via C/C++ by Jeffrey Richter and Christophe Nasarre
Windows System Programming by Johnson M. Hart

(264)

وجدي عصام مهندس برمجيات مهتم بعلوم الحاسب وبالأخص مجال الخوارزميات وهندسة البرمجيات وحماية التطبيقات،

LEAVE YOUR COMMENT

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *

مشاركة