تُعَد شيفرة نظام التشغيل البرمجية للقراءة فقط وتكون منفصلة عن البيانات، لذا إن لم تتمكن البرامج من تعديل الشيفرة البرمجية مع وجود كميات كبيرة من الشيفرة البرمجية المشتركة أمرًا منطقيًا، إذ يجب مشاركتها بين العديد من الملفات القابلة للتنفيذ بدلًا من تكرارها لكل ملف منها.
يمكن تطبيق ذلك بسهولة باستخدام الذاكرة الوهمية، إذ يمكن الرجوع بسهولة إلى صفحات الذاكرة الحقيقية التي جرى تحميل شيفرة المكتبة البرمجية إليها من خلال عدد من الصفحات الوهمية في عددٍ من فضاءات العناوين. لذا يمكن لكل عملية الوصول إلى شيفرة المكتبة البرمجية باستخدام أيّ عنوان وهمي تريده، بينما يكون لديك نسخة حقيقية واحدة فقط من هذه الشيفرة في ذاكرة النظام.
وبذلك توصل المبرمجون بسرعة إلى فكرة المكتبة المشتركة 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، فيمكن تقسيمه إلى ما يلي:
- R_IA64: تبدأ جميع الانتقالات بهذه البادئة.
- DIR64: انتقال من النوع 64 بت المباشر.
- 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:
$ 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.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.