السلام عليكم ورحمة الله وبركاته
تتمّة لكود الأستاذ Z3r0n3 في هذه المشاركة
http://arabteam2000-...ر/#entry1107066
أثناء تطبيق استخدام Procedure حدثت معي بعض الأخطاء , ورغبت أن ألخصها للحذر من الوقوع فيها .. ثم رأيت أنه يمكن استغلالها لكتابة كود بطريقة مبتكرة
فهرس المقال :
1- مقدمة عن الإجراء الفرعي وكيفية استخدامه
2- مخاطر الاستخدام الخاطئ للإجراء الفرعي
3- طرق مبتكرة للاستغلال الخاطئ لـ call و ret
مقدمة عن الإجراءات الفرعية :
الإجراء الفرعي عملياً هو مجرد عملية قفز إلى موضع معين من الكود في الذاكرة وتنفيذ الخطوات حتى الوصول إلى التعليمة ret
ويمكن الاستعاضة عنه تقريباً بتعليمة jmp للقفز إلى بداية الإجراء وتعليمة jmp أخرى للعودة إلى مكان الاستدعاء .. ولكن استخدام call واستدعاء procedure أسهل بكثير
لا يوجد تعريف للإجراء بشكل صريح داخل الكود , ولكنه يحتوي على تعليمة ret في نهايته غالبا .
مثال على استخدامه :
وسيناريو استدعاء إجراء وتنفيذه هو التالي :
أولا: يتم دفع مسجل مقطع الكود ثم مسجل مؤشر التعليمات (يؤشر على التعليمة التالية لتعليمة الاستدعاء) في المكدس لحفظ المكان الحالي في الكود
ثم يتم تحميل القيم الجديدة لموضع الإجراء الفرعي الذي نرغب في تنفيذه .
(ملاحظة : في حالة الانتقال إلى إجراء في نفس المقطع يتم دفع مؤشر التعليمات فقط في المكدس .. وهذا ما نناقشه في هذه المشاركة )
ثانياً : يتم تنفيذ التعليمات بشكل طبيعي إلى أن نصل إلى التعليمة ret
ثالثاً : يتم اخراج قيمة مؤشر التعليمات ثم قيمة مسجل مقطع الكود المحفوظين في المكدس إلى مسجلاتهما الخاصة
رابعا : لو قمنا بإعطاء التعليمة ret وسيطاً , فسيتم زيادة قيمة مؤشر المكدّس بقيمة هذا الوسيط , ثم سنتابع تنفيذ تعليمات الكود التي يشير لها IP بشكل طبيعي .
المخاطر تأتي من استخدام الخطوات السابقة بشكل مختلف كما يلي :
الخطر الأول : إذا قمنا باستخدام المكدّس داخل كود الإجراء الفرعي بشكل خاطئ .. فسيؤدي هذا إلى احتمال عدم رجوعنا إلى مكان الاستدعاء حتى بعد الوصول إلى التعليمة ret
فقد نقوم بتحميل قيم مغايرة لقيم المؤشر ومسجل المقطع إن قمنا بتخزين قيم إضافية في المكدّس ولم نخرجها قبل انتهاء الإجراء الفرعي .
الخطر الثاني : إذا لم نكتب التعليمة ret فسيتم متابعة تنفيذ الكود المخزّن بعد انتهاء الإجراء ..
ولن نرجع إلى الكود الأصلي (الذي قام بالاستدعاء ) إلى في حال واجهنا ret مصادفة .. وطبعا هذا سيؤدي لتنفيذ تعليمات غير مرغوبة
الخطر الثالث : قد نقوم بإعطاء أي قيمة كوسيط للتعليمة ret مما قد يؤدي بنا إلى تجاوز حجم المكدّس كله والانتقال إلى قمته فوراً ..(مثلا لو كان مؤشر المكدس على FFFC وكان الوسيط 20 مثلاً .. فسننتقل إلى 001E )
والآن : ما رأيك في استخدام المخاطر السابقة في زيادة التحكم بالكود
----- يمكننا الآن تغيير قيمة المؤشر ip كما نريد بالطريقة التالية :
1- ندفع القيمة الجديدة في المكدس
2- ننفذ التعليمة ret
سيتم تحميل آخر قيمة في المكدّس إلى المسجّل IP ( مؤشر التعليمات ) مباشرة بفضل ret
انظر إلى المثال التالي :
كما يمكننا استخدام call للقفز مع التضحية بخانة أو اثنتين من المكدّس ..
انظر المثال التالي :
(انتبه : الكود في حلقة لا نهائية ولكنه مجرد توضيح )
هناك عدد غير محدود من الأفكار التي يمكننا بها استغلال خواص ret و call الفريدة .. وسأختم بالفكرة التالية :
ما رأيك لو نستدعي الإجراء بشكل عودي بدون ret ولكن مع وضع شرط على مؤشر المكدس
فيما يلي مثال يوضّح استخدام الإجراءات .. (الكود يقوم بعملية ضرب رقمين )
تخيّل أننا نريد تغيير الآلية الأساسية لعملية الضرب التي استخدمناها ..
يمكننا ببساطة كتابة إجراء جديد .. بدلا من القديم .. دون المساس بأجزاء أخرى من الكود .. ودون عناء البحث عن موضع عملية الضرب :
الكود القديم :
الكود السابق يقوم بالجمع لتنفيذ الضرب ضمن حلقة
ما رأيك لو أحببت القيام بالعملية بطريقة أخرى :
الآن لدينا 3 إجرائيات تقوم بنفس الوظيفة ولها نفس الواجهة (أي طريقة إدخال الوسطاء والرجوع بالنتيجة )
ولنا حرية اختيار أي منها داخل برنامجنا ... وهذا يبيّن أهمية تجزئة البرنامج ... فرّق تسُد
والله ولي التوفيق
الرابط الأصلي
تتمّة لكود الأستاذ Z3r0n3 في هذه المشاركة
http://arabteam2000-...ر/#entry1107066
أثناء تطبيق استخدام Procedure حدثت معي بعض الأخطاء , ورغبت أن ألخصها للحذر من الوقوع فيها .. ثم رأيت أنه يمكن استغلالها لكتابة كود بطريقة مبتكرة
فهرس المقال :
1- مقدمة عن الإجراء الفرعي وكيفية استخدامه
2- مخاطر الاستخدام الخاطئ للإجراء الفرعي
3- طرق مبتكرة للاستغلال الخاطئ لـ call و ret
مقدمة عن الإجراءات الفرعية :
الإجراء الفرعي عملياً هو مجرد عملية قفز إلى موضع معين من الكود في الذاكرة وتنفيذ الخطوات حتى الوصول إلى التعليمة ret
ويمكن الاستعاضة عنه تقريباً بتعليمة jmp للقفز إلى بداية الإجراء وتعليمة jmp أخرى للعودة إلى مكان الاستدعاء .. ولكن استخدام call واستدعاء procedure أسهل بكثير
لا يوجد تعريف للإجراء بشكل صريح داخل الكود , ولكنه يحتوي على تعليمة ret في نهايته غالبا .
مثال على استخدامه :
13DC:0100 mov ah,2 13DC:0102 mov dl,30 13DC:0104 int 21 13DC:0106 call 10D 13DC:0109 int 20 13DC:010B nop 13DC:010C nop 13DC:010D mov dl,31 13DC:010F int 21 13DC:0111 mov dl,32 13DC:0113 int 21 13DC:0115 retحيث استخدمنا call نعتبر التعليمات بين العنوان 10D وبين الوصول إلى ret إجراءا فرعيا ..
وسيناريو استدعاء إجراء وتنفيذه هو التالي :
أولا: يتم دفع مسجل مقطع الكود ثم مسجل مؤشر التعليمات (يؤشر على التعليمة التالية لتعليمة الاستدعاء) في المكدس لحفظ المكان الحالي في الكود
ثم يتم تحميل القيم الجديدة لموضع الإجراء الفرعي الذي نرغب في تنفيذه .
(ملاحظة : في حالة الانتقال إلى إجراء في نفس المقطع يتم دفع مؤشر التعليمات فقط في المكدس .. وهذا ما نناقشه في هذه المشاركة )
ثانياً : يتم تنفيذ التعليمات بشكل طبيعي إلى أن نصل إلى التعليمة ret
ثالثاً : يتم اخراج قيمة مؤشر التعليمات ثم قيمة مسجل مقطع الكود المحفوظين في المكدس إلى مسجلاتهما الخاصة
رابعا : لو قمنا بإعطاء التعليمة ret وسيطاً , فسيتم زيادة قيمة مؤشر المكدّس بقيمة هذا الوسيط , ثم سنتابع تنفيذ تعليمات الكود التي يشير لها IP بشكل طبيعي .
المخاطر تأتي من استخدام الخطوات السابقة بشكل مختلف كما يلي :
الخطر الأول : إذا قمنا باستخدام المكدّس داخل كود الإجراء الفرعي بشكل خاطئ .. فسيؤدي هذا إلى احتمال عدم رجوعنا إلى مكان الاستدعاء حتى بعد الوصول إلى التعليمة ret
فقد نقوم بتحميل قيم مغايرة لقيم المؤشر ومسجل المقطع إن قمنا بتخزين قيم إضافية في المكدّس ولم نخرجها قبل انتهاء الإجراء الفرعي .
الخطر الثاني : إذا لم نكتب التعليمة ret فسيتم متابعة تنفيذ الكود المخزّن بعد انتهاء الإجراء ..
ولن نرجع إلى الكود الأصلي (الذي قام بالاستدعاء ) إلى في حال واجهنا ret مصادفة .. وطبعا هذا سيؤدي لتنفيذ تعليمات غير مرغوبة
الخطر الثالث : قد نقوم بإعطاء أي قيمة كوسيط للتعليمة ret مما قد يؤدي بنا إلى تجاوز حجم المكدّس كله والانتقال إلى قمته فوراً ..(مثلا لو كان مؤشر المكدس على FFFC وكان الوسيط 20 مثلاً .. فسننتقل إلى 001E )
والآن : ما رأيك في استخدام المخاطر السابقة في زيادة التحكم بالكود
----- يمكننا الآن تغيير قيمة المؤشر ip كما نريد بالطريقة التالية :
1- ندفع القيمة الجديدة في المكدس
2- ننفذ التعليمة ret
سيتم تحميل آخر قيمة في المكدّس إلى المسجّل IP ( مؤشر التعليمات ) مباشرة بفضل ret
انظر إلى المثال التالي :
13DC:0100 mov ax,100 13DC:0103 push ax 13DC:0104 retالمثال السابق يستخدم push ثم ret للقفز .. لاحظ أننا نحدد العنوان الحقيقي عوضا عن تحديد الفرق بين عنوان jmp والعنوان المراد الوصول له في الحالة المعتادة .
كما يمكننا استخدام call للقفز مع التضحية بخانة أو اثنتين من المكدّس ..
انظر المثال التالي :
13DC:0100 mov ah,2 13DC:0102 mov dl,41 13DC:0104 int 21 13DC:0106 call 100الكود السابق يدخل في حلقة لا نهائية من طباعة A ولكنه يخرج بعد امتلاء المكدس بالعنوان 106
يمكننا مثلاً أن نستعمل قيمة العنوان في عملياتنا وذلك كما يلي :
13DC:0100 pop dx 13DC:0101 mov ah,2 13DC:0103 int 21 13DC:0105 nop 13DC:0106 nop 13DC:0107 nop 13DC:0108 nop 13DC:0109 call 100جعلنا الكود الموجود في 100 إجراء فرعيا يقوم بسحب الIP من المكدس وطباعة المحرف الموافق له
(انتبه : الكود في حلقة لا نهائية ولكنه مجرد توضيح )
هناك عدد غير محدود من الأفكار التي يمكننا بها استغلال خواص ret و call الفريدة .. وسأختم بالفكرة التالية :
ما رأيك لو نستدعي الإجراء بشكل عودي بدون ret ولكن مع وضع شرط على مؤشر المكدس
13DC:0100 call 103 13DC:0103 mov bx,sp 13DC:0105 cmp bx,FFCC 13DC:0108 jle 114 13DC:010A push bx 13DC:010B mov ah,2 13DC:010D mov dl,31 13DC:010F int 21 13DC:0111 call 103 13DC:0114 int 20اقرأ الكود السابق وحاول معرفة كيف يمكنك تحديد عدد الاستدعاءات ...
فيما يلي مثال يوضّح استخدام الإجراءات .. (الكود يقوم بعملية ضرب رقمين )
;هذا الكود يوضّح عملية ضرب رقمين من الدخل وطباعتهما باستخدام العمليات ;Procedures ; ويمكنك قراءته وفهمه بسهولة بسبب القاعدة : فرّق تسد :) إنها السيطرة .model small .stack 100h .data .code;كل استدعاء يدل على وظيفته من اسمه call read_number call write_Multiplication_Sign call read_number call write_Equal_Sign call find_Result call print_Result call End_program read_number proc mov ax,0100h;نقوم بمقاطعة الدخل لحرف واحد int 21h sub al,30h;نحول الحرف الى رقم mov ah,00h;نصفّر الجزء العلوي من المسجل حتى يختفظ المسجّل بشكل كامل بالقيمة لنتمكن من دفعه للمكدّس pop bx;المكدّس يحتفظ بقيمة مؤشّر التعليمات لذلك يجب أن نحافظ عليه push ax;نصع نتيجة الإجراء في المكدّس push bx;ونعيد مؤشر التعليمات الى المكدّس ret;ونعود إلى التعليمة التي يشير لها مؤشر التعليمات read_number endp write_Multiplication_Sign proc mov ah,02h;تخزين قيمة مقاطعة الخرج لحرف واحد mov dl,2Ah;تخزين قيمة الآسكي لإشارة الجمع int 21h;طباعة الحرف المخزّن في المسجل السابق ret;العودة لحيث يؤشّر مؤشر التعليمات write_Multiplication_Sign endp write_Equal_Sign proc;نفس الإجراء السابق ولكن مع تغيير قيمة الآسكي لتطبع إشارة المساواة mov ah,02h mov dl,3Dh int 21h ret write_Equal_Sign endp find_Result proc pop cx;نحتفظ بمؤشر التعليمات pop ax;نأخذ الرقم الأول من المكدّس pop bx;نأخذ الرقم الثاني من المكدّس push cx;نرجع مؤشر التعليمات call multiply_Ax_Bx;نستعدي إجراء ضرب هذين المسجّلين ret find_Result endp multiply_Ax_Bx proc mul bx;سنستخدم الضرب العادي ret multiply_Ax_Bx endp print_Result proc;ax;يقوم بطباعة النتيجة الموجودة في mov cl,0Ah;نخزن الرقم 10 div cl;على 10;ax;نقسم ;حسب آلية عمل تعليمة القسمة;ah;وباقي القسمة في;al;ستخزّن نتيجة القسمة في ;al;والعشرات في;ah;الآحاد في; mov dx,ax; mov ah,02h;نضع قيمة تعليمة طباعة حرف add dh,30h;نحوّل الآحاد من رقم إلى حرف add dl,30h;نحول العشرات من رقم إلى حرف int 21h;نطبع العشرات mov dl,dh; int 21h;نطبع الآحاد ;dl;الطباعة تتم للقيمة الموجودة في ret print_Result endp End_Program proc;يقوم بتنفيذ مقاطعة الخروج من البرنامج mov ah,4Ch int 21h ret End_Program endp endوالآن سأبين أهمية "تفصيص" البرنامج لإجراءات ...
تخيّل أننا نريد تغيير الآلية الأساسية لعملية الضرب التي استخدمناها ..
يمكننا ببساطة كتابة إجراء جديد .. بدلا من القديم .. دون المساس بأجزاء أخرى من الكود .. ودون عناء البحث عن موضع عملية الضرب :
الكود القديم :
multiply_Ax_Bx proc mul bx ret multiply_Ax_Bx endpنريد تغييره إلى :
multiply_Ax_Bx_2 proc mov cx,ax mov ax,0 mab2start: cmp bx,0 je ret_ dec bx add ax,cx jmp mab2start ret_: ret multiply_Ax_Bx_2 endp(قمت بتغيير الاسم لهدف سأبينه لاحقا)
الكود السابق يقوم بالجمع لتنفيذ الضرب ضمن حلقة
ما رأيك لو أحببت القيام بالعملية بطريقة أخرى :
multiply_Ax_Bx_3 proc mov cx,bx mov bx,ax mov ax,0 cmp cx,0 je ret_ mab3start: add ax,bx loop mab2start ret_2: ret multiply_Ax_Bx_3 endpالكود السابق يستخدم loop لإجراء الحلقة ..
الآن لدينا 3 إجرائيات تقوم بنفس الوظيفة ولها نفس الواجهة (أي طريقة إدخال الوسطاء والرجوع بالنتيجة )
ولنا حرية اختيار أي منها داخل برنامجنا ... وهذا يبيّن أهمية تجزئة البرنامج ... فرّق تسُد
والله ولي التوفيق
الرابط الأصلي
ليست هناك تعليقات:
إرسال تعليق