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

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


Ola Abbas

يوفر وجود الربط الديناميكي Dynamic Linking بعض المزايا التي يمكننا الاستفادة منها وبعض المشاكل الإضافية التي يجب حلها للحصول على نظام فعّال.

إصدارات المكتبات

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

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

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

نظام sonames

يُستخدَم نظام sonames لإضافة بعض المعلومات الإضافية إلى مكتبة للمساعدة في تحديد الإصدارات. يسرد التطبيق المكتبات التي يريدها في الحقول DT_NEEDED ضمن القسم الديناميكي للملف الثنائي، وتوجد المكتبة الفعلية في ملف على القرص الصلب ضمن المجلد ‎/lib لمكتبات النظام الأساسية أو المجلد ‎/usr/lib للمكتبات الاختيارية.

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

تشكّل الحاجة إلى الاحتفاظ بكل تعديل مكتبة رئيسي أو ثانوي في ملف منفصل على القرص الصلب أساسَ تسلسل المكتبات الهرمي. يكون اسم المكتبة هو libNAME.so.MAJOR.MINOR حسب العِرف المتبع، حيث يمكنك اختياريًا الحصول على إطلاق Release بوصفه معرفًا نهائيًا بعد العدد الثانوي، ويكفي ذلك لتمييز جميع إصدارات المكتبة المختلفة.

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

يكون soname بالشكل libNAME.so.MAJOR، ويجب ضبطه في الحقل DT_SONAME من القسم الديناميكي لمكتبة مشتركة، حيث يمكن لمؤلف المكتبة تحديد هذا الإصدار عند إنشاء المكتبة.

يمكن أن يحدّد كل ملف مكتبة للإصدار الثانوي على القرص الصلب رقمَ الإصدار الرئيسي نفسه في الحقل DT_SONAME، مما يسمح للرابط الديناميكي بمعرفة أن ملف المكتبة يطبّق تعديلًا رئيسيًا معينًا لواجهتي API و ABI الخاصتين بالمكتبة.

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

الجزء الأخير من التسلسل الهرمي هو اسم تصريف Compile Name المكتبة. إن أردت تصريف برنامجك لربطه بمكتبة، فيمكنك استخدام الراية ‎-lNAME التي تبحث عن الملف libNAME.so في مسار بحث المكتبة.

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

يوضح الشكل التالي العملية العامة لنظام sonames:

 العملية العامة لنظام sonames للربط الديناميكي مع المكتبات

كيف يبحث الرابط الديناميكي عن المكتبات

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

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

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

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

يمكنك تحليل ذلك باستخدام ‎/sbin/ldconfig -p الموجود ضمن الملف ‎/etc/ldconfig.so.cache. إن لم يُعثَر على المكتبة في الذاكرة المخبئية، فسيعود الرابط الديناميكي إلى الخيار الأبطأ المتمثل في المرور على نظام الملفات، وبالتالي يجب إعادة تشغيل برنامج ldconfig عند تثبيت مكتبات جديدة.

البحث عن الرموز

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

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

جدول الرموز الديناميكي

هناك ثلاثة أقسام تلعب جميعها دورًا في وصف الرموز الديناميكية. لنلقِ أولًا نظرة على تعريف رمز من مواصفات ملف ELF كما يلي:

typedef struct {
  Elf32_Word    st_name;
  Elf32_Addr    st_value;
  Elf32_Word    st_size;
  unsigned char st_info;
  unsigned char st_other;
  Elf32_Half    st_shndx;
} Elf32_Sym;
الحقل القيمة
st_name فهرس إلى جدول السلاسل النصية
st_value القيمة الموجودة في كائن مشترك قابل للنقل، حيث تحتفظ هذه القيمة بالإزاحة عن قسم الفهرس المعطى في الحقل st_shndx
st_size أي حجم مرتبط بالرمز
st_info معلومات عن ارتباط Binding الرمز الذي سنشرحه لاحقًا ويكون نوع هذا الرمز دالة أو كائن أو غير ذلك
st_other غير مُستخدَم حاليًا
st_shndx فهرس القسم الذي يوجد فيه الرمز (اطّلع على الحقل st_value)

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

يمكن تسريع هذه العملية من خلال تقديم قسم ثالث يسمى ‎.hash يحتوي على جدول تعمية Hash Table لأسماء رموز مدخلات جدول الرموز. يُحسَب جدول التعمية مسبقًا عند إنشاء المكتبة ويسمح للرابط الديناميكي بالعثور على مدخلة الرمز بصورة أسرع باستخدام عملية بحث واحدة أو اثنتين فقط.

ارتباط الرموز Symbol Binding

تشير عملية العثور على عنوان رمز إلى عملية ارتباط هذا الرمز، ولكن ارتباط الرموز Symbol Binding له معنًى منفصل، إذ تفرض عملية ارتباط الرموز رؤيتها خارجيًا أثناء عملية الربط الديناميكي. يُعَد الرمز المحلي Local Symbol غير مرئي خارج ملف الكائن المُعرَّف ضمنه، بينما يُعَد الرمز العام Global Symbol مرئيًا لملفات الكائنات الأخرى ويمكن أن يحقِّق المراجعَ غير المُعرَّفة في كائنات أخرى. يكون المرجع الضعيف Weak Reference نوعًا خاصًا من المراجع العامة ذات الأولوية المنخفضة، مما يعني أنه مُصمَّم لتجاوزه كما سنرى لاحقًا.

يوضح المثال التالي برنامجًا بلغة سي C نحلّله لفحص ارتباطات الرموز:

$ cat test.c
static int static_variable;

extern int extern_variable;

int external_function(void);

int function(void)
{
  return external_function();
}

static int static_function(void)
{
  return 10;
}

#pragma weak weak_function
int weak_function(void)
{
  return 10;
}

$ gcc -c test.c
$ objdump --syms test.o

test.o:     file format elf32-powerpc

SYMBOL TABLE:
00000000 l    df *ABS*  00000000 test.c
00000000 l    d  .text  00000000 .text
00000000 l    d  .data  00000000 .data
00000000 l    d  .bss   00000000 .bss
00000038 l     F .text  00000024 static_function
00000000 l    d  .sbss  00000000 .sbss
00000000 l     O .sbss  00000004 static_variable
00000000 l    d  .note.GNU-stack        00000000 .note.GNU-stack
00000000 l    d  .comment       00000000 .comment
00000000 g     F .text  00000038 function
00000000         *UND*  00000000 external_function
0000005c  w    F .text  00000024 weak_function

$ nm test.o
         U external_function
00000000 T function
00000038 t static_function
00000000 s static_variable
0000005c W weak_function

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

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

تجاوز الرموز Overriding Symbols

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

$ cat override.c
#define _GNU_SOURCE 1
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <dlfcn.h>

pid_t getpid(void)
{
  pid_t (*orig_getpid)(void) = dlsym(RTLD_NEXT, "getpid");
  printf("Calling GETPID\n");

  return orig_getpid();
}

$ cat test.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void)
{
  printf("%d\n", getpid());
}

$ gcc -shared -fPIC -o liboverride.so override.c -ldl
$ gcc -o test test.c
$ LD_PRELOAD=./liboverride.so ./test
Calling GETPID
15187

تجاوزنا في المثال السابق الدالة getpid لطباعة عبارة صغيرة عند استدعائها. نستخدم الدالة dlysm التي توفرها مكتبة libc مع وسيط يخبرها بالاستمرار والعثور على الرمز التالي المسمَّى getpid.

الرموز الضعيفة

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

لكن كان ذلك غير صحيح بالنسبة لنص معيار يونكس في ذلك الوقت SysVr4 الذي يفرض أنه يجب أن يتعامل الرابط الساكن مع الرموز الضعيفة التي يجب أن تظل بعيدة عن الرابط الديناميكي. تطابق تقديم لينكس الخاص بجعل الرابط الديناميكي يتجاوز الرموز الضعيفة مع منصة IRIX الخاصة بشركة SGI واختلف عن الأنظمة الأخرى مثل Solaris و AIX في ذلك الوقت. لذا لغى المطورون هذا السلوك عندما أدركوا أنه ينتهك المعيار، وتغير السلوك القديم ليتطلّب ضبط راية بيئة خاصة LD_DYNAMIC_WEAK.

تحديد ترتيب الارتباط

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

لكن تحتاج بعض المكتبات المشتركة إلى طريقة لتجاوز هذا السلوك، إذ يجب أن تخبر هذه المكتبات الرابط الديناميكي بأنه يجب أن ينظر أولًا بداخلها عن هذه الرموز بدلًا من العمل بصورة عكسية من آخر مكتبة مُحمَّلة. يمكن للمكتبات ضبط الراية DT_SYMBOLIC في ترويسة القسم الديناميكي للحصول على هذا السلوك، إذ يمكن ضبط هذه الراية من خلال تمرير الراية ‎-Bsymbolic عبر سطر أوامر الروابط الساكنة عند بناء المكتبة المشتركة، حيث تتحكم هذه الراية برؤية الرمز Symbol Visibility. لا يمكن تجاوز الرموز الموجودة في المكتبة، لذا يمكن عَدُّها خاصةً بالمكتبة المُحمَّلة.

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

تحديد إصدار الرموز Symbol Versioning

يأتي النظام الأفضل من خلال استخدام تحديد إصدار الرموز، حيث يمكننا تحديد بعض المدخلات الإضافية للرابط الساكن لمنحه بعض المعلومات الإضافية حول الرموز في المكتبة المشتركة كما يلي:

$ cat Makefile
all: test testsym

clean:
        rm -f *.so test testsym

liboverride.so : override.c
        $(CC) -shared -fPIC -o liboverride.so override.c

libtest.so : libtest.c
        $(CC) -shared -fPIC -o libtest.so libtest.c

libtestsym.so : libtest.c
        $(CC) -shared -fPIC -Wl,-Bsymbolic -o libtestsym.so libtest.c

test : test.c libtest.so liboverride.so
        $(CC) -L. -ltest -o test test.c

testsym : test.c libtestsym.so liboverride.so
        $(CC) -L. -ltestsym -o testsym test.c

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

int foo(void) {
  printf("libtest foo called\n");
  return 1;
}

int test_foo(void)
{
  return foo();
}

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

int foo(void)
{
  printf("override foo called\n");
  return 0;
}

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

int main(void)
{
  printf("%d\n", test_foo());
}

$ cat Versions
{global: test_foo;  local: *; };

$ gcc -shared -fPIC -Wl,-version-script=Versions -o libtestver.so libtest.c

$ gcc -L. -ltestver -o testver test.c

$ LD_LIBRARY_PATH=. LD_PRELOAD=./liboverride.so ./testver
libtest foo called

100000574 l     F .text    00000054              foo
000005c8 g     F .text    00000038              test_foo

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

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

جاء مفهوم تحديد إصدار الرموز Symbol Versioning من تلك الفكرة، حيث يمكنك تحديد إصدارات متعددة من الرمز نفسه ضمن المكتبة نفسها. يُلحِق الرابط الساكن بعض معلومات الإصدار بعد اسم الرمز مثل ‎@VER الذي يصف الإصدار المعطى للرمز.

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

ترجمة -وبتصرُّف- للقسم Working with libraries and the linker من فصل Dynamic Linking من كتاب 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.


×
×
  • أضف...