ما يحدث وراء الكواليس C #: أساسيات العمل مع المكدس

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

تنصل


قبل متابعة القصة ، أوصي بشدة بقراءة أول منشور حول StructLayout ، وهناك مثال سيُستخدم في هذه المقالة.

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

أود أيضًا أن أحذر من أن هذه المقالة لا تحتوي على مواد يجب استخدامها في مشاريع حقيقية.

النظرية الأولى


أي رمز يصبح في نهاية المطاف مجموعة من أوامر الجهاز. المفهوم الأكثر هو تمثيلهم في شكل تعليمات لغة التجميع التي تتوافق مباشرة مع واحد (أو عدة) تعليمات الجهاز.


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

الآن دعونا نلقي نظرة على الجزء التالي من التعليمات البرمجية في لغة التجميع (لقد حذفت بعض المكالمات المتأصلة في وضع التصحيح).

جيم #:

public class StubClass { public static int StubMethod(int fromEcx, int fromEdx, int fromStack) { int local = 5; return local + fromEcx + fromEdx + fromStack; } public static void CallingMethod() { int local1 = 7, local2 = 8, local3 = 9; int result = StubMethod(local1, local2, local3); } } 

تعدين الذهب حرفيا:

 StubClass.StubMethod(Int32, Int32, Int32) 1: push ebp 2: mov ebp, esp 3: sub esp, 0x10 4: mov [ebp-0x4], ecx 5: mov [ebp-0x8], edx 6: xor edx, edx 7: mov [ebp-0xc], edx 8: xor edx, edx 9: mov [ebp-0x10], edx 10: nop 11: mov dword [ebp-0xc], 0x5 12: mov eax, [ebp-0xc] 13: add eax, [ebp-0x4] 14: add eax, [ebp-0x8] 15: add eax, [ebp+0x8] 16: mov [ebp-0x10], eax 17: mov eax, [ebp-0x10] 18: mov esp, ebp 19: pop ebp 20: ret 0x4 StubClass.CallingMethod() 1: push ebp 2: mov ebp, esp 3: sub esp, 0x14 4: xor eax, eax 5: mov [ebp-0x14], eax 6: xor edx, edx 7: mov [ebp-0xc], edx 8: xor edx, edx 9: mov [ebp-0x8], edx 10: xor edx, edx 11: mov [ebp-0x4], edx 12: xor edx, edx 13: mov [ebp-0x10], edx 14: nop 15: mov dword [ebp-0x4], 0x7 16: mov dword [ebp-0x8], 0x8 17: mov dword [ebp-0xc], 0x9 18: push dword [ebp-0xc] 19: mov ecx, [ebp-0x4] 20: mov edx, [ebp-0x8] 21: call StubClass.StubMethod(Int32, Int32, Int32) 22: mov [ebp-0x14], eax 23: mov eax, [ebp-0x14] 24: mov [ebp-0x10], eax 25: nop 26: mov esp, ebp 27: pop ebp 28: ret 

أول شيء يجب ملاحظته هو أن EBP وسجلات ESP والعمليات معهم.

هناك اعتقاد خاطئ بأن سجل EBP مرتبط بطريقة أو بأخرى بالمؤشر الموجود في الجزء العلوي من الحزمة وهو أمر شائع بين أصدقائي. يجب أن أقول أنه ليس كذلك.

سجل ESP هو المسؤول عن الإشارة إلى الجزء العلوي من المكدس. في المقابل ، مع كل تعليمات PUSH (وضع قيمة في الجزء العلوي من المكدس) يتم تقليل قيمة سجل ESP (المكدس ينمو نحو عناوين أصغر) ، ومع كل تعليمات POP يتم زيادة. أيضًا ، يدفع أمر CALL عنوان المرسل على المكدس ، وبالتالي يقلل من قيمة سجل ESP . في الواقع ، لا يتم تغيير سجل ESP فقط عند تنفيذ هذه التعليمات (على سبيل المثال ، عند إجراء مكالمات المقاطعة ، يحدث نفس الشيء مع تعليمات CALL ).

سوف تنظر StubMethod () .

في السطر الأول ، يتم حفظ محتوى سجل EBP (يتم وضعه في مكدس). قبل العودة من دالة ، سيتم استعادة هذه القيمة.

يخزن السطر الثاني القيمة الحالية لعنوان الجزء العلوي من الرصة (يتم نقل قيمة السجل ESP إلى EBP ). بعد ذلك ، ننقل الجزء العلوي من الرصة إلى العديد من المواضع التي نحتاجها لتخزين المتغيرات والمعلمات المحلية (الصف الثالث). شيء مثل تخصيص الذاكرة لجميع الاحتياجات المحلية - إطار مكدس . في الوقت نفسه ، يعد سجل EBP نقطة انطلاق في سياق المكالمة الحالية. يعتمد العنوان على هذه القيمة.

كل ما سبق يسمى مقدمة الوظيفة .

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

تذكير Fastcall : في .net ، يتم استخدام اصطلاح استدعاء fastcall .
تحكم اصطلاح استدعاء الموقع وترتيب المعلمات التي تم تمريرها إلى الدالة.
يتم تمرير المعلمتين الأولى والثانية عبر سجلات ECX و EDX ، على التوالي ، يتم نقل المعلمات اللاحقة عبر المكدس. (هذا لأنظمة 32 بت ، كما هو الحال دائمًا. في الأنظمة 64 بت ، مرت أربعة معلمات من خلال السجلات ( RCX ، RDX ، R8 ، R9 ))

بالنسبة للطرق غير الثابتة ، تكون المعلمة الأولى ضمنية وتحتوي على عنوان المثيل الذي تسمى عليه (هذا العنوان).

في السطور 4 و 5 ، يتم تخزين المعلمات التي تم تمريرها من خلال السجلات (أول 2) على المكدس.

التالي هو تنظيف المساحة على المكدس للمتغيرات المحلية ( إطار مكدس ) وتهيئة المتغيرات المحلية.

تجدر الإشارة إلى أن نتيجة الوظيفة في السجل EAX .

في الأسطر 12-16 ، تحدث إضافة المتغيرات المطلوبة. أود أن ألفت انتباهكم إلى السطر 15. هناك قيمة الوصول عن طريق العنوان أكبر من بداية المكدس ، أي إلى مكدس الأسلوب السابق. قبل الاتصال ، يقوم المتصل بدفع معلمة إلى أعلى المكدس. هنا نقرأها. يتم الحصول على نتيجة الإضافة من السجل EAX ووضعها على المكدس. نظرًا لأن هذه هي القيمة المرجعة لـ StubMethod () ، يتم وضعها مرة أخرى في EAX . بالطبع ، مجموعات التعليمات السخيفة هذه متأصلة فقط في وضع التصحيح ، لكنها توضح تمامًا كيف يبدو رمزنا بدون محسّن ذكي يقوم بحصة الأسد في العمل.

في السطور 18 و 19 ، تتم استعادة كل من EBP السابق (طريقة الاستدعاء) والمؤشر إلى أعلى المكدس (في الوقت الذي يطلق عليه الأسلوب). السطر الأخير هو العودة من الوظيفة. حول القيمة 0x4 سأقول في وقت لاحق قليلا.

مثل هذا التسلسل من الأوامر يسمى خاتمة الوظيفة.

الآن دعونا نلقي نظرة على CallingMethod () . دعنا نذهب مباشرة إلى السطر 18. هنا نضع المعلمة الثالثة في الجزء العلوي من المكدس. يرجى ملاحظة أننا نقوم بذلك باستخدام تعليمة PUSH ، أي أن قيمة ESP تنخفض. يتم وضع المعلمات 2 الأخرى في السجلات ( fastcall ). التالي يأتي استدعاء الأسلوب StubMethod () . الآن دعونا نتذكر تعليمات RET 0x4 . هنا السؤال التالي ممكن: ما هو 0x4؟ كما ذكرت أعلاه ، لقد دفعنا المعلمات الدالة تسمى على المكدس. ولكن الآن نحن لسنا في حاجة إليها. يشير 0x4 إلى عدد البايتات التي يجب مسحها من المكدس بعد استدعاء الوظيفة. منذ أن كانت المعلمة واحدة ، تحتاج إلى مسح 4 بايت.

هنا صورة تقريبية للمكدس:



وبالتالي ، إذا انقلبنا ورأينا ما الذي يكمن في المكدس مباشرة بعد استدعاء الأسلوب ، فإن أول شيء سنراه EBP ، والذي تم دفعه إلى المكدس (في الواقع ، حدث هذا في السطر الأول من الطريقة الحالية). والشيء التالي سيكون عنوان المرسل. إنه يحدد المكان ، وهناك لاستئناف التنفيذ بعد الانتهاء من وظيفتنا (المستخدمة من قبل RET ). وبعد هذه الحقول مباشرةً ، سنرى معلمات الوظيفة الحالية (بدءًا من الثالث ، يتم تمرير المعلمتين الأوليين من خلال السجلات). ورائهم يخفي كومة طريقة الاتصال!

يشرح الحقلان الأول والثاني المذكوران سابقًا ( EBP وعنوان المرسل) الإزاحة في + 0x8 عندما نصل إلى المعلمات.

في المقابل ، يجب أن تكون المعلمات في الجزء العلوي من المكدس في ترتيب محدد بدقة قبل استدعاء وظيفة. لذلك ، قبل استدعاء الأسلوب ، يتم دفع كل معلمة على المكدس.
ولكن ماذا لو لم يدفعوا ، وستظل الوظيفة تأخذهم؟

مثال صغير


لذلك ، تسببت كل الحقائق المذكورة أعلاه لي رغبة غامرة لقراءة مكدس الطريقة التي سوف استدعاء طريقتي. فكرة أنني في موقف واحد فقط من الوسيطة الثالثة (ستكون الأقرب إلى مجموعة مكالمات طريقة الاتصال) هي البيانات العزيزة التي أريد تلقيها كثيرًا ، ولم تسمح لي بالنوم.

وبالتالي ، لقراءة مكدس طريقة الاتصال ، أحتاج إلى تسلق أبعد قليلاً من المعلمات.

عند الإشارة إلى المعلمات ، يعتمد حساب عنوان معلمة معينة فقط على حقيقة أن المتصل دفعها جميعًا إلى المكدس.

لكن المرور الضمني لمعلمة EDX (من يهمه الأمر - المقال السابق ) يجعلني أعتقد أنه يمكننا التغلب على المترجم في بعض الحالات.

تسمى الأداة التي استخدمتها للقيام بذلك StructLayoutAttribute (توجد ميزات al في المقالة الأولى ). / / في يوم ما سوف أتعلم أكثر قليلاً من هذه السمة ، أعدك بذلك

نحن نستخدم نفس الطريقة المفضلة مع أنواع المراجع المتداخلة.

في الوقت نفسه ، إذا كانت الأساليب المتداخلة تحتوي على عدد مختلف من المعلمات ، فلن يقوم المحول البرمجي بدفع المعلمات المطلوبة إلى المكدس (على الأقل لأنه لا يعرف أي منها).
ومع ذلك ، فإن الطريقة التي تسمى فعليًا (مع الإزاحة نفسها من نوع مختلف) ، تتحول إلى عناوين إيجابية بالنسبة إلى مكدسها ، أي تلك التي تخطط للعثور على المعلمات.

ولكن لا أحد يمر المعلمات وتبدأ الطريقة في قراءة مكدس طريقة الاتصال. وعنوان الكائن (مع خاصية Id ، والذي يتم استخدامه في WriteLine () ) هو في المكان ، حيث من المتوقع المعلمة الثالثة.

الرمز في المفسد
 using System; using System.Runtime.InteropServices; namespace Magic { public class StubClass { public StubClass(int id) { Id = id; } public int Id; } [StructLayout(LayoutKind.Explicit)] public class CustomStructWithLayout { [FieldOffset(0)] public Test1 Test1; [FieldOffset(0)] public Test2 Test2; } public class Test1 { public virtual void Useless(int skipFastcall1, int skipFastcall2, StubClass adressOnStack) { adressOnStack.Id = 189; } } public class Test2 { public virtual int Useless() { return 888; } } class Program { static void Main() { Test2 objectWithLayout = new CustomStructWithLayout { Test2 = new Test2(), Test1 = new Test1() }.Test2; StubClass adressOnStack = new StubClass(3); objectWithLayout.Useless(); Console.WriteLine($"MAGIC - {adressOnStack.Id}"); // MAGIC - 189 } } } 


لن أعطي رمز لغة التجميع ، كل شيء واضح هناك ، لكن إذا كان هناك أي أسئلة ، سأحاول الإجابة عليها في التعليقات

أفهم تمامًا أنه لا يمكن استخدام هذا المثال في الممارسة العملية ، لكن في رأيي ، قد يكون مفيدًا للغاية لفهم مخطط العمل العام.

Source: https://habr.com/ru/post/ar447274/


All Articles