اذهب إلى المحتوى

العمليات التي تسبق بدء تنفيذ برنامج في نظام التشغيل


Ola Abbas

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

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

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

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...