2015/08/01

الإجراءات الفرعية Procedures في الأسمبلي

السلام عليكم ورحمة الله وبركاته
تتمّة لكود الأستاذ 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 إجرائيات تقوم بنفس الوظيفة ولها نفس الواجهة (أي طريقة إدخال الوسطاء والرجوع بالنتيجة )
ولنا حرية اختيار أي منها داخل برنامجنا ... وهذا يبيّن أهمية تجزئة البرنامج ... فرّق تسُد :)

والله ولي التوفيق

الرابط الأصلي

ليست هناك تعليقات:

إرسال تعليق