Featured image of post كيف تتوحد البرامج المكتوبة بشكل منفصل لتصبح برنامجاً واحداً؟ آليات عمل الرابط والمحمل

كيف تتوحد البرامج المكتوبة بشكل منفصل لتصبح برنامجاً واحداً؟ آليات عمل الرابط والمحمل

مقدمة: البناة الخفيون للبرمجيات الحديثة

في حياتنا اليومية كمطورين، نكتب بانتظام أوامر مثل:

1
gcc -O2 main.c utils.c math.c -o myprogram

غالباً ما نطلق على هذا المسار مسمى “الترجمة البرمجية” (Compilation)، لكن المترجم ينجز نصف الرحلة فقط. أما النصف الثاني—حياكة العشرات من ملفات الكائنات والمكتبات البرمجية المنفصلة، وحل العناوين المتضاربة، وتحميل الملف التنفيذي في الذاكرة لتشغيله عبر المعالج—فيقع تماماً على عاتق بطلين مجهولين: الرابط (Linker) والمحمل (Loader).

  flowchart TD
    subgraph BuildPipeline ["من الشيفرة المصدرية إلى التنفيذ"]
        SourceA["main.c"] -->|المترجم| ObjA["main.o (ملف كائن)"]
        SourceB["utils.c"] -->|المترجم| ObjB["utils.o (ملف كائن)"]
        Lib["libc.so / libm.a\n(مكتبات النظام)"]

        ObjA --> Linker["★ الرابط (ld)\n[حل الرموز & إعادة التوطين]"]
        ObjB --> Linker
        Lib --> Linker

        Linker --> Exec["myprogram (ملف تنفيذي بصيغة ELF)"]
    end

    Exec -->|execve| Loader["★ المحمل (النواة & ld-linux.so)\n[تخطيط الذاكرة & الربط الديناميكي]"]
    Loader --> CPU["تنفيذ المعالج (_start -> main)"]

    style BuildPipeline fill:#f9f9f9,stroke:#333,stroke-width:2px
    style Linker fill:#ffe6e6,stroke:#d9534f,stroke-width:2px
    style Loader fill:#e6f3ff,stroke:#0066cc,stroke-width:2px

لولا الرابط، لاستحال تطبيق مبادئ البرمجة المعيارية، ولكنا مضطرين لكتابة ملايين الأسطر في ملف واحد ضخم.


1. تشريح ملف الكائن: معيار صيغة ELF

الملفات الناتجة عن التجميع (.o) تسمى ملفات كائنات قابلة لإعادة التوطين وفق صيغة ELF (Executable and Linkable Format):

  • .text: تعليمات لغة الآلة التنفيذية.
  • .rodata: الثوابت والنصوص النصية للقراءة فقط.
  • .data: المتغيرات العامة والساكنة التي تم إسناد قيم ابتدائية لها.
  • .bss: المتغيرات العامة غير المعرفة. لا تستهلك أي مساحة على القرص الصلب! حيث تخصص لها الذاكرة وتصفر عند تشغيل البرنامج.
  • .symtab: جدول الرموز (سجل أسماء الدوال والمتغيرات).
  • .rela.text: سجلات إعادة التوطين التي تحدد العناوين المؤقتة الواجب تصحيحها.

2. حل الرموز (Symbol Resolution)

يقوم الرابط بربط كل استدعاء خارجي بتعريفه الحقيقي.

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

  • قوي: تعريفات الدوال والمتغيرات العامة ذات القيم الابتدائية (int x = 1;).
  • ضعيف: المتغيرات العامة غير المهيأة (int x;).

قواعد الفصل:

  1. تكرار الرموز القوية يولد خطأ فورياً (multiple definition).
  2. الرمز القوي يتفوق دائماً على الرمز الضعيف.
  3. عند تزاحم رموز ضعيفة، يختار الرابط واحداً عشوائياً (خطر تلف الذاكرة).

أهمية ترتيب المكتبات: يقوم الرابط بمسح المعاملات من اليسار إلى اليمين في مسار واحد، لذا يجب كتابة -lm دائماً بعد main.c.


3. إعادة التوطين (Relocation)

يضع المترجم أصفاراً كعناوين وهمية مؤقتة للدوال الخارجية:

1
callq 0x00000000  # إزاحة وهمية
$$\text{القيمة} = S + A - P$$

($S$: عنوان الرمز، $A$: الإضافة، $P$: موقع التعليمة).


4. الربط الديناميكي: شيفرة مستقلة عن الموقع (PIC) وجداول GOT و PLT

لتفادي تكرار المكتبات في الذاكرة، ظهرت المكتبات المشتركة (.so). وهي تعتمد على الشيفرة المستقلة عن الموقع (PIC) بالاستناد إلى جدولين:

  • GOT (Global Offset Table): يخزن العناوين الحقيقية في الذاكرة.
  • PLT (Procedure Linkage Table): جدول قفز في مقطع الشيفرة.

بفضل الربط الكسول (Lazy Binding)، لا يتم حل عنوان الدالة عبر الرابط الديناميكي (ld-linux.so) إلا عند استدعائها الفعلي للمرة الأولى.


5. المحمل: من _start إلى main()

عند تشغيل البرنامج عبر الأمر execve، يقوم محمل النواة بإسقاط أجزاء ELF في الذاكرة الافتراضية (mmap)، وتهيئة المكدس، ثم القفز إلى نقطة البداية الحقيقية: _start. تقوم بيئة تشغيل لغة C بتنفيذ الدوال البنائية العامة في .init_array قبل تسليم الدفة أخيراً للدالة main().

6. أدوات التشخيص وتحليل الثنائيات للمهندسين

يعتمد مهندسو النظم على حزمة من الأدوات المتقدمة لفحص ملفات ELF وتحليل سلوك الرابط والمحمل:

  • readelf: لقراءة ترويسات ELF وتفاصيل المقاطع والجداول:
    1
    2
    
    readelf -h myprogram    # فحص ترويسة الملف
    readelf -l myprogram    # فحص ترويسات البرامج (Segments)
    
  • nm: لاستعراض جدول الرموز البرمجية، وتتبع الدوال غير المعرفة التي تسبب أخطاء الربط:
    1
    
    nm -u myprogram        # إظهار الرموز غير المعرفة فقط
    
  • objdump: لتفكيك الشيفرة التنفيذية وقراءة سجلات إعادة التوطين بدقة:
    1
    2
    
    objdump -d main.o      # تفكيك الشيفرة الآلية
    objdump -r main.o      # عرض إدخالات إعادة التوطين
    
  • ldd: لاستعراض كافة المكتبات المشتركة الديناميكية التي يعتمد عليها البرنامج التنفيذي وتحديد المفقود منها فوراً.
comments powered by Disqus