بسم الله الرحمن الرحيم.
بعد أن أخذنا نظرة عامة عن هيكلية الحاسوب في المقال الأول، وتعمقنا في فهم العمليات التي تجري داخل المعالج في المقال الثاني، سنتعرف اليوم على الأساس النظري الذي يربط التعليمات الثنائية بالعمليات التي ينفذها المعالج. هذا المقال هو أيضاً مقدمة عملية للغة Assembly؛ إذ يشرح بنية الكود الخاص بها. وهذه المواضيع التي سنغطيها:
بنية مجموعة التعليمات (Instruction Set Architecture)
بنية مجموعة التعليمات (ISA) هي الأساس النظري الذي يربط بين الـ Machine Code والتعليمات التي ينفذها الـ CPU:
شكل (1): موقع الـ ISA بين البرمجيات والعتاد
وتحدد بنية ISA: التعليمات المدعومة من المعالج، وأنواع البيانات (Data Types)، والسجلات (Registers)، وطريقة الوصول إلى الذاكرة (Memory Access)، ونماذج العنونة (Addressing Modes)، وغيرها من التفاصيل الأساسية.
بمعنى آخر: الـ ISA هي لغة التواصل بين البرمجيات Software والعتاد Hardware — دون الدخول في التفاصيل الدقيقة للتصميم الداخلي (Microarchitecture).
خصائص تعليمات الآلة (Machine Instruction Characteristics)
تتكوّن تعليمة الآلة من جزأين:
• رمز العملية (Opcode):
الذي يحدد نوع الإجراء المراد تنفيذه، أي الأمر نفسه (مثل mov، add… إلخ).
• المعاملات (Operands): التي تشير إلى البيانات المستهدفة التي سيطرأ عليها الإجراء. وتخزن كل تعليمة في الذاكرة كسلسلة من البايتات.
معلومات أساسية
صيغة التعليمة وطولها: كما ذكرنا، تتكون كل تعليمة من Opcode وOperands، وفي معمارية x86 يختلف طول التعليمات من 1 إلى 15 بايتاً.
عدد المعاملات: تسمح تعليمات x86 بوجود صفر إلى ثلاثة معاملات، ومعظم Opcodes ذات معاملين: المصدر (Source) والوجهة (Destination). وفي صيغة Intel تأتي الوجهة أولاً ثم المصدر. على سبيل المثال:
mov eax, ebx
تحتوي على معاملين: المسجل eax وهو الوجهة (Destination) التي ستستقبل القيمة، والمسجل ebx وهو المصدر (Source) الذي ستُقرأ منه القيمة.
ترتيب البايتات (Endianness):
تستخدم معمارية x86 الترتيب الصغير (Little Endian)، أي أن البايت الأقل أهمية يخزن أولاً في الذاكرة؛ فالقيمة 0x12345678 تخزن في الذاكرة بالترتيب: 78 56 34 12.
مثال:
mov eax, [a] ; نقل قيمة المتغير a إلى المسجل EAX
add eax, [b] ; أصبحت قيمة a في EAX فأضفنا إليها قيمة المتغير b
mov [sum], eax ; نقل القيمة الناتجة من EAX إلى المتغير sum
أنواع المعاملات (Types of Operands)
المعامل في الـ Machine Code هو ما يشير إلى البيانات الجاري معالجتها، ويمكن أن يكون أحد الأنواع التالية:
1. قيمة ثابتة (Immediate): قيمة عددية مباشرة مضمنة في التعليمة نفسها. مثل:
mov ebx, 5 ; نقل العدد الثابت 5 إلى المسجل EBX
2. مسجل (Register): قد يكون المعامل مسجلاً من وحدة المعالجة. مثل:
mov eax, ebx ; نقل محتوى السجل EBX إلى السجل EAX
3. موقع في الذاكرة (Memory Address): قد يكون المعامل عنواناً في الذاكرة. مثل:
mov eax, [ebx] ; نقل القيمة المخزنة في العنوان المحفوظ في EBX إلى EAX
مثال جامع:
mov eax, [a] ; هنا a (ذاكرة) و eax (مسجل)
add eax, [b] ; b (ذاكرة)
mov [sum], eax ; sum (ذاكرة)
أنواع البيانات في معمارية x86 (x86 Data Types)
يدعم معالج x86 أنواعاً مختلفة من البيانات، أهمها:
- Byte: الوحدة الأساسية للبيانات، وتساوي 8 بتات.
- Word: 16 بتاً.
- DoubleWord: 32 بتاً.
- QuadWord: 64 بتاً.
مثال: لنفرض أن لدينا هذا الكود بلغة C:
char c = 'A';
short s = 1000;
int i = 100000;
فهذه هي طريقة التعامل مع أحجامها في Assembly:
mov al, 'A' ; المسجل AL هو قسم من AX بحجم 8 بتات
mov ax, 1000 ; المسجل AX هو نصف المسجل EAX بحجم 16 بتاً
mov eax, 100000 ; المسجل EAX كاملاً بحجم 32 بتاً
أنواع العمليات (Types of Operations)
بشكل عام هناك عمليات رئيسية يتألف منها أي كود تقريباً — يمكن القول إن 90% من أي برنامج يتكون من هذه الـ Opcodes:
• العمليات الحسابية والمنطقية (Arithmetic & Logic):
وتشمل تعليمات الجمع add والطرح sub والضرب mul والقسمة div، والعمليات المنطقية not وand وor وxor. تستخدم هذه التعليمات لتعديل القيم في المسجلات أو الذاكرة.
• نقل البيانات (Data Transfer):
تعليمات لنقل البيانات بين المسجلات والذاكرة أو المداخل والمخارج. أشهرها أمر mov الذي ينقل قيمة من المصدر (مسجل أو ذاكرة أو قيمة ثابتة) إلى الوجهة (مسجل أو ذاكرة). ولدينا أيضاً push وpop أوامر التعديل على الستاك.
• التحكم بمسار التنفيذ (Control Flow):
تعليمات التحكم بسير البرنامج: القفز غير المشروط jmp، والقفز المشروط بناءً على مقارنة مثل cmp مع je/jne/jle… إلخ، بالإضافة إلى تعليمات استدعاء الدوال call وret. وتعتمد هذه التعليمات على مقارنة القيم وتغيير قيمة مؤشر التعليمات (Instruction Pointer).
• الإدخال والإخراج (I/O):
في x86 توجد تعليمات خاصة للتعامل مع أجهزة الدخل والخرج مثل in وout، وهي تسمح بالقراءة والكتابة إلى المنافذ (Ports) الخاصة بالأجهزة.
مثال: لنفرض أن لدينا هذا الكود بلغة C:
int a = 10, b = 3, c; // تعريف المتغيرات a وb وc
if (a > b) c = a - b; else c = 0; // إذا كان a أكبر من b فإن c = a - b، وإلا فإن c = 0
فهذا تمثيله في Assembly:
mov eax, [a]
cmp eax, [b] ; مقارنة a مع b
jle L_else ; إذا كانت a <= b انتقل إلى فرع else
sub eax, [b] ; وإلا نفّذ: a = a - b
mov [c], eax
jmp L_end ; تجاوز فرع else بعد إتمام الفرع الشرطي
L_else:
mov dword [c], 0 ; حالة else: c = 0
L_end:
لاحظ كيف تحول شرط if إلى تعليمة cmp تضبط الأعلام، ثم قفزة مشروطة jle تفحص هذه الأعلام: إذا لم يتحقق الشرط (أي a <= b) يقفز التنفيذ إلى فرع else، وإذا تحقق يستمر تنفيذ الطرح ثم يقفز فوق فرع else بتعليمة jmp غير مشروطة.
الخلاصة
من خلال فهمك لبنية مجموعة التعليمات (ISA) — ومعرفة أن تعليمة الآلة تتكون من Opcode وOperands وأنواع كلٍّ منهما — تصبح قادراً على ترجمة الكود الثنائي إلى Assembly وفهم كيف يسير البرنامج خطوة بخطوة.
لا تنسونا من صالح دعائكم.