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

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

تنويه


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

يتم تقديم جميع التعليمات البرمجية وراء المستوى الأعلى لوضع التصحيح ، هو الذي يظهر الأساس المفاهيمي. أيضًا ، يتم اعتبار كل ما سبق لمنصة 32 بت. تحسين 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 (يضع القيمة في أعلى المكدس) ، يتم إنقاص قيمة هذا السجل (ينمو المكدس باتجاه العناوين الأقل) ، ومع كل عملية POP ، يزداد. يدفع الأمر CALL أيضًا عنوان الإرجاع إلى المكدس ، وبالتالي تقليل قيمة سجل ESP أيضًا. في الواقع ، لا يتم تغيير سجل ESP فقط عند تنفيذ هذه التعليمات (على سبيل المثال ، عندما يتم تنفيذ مكالمات المقاطعة ، يحدث نفس الشيء عند تنفيذ تعليمات CALL ).

ضع في الاعتبار StubMethod.

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

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

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

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

تذكير بشأن fastcall : يستخدم .net الأصلي اصطلاح استدعاء fastcall .
تحكم الاتفاقية موقع وترتيب المعلمات التي تم تمريرها إلى الوظيفة.
مع fastcall ، يتم تمرير المعلمتين الأولى والثانية من خلال تسجيلات ECX و EDX ، على التوالي ، ويتم تمرير المعلمات اللاحقة من خلال المكدس.

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

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

التالي هو تنظيف مساحة المكدس للمتغيرات المحلية وتهيئة المتغيرات المحلية.

من الجدير بالذكر أن نتيجة الوظيفة موجودة في سجل EAX .

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

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

يعود السطر الأخير. حول القيمة 0x4 سأقول أقل قليلاً.
هذا التسلسل من الأوامر يسمى الخاتمة الوظيفية.

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

فيما يلي صورة مكدس عينة:



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

مثال صغير


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

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

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

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

الأداة التي فعلت ذلك تسمى StructLayoutAttribute (الميزات في المقالة الأولى ). // يوما ما سأتعلم شيئا آخر غير هذه السمة ، أعدك.

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

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

ولكن هناك لا يجدها ويبدأ في قراءة كومة طريقة الاستدعاء.

كود المفسد
 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/ar427465/


All Articles