P01NT
0x09 الذاكرة 10/19

المؤشرات وإدارة الذاكرة

المؤشرات بأنواعها وحسابات العناوين وأقسام الذاكرة الخمسة — وموقع المؤشرات من الثغرات الشهيرة.

· 7 دقائق قراءة · 1445 كلمة
المحتويات

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

في هذا المقال سنتعرف على المؤشرات (Pointers) وأنواعها، ونأخذ مقدمة عن إدارة وتخصيص الذاكرة (Memory Management and Allocation). وهو آخر مقال في سلسلة الأساسيات إن شاء الله.

ملاحظة: لن نتوسع في memory management بالقدر المطلوب — سنشرح المفاهيم الأساسية التي تساعدنا على فهم المؤشرات، لأن الموضوع يستحق مقالاً خاصاً.

المؤشرات أداة قوية تتيح التحكم المباشر في الذاكرة، مما يجعلها ذات أهمية قصوى عند تحليل البرامج وفهم التعليمات الثنائية.

تخطيط الذاكرة (Memory Layout)

قبل شرح المؤشرات، يجب أن تكون لدينا خلفية عن تخطيط الذاكرة وتخصيصها. في لغة C، كل متغير يُخزن في موقع ذاكرة محدد بعنوان فريد: المتغير المحلي يُخزن في الـ stack، والعام في الـ globals — ونستخدم المؤشر للوصول إلى عنوان المتغير، فنحصل على تحكم دقيق بكيفية استخدام البرنامج للذاكرة.

أقسام الذاكرة

القسمالوظيفةملاحظات
Text Segmentيحتوي على التعليمات التنفيذية (machine code) المترجمةللقراءة فقط (read-only)
Data Segmentالمتغيرات العامة/الثابتة المهيأة بقيم صحيحةint x = 10;
BSS Segmentالمتغيرات العامة/الثابتة غير المهيأةint x; ← قيمتها 0 افتراضياً
Stackالمتغيرات المحلية ومعلومات الدوالمؤقت، LIFO، يُلغى عند انتهاء الدالة
Heapالتخصيص الديناميكييدوي، يبقى حتى free
+-----------------+  العنوان العالي
|      Stack      |  ينمو للأسفل (تخصيص مؤقت)
|        |        |
|        v        |
|        ^        |
|      Heap       |  ينمو للأعلى (تخصيص يدوي)
+-----------------+
|      BSS        |  غير مهيأ
|     Data        |  مهيأ
|     Text        |  للقراءة فقط
+-----------------+  العنوان المنخفض
  • Stack: LIFO (آخر من يدخل أول من يخرج). العمليات: push (إضافة) و pop (حذف). مؤقت — تُلغى مساحته بمجرد انتهاء الدالة.
  • Heap: تخصيص ديناميكي بـ malloc, calloc, realloc — يبقى محجوزاً حتى free.

بالإدارة: الـ Stack و Data و BSS تُدار تلقائياً بواسطة النظام، أما الـ Heap فتحتاج إدارة يدوية من المبرمج.

بالأداء: المكدس أسرع من الكومة بسبب بساطة إدارته، لكن حجمه محدود مقارنة بها.

تخصيص الذاكرة (Memory Allocation)

حجز مساحة في ذاكرة الحاسب لتخزين البيانات أثناء تنفيذ البرنامج. في C نخصص بثلاث طرق:

النوعمتىأينمدى الحياة
ثابت (Static)وقت الترجمةData/BSSكامل مدة تشغيل البرنامج
تلقائي (Automatic)عند استدعاء الدالةStackحتى انتهاء الدالة
ديناميكي (Dynamic)أثناء التشغيلHeapحتى free
static int x = 10;      // ثابت — Data segment
int y;                  // تلقائي — Stack
char *p = malloc(100);  // ديناميكي — Heap
free(p);                // يجب تحريره يدوياً

مسؤولية المبرمج: C لا تحتوي Garbage Collection مثل Java — المبرمج مسؤول عن تحرير الذاكرة الديناميكية يدوياً لتجنب تسرب الذاكرة (Memory Leak).

المؤشرات (Pointers)

المؤشرات متغيرات تخزن عناوين الذاكرة لمتغيرات أخرى. إذا كان عندك متغير صحيح int، يمكنك إنشاء مؤشر يشير إلى عنوانه لتقرأ قيمته أو تعدلها بشكل غير مباشر — وهذا ما يجعلها أداة قوية لإدارة الذاكرة.

data_type *pointer_name;

كيفية استخدام المؤشرات

1. تعريف المؤشر وتخصيص عنوان له:

int a = 10;     // متغير عادي
int *p = &a;    // تعريف المؤشر وربطه بعنوان المتغير
// &a  : يعطي عنوان المتغير a
// p   : يحتوي على عنوان a
// *p  : يصل إلى قيمة المتغير الموجود في العنوان

2. إلغاء الإشارة (Dereferencing):

printf("Value of a: %d\n", *p); // *p يعطي قيمة a وهي 10

3. تعديل القيمة عبر المؤشر:

*p = 20;   // تغيير قيمة a عبر المؤشر
printf("New value of a: %d\n", a); // الناتج: 20

مثال شامل

#include <stdio.h>

int main() {
    int x = 10;
    int *ptr = &x;
    printf("قيمة x: %d\n", x);          // 10
    printf("عنوان x: %p\n", &x);        // عنوان الذاكرة
    printf("قيمة ptr: %p\n", ptr);      // نفس عنوان x
    printf("القيمة عبر ptr: %d\n", *ptr); // 10
    return 0;
}

أنواع المؤشرات

1. المؤشرات الأساسية

int x = 5;
int *ptr = &x;

2. المؤشرات إلى المؤشرات (Pointer to Pointer)

int x = 10;
int *ptr = &x;
int **ptr2 = &ptr; // مؤشر إلى المؤشر ptr
printf("%d", **ptr2); // 10

3. المؤشرات إلى المصفوفات (Array Pointers)

اسم المصفوفة يتحول إلى مؤشر لأول عنصر عند استخدامه في التعبيرات. a[i] مكافئ لـ *(a + i):

int arr[] = {1, 2, 3};
int *ptr = arr; // يشير إلى العنصر الأول

4. المؤشرات إلى الدوال (Function Pointers)

تشير إلى عنوان بداية دالة، مما يتيح استدعاءها ديناميكياً:

#include <stdio.h>

int add(int a, int b) {
    return a + b;
}

int main() {
    int (*func_ptr)(int, int) = add;
    int result = func_ptr(3, 4);
    printf("النتيجة: %d\n", result);  // 7
    return 0;
}

5. المؤشرات الفارغة (Void Pointers)

يمكنها الإشارة إلى أي نوع من البيانات:

void *ptr;
int a = 10;
ptr = &a; // يشير إلى عدد صحيح الآن

ملاحظة: قبل استخدام void * يجب تحويله إلى نوع محدد (casting) — وإلا فلن تعرف حجم البيانات التي يشير إليها.

6. المؤشرات الثابتة — تمييز مهم

هناك ثلاثة أنواع متشابهة يجب التمييز بينها بدقة (لأن الـ assembly يكشف أيها استُخدم):

int *const ptr = &x;    // المؤشر ثابت: لا يمكن إعادة توجيهه، لكن يمكن تعديل قيمة x
const int *ptr = &x;    // القيمة ثابتة: يمكن تغيير المؤشر نفسه، لكن لا قيمة x
const int *const ptr;   // كلاهما ثابت

حسابات العناوين (Address Arithmetic)

الجمع والطرح على المؤشرات يتحرك بناءً على حجم نوع البيانات:

int arr[3] = {10, 20, 30};
int *ptr = arr;
printf("%d\n", *ptr);  // 10
ptr++;                 // ينتقل 4 بايت للأمام (حجم int)
printf("%d\n", *ptr);  // 20

تذكر من مقال Data Types: زيادة مؤشر int تتحرك 4 بايت، وزيادة مؤشر char تتحرك 1 بايت.

المؤشرات مع الدوال (التمرير بالمرجع)

تمرير العنوان للدالة يسمح بتعديل القيم الأصلية داخل الدالة — لأن المعاملات العادية تُمرر بالقيمة (نسخة):

#include <stdio.h>

void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main() {
    int x = 5, y = 10;
    swap(&x, &y);   // تمرير العناوين
    printf("x = %d, y = %d\n", x, y); // x = 10, y = 5
    return 0;
}

المؤشرات والسلاسل النصية

char *str = "Hello, World!";
printf("%s\n", str);        // النص كاملاً
printf("%c\n", *str);       // H — أول حرف
printf("%c\n", *(str + 1)); // e — الحرف الثاني

نقطة مهمة لمجال RE: النصوص الحرفية (string literals) تُخزن في قسم للقراءة فقط (.rodata). محاولة تعديلها عبر المؤشر تكسر البرنامج — ولهذا المترجم قد يحذرك، والـ segment نفسه يظهر read-only في الـ binaries.

أهمية المؤشرات في الثغرات الأمنية

1. Buffer Overflow

تخصيص مساحة محدودة وتجاوزها بمؤشر غير آمن:

void vulnerable_function(char *input) {
    char buffer[10];
    strcpy(buffer, input); // يمكن تجاوز حجم buffer
}

في الهندسة العكسية، تُحلل هذه الثغرات لاستغلالها أو فهم كيفية عمل الهجمات.

2. Use After Free (UAF)

تحرير المؤشر ثم استخدامه لاحقاً:

char *ptr = malloc(10);
free(ptr);
strcpy(ptr, "data"); // استخدام المؤشر بعد تحريره

3. Null Pointer Dereference

إلغاء الإشارة إلى عنوان فارغ يؤدي لانهيار البرنامج:

int *ptr = NULL;
*ptr = 5; // انهيار

4. Double Free

تحرير نفس المؤشر مرتين — قد يؤدي لثغرات في تخصيص الذاكرة:

char *ptr = malloc(10);
free(ptr);
free(ptr); // تحرير مرتين

UAF و Double Free هما أساس Heap Exploitation (مثل tcache poisoning) — موضوع سنتوسع فيه في سلسلة قادمة.

المؤشرات في الـ Assembly — تتويج السلسلة

هذا القسم يربط كل ما تعلمناه في السلسلة بالـ disassembly:

أ. المؤشر = نمط العناوين (Addressing Modes)

تعبير Cالـ Assemblyالمعنى
*ptr (قراءة)mov eax, [ebx]قراءة القيمة في العنوان
ptr[i]mov eax, [ebx + ecx*4]مؤشر + إزاحة (index × حجم)
&xlea eax, [ebp-4]حساب العنوان دون قراءة القيمة
*ptr = val (كتابة)mov [ebx], eaxكتابة القيمة في العنوان
  • mov eax, [address] = إلغاء الإشارة (dereference).
  • lea = الحصول على العنوان نفسه — الأداة الأهم للمؤشرات.

ب. التمرير بالمرجع في الـ assembly

عند استدعاء swap(&x, &y) في الـ disassembly:

lea eax, [ebp-8]   ; عنوان y
push eax
lea eax, [ebp-4]   ; عنوان x
push eax
call swap

; داخل swap:
mov eax, [ebp+8]   ; قراءة العنوان (ليس القيمة!)
mov ecx, [eax]     ; قراءة القيمة الفعلية من العنوان

المعاملات التي تُمرر بالمرجع تظهر كـ عناوين على الـ stack — ولاحظ الفرق: [ebp+8] يعطي العنوان، ثم [eax] يعطي القيمة الحقيقية.

ج. المؤشر إلى دالة = عنوان كود

المؤشر إلى دالة في الـ disassembly هو عنوان مباشر للكود، وقد يظهر في جداول (jump tables، أو vtables في C++). تتبع هذه العناوين جزء من تحليل malware — خصوصاً في تقنيات إخفاء الـ API.

د. الاستنتاج النهائي

في الهندسة العكسية، المؤشرات هي المفتاح لفهم تدفق البيانات بين أجزاء البرنامج — لأنها تتحول مباشرة إلى أنماط عناوين في الـ assembly يمكن التعرف عليها وربطها بالثغرات.

تمرين ختامي للسلسلة

  1. اكتب int *ptr = &x; و printf لكل من x, &x, ptr, *ptr — ثم افحص الـ assembly الناتج وحدد أنماط lea و mov [..].
  2. اكتب swap وافحصها بـ gcc -S — ولاحظ أن [ebp+8] و [ebp+12] يحتويان العناوين، وأن التعديل يتم عبر [eax].
  3. جرّب تعديل نص حرفي (str[0] = 'X') — ولاحظ الانهيار واشرحه بمعرفتك بأقسام الذاكرة (.rodata للقراءة فقط).

انتهت سلسلة أساسيات لغة C. إن شاء الله ستكون هناك سلسلة للأمور المتقدمة مع توسع أكبر — والمرحلة القادمة ستركز على أمور النظام (System) لأن قوة لغة C في تعاملها مع الـ OS وسرعتها.