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

مفاهيم متقدمة متعلقة بصيغة ملفات ELF القابلة للتنفيذ


Ola Abbas

تعرفنا في المقال السابق على الملفات القابلة للتنفيذ في نظام التشغيل وتمثيلها باستخدام الصيغة ELF وسنوضح في هذا المقال بعض المفاهيم المتعلقة بصيغة ملفات ELF مثل تنقيح الأخطاء Debugging وكيفية إنشاء أقسام مخصصة فيها وسكربتات الرابط Linker Scripts التي يستخدمها الرابط لبناء الأقسام Sections المُكوِّنة للمقاطع Segments، ولكن لنتعرّف أولًا على مفهوم ملفات ELF القابلة للتنفيذ.

ملفات ELF القابلة للتنفيذ

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

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

$ readelf --segments /bin/ls

Elf file type is EXEC (Executable file)
Entry point 0x4046d4
There are 8 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040
                 0x00000000000001c0 0x00000000000001c0  R E    8
  INTERP         0x0000000000000200 0x0000000000400200 0x0000000000400200
                 0x000000000000001c 0x000000000000001c  R      1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x0000000000019ef4 0x0000000000019ef4  R E    200000
  LOAD           0x000000000001a000 0x000000000061a000 0x000000000061a000
                 0x000000000000077c 0x0000000000001500  RW     200000
  DYNAMIC        0x000000000001a028 0x000000000061a028 0x000000000061a028
                 0x00000000000001d0 0x00000000000001d0  RW     8
  NOTE           0x000000000000021c 0x000000000040021c 0x000000000040021c
                 0x0000000000000044 0x0000000000000044  R      4
  GNU_EH_FRAME   0x0000000000017768 0x0000000000417768 0x0000000000417768
                 0x00000000000006fc 0x00000000000006fc  R      4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     8

Section to Segment mapping:
  Segment Sections...
    00     
    01     .interp 
    02     .interp .note.ABI-tag .note.gnu.build-id .hash .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame 
    03     .ctors .dtors .jcr .dynamic .got .got.plt .data .bss 
    04     .dynamic 
    05     .note.ABI-tag .note.gnu.build-id 
    06     .eh_frame_hdr 
    07

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

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

تنقيح الأخطاء Debugging

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

$ cat coredump.c
int main(void) {
  char *foo = (char*)0x12345;
  *foo = 'a';

  return 0;
}

$ gcc -Wall -g -o coredump coredump.c

$ ./coredump
Segmentation fault (core dumped)

$ file ./core
./core: ELF 32-bit LSB core file Intel 80386, version 1 (SYSV), SVR4-style, from './coredump'

$ gdb ./coredump
...
(gdb) core core
[New LWP 31614]
Core was generated by `./coredump'.
Program terminated with signal 11, Segmentation fault.
#0  0x080483c4 in main () at coredump.c:3
3        *foo = 'a';
(gdb)

وبالتالي فإن ملف التفريغ الأساسي هو مجرد ملف ELF يحتوي على مجموعة من الأقسام التي يفهمها منقح الأخطاء لتمثيل أجزاء من البرنامج المُشغَّل.

الرموز ومعلومات تنقيح الأخطاء

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

يمكن أن يؤدي تضمين معلومات تنقيح الأخطاء إلى جعل الملفات والمكتبات القابلة للتنفيذ كبيرة جدًا، إذ لا تزال تشغل مساحة كبيرة على القرص الصلب بالرغم من أنها ليست مطلوبة في الذاكرة للتشغيل الفعلي، وبالتالي يجب إزالة هذه المعلومات من ملف ELF. يمكن نقل كل من الملفات التي أًزيلت منها هذه المعلومات والملفات التي لم تًُزال منها هذه المعلومات، ولكن توفّر معظم طرق توزيع أو نشر الملفات الثنائية binary distribution الحالية معلومات لتنقيح الأخطاء في ملفات منفصلة. يمكن استخدام أداة objcopy لاستخراج معلومات تنقيح الأخطاء (‎--only-keep-debug) ثم إضافة رابط في الملف القابل للتنفيذ الأصلي إلى هذه المعلومات المُزالَة (‎--add-gnu-debuglink)، ثم سيكون هناك قسم خاص بالاسم ‎.gnu_debuglink موجود في الملف القابل للتنفيذ الأصلي ويحتوي على قيمة فريدة بحيث يمكن لمنقح الأخطاء عند بدء جلسات تنقيح الأخطاء التأكدَ من أنه يربط معلومات تنقيح الأخطاء الصحيحة بالملف التنفيذي الصحيح.

يوضح المثال التالي إزالة معلومات تنقيح الأخطاء إلى ملفات منفصلة باستخدام الأداة objcopy:

$ gcc -g -shared -o libtest.so libtest.c
$ objcopy --only-keep-debug libtest.so libtest.debug
$ objcopy --add-gnu-debuglink=libtest.debug libtest.so
$ objdump -s -j .gnu_debuglink libtest.so

libtest.so:     file format elf32-i386

Contents of section .gnu_debuglink:
 0000 6c696274 6573742e 64656275 67000000  libtest.debug...
 0010 52a7fd0a                             R...

تشغل الرموز مساحة أقل بكثير، ولكنها تُعَد هدفًا للإزالة من الخرج النهائي، إذ لن تكون هناك حاجة لبقاء معظم الرموز بمجرد ربط ملفات التعليمات المُصرَّفة object files لملف قابل للتنفيذ بالصورة النهائية. تُعَد الرموز مطلوبة لإصلاح مدخلات المنقولات Relocation، ولكن لن تكون الرموز بعد ذلك ضرورية تمامًا لتشغيل البرنامج النهائي. توفر سلسلة أدوات GNU لتجريد البرنامج في نظام لينكس خياراتٍ لإزالة الرموز. لاحظ أنه يجب تحليل بعض الرموز في وقت التشغيل (للربط الديناميكي Dynamic Linking)، ولكنها تُوضَع في جداول رموز ديناميكية منفصلة حتى لا تُزال وتجعل الخرج النهائي عديم الفائدة.

التفريغ الأساسي Coredump

يُعَد التفريغ الأساسي مجرد ملف ELF. يوضح المثال التالي مرونة صيغة ELF بوصفها صيغة ثنائية:

$ readelf --all ./core
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              CORE (Core file)
  Machine:                           Intel 80386
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          52 (bytes into file)
  Start of section headers:          0 (bytes into file)
  Flags:                             0x0
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         15
  Size of section headers:           0 (bytes)
  Number of section headers:         0
  Section header string table index: 0

There are no sections in this file.

There are no sections to group in this file.

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  NOTE           0x000214 0x00000000 0x00000000 0x0022c 0x00000     0
  LOAD           0x001000 0x08048000 0x00000000 0x01000 0x01000 R E 0x1000
  LOAD           0x002000 0x08049000 0x00000000 0x01000 0x01000 RW  0x1000
  LOAD           0x003000 0x489fc000 0x00000000 0x01000 0x1b000 R E 0x1000
  LOAD           0x004000 0x48a17000 0x00000000 0x01000 0x01000 R   0x1000
  LOAD           0x005000 0x48a18000 0x00000000 0x01000 0x01000 RW  0x1000
  LOAD           0x006000 0x48a1f000 0x00000000 0x01000 0x153000 R E 0x1000
  LOAD           0x007000 0x48b72000 0x00000000 0x00000 0x01000     0x1000
  LOAD           0x007000 0x48b73000 0x00000000 0x02000 0x02000 R   0x1000
  LOAD           0x009000 0x48b75000 0x00000000 0x01000 0x01000 RW  0x1000
  LOAD           0x00a000 0x48b76000 0x00000000 0x03000 0x03000 RW  0x1000
  LOAD           0x00d000 0xb771c000 0x00000000 0x01000 0x01000 RW  0x1000
  LOAD           0x00e000 0xb774d000 0x00000000 0x02000 0x02000 RW  0x1000
  LOAD           0x010000 0xb774f000 0x00000000 0x01000 0x01000 R E 0x1000
  LOAD           0x011000 0xbfeac000 0x00000000 0x22000 0x22000 RW  0x1000

There is no dynamic section in this file.

There are no relocations in this file.

There are no unwind sections in this file.

No version information found in this file.

Notes at offset 0x00000214 with length 0x0000022c:
  Owner                 Data size    Description
  CORE                 0x00000090    NT_PRSTATUS (prstatus structure)
  CORE                 0x0000007c    NT_PRPSINFO (prpsinfo structure)
  CORE                 0x000000a0    NT_AUXV (auxiliary vector)
  LINUX                0x00000030    Unknown note type: (0x00000200)

$ eu-readelf -n ./core

Note segment of 556 bytes at offset 0x214:
  Owner          Data size  Type
  CORE                 144  PRSTATUS
    info.si_signo: 11, info.si_code: 0, info.si_errno: 0, cursig: 11
    sigpend: <>
    sighold: <>
    pid: 31614, ppid: 31544, pgrp: 31614, sid: 31544
    utime: 0.000000, stime: 0.000000, cutime: 0.000000, cstime: 0.000000
    orig_eax: -1, fpvalid: 0
    ebx:     1219973108  ecx:     1243440144  edx:              1
    esi:              0  edi:              0  ebp:     0xbfecb828
    eax:          74565  eip:     0x080483c4  eflags:  0x00010286
    esp:     0xbfecb818
    ds: 0x007b  es: 0x007b  fs: 0x0000  gs: 0x0033  cs: 0x0073  ss: 0x007b
  CORE                 124  PRPSINFO
    state: 0, sname: R, zomb: 0, nice: 0, flag: 0x00400400
    uid: 1000, gid: 1000, pid: 31614, ppid: 31544, pgrp: 31614, sid: 31544
    fname: coredump, psargs: ./coredump 
  CORE                 160  AUXV
    SYSINFO: 0xb774f414
    SYSINFO_EHDR: 0xb774f000
    HWCAP: 0xafe8fbff  <fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov clflush dts acpi mmx fxsr sse sse2 ss tm pbe>
    PAGESZ: 4096
    CLKTCK: 100
    PHDR: 0x8048034
    PHENT: 32
    PHNUM: 8
    BASE: 0
    FLAGS: 0
    ENTRY: 0x8048300
    UID: 1000
    EUID: 1000
    GID: 1000
    EGID: 1000
    SECURE: 0
    RANDOM: 0xbfecba1b
    EXECFN: 0xbfecdff1
    PLATFORM: 0xbfecba2b
    NULL
  LINUX                 48  386_TLS
    index: 6, base: 0xb771c8d0, limit: 0x000fffff, flags: 0x00000051
    index: 7, base: 0x00000000, limit: 0x00000000, flags: 0x00000028
    index: 8, base: 0x00000000, limit: 0x00000000, flags: 0x00000028

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

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

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

من المخرجات الأخرى المتجه المساعد الحالي Auxiliary Vector أو AUXV اختصارًا. تصف 386_TLS مدخلات جدول الواصفات العام المستخدمة في تقديم Implementation معمارية x86 لمخزن محلي قائم على الخيوط thread-local storage. سيكون هناك مدخلات مكررة لكل خيط قيد التشغيل بالنسبة للتطبيق متعدد الخيوط، حيث سيفهم منقح الأخطاء ذلك وهذه هي الطريقة التي يطبّق بها منقح الأخطاء gdb الأمر thread لإظهار الخيوط والتبديل بينها.

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

إنشاء أقسام مخصصة

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

يمكن لأداة modinfo فحص هذه المعلومات في وحدةٍ ما وتقديمها للمستخدم. يوضح المثال التالي استخدام مثال عن وحدة نواة لينكس fuse التي تسمح لمكتبات مساحة المستخدم بتوفير تقديمات نظام الملفات للنواة:

$ cd /lib/modules/$(uname -r)

$ sudo modinfo ./kernel/fs/fuse/fuse.ko 
filename:       /lib/modules/3.2.0-4-amd64/./kernel/fs/fuse/fuse.ko
alias:          devname:fuse
alias:          char-major-10-229
license:        GPL
description:    Filesystem in Userspace
author:         Miklos Szeredi <miklos@szeredi.hu>
depends:        
intree:         Y
vermagic:       3.2.0-4-amd64 SMP mod_unload modversions 
parm:           max_user_bgreq:Global limit for the maximum number of backgrounded requests an unprivileged user can set (uint)
parm:           max_user_congthresh:Global limit for the maximum congestion threshold an unprivileged user can set (uint)

$ objdump -s -j .modinfo ./kernel/fs/fuse/fuse.ko 

./kernel/fs/fuse/fuse.ko:     file format elf64-x86-64

Contents of section .modinfo:
 0000 616c6961 733d6465 766e616d 653a6675  alias=devname:fu
 0010 73650061 6c696173 3d636861 722d6d61  se.alias=char-ma
 0020 6a6f722d 31302d32 32390070 61726d3d  jor-10-229.parm=
 0030 6d61785f 75736572 5f636f6e 67746872  max_user_congthr
 0040 6573683a 476c6f62 616c206c 696d6974  esh:Global limit
 0050 20666f72 20746865 206d6178 696d756d   for the maximum
 0060 20636f6e 67657374 696f6e20 74687265   congestion thre
 0070 73686f6c 6420616e 20756e70 72697669  shold an unprivi
 0080 6c656765 64207573 65722063 616e2073  leged user can s
 0090 65740070 61726d74 7970653d 6d61785f  et.parmtype=max_
 00a0 75736572 5f636f6e 67746872 6573683a  user_congthresh:
 00b0 75696e74 00706172 6d3d6d61 785f7573  uint.parm=max_us
 00c0 65725f62 67726571 3a476c6f 62616c20  er_bgreq:Global 
 00d0 6c696d69 7420666f 72207468 65206d61  limit for the ma
 00e0 78696d75 6d206e75 6d626572 206f6620  ximum number of 
 00f0 6261636b 67726f75 6e646564 20726571  backgrounded req
 0100 75657374 7320616e 20756e70 72697669  uests an unprivi
 0110 6c656765 64207573 65722063 616e2073  leged user can s
 0120 65740070 61726d74 7970653d 6d61785f  et.parmtype=max_
 0130 75736572 5f626772 65713a75 696e7400  user_bgreq:uint.
 0140 6c696365 6e73653d 47504c00 64657363  license=GPL.desc
 0150 72697074 696f6e3d 46696c65 73797374  ription=Filesyst
 0160 656d2069 6e205573 65727370 61636500  em in Userspace.
 0170 61757468 6f723d4d 696b6c6f 7320537a  author=Miklos Sz
 0180 65726564 69203c6d 696b6c6f 7340737a  eredi <miklos@sz
 0190 65726564 692e6875 3e000000 00000000  eredi.hu>.......
 01a0 64657065 6e64733d 00696e74 7265653d  depends=.intree=
 01b0 59007665 726d6167 69633d33 2e322e30  Y.vermagic=3.2.0
 01c0 2d342d61 6d643634 20534d50 206d6f64  -4-amd64 SMP mod
 01d0 5f756e6c 6f616420 6d6f6476 65727369  _unload modversi
 01e0 6f6e7320 00                          ons .     

تحلّل الأداة modinfo القسم ‎.modinfo المُضمَّن في ملف الوحدة لتقديم تفاصيلها. يوضح المثال التالي كيفية وضع حقل "المؤلف Author" في الوحدة، حيث تأتي هذه الشيفرة البرمجية غالبًا من include/linux/module.h:

/*
 * ابدأ من الأسفل ثم انتقل إلى الأعلى
 */

 ‫/* وحدات الماكرو غير المباشرة مطلوبة للصق الوسيط المُوسَّع مثل الماكرو‫ __LINE__ */
#define ___PASTE(a,b) a##b
#define __PASTE(a,b) ___PASTE(a,b)


#define __UNIQUE_ID(prefix) __PASTE(__PASTE(__UNIQUE_ID_, prefix), __COUNTER__)

/* تحويل الماكرو غير المباشر إلى سلسلة نصية. يسمح إنشاء مستويين للمعامل بأن يكون ماكرو بحد ذاته، حيث يتحوّل‫ __stringify(FOO)‎ مثلًا إلى السلسلة "bar" عند التصريف باستخدام ‎-DFOO=bar.  */

#define __stringify_1(x...)     #x
#define __stringify(x...)       __stringify_1(x)

#define __MODULE_INFO(tag, name, info)                                    \
static const char __UNIQUE_ID(name)[]                                     \
  __used __attribute__((section(".modinfo"), unused, aligned(1)))         \
  = __stringify(tag) "=" info

‫/* معلومات عامة للصيغة‫ tag = "info"‎ */
#define MODULE_INFO(tag, info) __MODULE_INFO(tag, tag, info)

/*
* ‫استخدم للمؤلفين المتعددين الذين يستخدمون "Name <email>‎" أو "Name" فقط 
‫تعليمات أو سطور MODULE_AUTHOR()‎ متعددة *  
*/
#define MODULE_AUTHOR(_author) MODULE_INFO(author, _author)

/* ---- */

MODULE_AUTHOR("Your Name <your@name.com>");

لنبدأ من الجزء السفلي حيث نرى أن الوحدة MODULE_AUTHOR تغلّف الماكرو الأعم ‎__MODULE_INFO، ويمكننا أن نرى أننا نبني متغيرًا من النوع static const char []‎ ليحتوي على السلسلة النصية "author=Your Name <your@name.com>‎". لاحظ أن المتغير لديه معامل إضافي ‎__attribute__((section(".modinfo")))‎ يخبر المصرّف بعدم وضع هذا المتغير في قسم البيانات data مع المتغيرات الأخرى، ولكن يمكن إخفاؤه في قسم ELF الخاص به الذي اسمه ‎.modinfo. توقِف المعاملات الأخرى المتغير الذي يجري تحسينه لأنه يبدو غير مُستخدَم وللتأكد من أننا نضع المتغيرات بجانب بعضها بعضًا من خلال تحديد المحاذاة.

هناك استخدام واسع النطاق لتحويل وحدات الماكرو إلى سلاسل نصية Stringification Macros، وهي حيل تُستخدَم في معالج لغة C المسبق لضمان أن السلاسل النصية والتعاريف يمكن أن تكون مع بعضها البعض. يوفّر المصرِّف gcc التعريف الخاص __COUNTER__ الذي يوفر قيمة فريدة ومتزايدة في كل استدعاء، مما يسمح باستدعاءات وحدة MODULE_AUTHOR متعددة في ملف واحد دون استخدام اسم المتغير نفسه.

يمكننا فحص الرموز الموضوعة في الوحدة النهائية لمعرفة النتيجة النهائية كما يلي:

$ objdump --syms ./fuse.ko | grep modinfo

0000000000000000 l    d  .modinfo    0000000000000000 .modinfo
0000000000000000 l     O .modinfo    0000000000000013 __UNIQUE_ID_alias1
0000000000000013 l     O .modinfo    0000000000000018 __UNIQUE_ID_alias0
000000000000002b l     O .modinfo    0000000000000011 __UNIQUE_ID_alias8
000000000000003c l     O .modinfo    000000000000000e __UNIQUE_ID_alias7
000000000000004a l     O .modinfo    0000000000000068 __UNIQUE_ID_max_user_congthresh6
00000000000000b2 l     O .modinfo    0000000000000022 __UNIQUE_ID_max_user_congthreshtype5
00000000000000d4 l     O .modinfo    000000000000006e __UNIQUE_ID_max_user_bgreq4
0000000000000142 l     O .modinfo    000000000000001d __UNIQUE_ID_max_user_bgreqtype3
000000000000015f l     O .modinfo    000000000000000c __UNIQUE_ID_license2
000000000000016b l     O .modinfo    0000000000000024 __UNIQUE_ID_description1
000000000000018f l     O .modinfo    000000000000002a __UNIQUE_ID_author0
00000000000001b9 l     O .modinfo    0000000000000011 __UNIQUE_ID_alias0
00000000000001d0 l     O .modinfo    0000000000000009 __module_depends
00000000000001d9 l     O .modinfo    0000000000000009 __UNIQUE_ID_intree1
00000000000001e2 l     O .modinfo    000000000000002f __UNIQUE_ID_vermagic0

سكربتات الرابط Linker Scripts

تتمثل وظيفة الرابط في بناء الأقسام Sections لتشكيل المقاطع Segments من خلال استخدام سكربت الرابط الذي يصِف مكان بدء المقاطع والأقسام الموجودة فيها ويحدد المعاملات الأخرى.

يوضح المثال الآتي مقتطفًا من سكربت الرابط الافتراضي الذي سيعرضه الرابط عند إعطاء الراية التفصيلية Verbose باستخدام الرايتين ‎-Wl و‎--verbose مع gcc. السكربت الافتراضي مُضمَّنٌ في الرابط ويعتمد على تعريفات واجهة API المعيارية لإنشاء برامج عاملة لمساحة مستخدمٍ خاصة بمنصة البناء.

$ gcc -Wl,--verbose -o test test.c
GNU ld (GNU Binutils for Debian) 2.26
...
using internal linker script:
==================================================
OUTPUT_FORMAT("elf64-x86-64", "elf64-x86-64",
          "elf64-x86-64")
OUTPUT_ARCH(i386:x86-64)
ENTRY(_start)
SEARCH_DIR("=/usr/local/lib/x86_64-linux-gnu"); ...
SECTIONS
{
  ‫/* أقسام للقراءة فقط مُدمَجة في مقطع النص‫ text segment: */
  PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x400000)); . = SEGMENT_START("text-segment", 0x400000) + SIZEOF_HEADERS;
  .interp         : { *(.interp) }
  .note.gnu.build-id : { *(.note.gnu.build-id) }
  .hash           : { *(.hash) }
  .gnu.hash       : { *(.gnu.hash) }
  .dynsym         : { *(.dynsym) }
  .dynstr         : { *(.dynstr) }
  .gnu.version    : { *(.gnu.version) }
  .gnu.version_d  : { *(.gnu.version_d) }
  .gnu.version_r  : { *(.gnu.version_r) }
  .rela.dyn       :
    {
    ...
    }
  PROVIDE (etext = .);
  .rodata         : { *(.rodata .rodata.* .gnu.linkonce.r.*) }
  .rodata1        : { *(.rodata1) }
...

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

ترجمة -وبتصرُّف- للقسمين ELF Executables و Extending ELF concepts من فصل Behind the process من كتاب 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.


×
×
  • أضف...