بسم الله الرحمن الرحيم.
في هذا المقال سنتحدث عن الـ Structure — أول أنواع البيانات المعرّفة من المستخدم.
أساسيات الـ Structure
عرفنا أن لغة C فيها أنواع بيانات أساسية (Standard Data Types): int, char, float, double…
ماذا لو أردنا التعبير عن كائن (Object) من الواقع — مثل الإنسان الذي يحمل اسماً و عمراً؟ نحتاج مزج String (مصفوفة أحرف) مع int في كائن واحد.
هنا يأتي الـ Structure: تشبه المصفوفة، لكنها تجمع متغيرات من أنماط متعددة (بينما المصفوفة من نمط واحد). وبما أن المطور من صنعها، تُصنف ضمن User Defined Data Types.
تعريف الـ Structure
struct structName {
dataType member1;
dataType member2;
// ...
dataType memberN;
};
الإعلان والتهيئة
struct human {
int age;
char name[10];
};
struct human h1, h2; // الإعلان عن المتغيرين
h1.age = 33; // التعبئة لاحقاً عضواً عضواً
struct human h3 = {33, "mj3s"}; // تهيئة عند التعريف (بترتيب تعريف الأعضاء)
الوصول إلى الأعضاء
هناك طريقتان رئيسيتان للوصول إلى أعضاء الـ struct:
1. عامل النقطة (Dot Operator .)
struct human h1;
h1.age = 36; // الوصول عبر الكائن مباشرة
2. عامل السهم (Arrow Operator ->)
يُستخدم مع المؤشر إلى struct:
struct human *ptr = &h1;
ptr->age = 36; // الوصول عبر المؤشر
ما يعادل السهم:
(*ptr).ageتعني نفسptr->age— السهم اختصار لإلغاء الإشارة + النقطة. لكن الأسلوبان متكافئان، والسهم هو الأكثر استخداماً.
مصفوفة من الـ Structures
struct human hmn[10]; // مصفوفة من 10 كائنات human
عملية الإسناد على الـ struct
العمليات الحسابية لا تنطبق على الـ struct، باستثناء الإسناد =:
struct human h1 = {33, "mj3s"};
struct human h2;
h2 = h1; // نسخ كل الأعضاء — صحيح
typedef — اسم مستعار
typedef: تعطي اسمًا مستعاراً (Alias) لنوع بيانات موجود.
typedef <existingName> <aliasName>;
مثال:
typedef unsigned int u32;
u32 x; // متغير من نوع unsigned int
تحسين شكل الـ struct بـ typedef
بدلاً من كتابة struct student في كل مرة:
typedef struct student {
int age;
int light;
} stud;
الآن نستخدم الاسم المستعار مباشرة:
stud s1, s2; // بدل struct student s1, s2;
stud ahmed = {10, 30};
int main() {
stud *ptr = &ahmed;
printf("ahmed age is %d\n", ptr->age);
return 0;
}
تمرير الـ struct إلى الدوال
طريقتان:
void f1(struct human h) { // تمرير بالنسخة (قيمة) — نسخ كل الأعضاء
h.age = 99; // لا يؤثر على الأصل
}
void f2(struct human *h) { // تمرير بالمرجع (الأكثر كفاءة)
h->age = 99; // يؤثر على الأصل
}
لماذا المرجع أكثر كفاءة؟ تمرير struct كبيرة بالنسخة ينسخ كل الأعضاء على الـ stack — أما المرجع فيمرر عنواناً واحداً فقط.
الـ Structure Padding
مفهوم Width و Depth
الذاكرة لها width (عرض القراءة لكل Access) و depth (عدد المواقع):
- معالج 32-bit: width = 4 بايت (كل Access يجلب 4 بايتات).
- معالج 64-bit: width = 8 بايت.
مساحة الذاكرة = width × depth. مثال: width 4 بايت × depth 1000 = 4000 بايت.
المشكلة بدون padding
افترض:
struct student {
char a; // 1 بايت
short b; // 2 بايت
short c; // 2 بايت
};
بدون padding قد ينقسم المتغير b (2 بايت) بين موقعين في الذاكرة — وهذا يتطلب عمليات دمج قد تصل إلى 7 تعليمات في الـ assembly لقراءة قيمة واحدة!
الحل: Padding
الـ compiler يترك مساحات فارغة (padding) لتتم محاذاة المتغيرات بشكل يتناسب مع عرض الذاكرة — فيقرأها في Access واحد:
- ميزة: أداء أعلى بكثير.
- عيب: مساحة أكبر في الذاكرة.
تأثير الترتيب على الـ padding
ترتيب الأعضاء يؤثر على الحجم الإجمالي:
struct bad {
char a; // 1
int b; // 4
char c; // 1
}; // sizeof = 12 (وليس 6!)
المساحة الأساسية 6 بايت، لكن بسبب المحاذاة أصبح الحجم 12 بايت — إهدار 6 بايت.
قاعدة ذهبية: رتب الأعضاء من الأكبر حجماً إلى الأصغر (int, short, char) لتقليل الـ padding وتوفير الذاكرة.
إيقاف الـ padding: packed
لإجبار الـ compiler على عدم إضافة padding (محاذاة صفرية):
struct base {
// body
} __attribute__((packed));
متى نستخدم packed؟ في تنسيقات الملفات والبروتوكولات التي تتطلب ترتيباً صارماً بدون مسافات — لأن الملف المخزن لا يحتمل padding.
ربط الـ RE — لماذا الـ struct أساسي في التحليل
- الـ struct في الـ assembly = إزاحات (Offsets): الـ struct يتحول إلى حساب إزاحات ثابتة من عنوان البداية. الوصول
ptr->ageيصبح:
mov eax, [edi+8] ; قراءة العضو age الواقع على إزاحة 8
mov edx, [edi+12] ; قراءة العضو name الواقع على إزاحة 12
عند التحليل، رؤية نمط [reg+offset] المتكرر بنفس الـ register تكشف لك وجود struct — وهذه مهارة أساسية في قراءة أي disassembly.
الـ padding يفسر الأحجام: رؤية
sizeof(struct)= 12 بينما مجموع الأعضاء 6 — المحلل يحتاج نفس الحساب ليتوقع حجم البايفر ومتى تبدأ الأعضاء. الخطأ في تقدير الحجم سبب أخطاء في تحليل الـ heap والـ stack.packed في الـ RE: الـ structs المصفوفة تظهر في تنسيقات الملفات والبروتوكولات (الملف لا يحتمل padding) — ومعرفتها تفيد في تحليل تنسيقات الملفات (file formats) وفك البروتوكولات.
الأعضاء = متغيرات مجاورة في الذاكرة: فهم ترتيب الأعضاء في الذاكرة يفسر لماذا overflow على عضو واحد يطال الأعضاء التالية (أساس heap/stack struct corruption).
خلاصة
الـ struct هو وحدة تنظيم البيانات الأساسية في C — وفي الـ RE يتحول إلى خريطة إزاحات بسيطة لكنها محورية. أتقن حساب الـ padding وترتيب الأعضاء، وستقرأ بنية أي struct في الـ disassembly فوراً.