الحالة الأولية لتسجيلات البرنامج والمكدس على Linux ARM
سؤال
ألعب حاليًا بتجميع ARM على نظام Linux كتمرين تعليمي.أنا أستخدم التجميع "العاري"، أي.لا يوجد libcrt أو libgcc.هل يمكن لأي شخص أن يوجهني إلى معلومات حول الحالة التي سيكون عليها مؤشر المكدس والسجلات الأخرى في بداية البرنامج قبل استدعاء التعليمات الأولى؟من الواضح أن نقاط pc/r15 عند _start، ويبدو أن الباقي تمت تهيئته إلى 0، مع استثناءين؛يشير sp/r13 إلى عنوان بعيد خارج برنامجي، ويشير r1 إلى عنوان أعلى قليلاً.
إذن لبعض الأسئلة القوية:
- ما هي القيمة في r1؟
- هل القيمة في sp مكدس شرعي مخصص بواسطة النواة؟
- إذا لم يكن الأمر كذلك، ما هي الطريقة المفضلة لتخصيص المكدس؛باستخدام brk أو تخصيص قسم .bss ثابت؟
كل التقدير لكل النصائح.
المحلول
إليك ما أستخدمه لبدء برنامج Linux/ARM مع المترجم الخاص بي:
/** The initial entry point.
*/
asm(
" .text\n"
" .globl _start\n"
" .align 2\n"
"_start:\n"
" sub lr, lr, lr\n" // Clear the link register.
" ldr r0, [sp]\n" // Get argc...
" add r1, sp, #4\n" // ... and argv ...
" add r2, r1, r0, LSL #2\n" // ... and compute environ.
" bl _estart\n" // Let's go!
" b .\n" // Never gets here.
" .size _start, .-_start\n"
);
كما ترون، أنا فقط أحصل على عناصر argc وargv والبيئة من المكدس في [sp].
توضيح بسيط:يشير مؤشر المكدس إلى منطقة صالحة في ذاكرة العملية.r0 وr1 وr2 وr3 هي المعلمات الثلاثة الأولى للوظيفة التي يتم استدعاؤها.أقوم بملءها بـ argc و argv وEnviron على التوالي.
نصائح أخرى
نظرًا لأن هذا هو Linux، يمكنك إلقاء نظرة على كيفية تنفيذه بواسطة النواة.
يبدو أن السجلات يتم ضبطها عن طريق الاتصال بـ start_thread
في نهاية load_elf_binary
(إذا كنت تستخدم نظام Linux حديثًا، فسيستخدم تنسيق ELF دائمًا تقريبًا).بالنسبة لـ ARM، يبدو أن السجلات قد تم ضبطها على النحو التالي:
r0 = first word in the stack
r1 = second word in the stack
r2 = third word in the stack
sp = address of the stack
pc = binary entry point
cpsr = endianess, thumb mode, and address limit set as needed
من الواضح أن لديك مكدسًا صالحًا.أعتقد أن القيم r0
-r2
هي غير هامة، ويجب عليك بدلاً من ذلك قراءة كل شيء من المكدس (سترى لماذا أعتقد ذلك لاحقًا).الآن، دعونا نلقي نظرة على ما هو موجود على المكدس.ما ستقرأه من المكدس يتم ملؤه create_elf_tables
.
أحد الأشياء المثيرة للاهتمام التي يجب ملاحظتها هنا هو أن هذه الوظيفة مستقلة عن البنية، لذا سيتم وضع نفس الأشياء (في الغالب) على المكدس في كل بنية Linux قائمة على ELF.ما يلي موجود على المكدس، بالترتيب الذي ستقرأه به:
- عدد المعلمات (
argc
فيmain()
). - مؤشر واحد إلى سلسلة C لكل معلمة، متبوعًا بصفر (هذه هي محتويات
argv
فيmain()
;argv
سيشير إلى أول هذه المؤشرات). - مؤشر واحد إلى سلسلة C لكل متغير بيئة، متبوعًا بصفر (هذه هي محتويات الملف الذي نادرًا ما يُرى
envp
المعلمة الثالثةmain()
;envp
سيشير إلى أول هذه المؤشرات). - "المتجه المساعد"، وهو عبارة عن سلسلة من الأزواج (نوع متبوع بقيمة)، منتهية بزوج بصفر (
AT_NULL
) في العنصر الأول.يحتوي هذا المتجه المساعد على بعض المعلومات المثيرة للاهتمام والمفيدة، والتي يمكنك رؤيتها (إذا كنت تستخدم glibc) عن طريق تشغيل أي برنامج مرتبط ديناميكيًا باستخدام الملفLD_SHOW_AUXV
تم تعيين متغير البيئة على1
(على سبيل المثالLD_SHOW_AUXV=1 /bin/true
).هذا هو المكان الذي يمكن أن تختلف فيه الأشياء قليلاً اعتمادًا على الهندسة المعمارية.
وبما أن هذا الهيكل هو نفسه بالنسبة لكل معمارية، يمكنك أن تبحث على سبيل المثال في الرسم الموجود في الصفحة 54 من سيسف 386 أبي للحصول على فكرة أفضل عن كيفية تناسب الأشياء معًا (لاحظ، مع ذلك، أن ثوابت نوع المتجهات المساعدة في تلك الوثيقة تختلف عما يستخدمه Linux، لذا يجب عليك إلقاء نظرة على ترويسات Linux الخاصة بها).
الآن يمكنك أن ترى لماذا محتويات r0
-r2
هي القمامة.الكلمة الأولى في المكدس هي argc
, والثاني هو مؤشر لاسم البرنامج (argv[0]
)، والثالث ربما كان صفرًا بالنسبة لك لأنك اتصلت بالبرنامج بدون وسائط (سيكون argv[1]
).أعتقد أنهم تم إعدادهم بهذه الطريقة لكبار السن a.out
تنسيق ثنائي، والذي كما ترون في create_aout_tables
يضع argc
, argv
, ، و envp
في المكدس (حتى ينتهي بهم الأمر في r0
-r2
بالترتيب المتوقع لإجراء مكالمة إلى main()
).
وأخيرا، لماذا كان r0
صفر لك بدلاً من واحد (argc
يجب أن يكون واحدًا إذا اتصلت بالبرنامج بدون وسائط)؟أعتقد أن شيئًا ما عميقًا في آلية syscall قد قام بالكتابة فوقه بالقيمة المرجعة لاستدعاء النظام (والتي ستكون صفرًا منذ نجاح exec).يمكنك أن ترى في kernel_execve
(الذي لا يستخدم آلية syscall، نظرًا لأنه ما تستدعيه النواة عندما تريد تنفيذ الأمر exec من وضع kernel) والذي يقوم بالكتابة فوقه عمدًا r0
مع قيمة الإرجاع do_execve
.
هنا uClibc crt.يبدو أنه يشير إلى أن كافة السجلات غير محددة باستثناء r0
(الذي يحتوي على مؤشر دالة ليتم التسجيل به atexit()
) و sp
الذي يحتوي على عنوان مكدس صالح.
إذن القيمة التي تراها r1
ربما لا يكون شيئًا يمكنك الاعتماد عليه.
يتم وضع بعض البيانات على المكدس من أجلك.
لم أستخدم ARM Linux مطلقًا ولكني أقترح عليك إما إلقاء نظرة على مصدر libcrt ومعرفة ما يفعلونه، أو استخدام gdb للدخول إلى ملف قابل للتنفيذ موجود.لا يجب أن تحتاج إلى الكود المصدري، فقط قم بالخطوة خلال كود التجميع.
كل ما تحتاج إلى معرفته يجب أن يحدث داخل الكود الأول الذي يتم تنفيذه بواسطة أي ثنائي قابل للتنفيذ.
أتمنى أن يساعدك هذا.
توني