P01NT
0x16 متقدم 17/19

الهياكل Struct

الهياكل وtypedef وعامل السهم، وعملية Padding والمحاذاة — وكيف يتحول struct إلى خريطة إزاحات في التحليل.

· 4 دقائق قراءة · 848 كلمة
المحتويات

بسم الله الرحمن الرحيم.

في هذا المقال سنتحدث عن الـ 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 أساسي في التحليل

  1. الـ struct في الـ assembly = إزاحات (Offsets): الـ struct يتحول إلى حساب إزاحات ثابتة من عنوان البداية. الوصول ptr->age يصبح:
mov eax, [edi+8]   ; قراءة العضو age الواقع على إزاحة 8
mov edx, [edi+12]  ; قراءة العضو name الواقع على إزاحة 12

عند التحليل، رؤية نمط [reg+offset] المتكرر بنفس الـ register تكشف لك وجود struct — وهذه مهارة أساسية في قراءة أي disassembly.

  1. الـ padding يفسر الأحجام: رؤية sizeof(struct) = 12 بينما مجموع الأعضاء 6 — المحلل يحتاج نفس الحساب ليتوقع حجم البايفر ومتى تبدأ الأعضاء. الخطأ في تقدير الحجم سبب أخطاء في تحليل الـ heap والـ stack.

  2. packed في الـ RE: الـ structs المصفوفة تظهر في تنسيقات الملفات والبروتوكولات (الملف لا يحتمل padding) — ومعرفتها تفيد في تحليل تنسيقات الملفات (file formats) وفك البروتوكولات.

  3. الأعضاء = متغيرات مجاورة في الذاكرة: فهم ترتيب الأعضاء في الذاكرة يفسر لماذا overflow على عضو واحد يطال الأعضاء التالية (أساس heap/stack struct corruption).

خلاصة

الـ struct هو وحدة تنظيم البيانات الأساسية في C — وفي الـ RE يتحول إلى خريطة إزاحات بسيطة لكنها محورية. أتقن حساب الـ padding وترتيب الأعضاء، وستقرأ بنية أي struct في الـ disassembly فوراً.