المكتبات وكيفية استدعاء دوالها ديناميكيا في معمارية الحاسوب


المكتبات وكيفية استدعاء دوالها ديناميكيا في معمارية الحاسوب April 28, 2023 at 09:00PM

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

المكتبات الساكنة Static Libraries

الطريقة الأكثر مباشرة لاستخدام دالة المكتبة هي ربط ملفات الكائنات من المكتبة مباشرة بملفك النهائي القابل للتنفيذ كما هو الحال مع تلك الملفات التي صرَّفتها بنفسك، وعندها تُسمَّى المكتبة مكتبة ساكنة، لأن المكتبة ستبقى دون تغيير ما لم يُعاد تصريف البرنامج. تُعَد هذه الطريقة لاستخدام مكتبة الطريقة الأسهل لأن النتيجة النهائية هي ملف قابل للتنفيذ بسيط بدون اعتماديات.

تُعَد المكتبة الساكنة مجموعةً من ملفات الكائنات، حيث يُحتفَظ بملفات الكائنات في سجل Archive، مما يؤدي إلى استخدام لاحقتها المعتادة ‎.a. يمكنك التفكير في هذه السجلات بوصفها ملفًا مضغوطًا ولكن بدون ضغط. يوضّح المثال التالي كيفية إنشاء مكتبة ساكنة بسيطة ويقدم بعض الأدوات الشائعة للتعامل مع المكتبات:

$ cat library.c
/* دالة مكتبة */
int function(int input)
{
  return input + 10;
}

$ cat library.h
/* تعريف الدالة */
int function(int);

$ cat program.c
#include <stdio.h>
/* ترويسة ملف المكتبة */
#include "library.h"

int main(void)
{
  int d = function(100);

  printf("%d\n", d);
}

$ gcc -c library.c
$ ar rc libtest.a library.o
$ ranlib ./libtest.a
$ nm --print-armap ./libtest.a

Archive index:
function in library.o

library.o:
00000000 T function

$ gcc -L . program.c -ltest -o program

$ ./program
110

أولًا، نصرّف مكتبتنا إلى ملف كائن كما رأينا سابقًا. لاحظ أننا نحدد واجهة API الخاصة بالمكتبة في ترويسة الملف، حيث تتكون واجهة API من تعريفات الدوال الموجودة في المكتبة حتى يعرف المُصرِّف أنواع الدوال عند إنشاء ملفات الكائنات التي تشير إلى المكتبة مثل الملف program.c الذي يُضمَّن باستخدام ‎#include في ترويسة الملف.

ننشئ سجل مكتبة باستخدام الأمر ar الذي يمثل اختصارًا للكلمة "سجل Archive". تُسبَق أسماء ملفات المكتبة الساكنة بالبادئة lib ويكون لها اللاحقة ‎.a حسب العرف المتَّبع. يخبر الوسيطُ c البرنامجَ بإنشاء السجل Archive، ويخبر a السجل بإضافة ملفات الكائنات المحددة في ملف المكتبة. تنبثق السجلات المُنشَأة باستخدام الأمر ar في أماكن مختلفة من أنظمة لينكس بخلاف إنشاء مكتبات ساكنة.

أحد التطبيقات المستخدمة على نطاق واسع هي التطبيقات المُستخدَمة في صيغة حزم ‎.deb مع أنظمة دبيان Debian وأوبنتو Ubuntu وبعض أنظمة لينكس الأخرى، حيث تستخدم ملفات deb السجلات للاحتفاظ بجميع ملفات التطبيق مع بعضها البعض في ملف حزمة واحد. تستخدم حزم RedHat RPM صيغةً بديلةً ولكنها مشابهة لصيغة deb وتُسمَّى cpio. يُعَد ملف tar التطبيقَ الأساسي لحفظ الملفات مع بعضها بعضًا، وهو صيغة شائعة لتوزيع الشيفرة المصدرية.

نستخدم بعد ذلك تطبيق ranlib لإنشاء ترويسة في المكتبة باستخدام رموز محتويات ملف الكائن، مما يساعد المصرِّف على الإشارة إلى الرموز بسرعة، إذ يمكن أن تبدو هذه الخطوة زائدة في حالة وجود رمز واحد فقط ، ولكن يمكن أن تحتوي مكتبة كبيرة على آلاف الرموز مما يعني أن الفهرس يمكن أن يسارع بصورة كبيرة في العثور على المراجع. نفحص هذه الترويسة الجديدة باستخدام تطبيق nm. لاحظ وجود الرمز function الخاص بالدالة function()‎ عند إزاحة بمقدار صفر كما هو متوقع.

يمكنك بعد ذلك تحديد المكتبة للمصرِّف باستخدام الخيار ‎-lname حيث يكون الاسم هو اسم ملف المكتبة بدون البادئة lib. كما نوفر مجلد بحث إضافي للمكتبات وهو المجلد الحالي (‎-L .‎)، لأنه لا يمكن البحث عن المكتبات في المجلد الحالي افتراضيًا. النتيجة النهائية هي ملف قابل للتنفيذ مع المكتبة الجديدة المُضمَّنة.

عيوب الربط الساكن

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

تُضمَّن مكتبة C التي هي glibc مثلًا في جميع البرامج، وتوفر جميع الدوال الشائعة مثل printf.

المكتبات المشتركة

تُعَد المكتبات المشتركة طريقةً للتغلب على المشاكل التي تشكّلها المكتبات الساكنة. تُحمَّل المكتبة المشتركة ديناميكيًا في وقت التشغيل لكل تطبيق يحتاجها، حيث يستخدم التطبيق مؤشرات تتطلب مكتبة معينة، وتُحمَّل المكتبة في الذاكرة وتُنفَّذ عند استدعاء الدالة. إن حُمِّلت المكتبة لتطبيق آخر، فيمكن مشاركة الشيفرة البرمجية بين التطبيقين، مما يوفر موارد كبيرة مع المكتبات شائعة الاستخدام.

يُعَد الربط الديناميكي الذي تحدثنا عنه سابقًا أحد الأجزاء الأكثر تعقيدًا في نظام التشغيل الحديث.

جدول البحث عن الإجراءات Procedure Lookup Table

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

تحتوي عملية الربط الديناميكي Dynamic Linking الكثير من العمليات الحسابية، لأنها تتضمن النظر والبحث عبر العديد من الجداول، لذا يمكن تحسين الأداء عند تطبيق أيّ شيء لتقليل هذا الحِمل الناتج عن هذه العمليات الحسابية الكثيرة. يسهّل جدول البحث عن الإجراءات Procedure Lookup Table -أو PLT اختصارًا- ما يسمى بالارتباط الكسول Lazy Binding في البرامج، حيث يُعَد الارتباط Binding مرادفًا لعملية إصلاح المتغيرات الموجودة في جدول GOT الموضحة سابقًا، إذ يُقال أن المدخلة مرتبطة بعنوانها الفعلي عند إصلاحها.

يتضمن البرنامج في بعض الأحيان دالةً من مكتبة، ولكنه لا يستدعيها أبدًا اعتمادًا على دخل المستخدم. تحتوي عملية الارتباط الخاصة بهذه الدالة الكثير من العمليات لتطبيقها، لأنها تتضمن تحميل الشيفرة البرمجية والبحث في الجداول والكتابة في الذاكرة، لذا تُعَد المتابعة في عملية ارتباط دالة غير مُستخدَمة مضيعة للوقت، حيث يؤجِّل الارتباط الكسول هذه العملية حتى يستدعي جدولُ PLT الدالةَ الفعلية.

لكل دالة في مكتبةٍ مدخلةٌ في جدول PLT تؤشّر في البداية إلى بعض الشيفرات البرمجية الوهمية Dummy Code الخاصة. إن استدعى البرنامج الدالة، فهذا يعني أنه يستدعي مدخلة من جدول PLT باستخدام الطريقة نفسها للإشارة إلى المتغيرات في جدول GOT نفسها.

تحمّل هذه الدالة الوهمية بعض المعاملات التي تريد تمريرها إلى الرابط الديناميكي لتتمكن من تحليل الدالة ثم استدعاء دالة بحث خاصة بالرابط الديناميكي. يجد الرابط الديناميكي عنوان الدالة الفعلي، ويكتب هذا الموقع في استدعاء الملف الثنائي في أعلى استدعاء الدالة الوهمية، وبالتالي يمكن تحميل العنوان دون الحاجة إلى العودة إلى المحمل الديناميكي مرة أخرى في المرة التالية لاستدعاء الدالة. إن لم تُستدعَى دالةٌ مطلقًا، فلن تُعدَّل مدخلة جدول PLT أبدًا ولكن لن يكون هناك وقت تشغيل إضافي.

كيفية عمل جدول PLT

يجب أن تبدأ الآن في إدراك أن هناك قدرًا لا بأس به من العمل في تحليل رمز ديناميكي. لنطلع على تطبيق "hello World" البسيط الذي يجري استدعاء مكتبة واحد فقط هو استدعاء الدالة printf لعرض السلسلة النصية للمستخدم كما يلي:

           $ cat hello.c
#include <stdio.h>

int main(void)
{
  printf("Hello, World!\n");
  return 0;
}

$ gcc -o hello hello.c

$ readelf --relocs ./hello

Relocation section '.rela.dyn' at offset 0x3f0 contains 2 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
6000000000000ed8  000700000047 R_IA64_FPTR64LSB  0000000000000000 _Jv_RegisterClasses + 0
6000000000000ee0  000900000047 R_IA64_FPTR64LSB  0000000000000000 __gmon_start__ + 0

Relocation section '.rela.IA_64.pltoff' at offset 0x420 contains 3 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
6000000000000f10  000200000081 R_IA64_IPLTLSB    0000000000000000 printf + 0
6000000000000f20  000800000081 R_IA64_IPLTLSB    0000000000000000 __libc_start_main + 0
6000000000000f30  000900000081 R_IA64_IPLTLSB    0000000000000000 __gmon_start__ + 0

يمكننا أن نرى في المثال السابق أن لدينا الانتقال R_IA64_IPLTLSB للرمز printf الذي يمثل وضع عنوان رمز هذه الدالة في عنوان الذاكرة 0x6000000000000f10. يجب أن نبدأ في البحث بصورة أعمق للعثور على الإجراء الدقيق الذي يعطينا الدالة.

سنلقي في المثال التالي نظرة على تفكيك الدالة الرئيسية main()‎ الخاصة بالبرنامج:

           4000000000000790 <main>:
4000000000000790:       00 08 15 08 80 05       [MII]       alloc r33=ar.pfs,5,4,0
4000000000000796:       20 02 30 00 42 60                   mov r34=r12
400000000000079c:       04 08 00 84                         mov r35=r1
40000000000007a0:       01 00 00 00 01 00       [MII]       nop.m 0x0
40000000000007a6:       00 02 00 62 00 c0                   mov r32=b0
40000000000007ac:       81 0c 00 90                         addl r14=72,r1;;
40000000000007b0:       1c 20 01 1c 18 10       [MFB]       ld8 r36=[r14]
40000000000007b6:       00 00 00 02 00 00                   nop.f 0x0
40000000000007bc:       78 fd ff 58                         br.call.sptk.many b0=4000000000000520 <_init+0xb0>
40000000000007c0:       02 08 00 46 00 21       [MII]       mov r1=r35
40000000000007c6:       e0 00 00 00 42 00                   mov r14=r0;;
40000000000007cc:       01 70 00 84                         mov r8=r14
40000000000007d0:       00 00 00 00 01 00       [MII]       nop.m 0x0
40000000000007d6:       00 08 01 55 00 00                   mov.i ar.pfs=r33
40000000000007dc:       00 0a 00 07                         mov b0=r32
40000000000007e0:       1d 60 00 44 00 21       [MFB]       mov r12=r34
40000000000007e6:       00 00 00 02 00 80                   nop.f 0x0
40000000000007ec:       08 00 84 00                         br.ret.sptk.many b0;;

يجب أن يكون استدعاء العنوان 0x4000000000000520 هو استدعاء الدالة printf، حيث يمكننا معرفة مكان هذا العنوان من خلال الاطلاع الأقسام Sections باستخدام الأداة readelf كما يلي:

           $ readelf --sections ./hello
There are 40 section headers, starting at offset 0x25c0:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
...
  [11] .plt              PROGBITS         40000000000004c0  000004c0
       00000000000000c0  0000000000000000  AX       0     0     32
  [12] .text             PROGBITS         4000000000000580  00000580
       00000000000004a0  0000000000000000  AX       0     0     32
  [13] .fini             PROGBITS         4000000000000a20  00000a20
       0000000000000040  0000000000000000  AX       0     0     16
  [14] .rodata           PROGBITS         4000000000000a60  00000a60
       000000000000000f  0000000000000000   A       0     0     8
  [15] .opd              PROGBITS         4000000000000a70  00000a70
       0000000000000070  0000000000000000   A       0     0     16
  [16] .IA_64.unwind_inf PROGBITS         4000000000000ae0  00000ae0
       00000000000000f0  0000000000000000   A       0     0     8
  [17] .IA_64.unwind     IA_64_UNWIND     4000000000000bd0  00000bd0
       00000000000000c0  0000000000000000  AL      12     c     8
  [18] .init_array       INIT_ARRAY       6000000000000c90  00000c90
       0000000000000018  0000000000000000  WA       0     0     8
  [19] .fini_array       FINI_ARRAY       6000000000000ca8  00000ca8
       0000000000000008  0000000000000000  WA       0     0     8
  [20] .data             PROGBITS         6000000000000cb0  00000cb0
       0000000000000004  0000000000000000  WA       0     0     4
  [21] .dynamic          DYNAMIC          6000000000000cb8  00000cb8
       00000000000001e0  0000000000000010  WA       5     0     8
  [22] .ctors            PROGBITS         6000000000000e98  00000e98
       0000000000000010  0000000000000000  WA       0     0     8
  [23] .dtors            PROGBITS         6000000000000ea8  00000ea8
       0000000000000010  0000000000000000  WA       0     0     8
  [24] .jcr              PROGBITS         6000000000000eb8  00000eb8
       0000000000000008  0000000000000000  WA       0     0     8
  [25] .got              PROGBITS         6000000000000ec0  00000ec0
       0000000000000050  0000000000000000 WAp       0     0     8
  [26] .IA_64.pltoff     PROGBITS         6000000000000f10  00000f10
       0000000000000030  0000000000000000 WAp       0     0     16
  [27] .sdata            PROGBITS         6000000000000f40  00000f40
       0000000000000010  0000000000000000 WAp       0     0     8
  [28] .sbss             NOBITS           6000000000000f50  00000f50
       0000000000000008  0000000000000000  WA       0     0     8
  [29] .bss              NOBITS           6000000000000f58  00000f50
       0000000000000008  0000000000000000  WA       0     0     8
  [30] .comment          PROGBITS         0000000000000000  00000f50
       00000000000000b9  0000000000000000           0     0     1
  [31] .debug_aranges    PROGBITS         0000000000000000  00001010
       0000000000000090  0000000000000000           0     0     16
  [32] .debug_pubnames   PROGBITS         0000000000000000  000010a0
       0000000000000025  0000000000000000           0     0     1
  [33] .debug_info       PROGBITS         0000000000000000  000010c5
       00000000000009c4  0000000000000000           0     0     1
  [34] .debug_abbrev     PROGBITS         0000000000000000  00001a89
       0000000000000124  0000000000000000           0     0     1
  [35] .debug_line       PROGBITS         0000000000000000  00001bad
       00000000000001fe  0000000000000000           0     0     1
  [36] .debug_str        PROGBITS         0000000000000000  00001dab
       00000000000006a1  0000000000000001  MS       0     0     1
  [37] .shstrtab         STRTAB           0000000000000000  0000244c
       000000000000016f  0000000000000000           0     0     1
  [38] .symtab           SYMTAB           0000000000000000  00002fc0
       0000000000000b58  0000000000000018          39    60     8
  [39] .strtab           STRTAB           0000000000000000  00003b18
       0000000000000479  0000000000000000           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)

يوجد هذا العنوان في القسم ‎.plt كما هو متوقع حيث يوجد استدعاؤها في جدول PLT. لكن لنواصل البحث أكثر ولنفكك القسم ‎.plt لنرى ما يفعله هذا الاستدعاء كما يلي:

            40000000000004c0 <.plt>:
40000000000004c0:       0b 10 00 1c 00 21       [MMI]       mov r2=r14;;
40000000000004c6:       e0 00 08 00 48 00                   addl r14=0,r2
40000000000004cc:       00 00 04 00                         nop.i 0x0;;
40000000000004d0:       0b 80 20 1c 18 14       [MMI]       ld8 r16=[r14],8;;
40000000000004d6:       10 41 38 30 28 00                   ld8 r17=[r14],8
40000000000004dc:       00 00 04 00                         nop.i 0x0;;
40000000000004e0:       11 08 00 1c 18 10       [MIB]       ld8 r1=[r14]
40000000000004e6:       60 88 04 80 03 00                   mov b6=r17
40000000000004ec:       60 00 80 00                         br.few b6;;
40000000000004f0:       11 78 00 00 00 24       [MIB]       mov r15=0
40000000000004f6:       00 00 00 02 00 00                   nop.i 0x0
40000000000004fc:       d0 ff ff 48                         br.few 40000000000004c0 <_init+0x50>;;
4000000000000500:       11 78 04 00 00 24       [MIB]       mov r15=1
4000000000000506:       00 00 00 02 00 00                   nop.i 0x0
400000000000050c:       c0 ff ff 48                         br.few 40000000000004c0 <_init+0x50>;;
4000000000000510:       11 78 08 00 00 24       [MIB]       mov r15=2
4000000000000516:       00 00 00 02 00 00                   nop.i 0x0
400000000000051c:       b0 ff ff 48                         br.few 40000000000004c0 <_init+0x50>;;
4000000000000520:       0b 78 40 03 00 24       [MMI]       addl r15=80,r1;;
4000000000000526:       00 41 3c 70 29 c0                   ld8.acq r16=[r15],8
400000000000052c:       01 08 00 84                         mov r14=r1;;
4000000000000530:       11 08 00 1e 18 10       [MIB]       ld8 r1=[r15]
4000000000000536:       60 80 04 80 03 00                   mov b6=r16
400000000000053c:       60 00 80 00                         br.few b6;;
4000000000000540:       0b 78 80 03 00 24       [MMI]       addl r15=96,r1;;
4000000000000546:       00 41 3c 70 29 c0                   ld8.acq r16=[r15],8
400000000000054c:       01 08 00 84                         mov r14=r1;;
4000000000000550:       11 08 00 1e 18 10       [MIB]       ld8 r1=[r15]
4000000000000556:       60 80 04 80 03 00                   mov b6=r16
400000000000055c:       60 00 80 00                         br.few b6;;
4000000000000560:       0b 78 c0 03 00 24       [MMI]       addl r15=112,r1;;
4000000000000566:       00 41 3c 70 29 c0                   ld8.acq r16=[r15],8
400000000000056c:       01 08 00 84                         mov r14=r1;;
4000000000000570:       11 08 00 1e 18 10       [MIB]       ld8 r1=[r15]
4000000000000576:       60 80 04 80 03 00                   mov b6=r16
400000000000057c:       60 00 80 00                         br.few b6;;    

إذًا لنمر على التعليمات، حيث أضفنا أولًا القيمة 80 إلى القيمة الموجودة في المسجّل r1، وخزّناها في المسجّل r15. سيؤشّر المسجل r1 إلى جدول GOT، مما يعني تخزين المسجل r15 الذي يحتوي على 80 بايت في جدول GOT. ثانيًا، حمّلنا القيمة المخزنة في هذا الموقع من جدول GOT إلى المسجّل r16، ثم زدنا القيمة الموجودة في المسجل r15 بمقدار 8 بايتات. ثالثًا، خزّنا المسجّل r1 -أو موقع جدول GOT- في المسجّل r14 وضبطنا القيمة الموجودة في المسجل r1 لتكون القيمة الموجودة في 8 بايتات التالية للمسجّل r15، ثم نتفرّع إلى المسجل r16.

ناقشنا سابقًا كيفية استدعاء الدوال باستخدام واصف الدالة Function Descriptor الذي يحتوي على عنوان الدالة وعنوان المؤشر العام. يمكننا أن نرى أن مدخلة جدول PLT تحمّل أولًا قيمة الدالة، مما يؤدي إلى الانتقال بمقدار 8 بايتات إلى الجزء الثاني من واصف الدالة ثم تحميل تلك القيمة في مسجّل العملية Op Register قبل استدعاء الدالة.

نعلم أن المسجل r1 سيؤشّر إلى جدول GOT، ثم سنذهب بمقدار 80 بايت بعد جدول GOT أي بمقدار (0x50).

           $ objdump --disassemble-all ./hello 
Disassembly of section .got:

6000000000000ec0 <.got>:
        ...
6000000000000ee8:       80 0a 00 00 00 00                   data8 0x02a000000
6000000000000eee:       00 40 90 0a                         dep r0=r0,r0,63,1
6000000000000ef2:       00 00 00 00 00 40       [MIB] (p20) break.m 0x1
6000000000000ef8:       a0 0a 00 00 00 00                   data8 0x02a810000
6000000000000efe:       00 40 50 0f                         br.few 6000000000000ef0 <_GLOBAL_OFFSET_TABLE_+0x30>
6000000000000f02:       00 00 00 00 00 60       [MIB] (p58) break.m 0x1
6000000000000f08:       60 0a 00 00 00 00                   data8 0x029818000
6000000000000f0e:       00 40 90 06                         br.few 6000000000000f00 <_GLOBAL_OFFSET_TABLE_+0x40>
Disassembly of section .IA_64.pltoff:

6000000000000f10 <.IA_64.pltoff>:
6000000000000f10:       f0 04 00 00 00 00       [MIB] (p39) break.m 0x0
6000000000000f16:       00 40 c0 0e 00 00                   data8 0x03b010000
6000000000000f1c:       00 00 00 60                         data8 0xc000000000
6000000000000f20:       00 05 00 00 00 00       [MII] (p40) break.m 0x0
6000000000000f26:       00 40 c0 0e 00 00                   data8 0x03b010000
6000000000000f2c:       00 00 00 60                         data8 0xc000000000
6000000000000f30:       10 05 00 00 00 00       [MIB] (p40) break.m 0x0
6000000000000f36:       00 40 c0 0e 00 00                   data8 0x03b010000
6000000000000f3c:       00 00 00 60                         data8 0xc000000000

إذا أضفنا القيمة 0x50 إلى العنوان 0x6000000000000ec0، فسنصل إلى العنوان 0x6000000000000f10 أو القسم ‎.IA_64.pltoff.

يمكننا فك شيفرة خرج البرنامج objdump لنتمكّن من رؤية ما جرى تحميله بالضبط. يؤدي تبديل ترتيب البايت لأول 8 بايتات f0 04 00 00 00 00 00 40 إلى الحصول على العنوان 0x4000000000004f0، إذ يبدو هذا العنوان مألوفًا، حيث إذا نظرنا إلى الوراء في ناتج التجميع الخاص بجدول PLT ، فسنرى ذلك العنوان.

أولًا تضع الشيفرة البرمجية الموجودة عند العنوان 0x4000000000004f0 قيمة صفرية في المسجل r15، ثم تتفرع مرة أخرى إلى العنوان 0x40000000000004c0، ولكن يُعَد هذا العنوان بداية القسم PLT. يمكننا تتبّع هذه الشيفرة البرمجية، إذ نحفظ أولًا قيمة المؤشر العام في المسجل r2، ثم نحمل ثلاث قيم بحجم 8 بايتات في المسجلات r16 وr17 وr1، ثم نتفرع إلى العنوان الموجود في المسجل r17، حيث يمثّل تلك العملية الاستدعاء الفعلي للرابط الديناميكي.

يجب أن نتعمق قليلًا في فهم واجهة ABI التي تعطينا مفهومين لنفهم بالضبط ما يجري تحميله الآن، وهذا المفهومان هما أنه يجب أن تحتوي البرامج المرتبطة ديناميكيًا على قسم خاص يسمى القسم DT_IA_64_PLT_RESERVE الذي يمكنه الاحتفاظ بثلاث قيم بحجم 8 بايتات، ويوجد مؤشر في مكان وجود هذه المنطقة المحجوزة في المقطع الديناميكي للملف الثنائي الموضّح في المثال التالي:

Dynamic segment at offset 0xcb8 contains 25 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6.1]
 0x000000000000000c (INIT)               0x4000000000000470
 0x000000000000000d (FINI)               0x4000000000000a20
 0x0000000000000019 (INIT_ARRAY)         0x6000000000000c90
 0x000000000000001b (INIT_ARRAYSZ)       24 (bytes)
 0x000000000000001a (FINI_ARRAY)         0x6000000000000ca8
 0x000000000000001c (FINI_ARRAYSZ)       8 (bytes)
 0x0000000000000004 (HASH)               0x4000000000000200
 0x0000000000000005 (STRTAB)             0x4000000000000330
 0x0000000000000006 (SYMTAB)             0x4000000000000240
 0x000000000000000a (STRSZ)              138 (bytes)
 0x000000000000000b (SYMENT)             24 (bytes)
 0x0000000000000015 (DEBUG)              0x0
 0x0000000070000000 (IA_64_PLT_RESERVE)  0x6000000000000ec0 -- 0x6000000000000ed8
 0x0000000000000003 (PLTGOT)             0x6000000000000ec0
 0x0000000000000002 (PLTRELSZ)           72 (bytes)
 0x0000000000000014 (PLTREL)             RELA
 0x0000000000000017 (JMPREL)             0x4000000000000420
 0x0000000000000007 (RELA)               0x40000000000003f0
 0x0000000000000008 (RELASZ)             48 (bytes)
 0x0000000000000009 (RELAENT)            24 (bytes)
 0x000000006ffffffe (VERNEED)            0x40000000000003d0
 0x000000006fffffff (VERNEEDNUM)         1
 0x000000006ffffff0 (VERSYM)             0x40000000000003ba
 0x0000000000000000 (NULL)               0x0

لاحظ أننا حصلنا على قيمة جدول GOT نفسه، وهذا يعني أن أول ثلاث مدخلات بحجم 8 بايتات في جدول GOT تمثل المنطقة المحجوزة، وبالتالي سيُؤشَّر إليها دائمًا باستخدام المؤشر العام.

يجب أن يملأ الرابط الديناميكي هذه القيم عند بدء تشغيله، حيث تحدّد واجهة ABI أنه يجب ملء القيمة الأولى بواسطة الرابط الديناميكي الذي يمنح هذه الوحدة معرفًا فريدًا، والقيمة الثانية هي قيمة المؤشر العام للرابط الديناميكي، والقيمة الثالثة هي عنوان الدالة التي تبحث عن الرمز وتصلحه.

يوضّح المثال التالي شيفرة برمجية في الرابط الديناميكي لإعداد قيم خاصة من المكتبة libc أو من sysdeps/ia64/dl-machine.h:

/* ‫إعداد الكائن المحمَّل الموصوف باستخدام المتغير L حتى تقفز مدخلات جدول PLT التي ليس لها انتقالات إلى ‫شيفرة الإصلاح البرمجية عند الطلب في ملف dl-runtime.c.  */

static inline int __attribute__ ((unused, always_inline))
elf_machine_runtime_setup (struct link_map *l, int lazy, int profile)
{
  extern void _dl_runtime_resolve (void);
  extern void _dl_runtime_profile (void);

  if (lazy)
    {
      register Elf64_Addr gp __asm__ ("gp");
      Elf64_Addr *reserve, doit;

        /*
         * ‫احذر من تبديل الأنواع Typecast هنا أو ستُضاف عناصر مؤشر l-l_addr
         */
      reserve = ((Elf64_Addr *)
                 (l->l_info[DT_IA_64 (PLT_RESERVE)]->d_un.d_ptr + l->l_addr));
     /* تعريف هذا الكائن المشترك  */
      reserve[0] = (Elf64_Addr) l;

     /* ستُستدعَى هذه الدالة لتطبيق الانتقال‫ Relocation  */
      if (!profile)
        doit = (Elf64_Addr) ((struct fdesc *) &_dl_runtime_resolve)->ip;
      else
        {
          if (GLRO(dl_profile) != NULL
             && _dl_name_match_p (GLRO(dl_profile), l))
            {
              /* ‫هذا هو الكائن الذي نبحث عنه. لنفترض أننا نريد استخدام التشخيص Profiling مع بدء المؤقتات */
              GL(dl_profile_map) = l;
            }
          doit = (Elf64_Addr) ((struct fdesc *) &_dl_runtime_profile)->ip;
        }

      reserve[1] = doit;
      reserve[2] = gp;
    }

  return lazy;
}

يمكننا أن نرى كيفية إعداد هذه القيم بواسطة الرابط الديناميكي من خلال النظر في الدالة التي تطبّق ذلك للملف الثنائي. يُضبَط المتغير reserve من مؤشر القسم PLT_RESERVE في الملف الثنائي. تمثل القيمة الفريدة الموضوعة في reserve[0]‎ عنوان خارطة الربط Link Map لهذا الكائن، حيث تُعَد خارطة الربط التمثيل الداخلي ضمن مكتبة glibc للكائنات المشتركة. نضع بعد ذلك عنوان الدالة ‎_dl_runtime_resolve في القيمة الثانية بافتراض أننا لا نستخدم عملية التشخيص Profiling، ثم تُضبط قيمة reserve[2]‎ على gp التي يمكن العثور عليها في المسجل r2 باستخدام الاستدعاء __asm__.

إذا عدنا إلى الوراء في واجهة ABI، فسنرى أنه يجب وضع فهرس انتقال للمدخلة في المسجل r15 ويجب تمرير المعرّف الفريد في المسجل r16. ضُبِط المسجل r15 مسبقًا في الشيفرة الاختبارية Stub Code قبل العودة إلى بداية جدول PLT. ألقِ نظرة على المدخلات، ولاحظ كيف تحمِّل كل مدخلة في جدول PLT المسجل r15 مع قيمة متزايدة، إذ لا ينبغي أن يكون ذلك مفاجئًا إذا نظرت إلى عمليات الانتقال، حيث يكون لانتقال الدالة printf العدد صفر.

نحمّل المسجل r16 من القيم التي هيّأها الرابط الديناميكي، ثم يمكننا تحميل عنوان الدالة والمؤشر العام والفرع في الدالة، ثم نشغّل دالة الرابط الديناميكي ‎_dl_runtime_resolve التي تعثر على الانتقال. يستخدم الانتقال اسم الرمز الذي حدّده للعثور على الدالة الصحيحة، حيث يمكن يتضمن ذلك تحميل المكتبة من القرص الصلب إن لم تكن موجودة في الذاكرة، وإلّا فيجب مشاركة الشيفرة البرمجية.

يوفر سجلُ الانتقال للرابط الديناميكي العنوانَ الذي يجب إصلاحه، حيث كان هذا العنوان موجودًا في جدول GOT ثم حمّلته شيفرة PLT الاختبارية، وهذا يعني أنه يمكن الحصول على عنوان الدالة المباشر أو ما يسمى بتقصير دورة الرابط الديناميكي بعد المرة الأولى التي تُستدعَى فيها الدالة أي في المرة الثانية لتحميلها.

رأينا الآلية الدقيقة لعمل جدول PLT والعمل الداخلي للرابط الديناميكي. النقاط المهمة التي يجب تذكرها هي:

  • تستدعي استدعاءات المكتبة في برنامجك الشيفرة الاختبارية في جدول PLT الخاص بالملف الثنائي.
  • تحمّل هذه الشيفرة الاختبارية عنوانًا وتقفز إليه.
  • يؤشّر هذا العنوان إلى دالةٍ في الرابط الديناميكي قادرةٍ على البحث عن الدالة الحقيقية من خلال النظر إلى المعلومات الواردة في مدخلة الانتقال لتلك الدالة.
  • يعيد الرابط الديناميكي كتابة العنوان الذي تقرأه الشيفرة الاختبارية، بحيث تنتقل الدالة مباشرة إلى العنوان الصحيح في المرة التالية لاستدعائها.

ترجمة -وبتصرُّف- للقسمين Libraries و Libraries من الفصلين Behind the process و Dynamic Linking من كتاب Computer Science from the Bottom Up لصاحبه Ian Wienand.

اقرأ أيضا

#oqpahameedq

تعليقات