مفهوم الربط الديناميكي Dynamic Linking في معمارية الحاسوب – الأنظمة والأنظمة المدمجة

229

[ad_1]

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

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

وبذلك توصل المبرمجون بسرعة إلى فكرة المكتبة المشتركة Shared Library التي -كما يوحي الاسم- يمكن مشاركتها بين العديد من الملفات القابلة للتنفيذ. يحتوي كل ملف قابل للتنفيذ على مرجع يقول: “أحتاج مكتبة Foo مثلًا”، حيث يُترَك الأمر للنظام عند تحميل البرنامج للتحقق من وجود برنامج آخر حمّل شيفرة هذه المكتبة في الذاكرة ثم مشاركتها من خلال ربط صفحات الملف القابل للتنفيذ مع الذاكرة الحقيقية، أو يمكنه تحميل المكتبة في ذاكرة الملف القابل للتنفيذ. تسمى هذه العملية بالربط الديناميكي Dynamic Linking، لأنها تطبّق جزءًا من عملية الربط مباشرةً عند تنفيذ البرامج في النظام.

تفاصيل المكتبة الديناميكية

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

تحتوي ترويسة ملف ELF على رايتين حصريتين هما ET_EXEC وET_DYN لتمييز ملف ELF بوصفه ملفًا قابلًا للتنفيذ أو ملفَ كائن مشترك.

تضمين المكتبات في ملف قابل للتنفيذ

يمكن تضمين المكتبات في ملف قابل للتنفيذ ولكن يجب أن نراعي مسألتين مهمتين متعلقتين بالمصرِّف والرابط الديناميكي كما سنوضح الآن.

التصريف Compilation

تمتلك ملفات الكائنات مراجعًا إلى دوال المكتبة تمامًا كما هو الحال مع أيّ مرجع خارجي آخر عندما تصرِّف برنامجك الذي يستخدم مكتبة ديناميكية. يجب تضمين ترويسة المكتبة ليعرف المصرِّف الأنواع المحددة للدوال التي تستدعيها، إذ يحتاج المصرِّف فقط معرفةَ الأنواع المرتبطة بالدالة (مثل أن تأخذ الدالة النوع int وتعيد النوع char *‎) بحيث يمكنه تخصيص مساحة لاستدعاء الدالة بصورة صحيحة.

لم يكن هذا هو الحال دائمًا مع معايير لغة C، إذ افترضت المصِّرفات سابقًا أن أيّ دالة غير معروفة تعيد قيمة من النوع int. يكون لحجم المؤشر في نظام 32 بت حجم النوع int نفسه، لذلك لا توجد مشكلة في ذلك، ولكن يكون حجم المؤشر ضعف حجم int في نظام 64 بت، لذلك إذا أعادت الدالة مؤشرًا، فستُدمَّر قيمتها. يُعَد ذلك الأمر غير مقبول، لأن المؤشر لن يؤشّر إلى ذاكرة صالحة، ولكن تغيّر معيار C99 بحيث يُطلَب منك تحديد أنواع الدوال المُضمَّنة.

الربط Linking

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

يمكننا فحص هذه الحقول باستخدام برنامج readelf. سنلقي فيما يلي نظرة على ملف ثنائي معياري ‎/bin/ls يمثل تحديد المكتبات الديناميكية:

$ readelf --dynamic /bin/ls 

Dynamic segment at offset 0x22f78 contains 27 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [librt.so.1]
 0x0000000000000001 (NEEDED)             Shared library: [libacl.so.1]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6.1]
 0x000000000000000c (INIT)               0x4000000000001e30
 ... snip ...

يمكنك أن ترى أن المثال السابق يحدد ثلاث مكتبات. المكتبة الأكثر شيوعًا التي تتشارك بها أغلبية البرامج الموجودة على النظام -إن لم تكن جميعها- هي مكتبة libc، وهناك بعض المكتبات الأخرى التي يحتاجها البرنامج ليعمل بصورة صحيحة.

تكون قراءة ملف ELF المباشرة مفيدةً أحيانًا، ولكن الطريقة المعتادة لفحص ملف قابل للتنفيذ مرتبط ديناميكيًا هي باستخدام أداة ldd التي تمر على اعتماديات المكتبات، حيث ستظهِر لك إذا كانت المكتبة معتمدة على مكتبة أخرى كما يلي:

$ ldd /bin/ls
        librt.so.1 => /lib/tls/librt.so.1 (0x2000000000058000)
        libacl.so.1 => /lib/libacl.so.1 (0x2000000000078000)
        libc.so.6.1 => /lib/tls/libc.so.6.1 (0x2000000000098000)
        libpthread.so.0 => /lib/tls/libpthread.so.0 (0x20000000002e0000)
        /lib/ld-linux-ia64.so.2 => /lib/ld-linux-ia64.so.2 (0x2000000000000000)
        libattr.so.1 => /lib/libattr.so.1 (0x2000000000310000)
$ readelf --dynamic /lib/librt.so.1

Dynamic segment at offset 0xd600 contains 30 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6.1]
 0x0000000000000001 (NEEDED)             Shared library: [libpthread.so.0]
 ... snip ...

يمكننا أن نرى في المثال السابق أن مكتبة libpthread مطلوبة من مكان ما، حيث إذا تعمّقنا قليلًا، فيمكننا أن نرى أنها مطلوبة من المكتبة librt.

الرابط الديناميكي Dynamic Linker

الرابط الديناميكي هو البرنامج الذي يدير المكتبات الديناميكية المشتركة بدلًا من الملف القابل للتنفيذ، ويعمل على تحميل المكتبات في الذاكرة وتعديل البرنامج في وقت التشغيل لاستدعاء الدوال الموجودة في المكتبة. يسمح ملف ELF للملفات القابلة للتنفيذ بتحديد المفسّر Interpreter الذي هو برنامج يجب استخدامه لتشغيل الملف القابل للتنفيذ. يضبط المصرِّف Compiler والرابط الساكن Static Linker مفسّر الملفات القابلة للتنفيذ الذي يعتمد على المكتبات الديناميكية ليكون الرابط الديناميكي.

يوضَح المثال التالي كيفية التحقق من مفسّر البرنامج:

ianw@lime:~/programs/csbu$ readelf --headers /bin/ls

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x4000000000000040 0x4000000000000040
                 0x0000000000000188 0x0000000000000188  R E    8
  INTERP         0x00000000000001c8 0x40000000000001c8 0x40000000000001c8
                 0x0000000000000018 0x0000000000000018  R      1
      [Requesting program interpreter: /lib/ld-linux-ia64.so.2]
  LOAD           0x0000000000000000 0x4000000000000000 0x4000000000000000
                 0x0000000000022e40 0x0000000000022e40  R E    10000
  LOAD           0x0000000000022e40 0x6000000000002e40 0x6000000000002e40
                 0x0000000000001138 0x00000000000017b8  RW     10000
  DYNAMIC        0x0000000000022f78 0x6000000000002f78 0x6000000000002f78
                 0x0000000000000200 0x0000000000000200  RW     8
  NOTE           0x00000000000001e0 0x40000000000001e0 0x40000000000001e0
                 0x0000000000000020 0x0000000000000020  R      4
  IA_64_UNWIND   0x0000000000022018 0x4000000000022018 0x4000000000022018
                 0x0000000000000e28 0x0000000000000e28  R      8

يمكنك أن ترى في المثال السابق أن المفسّر مضبوط ليكون ‎/lib/ld-linux-ia64.so.2 أي يمثل الرابط الديناميكي. تتحقق النواة Kernel عندما تحمّل الملف الثنائي للتنفيذ مما إذا كان الحقل PT_INTERP موجودًا، حيث إذا كان موجودًا، فيجب تحميل ما يؤشّر إليه في الذاكرة وتشغيله.

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

الانتقالات Relocations

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

العنوان الحدث
0x123456 عنوان الرمز “x”
0x564773 الدالة X

هناك العديد من أنواع الانتقالات لكل معمارية، حيث يُوثَّق السلوك الدقيق لكل نوع بوصفه جزءًا من واجهة ABI الخاصة بالنظام. يُعَد تعريف الانتقالات واضحًا وسهلًا كما في المثال التالي:

typedef struct {
  Elf32_Addr    r_offset;  <--- address to fix
  Elf32_Word    r_info;    <--- symbol table pointer and relocation type
}

typedef struct {
  Elf32_Addr    r_offset;
  Elf32_Word    r_info;
  Elf32_Sword   r_addend;
} Elf32_Rela

يشير الحقل r_offset إلى الإزاحة في الملف التي يجب إصلاحها، ويحدد الحقل r_info نوع الانتقال الذي يصِف بالضبط ما يجب تطبيقه لإصلاح هذه الشيفرة البرمجية. تُعَد قيمة الرمز أبسط عملية انتقال مُعرَّفة لمعماريةٍ ما، حيث يمكنك ببساطة في هذه الحالة استبدال عنوان الرمز في الموقع المحدد، وبالتالي سيكتمل إصلاح عملية الانتقال.

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

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

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

كيفية عمل الانتقالات

يوضح المثال التالي كيفية عمل الانتقالات، حيث سننشئ مكتبتين مشتركتين بسيطتين ونشير إلى إحدى المكتبتين من المكتبة الأخرى:

$ cat addendtest.c
extern int i[4];
int *j = i + 2;

$ cat addendtest2.c
int i[4];

$ gcc -nostdlib -shared -fpic -s -o addendtest2.so addendtest2.c
$ gcc -nostdlib -shared -fpic -o addendtest.so addendtest.c ./addendtest2.so

$ readelf -r ./addendtest.so

Relocation section '.rela.dyn' at offset 0x3b8 contains 1 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
0000000104f8  000f00000027 R_IA64_DIR64LSB   0000000000000000 i + 8

لدينا عملية انتقال واحدة في addendtest.so من النوع R_IA64_DIR64LSB الذي إن بحثت عنه في واجهة IA64 ABI، فيمكن تقسيمه إلى ما يلي:

  1. R_IA64: تبدأ جميع الانتقالات بهذه البادئة.
  2. DIR64: انتقال من النوع 64 بت المباشر.
  3. LSB: بما أن IA64 يمكن أن تعمل في أنماط Big Endian على تخزين البتات الأقل أهمية أولًا، و في أنماط Little Endian على تخزين البتات الأكثر أهمية أولًا، فسيكون هذا الانتقال Little Endian والذي يعني البايت الأقل أهميةً Least Significant Byte.

تقول واجهة ABI أن هذا الانتقال يمثل قيمة الرمز الذي يؤشّر إليه مع أيّ قيمة مضافة. يمكننا أن نرى أن لدينا قيمة مضافة مقدارها 8 ، حيث أن حجم النوع int يساوي 4 أو sizeof(int) == 4، ونقلنا قيمتين من النوع int في المصفوفة أي ‎*j = i + 2. لذا يمكن إصلاح هذا الانتقال في وقت التشغيل من خلال العثور على عنوان الرمز i ووضع قيمته بالإضافة إلى القيمة 8 في 0x104f8.

استقلال المواقع

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

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

جدول الإزاحة العام Global Offset Table

لا بد أنك لاحظت مشكلة خطيرة في الانتقالات Relocations عند التفكير في أهداف المكتبة المشتركة، حيث ذكرنا سابقًا أن ميزة المكتبة المشتركة مع الذاكرة الوهمية هي أن البرامج المتعددة يمكنها استخدام الشيفرة البرمجية الموجودة في الذاكرة من خلال مشاركة الصفحات.

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

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

المنطقة المخصصة لهذه العناوين تسمى بجدول الإزاحة العام Global Offset Table -أو GOT اختصارًا- الذي يتواجد في القسم ‎.got من ملف ELF.

يوضح الشكل التالي كيفية الوصول إلى الذاكرة باستخدام جدول GOT:

كيفية الوصول إلى الذاكرة باستخدام جدول GOT في معمارية الحاسوب

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

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

يوضح المثال التالي كيفية استخدام جدول GOT:

$ cat got.c
extern int i;

void test(void)
{
  i = 100;
}

$ gcc -nostdlib  -shared -o got.so ./got.c

$ objdump --disassemble ./got.so

./got.so:     file format elf64-ia64-little

Disassembly of section .text:

0000000000000410 <test>:
 410:   0d 10 00 18 00 21       [MFI]       mov r2=r12
 416:   00 00 00 02 00 c0                   nop.f 0x0
 41c:   81 09 00 90                         addl r14=24,r1;;
 420:   0d 78 00 1c 18 10       [MFI]       ld8 r15=[r14]
 426:   00 00 00 02 00 c0                   nop.f 0x0
 42c:   41 06 00 90                         mov r14=100;;
 430:   11 00 38 1e 90 11       [MIB]       st4 [r15]=r14
 436:   c0 00 08 00 42 80                   mov r12=r2
 43c:   08 00 84 00                         br.ret.sptk.many b0;;

$ readelf --sections ./got.so
There are 17 section headers, starting at offset 0x640:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .hash             HASH             0000000000000120  00000120
       00000000000000a0  0000000000000004   A       2     0     8
  [ 2] .dynsym           DYNSYM           00000000000001c0  000001c0
       00000000000001f8  0000000000000018   A       3     e     8
  [ 3] .dynstr           STRTAB           00000000000003b8  000003b8
       000000000000003f  0000000000000000   A       0     0     1
  [ 4] .rela.dyn         RELA             00000000000003f8  000003f8
       0000000000000018  0000000000000018   A       2     0     8
  [ 5] .text             PROGBITS         0000000000000410  00000410
       0000000000000030  0000000000000000  AX       0     0     16
  [ 6] .IA_64.unwind_inf PROGBITS         0000000000000440  00000440
       0000000000000018  0000000000000000   A       0     0     8
  [ 7] .IA_64.unwind     IA_64_UNWIND     0000000000000458  00000458
       0000000000000018  0000000000000000  AL       5     5     8
  [ 8] .data             PROGBITS         0000000000010470  00000470
       0000000000000000  0000000000000000  WA       0     0     1
  [ 9] .dynamic          DYNAMIC          0000000000010470  00000470
       0000000000000100  0000000000000010  WA       3     0     8
  [10] .got              PROGBITS         0000000000010570  00000570
       0000000000000020  0000000000000000 WAp       0     0     8
  [11] .sbss             NOBITS           0000000000010590  00000590
       0000000000000000  0000000000000000   W       0     0     1
  [12] .bss              NOBITS           0000000000010590  00000590
       0000000000000000  0000000000000000  WA       0     0     1
  [13] .comment          PROGBITS         0000000000000000  00000590
       0000000000000026  0000000000000000           0     0     1
  [14] .shstrtab         STRTAB           0000000000000000  000005b6
       000000000000008a  0000000000000000           0     0     1
  [15] .symtab           SYMTAB           0000000000000000  00000a80
       0000000000000258  0000000000000018          16    12     8
  [16] .strtab           STRTAB           0000000000000000  00000cd8
       0000000000000045  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)

يوضّح المثال السابق كيفية إنشاء مكتبة مشتركة بسيطة تشير إلى رمز خارجي. لا نعرف عنوان هذا الرمز في وقت التصريف، لذلك نتركه للرابط الديناميكي لإصلاحه في وقت التشغيل. لكننا نريد أن تبقى الشيفرة البرمجية قابلةً للمشاركة في حالة رغبة العمليات الأخرى في استخدام هذه الشيفرة، حيث يوضّح التفكيك Disassembly كيفية تطبيق ذلك باستخدام القسم ‎.got. يُعرف المسجّل r1 في معمارية IA64 التي صُرِّفت المكتبة من أجلها بالمؤشر العام Global Pointer الذي يؤشّر دائمًا إلى مكان تحميل القسم ‎.got في الذاكرة.

إذا ألقينا نظرة على خرج الأداة readelf، فيمكننا أن نرى أن القسم ‎.got يبدأ عند عنوان يبعد بمقدار 0x10570 بايت عن مكان تحميل المكتبة في الذاكرة. لذا إذا حُمِّلت المكتبة في الذاكرة عند العنوان 0x6000000000000000، فسيكون القسم ‎.got موجودًا عند العنوان 0x6000000000010570، وسيؤشّر المسجّل r1 دائمًا إلى هذا العنوان.

كما يمكننا أن نرى أننا نخزن القيمة 100 في عنوان الذاكرة الموجود في المسجّل r15 الذي يحتوي على قيمة عنوان الذاكرة المخزن في المسجل 14، حيث حمّلنا هذا العنوان من خلال إضافة عدد صغير إلى المسجّل 1. يُعَد جدول GOT مجرد قائمة طويلة من المدخلات، حيث تكون كل مدخلة خاصةً بمتغير خارجي، مما يعني أن مدخلة جدول GOT الخاصة بالمتغير الخارجي i تخزّن 24 بايتًا أو 3 عناوين بحجم 64 بتًا.

$ readelf --relocs ./got.so

Relocation section '.rela.dyn' at offset 0x3f8 contains 1 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000010588  000f00000027 R_IA64_DIR64LSB   0000000000000000 i + 0

كما يمكننا التحقق من انتقال مدخلة جدول GOT، حيث يمثل الانتقالُ استبدالَ القيمة عند الإزاحة 10588 بموقع الذاكرة الذي خُزِّن فيه الرمز i.

يبدأ القسم ‎.got عند الإزاحة 0x10570 عن الخرج السابق، حيث رأينا كيف تحمّل الشيفرة البرمجية عنوانًا يبعد عن القسم ‎.got بمقدار 0x18 (أو 24 في النظام العشري)، مما يمنحنا عنوانًا مقداره 0x10570 + 0x18 = 0x10588 يمثّل العنوان الذي طُبِّق الانتقال لأجله. لذا يجب أن يصلح الرابط الديناميكي الانتقال قبل أن يبدأ البرنامج للتأكد من أن قيمة الذاكرة عند الإزاحة 0x10588 هي عنوان المتغير العام i.

ترجمة -وبتصرُّف- للأقسام Code Sharing و The Dynamic Linker و Global Offset Tables من فصل Dynamic Linking من كتاب Computer Science from the Bottom Up لصاحبه Ian Wienand.

اقرأ أيضًا

[ad_2]

المصدر