ذكرنا سابقًا أن البرنامج لا يبدأ بالدالة الرئيسية main()
، حيث سنختبر في هذا المقال ما يحدث لبرنامج مرتبط ديناميكيًا عند تحميله وتشغيله.
تخصّص النواة أولًا البنى لعملية جديدة وتقرأ ملف ELF المُحدَّد من القرص الصلب استجابةً لاستدعاء النظام exec
. ذكرنا أن صيغة ELF لديها حقل لمفسّر Interpreter البرنامج هو PT_INTERP
الذي يمكن ضبطه لتفسير البرنامج، حيث يكون المفسِّر بالنسبة للتطبيقات المرتبطة ديناميكيًا هو الرابط الديناميكي Dynamic Linker -أو ld.so- الذي يسمح بإجراء بعض عمليات الربط مباشرةً قبل بدء البرنامج.
كما تقرأ النواة شيفرة الرابط الديناميكي، وتبدأ البرنامج من عنوان نقطة الدخول entry point الذي تحدده. سنختبر دور الرابط الديناميكي بالتفصيل لاحقًا، ولكن يكفي أن نقول أنه يضبط بعض الإعدادات مثل تحميل المكتبات التي يتطلبها التطبيق كما هو محدد في القسم الديناميكي من الملف الثنائي، ثم يبدأ تنفيذ البرنامج الثنائي عند عنوان نقطة الدخول أي الدالة _init
.
اتصال النواة بالبرامج
تحتاج النواة Kernel إلى توصيل بعض الأشياء للبرامج عند بدء تشغيلها مثل وسائط البرنامج arguments ومتغيرات البيئة الحالية environment variables وبنية خاصة اسمها المتجه المساعد Auxiliary Vector أو auxv
اختصارًا. يمكنك أن تطلب من الرابط الديناميكي أن يُظهر لك بعضًا من خرج تنقيح الأخطاء من البنية auxv
من خلال تحديد قيمة البيئة كما يلي LD_SHOW_AUXV=1
. تسمح الوسائط والبيئة والأشكال المختلفة من استدعاء النظام exec
بتحديد هذه الأشياء للبرنامج التي يمكن للنواة توصيلها من خلال وضع جميع المعلومات المطلوبة في المكدس ليلتقطها البرنامج المُنشَأ حديثًا، وبالتالي يمكن للبرنامج عند بدء تشغيله استخدام مؤشر المكدس الخاص به للعثور على جميع معلومات بدء التشغيل المطلوبة.
المتجه المساعد هو بنية خاصة لنقل المعلومات مباشرةً من النواة إلى البرنامج المُشغَّل حديثًا، ويحتوي على معلومات خاصة بالنظام يمكن أن تكون مطلوبة مثل الحجم الافتراضي لصفحة الذاكرة الوهمية على النظام أو إمكانات العتاد، وهذه هي الميزات التي تحددها النواة للعتاد الأساسي ويمكن أن تستفيد منها برامج مساحة المستخدمين.
مكتبة النواة Kernel Library
ذكرنا سابقًا أن استدعاءات النظام بطيئة وأن الأنظمة الحديثة لديها آليات لتجنب الحِمل الناتج عن استدعاء مصيدة Trap للمعالج، حيث يمكن تنفيذ ذلك في نظام لينكس من خلال استخدام حيلة بين المحمل الديناميكي والنواة المتصلَين ببنية AUXV
، إذ تضيف النواة مكتبة مشتركة صغيرة إلى فضاء العناوين لكل عملية مُنشَأة حديثًا وتحتوي على دالة تجري استدعاءات النظام نيابة عنك. يكمن جمال هذا النظام في أنه إذا دعم العتاد الأساسي آلية استدعاء نظام سريعة، فيمكن للنواة استخدامها لكونها منشئة المكتبة، وإلّا فيمكنها استخدام النظام القديم لإنشاء مصيدة. تسمى هذه المكتبة linux-gate.so.1
لأنها بوابة إلى عمل النواة الداخلي.
تضيف النواة مدخلةً إلى البنية auxv
تسمَّى AT_SYSINFO_EHDR
عندما تبدأ الرابط الديناميكي، وهذه المدخلة هي العنوان الموجود في الذاكرة الذي توجد فيه مكتبة النواة الخاصة. يمكن للرابط الديناميكي عندما يبدأ البحثَ عن المؤشر AT_SYSINFO_EHDR
، فإن وُجد، فستُحمَّل تلك المكتبة للبرنامج. لا يملك البرنامج أيّ فكرة عن وجود هذه المكتبة، لأنها تُعَد ترتيبًا خاصًا بين الرابط الديناميكي والنواة.
ذكرنا أن المبرمجين يجرون استدعاءات النظام بطريقة غير مباشرة من خلال استدعاء الدوال في مكتبات النظام libc التي يمكنها التحقق مما إذا كان ملف النواة الثنائي الخاص محمَّلًا أم لا، فإذا كان الأمر كذلك، فيجب استخدام الدوال الموجودة ضمنه لإجراء استدعاءات النظام. إذا حددت النواة أن العتاد يمتلك القدرة المطلوبة، فيجب استخدام طريقة استدعاء النظام السريع.
بدء برنامج
تمرّر النواةُ المفسّرَ بعد تحميله إلى نقطة الدخول كما هو مذكور في ملف المفسّر (لاحظ عدم اختبار كيفية بدء الرابط الديناميكي). سيقفز الرابط الديناميكي إلى عنوان نقطة الدخول كما هو مذكور في ملف ELF الثنائي.
يوضح المثال التالي نتيجة تفكيك Disassembley بدء تشغيل البرنامج:
$ cat test.c int main(void) { return 0; } $ gcc -o test test.c $ readelf --headers ./test | grep Entry Entry point address: 0x80482b0 $ objdump --disassemble ./test [...] 080482b0 <_start>: 80482b0: 31 ed xor %ebp,%ebp 80482b2: 5e pop %esi 80482b3: 89 e1 mov %esp,%ecx 80482b5: 83 e4 f0 and $0xfffffff0,%esp 80482b8: 50 push %eax 80482b9: 54 push %esp 80482ba: 52 push %edx 80482bb: 68 00 84 04 08 push $0x8048400 80482c0: 68 90 83 04 08 push $0x8048390 80482c5: 51 push %ecx 80482c6: 56 push %esi 80482c7: 68 68 83 04 08 push $0x8048368 80482cc: e8 b3 ff ff ff call 8048284 <__libc_start_main@plt> 80482d1: f4 hlt 80482d2: 90 nop 80482d3: 90 nop 08048368 <main>: 8048368: 55 push %ebp 8048369: 89 e5 mov %esp,%ebp 804836b: 83 ec 08 sub $0x8,%esp 804836e: 83 e4 f0 and $0xfffffff0,%esp 8048371: b8 00 00 00 00 mov $0x0,%eax 8048376: 83 c0 0f add $0xf,%eax 8048379: 83 c0 0f add $0xf,%eax 804837c: c1 e8 04 shr $0x4,%eax 804837f: c1 e0 04 shl $0x4,%eax 8048382: 29 c4 sub %eax,%esp 8048384: b8 00 00 00 00 mov $0x0,%eax 8048389: c9 leave 804838a: c3 ret 804838b: 90 nop 804838c: 90 nop 804838d: 90 nop 804838e: 90 nop 804838f: 90 nop 08048390 <__libc_csu_init>: 8048390: 55 push %ebp 8048391: 89 e5 mov %esp,%ebp [...] 08048400 <__libc_csu_fini>: 8048400: 55 push %ebp [...]
يمكننا أن نرى في المثال البسيط السابق باستخدام أداة readelf
أن نقطة الدخول هي الدالة _start
في الملف الثنائي، ويمكننا أن نرى في عملية التفكيك دفع بعض القيم إلى المكدس. تمثل القيمة الأولى 0x8048400
الدالة __libc_csu_fini
، وتمثل القيمة 0x8048390
الدالة __libc_csu_init
، وتمثل القيمة 0x8048368
الدالة الرئيسية main()
، ثم تُستدعَى قيمة الدالة __libc_start_main
.
الدالة __libc_start_main
مُعرَّفة في مصادر مكتبة glibc ضمن sysdeps/generic/libc-start.c
، وتُعَد معقدةً جدًا ومخفيةً بين عدد كبير من التعريفات، حيث يجب أن تكون قابلة للنقل عبر عدد كبير جدًا من الأنظمة والمعماريات التي يمكن لمكتبة glibc العمل عليها. تطبّق هذه الدالة عددًا من الأشياء المحدَّدة المتعلقة بإعداد مكتبة C والتي لا يحتاج المبرمج العادي للقلق بشأنها. النقطة التالية التي تستدعي فيها المكتبةُ البرنامجَ هي عند التعامل مع شيفرة init
.
تُعَد الدالتان init
وfini
مفهومين خاصين يستدعيان أجزاءً من الشيفرة البرمجية الموجودة في المكتبات المشتركة والتي يمكن أن تحتاج لاستدعائها قبل أن تبدأ المكتبة أو عند إلغاء تحميل المكتبة على التوالي. يمكنك أن ترى كيف يمكن أن يكون ذلك مفيدًا لمبرمجي المكتبات لإعداد المتغيرات عند بدء تشغيل المكتبة أو لتنظيفها في النهاية. كان البحث عن الدالتين _init
و_fini
في المكتبة ممكنًا سابقًا، ولكن أصبح ذلك مقيدًا إلى حد ما حيث كان كل شيء مطلوبًا في هاتين الدالتين. سنوضح فيما يلي كيفية عمل الدالتين init
وfini
فقط.
يمكننا أن نرى الآن أن الدالة __libc_start_main
ستتلقى عددًا من معاملات الدخل في المكدس stack، إذ سيكون بإمكانها أولًا الوصول إلى وسائط البرنامج ومتغيرات البيئة والمتجه المساعد من النواة، ثم ستدفع دالة التهيئة إلى عناوين المكدس الخاصة بالدوال للتعامل مع الدالتين init
وfini
ثم عنوان الدالة الرئيسية نفسها.
نحتاج طريقةً ما للإشارة إلى أنه يجب استدعاء دالةٍ ما باستخدام init
أوfini
في الشيفرة المصدرية. نستخدم مع gcc سمات Attributes لتمييز دالتين بأنهما بانيتان Constructors أو ومدمرتان Destructors في برنامجنا الرئيسي. تُستخدَم هذه المصطلحات بصورة أكثر شيوعًا مع اللغات كائنية التوجه لوصف دورات حياة الكائن.
إليك مثال عن الباني والمدمر:
$ cat test.c #include <stdio.h> void __attribute__((constructor)) program_init(void) { printf("init\n"); } void __attribute__((destructor)) program_fini(void) { printf("fini\n"); } int main(void) { return 0; } $ gcc -Wall -o test test.c $ ./test init fini $ objdump --disassemble ./test | grep program_init 08048398 <program_init>: $ objdump --disassemble ./test | grep program_fini 080483b0 <program_fini>: $ objdump --disassemble ./test [...] 08048280 <_init>: 8048280: 55 push %ebp 8048281: 89 e5 mov %esp,%ebp 8048283: 83 ec 08 sub $0x8,%esp 8048286: e8 79 00 00 00 call 8048304 <call_gmon_start> 804828b: e8 e0 00 00 00 call 8048370 <frame_dummy> 8048290: e8 2b 02 00 00 call 80484c0 <__do_global_ctors_aux> 8048295: c9 leave 8048296: c3 ret [...] 080484c0 <__do_global_ctors_aux>: 80484c0: 55 push %ebp 80484c1: 89 e5 mov %esp,%ebp 80484c3: 53 push %ebx 80484c4: 52 push %edx 80484c5: a1 2c 95 04 08 mov 0x804952c,%eax 80484ca: 83 f8 ff cmp $0xffffffff,%eax 80484cd: 74 1e je 80484ed <__do_global_ctors_aux+0x2d> 80484cf: bb 2c 95 04 08 mov $0x804952c,%ebx 80484d4: 8d b6 00 00 00 00 lea 0x0(%esi),%esi 80484da: 8d bf 00 00 00 00 lea 0x0(%edi),%edi 80484e0: ff d0 call *%eax 80484e2: 8b 43 fc mov 0xfffffffc(%ebx),%eax 80484e5: 83 eb 04 sub $0x4,%ebx 80484e8: 83 f8 ff cmp $0xffffffff,%eax 80484eb: 75 f3 jne 80484e0 <__do_global_ctors_aux+0x20> 80484ed: 58 pop %eax 80484ee: 5b pop %ebx 80484ef: 5d pop %ebp 80484f0: c3 ret 80484f1: 90 nop 80484f2: 90 nop 80484f3: 90 nop $ readelf --sections ./test There are 34 section headers, starting at offset 0xfb0: Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [ 0] NULL 00000000 000000 000000 00 0 0 0 [ 1] .interp PROGBITS 08048114 000114 000013 00 A 0 0 1 [ 2] .note.ABI-tag NOTE 08048128 000128 000020 00 A 0 0 4 [ 3] .hash HASH 08048148 000148 00002c 04 A 4 0 4 [ 4] .dynsym DYNSYM 08048174 000174 000060 10 A 5 1 4 [ 5] .dynstr STRTAB 080481d4 0001d4 00005e 00 A 0 0 1 [ 6] .gnu.version VERSYM 08048232 000232 00000c 02 A 4 0 2 [ 7] .gnu.version_r VERNEED 08048240 000240 000020 00 A 5 1 4 [ 8] .rel.dyn REL 08048260 000260 000008 08 A 4 0 4 [ 9] .rel.plt REL 08048268 000268 000018 08 A 4 11 4 [10] .init PROGBITS 08048280 000280 000017 00 AX 0 0 4 [11] .plt PROGBITS 08048298 000298 000040 04 AX 0 0 4 [12] .text PROGBITS 080482e0 0002e0 000214 00 AX 0 0 16 [13] .fini PROGBITS 080484f4 0004f4 00001a 00 AX 0 0 4 [14] .rodata PROGBITS 08048510 000510 000012 00 A 0 0 4 [15] .eh_frame PROGBITS 08048524 000524 000004 00 A 0 0 4 [16] .ctors PROGBITS 08049528 000528 00000c 00 WA 0 0 4 [17] .dtors PROGBITS 08049534 000534 00000c 00 WA 0 0 4 [18] .jcr PROGBITS 08049540 000540 000004 00 WA 0 0 4 [19] .dynamic DYNAMIC 08049544 000544 0000c8 08 WA 5 0 4 [20] .got PROGBITS 0804960c 00060c 000004 04 WA 0 0 4 [21] .got.plt PROGBITS 08049610 000610 000018 04 WA 0 0 4 [22] .data PROGBITS 08049628 000628 00000c 00 WA 0 0 4 [23] .bss NOBITS 08049634 000634 000004 00 WA 0 0 4 [24] .comment PROGBITS 00000000 000634 00018f 00 0 0 1 [25] .debug_aranges PROGBITS 00000000 0007c8 000078 00 0 0 8 [26] .debug_pubnames PROGBITS 00000000 000840 000025 00 0 0 1 [27] .debug_info PROGBITS 00000000 000865 0002e1 00 0 0 1 [28] .debug_abbrev PROGBITS 00000000 000b46 000076 00 0 0 1 [29] .debug_line PROGBITS 00000000 000bbc 0001da 00 0 0 1 [30] .debug_str PROGBITS 00000000 000d96 0000f3 01 MS 0 0 1 [31] .shstrtab STRTAB 00000000 000e89 000127 00 0 0 1 [32] .symtab SYMTAB 00000000 001500 000490 10 33 53 4 [33] .strtab STRTAB 00000000 001990 000218 00 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings) I (info), L (link order), G (group), x (unknown) O (extra OS processing required) o (OS specific), p (processor specific) $ objdump --disassemble-all --section .ctors ./test ./test: file format elf32-i386 Contents of section .ctors: 8049528 ffffffff 98830408 00000000 ............
كانت دالة التهيئة __libc_csu_init
هي القيمة الأخيرة المدفوعة إلى المكدس من أجل الدالة __libc_start_main
. إذا اتبعنا سلسلة الاستدعاءات ابتداءً من __libc_csu_init
، فيمكننا أن نرى أنها تجري بعض الإعدادات ثم تستدعي الدالة _init
في الملف القابل للتنفيذ. تستدعي الدالة _init
في النهاية دالة تسمى __do_global_ctors_aux
، حيث إذا نظرنا إلى تفكيك هذه الدالة، فيمكننا أن نرى أنها تبدأ من العنوان 0x804952c
ثم تتكرر وتقرأ قيمة وتستدعيها. هذا العنوان الذي يمثل البداية موجود في القسم .ctors
من الملف، حيث إذا ألقينا نظرة عليه، فسنرى أنه يحتوي على القيمة الأولى -1 وعنوان الدالة بصيغة Big Endian أي تخزين البتات الأقل أهمية أولًا والقيمة صفر.
العنوان بصيغة Big Endian هو 0x08048398
أو عنوان الدالة program_init
، لذا فإن صيغة القسم .ctors
هي -1 أولًا ثم عنوان الدوال المطلوب استدعاؤها عند التهيئة، وأخيرًا القيمة صفر للإشارة إلى اكتمال القائمة. ستُستدعَى كل مدخلة، ولدينا في هذه الحالة دالة واحدة فقط.
أخيرًا، تستدعي الدالة __libc_start_main
الدالةَ الرئيسية main()
بمجرد اكتمالها باستدعاء الدالة _init
. تذكر أن هذه الدالة تمتلك إعداد المكدس الأولي باستخدام الوسائط ومؤشرات البيئة من النواة، وهذه هي الطريقة التي تحصل بها الدالة الرئيسية على الوسائط argc, argv[], envp[]
. تعمل العملية بعد ذلك وتكتمل مرحلة الإعداد.
تحدث عملية مماثلة مع القسم .dtors
للمدمرين Destructors عند إنهاء البرنامج، حيث تستدعيها الدالة __libc_start_main
عند اكتمال الدالة الرئيسية main()
.
لاحظ تطبيق الكثير من العمل قبل أن يبدأ البرنامج وحتى بعد أن تعتقد أنه انتهى بقليل.
ترجمة -وبتصرُّف- للقسم Starting a process من فصل Behind the process من كتاب Computer Science from the Bottom Up لصاحبه Ian Wienand.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.