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

كيف تنشئ ملفا قابلا للتنفيذ Executable File من شيفرة برمجية مصدرية


Ola Abbas

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

البرامج المُصرَّفة Compiled Programs والبرامج المُفسَّرة Interpreted programs

يجب أن يكون البرنامج الذي يمكن تحميله مباشرةً في الذاكرة بصيغة ثنائية binary format، حيث تُسمَّى عملية تحويل الشيفرة البرمجية المكتوبة بلغةٍ مثل لغة C إلى ملف ثنائي جاهز للتنفيذ بعملية التصريف التي تُطبَّق باستخدام مصرِّف Compiler، والمثال الأكثر انتشارًا هو المصرِّف gcc.

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

تعمل البرامج المفسَّرة عادةً بصورة أبطأ من نظيرتها المُصرَّفة، حيث يمكن مصادفة حِمل البرنامج في قراءة وتفسير الشيفرة البرمجية مرةً واحدة فقط في البرامج المُصرَّفة، بينما يصادف البرنامج المفسَّر هذا الحِمل في كل مرة يُشغَّل فيها. لكن تمتلك اللغات المفسَّرة العديد من الجوانب الإيجابية، حيث تعمل العديد من اللغات المفسرة فعليًا في آلة افتراضية virtual machine مُجرَّدة من العتاد الأساسي. تُعَد لغتا بايثون Python و Perl 6 من اللغات التي تستخدم آلةً افتراضية تفسّر الشيفرة البرمجية.

الآلات الافتراضية Virtual Machines

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

تستخدم لغة جافا Java مثلًا نهجًا هجينًا يجمع بين التصريف والتفسير، فهي لغة مُصرَّفة جزئيًا ومُفسَّرة جزئيًا. تُصرَّف شيفرة جافا في برنامج يعمل ضمن آلة جافا الافتراضية Java Virtual Machine أو يشار إليها JVM اختصارًا، وبالتالي يمكن تشغيل البرنامج المُصرَّف على أيّ عتاد يحتوي على آلة JVM خاصة به، أي يمكنك أن تكتب شيفرتك البرمجية مرة واحدة وتشغّلها في أيّ مكان.

بناء ملف قابل للتنفيذ

هناك ثلاث خطوات منفصلة تتضمنها عملية إنشاء ملف قابل للتنفيذ عندما نتحدث عن المُصرِّفات وهذه الخطوات هي:

  1. التصريف Compiling
  2. التجميع Assembling
  3. الربط Linking

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

التصريف Compiling

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

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

هناك خطوة قبل تحليل الشيفرة البرمجية في الشيفرة المكتوبة بلغة C، حيث تسمَّى هذه الخطوة بالمعالجة المُسبَقة أو التمهيدية يقوم بها المعالج المسبق Pre-processor، وهو عبارة عن برنامج لاستبدال النصوص، حيث يُستبدَل مثلًا المتغير variable المُصرَّح عنه بالشكل ‎#define variable text بالنص text، ثم تُمرَّر هذه الشيفرة البرمجية المعالَجة مسبقًا إلى المصرِّف.

الصياغة

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

تُوصَف الصياغة في صيغة باكوس نور Backus-Naur Form -أو BNF اختصارًا- في أغلب الأحيان، وهي لغة يمكنك من خلالها وصف اللغات، والشكل الأكثر شيوعًا منها هو صيغة باكور نور الموسَّعة Extended Backus-Naur Form -أو EBNF اختصارًا- التي تسمح ببعض القواعد الإضافية الأكثر ملاءمة للغات الحديثة.

توليد شيفرة التجميع Assembly Generation

وظيفة المصرِّف هي ترجمة لغة عالية المستوى higher level language إلى شيفرة تجميع مناسبة للهدف من التصريف، فلكل معمارية مجموعة تعليمات مختلفة وأعداد مختلفة من المسجلات وقواعد مختلفة للتشغيل الصحيح.

المحاذاة Alignment

01_alignment.png

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

لا تستطيع وحدات المعالجة المركزية CPU تحميل قيمة في المسجل من موقع ذاكرة عشوائي، إذ يتطلب ذلك أن تحاذي المتغيرات حدودًا معينة. يمكننا أن نرى في الشكل السابق كيفية تحميل قيمة 32 بتًا (4 بايتات) في مسجل على آلة تتطلب محاذاة بمقدار 4 بايتات للمتغيرات.

يمكن تحميل المتغير الأول في المسجل مباشرةً، حيث يقع بين حدود 4 بايتات، ولكن يجتاز المتغير الثاني حدود 4 بايتات، مما يعني أنه ستكون هناك حاجة إلى عمليتي تحميل على الأقل للحصول على المتغير في مسجل واحد إحداهما للنصف السفلي أولًا ثم النصف العلوي.

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

حاشية البنية Structure Padding

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

إليك مثال عن حاشية بنية Struct Padding:

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

struct a_struct {
  char char_one;
  char char_two;
  int int_one;
};

int main(void)
{

  struct a_struct s;

  printf("%p : s.char_one\n" \
    "%p : s.char_two\n" \
    "%p : s.int_one\n", &s.char_one,
    &s.char_two, &s.int_one);

  return 0;

}

$ gcc -o struct struct.c

$ gcc -fpack-struct -o struct-packed struct.c

$ ./struct
0x7fdf6798 : s.char_one
0x7fdf6799 : s.char_two
0x7fdf679c : s.int_one

$ ./struct-packed
0x7fcd2778 : s.char_one
0x7fcd2779 : s.char_two
0x7fcd277a : s.int_one

أنشأنا في المثال السابق بنية تحتوي على بايتين من النوع char متبوعين بعدد صحيح بحجم 4 بايتات من النوع int. يضيف المصرِّف حاشية للبنية البنية كما يلي:

02_padding.png

نوجّه في المثال السابق المصرِّف إلى عدم حشو البنى، وبالتالي يمكننا أن نرى أن العدد الصحيح يبدأ مباشرةً بعد قيمتين من النوع char.

محاذاة خط الذاكرة المخبئية Cache line alignment

تحدثنا سابقًا عن استخدام الأسماء البديلة في الذاكرة المخبئية، وكيف يمكن ربط عدة عناوين مع سطر الذاكرة المخبئية نفسه. يجب أن يتأكد المبرمجون من أنهم لا يتسببون في ارتداد Bouncing في خطوط الذاكرة المخبئية عندما يكتبون برامجهم.

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

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

المقايضة بين المساحة والسرعة

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

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

وضع الافتراضات

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

إليك مثال عن محاذاة المكدس Stack Alignment:

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

struct a_struct {
  int a;
  int b;
};

int main(void)
{
  int i;
  struct a_struct s;
  printf("%p\n%p\ndiff %ld\n", &i, &s, (unsigned long)&s - (unsigned long)&i);
  return 0;
}
$ gcc-3.3 -Wall -o stack-3.3 ./stack.c
$ gcc-4.0 -o stack-4.0 stack.c

$ ./stack-3.3
0x60000fffffc2b510
0x60000fffffc2b520
diff 16

$ ./stack-4.0
0x60000fffff89b520
0x60000fffff89b524
diff  4

يمكننا أن نرى في المثال السابق المأخوذ من آلة إيتانيوم Itanium أن حاشية ومحاذاة المكدس تغيرت بصورة كبيرة بين إصدارات المصرِّف gcc، وهذا أمر متوقع ويجب على المبرمج مراعاته. كما يجب عليك التأكد من عدم وضع افتراضات حول حجم الأنواع أو قواعد المحاذاة.

مفاهيم لغة C الخاصة بالمحاذاة

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

يمكننا أخذ بعض الأمثلة من نواة لينكس Linux kernel التي يتعين عليها في أغلب الأحيان التعامل مع محاذاة صفحات الذاكرة ضمن النظام. إليك مثال عن التعامل مع محاذاة الصفحات:

[ include/asm-ia64/page.h ]

/*
 * ‫يحدّد PAGE_SHIFT حجم صفحة النواة الفعلي
 */
#if defined(CONFIG_IA64_PAGE_SIZE_4KB)
# define PAGE_SHIFT     12
#elif defined(CONFIG_IA64_PAGE_SIZE_8KB)
# define PAGE_SHIFT     13
#elif defined(CONFIG_IA64_PAGE_SIZE_16KB)
# define PAGE_SHIFT     14
#elif defined(CONFIG_IA64_PAGE_SIZE_64KB)
# define PAGE_SHIFT     16
#else
# error Unsupported page size!
#endif

#define PAGE_SIZE               (__IA64_UL_CONST(1) << PAGE_SHIFT)
#define PAGE_MASK               (~(PAGE_SIZE - 1))
#define PAGE_ALIGN(addr)        (((addr) + PAGE_SIZE - 1) & PAGE_MASK)

يمكننا أن نرى في المثال السابق أن هناك عددًا من الخيارات المختلفة لأحجام الصفحات داخل النواة التي تتراوح من 4 كيلوبايتات إلى 64 كيلوبايت. يَُعد الماكرو PAGE_SIZE واضحًا إلى حد ما، فهو يعطي حجم الصفحة الحالي المحدّد ضمن النظام عن طريق انزياح قيمته 1 باستخدام رقم الانزياح المُعطَى، ويعادل ذلك 2n حيث n هو انزياح الصفحة PAGE_SHIFT.

لدينا بعد ذلك تعريف قناع الصفحة PAGE_MASK الذي يسمح لنا بالعثور على تلك البتات الموجودة في الصفحة الحالية فقط، أي إزاحة offset العنوان في صفحته.

التحسين Optimisation

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

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

  • فك الحلقات Unrolling Loops: إذا احتوت الشيفرة البرمجية على حلقة مثل حلقة for أو while وكان لدى المُصرِّف فكرة عن عدد المرات التي ستنفّذ فيها، فسيكون فك الحلقة أكثر فاعلية بحيث تُنفَّذ تسلسليًا، إذ تُكرَّر شيفرة الحلقة الداخلية لتنفيذها عدد المرات ذاك أخرى بدلًا من تنفيذ الجزء الداخلي من الحلقة ثم العودة إلى البداية لتكرار العملية.
    تزيد هذه العملية من حجم الشيفرة البرمجية، إذ يمكن أن تسمح للمعالج بتنفيذ التعليمات بفعالية، حيث يمكن أن تتسبب الفروع في تقليل كفاءة خط أنابيب التعليمات الواردة إلى المعالج.

  • الدوال المضمنة Inlining Functions: يمكن وضع دوال مُضمَّنة لاستدعائها ضمن المستدعي callee، ويمكن للمبرمج تحديد ذلك للمصرِّف من خلال وضع الكلمة inline في تعريف الدالة، ويمكنك مقايضة حجم الشيفرة البرمجية بتسلسل تنفيذها من خلال ذلك.

  • توقع الفرع Branch Prediction: إذا صادف الحاسوب تعليمة if، فهناك نتيجتان محتملتان إما صحيحة أو خاطئة. يريد المعالج الاحتفاظ بأنابيبه الواردة ممتلئة قدر الإمكان، لذا لا يمكنه انتظار نتيجة الاختبار قبل وضع الشيفرة البرمجية في خط الأنابيب، وبالتالي يمكن للمصرِّف أن يتنبأ بالطريقة التي يُحتمَل أن يسير بها الاختبار. هناك بعض القواعد البسيطة التي يمكن أن يستخدمها المصرِّف لتخمين هذه الأمور، فمثلًا لا يُحتمَل أن تكون التعليمة if (val == -1)‎ صحيحةً، لأن القيمة ‎-1 تشير عادةً إلى رمز خطأ ونأمل ألّا تُشغَّل هذه التعليمة كثيرًا.

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

المجمع Assembler

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

المجمِّع assembly هو عملية آلية لتحويل شيفرة التجميع إلى صيغة ثنائية. يحتفظ المجمّع بجدول كبير لكل تعليمة ممكنة ولنظيرها الثنائي الذي يسمى شيفرة العملية Op Code. يدمج المجمّع شيفرات العمليات مع المسجلات المحدَّدة في شيفرة التجميع لإنتاج ملف ثنائي بوصفه خرجًا.

يُطلق على هذه الشيفرة بشيفرة التعليمات المُصرَّفة Object Code، وهي شيفرة غير قابلة للتنفيذ في هذه المرحلة، وتُعد مجرد تمثيل ثنائي للدخل الذي يمثل شيفرة برمجية مصدرية. يُفضَّل ألّا يضع المبرمج الشيفرة المصدرية بأكملها في ملفٍ واحد.

الرابط Linker

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

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

سنشرح فيما يلي بعض المصطلحات الأساسية لفهم عملية الربط.

الرموز Symbols

لجميع المتغيرات والدوال أسماء في الشيفرة المصدرية، إذ نشير إليها باستخدام هذه الأسماء. تتمثل إحدى طرق التفكير في تعليمة التصريح عن متغير int a في أنك تخبر المصرِّف بأن يحجز حيزًا من الذاكرة بحجم sizeof(int)‎، وبالتالي كلما استخدمت اسم المتغير a، فسيشير إلى هذه الذاكرة المخصَّصة، وكذلك الأمر بالنسبة للدالة التي تخبر المصرِّف بأن يحزّن هذه الشيفرة البرمجية في الذاكرة، ثم ينتقل إليها وينفّذها عند استدعاء الدالة function()‎. وبالتالي نستدعي الرمزين a و function لأنهما يُعَدان تمثيلًا رمزيًا لمنطقةٍ من الذاكرة.

تساعد هذه الرموز البشر على فهم البرمجة. لكن يمكنك القول أن المهمة الأساسية لعملية التصريف هي إزالة هذه الرموز، إذ لا يعرف المعالج ما يمثله الرمز a، فكل ما يعرفه هو أن لديه بعض البيانات في عنوان ذاكرة معين. تحوِّل عملية التصريف التعليمة a += 2 إلى العبارة "زيادة القيمة الموجودة في العنوان 0xABCDE من الذاكرة بمقدار 2".

اقتباس

إمكانية رؤية الرموز Symbol Visibility: لا بد أنك شاهدت في البرامج المكتوبة بلغة C المصطلحين static وextern مع المتغيرات، إذ يمكن أن تؤثر هذه المعدِّلات Modifiers على ما نسميه إمكانية رؤية الرموز.

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

تخبر الكلمة extern المصرِّف أنه لا ينبغي تخصيص أي مساحة في الذاكرة لهذا المتغير، ويجب ترك هذا الرمز في التعليمات المُصرَّفة لإصلاحه لاحقًا. لا يمكن للمصرِّف أن يعرف مكان تعريف الرمز فعليًا ولكن الرابط Linker يمكنه ذلك، فوظيفته هي النظر في جميع ملفات التعليمات المُصرَّفة ودمجها في ملف واحد قابل للتنفيذ. لذا سيرى الرابط هذا الرمز في الملف الثاني، وسيقول: "رأيت هذا الرمز مسبقًا في الملف 1، وأعلم أنه يشير إلى موقع الذاكرة 0x12345"، وبالتالي يمكن تعديل قيمة الرمز لتكون قيمة الذاكرة للمتغير الموجود في الملف الأول.

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

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

عملية الربط

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

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

تعرّفنا في هذا المقال على الخطوات الثلاث لبناء ملف قابل للتنفيذ هي: التصريف Compiling والتجميع Assembling والربط Linking، وسنطبّق في المقال التالي هذه الخطوات عمليًا لبناء ملف قابل للتنفيذ.

ترجمة -وبتصرُّف- للأقسام:

من فصل The Toolchain من كتاب 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.


×
×
  • أضف...