ذكرنا سابقًا أن البرنامج لا يبدأ بالدالة الرئيسية 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.

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