بسم الله الرحمن الرحيم.
بعد سلسلة من المقالات نكون قد انتهينا من أساسيات لغة C — ونتابع بإذن الله بسلسلة الـ Malware Development.
في هذا المقال سنتعرف على التوجيهات (Directives) الخاصة بالمعالج المسبق (Preprocessor) — وهي الحرف # المتبوع بكلمات محجوزة يفهمها الـ Preprocessor ويقوم باستبدالها قبل الترجمة.
نظرة على مراحل الـ Compilation (التي شرحناها في المقال الأول) — بالشكل المنفصل:
gcc -E file.c -o file.i # 1. المعالج المسبق Preprocessor gcc -S file.i -o file.s # 2. المترجم Compiler gcc -c file.s -o file.o # 3. المجمع Assembler gcc file.o -o file # 4. الرابط Linker
1. توجيهات التضمين (Inclusion Directives)
#include
يسمح بإعادة هيكلة البرنامج (Restructuring): تقسيم البرنامج الكبير إلى ملفات متعددة، كل ملف مسؤول عن وظيفة معينة:
• i/o.c — الإدخال والإخراج
• processing.c — المعالجة
• storing.c — التخزين
• retrieving.c — الاسترجاع
فإذا طلب العميل تعديل المعالجة فقط، تذهب للملف المعني دون المساس بالباقي.
ملاحظتان مهمتان:
- لا نستخدم المسار المطلق (Absolute Path) — مثل
#include "C:\\Users\\...\\Lab.h"— لأن أجهزة الـ Users تختلف، والجزء المعتمد على النظام يسبب أخطاء نقل.- الأفضل المسار النسبي (Relative Path) — ويُحسب حسب قواعد الـ Include Paths الخاصة بالمترجم.
2. توجيهات الماكرو (Macro Directives)
#define — تعريف الماكرو
ينقسم لنوعين:
Object-Like Macro (ماكرو يشبه الكائن)
مثال: معادلة مساحة الدائرة Area = πr². قيمة π (3.14) هي Magic Number — رقم غامض يصعب فهمه لمن يقرأ الكود. الحل: إعطاؤه اسماً:
#define PI 3.14
a = PI * r * r;
في مرحلة الـ Preprocessor يصبح:
a = 3.14 * r * r;
مثال عملي: برنامج يتحكم بمكيف السيارة حسب درجة الحرارة والموقع الجغرافي:
#define SHAM_HOT 35 // بلاد الشام: فوق 35 درجة
#define RUSSIA_HOT 15 // روسيا: فوق 15 شوب
الفوائد:
- قابلة للتعديل بسهولة في أي وقت.
- قابلية قراءة عالية للكود.
- لا Magic Numbers.
- لا تستهلك ذاكرة — لأن الاستبدال يحدث في مرحلة الـ Preprocessor.
أسلوبياً:
#defineتُكتب بحروف كبيرة للتمييز عن المتغيرات.
Function-Like Macro (ماكرو يشبه الدالة)
الدوال العادية في الـ assembly تمر عبر بناء إطار المكدس، وحفظ return address، واستدعاء call — وتستهلك وقت معالجة. للدوال الصغيرة (مثل الجمع) نستخدم ماكرو:
#define Add(x, y) ((x) + (y))
int main(void) {
int z = Add(3, 4); // بعد الـ preprocessor: z = 3 + 4
return 0;
}
مثال أكثر تعقيداً:
#include <stdio.h>
#define MIN(a, b) ((a) < (b) ? (a) : (b))
int main(void) {
printf("MIN between 10 and 20 is: %d\n", MIN(10, 20));
return 0;
}
لاحظ الأقواس الإضافية
((a) + (b))— تمنع أخطاء الأولوية عند الاستدعاء (شرحنا أولوية العمليات في مقال العوامل البتية).
الماكرو متعدد الأسطر (Multiline)
لمواصلة ماكرو لسطر جديد، نضع باك سلاش \ كآخر حرف في السطر (يتبعه سطر جديد مباشرة):
#define print() printf("not");\
printf("bit")
كلا السطرين جزء من الماكرو.
الماكرو المدمجة (Predefined Macros)
تعرّفها اللغة تلقائياً (في GCC/Clang):
| الماكرو | الوظيفة |
|---|---|
__DATE__ | تاريخ الترجمة الحالي |
__TIME__ | وقت الترجمة الحالي |
__FILE__ | اسم الملف الحالي |
__LINE__ | رقم السطر الحالي |
انتبه: أسماؤها برمزين سفليين في البداية والنهاية (
__LINE__وليس_LINE_).
#undef — إلغاء التعريف
عند الحاجة لإزالة ماكرو معين من نقطة معينة فصاعداً:
#define PI 3.14
{
// كود يستخدم PI
}
#undef PI // من هنا فصاعداً PI غير معرف
3. التوجيهات الشرطية (Conditional Directives)
#ifdef / #else / #endif
تشبه الشروط العادية، لكنها تُحل وقت المعالجة ويجب إنهاؤها بـ #endif:
#include <stdio.h>
#define NOINPUT
int main(void) {
int a = 0;
int z = 5;
#ifdef NOINPUT // إذا كان NOINPUT معرّفاً
a = z; // استخدم القيمة مباشرة
#else
printf("Enter a: ");
scanf("%d", &a);
#endif
printf("value of a: %d\n", a);
return 0;
}
#ifndef — منع التضمين المتعدد
إذا لم يكن المعرف معرّفاً نفذ — الاستخدام الأساسي: حماية التضمين المتكرر (Include Guard):
#ifndef HEADER_FILE
#define HEADER_FILE
// محتويات الملف
#endif
4. توجيهات متفرقة (Miscellaneous Directives)
#error
يوقف الترجمة بخطأ في مرحلة الـ Preprocessing:
#ifndef _MATH_H
#error "define MATH lib then compile"
#endif
تظهر الرسالة المجاورة للـ #error.
#warning
يخرج تحذيراً أو ملاحظة دون إيقاف الترجمة:
#warning "This is a warning"
#pragma — توجيهات المترجم
تخاطب المترجم (Compiler) وليس الـ Preprocessor:
#pragma pack(push, 1) // (راجع قسم الـ RE أدناه)
تنبيه: في المترجمات القديمة (مثل Turbo C/Borland) كانت توجد
#pragma startup/exitلتغيير نقطة البداية — وهي خاصة بتلك المترجمات فقط ولا تعمل في GCC/Clang. في C القياسي، التنفيذ يبدأ دائماً منmain.
ربط الـ RE — لماذا الـ Preprocessor أساسي
1. #pragma pack(n) — الأهم للـ RE
يتحكم بمحاذاة الـ struct — يتجاوز الـ padding الافتراضي بمحاذاة محددة. ضروري عند قراءة ملفات أو بروتوكولات مخزنة بلا padding (مثل تنسيقات الملفات المخصصة):
#pragma pack(push, 1) // محاذاة بايت واحد — بلا padding
typedef struct {
u16 magic;
u32 size;
u8 flags;
} file_header;
#pragma pack(pop)
عند التحليل: رؤية struct حجمه غير مضاعف للـ 4 (مثل 9 بايت) تدل غالباً أن الكود استخدم pack(1).
2. الـ Macros تختفي في الـ Binary
ما تراه كـ MIN(a,b) في المصدر يظهر في الـ disassembly كـ ternary موسّع (cmp + jl/jge) — لا أثر لاسم الماكرو. هذا يفسر لماذا نرى كوداً مكرراً في الـ asm بدل استدعاءات دوال.
3. __DATE__ و __TIME__ في الـ Malware
عينات الـ malware تفحص وقت البناء وتقارنه بالوقت الحالي — تقنية شائعة لاكتشاف أجهزة الـ VM أو كشف تشغيل العينة بعد مدة طويلة. رؤيتها في الـ strings دليل على سلوك خبيث محتمل.
4. #ifdef والبناء الشرطي
تُستخدم لفصل البناءات (Debug/Release، Windows/Linux). في الـ binary الناتج ترى نسخة واحدة فقط — والمحلل يلاحظ غياب المسارات المعطلة ويستنتج خيارات التجميع المستخدمة.
خلاصة
الـ Preprocessor هو المسرح الذي تُحل فيه كل هذه الاختيارات قبل الترجمة — ومخرجه (الملف .i) هو ما يراه المترجم فعلياً. في الـ RE، المعرفة بما حلّ (macros، #ifdef، pack) تفسر لك أشكالاً غريبة في الـ disassembly — من تكرار الكود إلى أحجام structs غير المتوقعة.
إن شاء الله تكون هذه السلسلة قد جلبت ما هو مطلوب منها. إن أخطأت فمن نفسي والشيطان، وإن أصبت فمن الله. لا تنسونا من صالح دعائكم.