مضى على الشبكة و يوم من العطاء.

[ شرح ] لغة C | تصميم الذاكرة في Stack vs Heap 0x11

N0Tb1t

./عضو جديد
>:: v1p ::<

firefox
linux

السمعة:

بسم الله
في هذا المقال رح نحكي عن تصميم الذاكرة الخاص بالStack والHeap ورح نوضح نقاط الاختلاف البيناتن

يمكن هاد المقال ما يغطي كل شي حرفياً بس انا واثق انو رح يوصلك فهم كامل عن الامور الي رح نحكي فيها

هات آيس قهوة أو آيس شاي واستمتع

Memory Layout:
متل ما حكينا من قبل وتحديداً بمقال المؤشرات انو ذاكرة البرامج بالانظمة الحديثة x86/x64 بتنقسم ل 5 أقسام رئيسية: والبعض بقول 4 ..
1 .Text Segment
2 .Data Segment
3 .Bss Segment
4 Heap

5 Stack

الي بيقولو 4 بيكونوا دامجين ال.Data مع ال.BSS تحت مسمى (Global Variables)

والشكل التالي بيوضح كيفية التقسيم:

WhatsApp Image 2025-06-17 at 5.57.28 AM.webp


نحنا هون تعاملنا مع الUser Space
لأن الKernal Space موضوع تاني

المهم متل ما واضح أنو الستاك بينمو من العناوين العالية إلى المنخفضة
و
الهيب بينمو من العناوين المنخفضة إلى العالية
شو السبب؟ مشان الستاك والهيب ينموا بدون تصادم

بما انو الستاك بيتم تنظيمه تلقائياً بالمعالج وبيكون أدائو سريع

بس في سر ورى السرعة الأداء شو هو؟
السر هو المسجلات Registers بتكون علاقة الستاك بالregisters مباشرة على عكس الهيب الي بيتعامل معهم بطريقة غير مباشرة

شو هني ال Registers؟ هي وحدات تخزينية صغيرة موجودة داخل الProcessor تستخدم خلال العمليات الحسابية التي تتطلب سرعة عالية ورح نذكر أهم هي الRegisters إلي رح نتعامل معها :
1.EAX(Accumulater Register): منستخدمو للاحتفاظ بالReturn Value في حالة استخدام الدوال (functions) وكمان منستخدمو بالعمليات الحسابية الجمع والطرح.

2.EBP(Base Pointer): منستخدمو للاحتفاظ بالMemory Address إلي بيشير إلى قاع الستاك وبالصورة قاع الستاك من فوق.

3.ESB(Stack Pointer): كمان منستخدمو للاحتفاظ بالMemory Address إلي بيشير إلى قمة الستاك وقمة الستاك بالصورة من تحت

هي الRegisters بنظام x86 طبعاً بتختلف مسمياتها من نظام لنظام على فرض بنظام x64 هيك اسمائها( RAX, RBP, RSP )
وبتكون سعتها بمعمارية x86 (32-bits)


ورح نتعامل بالامثلة الجاي مع الx86

شو هو الستاك؟
هو عبارة عن جزء من الذاكرة المنفصلة منطقياً وليس فيزيائياً خاص بتخزين الدوال المحلية (local) المعلنة داخل الدوال وبيانات الStack frame إطار المكدس (رح نحكي عنو تحت). بيتم حجز مساحة الستاك عبر (Pop/Push) تلقائياً عند دخول الدوال وإلغاء تخصيصها عند الانتهاء منها. الستاك بيتبع نمط LIFO (Last-In-First-Out) يعني المتغيرات التي تنشأ أولاً تزال آخراً.

شو هو الStack frame ممكن نلاقيه كمان باسم (activation record): هو بنية من البيانات التي تنشأ في الستاك عن استدعاء دالة (function call). بيحتوي على كل المعلومات اللازمة لتنفيذ الدالة وبينحذف عن انتهائها.

شو الهدف منو؟ بيعزل كل دالة عن التانية داخل الستاك (يعني كل استدعاء لدالة بينشىء إطار خاص فيها بالستاك) ،تمام طيب شو هي مكونات أو اقسام هاد الإطار ؟

1.باراميترات الدالة (Parameters OR Arguments): القيم المدخلة إلى الدالة.

2.عنوان العودة (Return Address): هو العنوان إلي لازم يرجعلو البرنامج بعد ما تنتهي الدالة الحالية من تنفيذها.

3.مؤشر الإطار(Frame Pointer): هاد المؤشر بيشير إلى قاعدة إطار الستاك الحالي ( لما نستدعي دالة جديدة بالبرنامج ، هو إلي بيخلينا نوصل للمتغيرات المحلية لهي الدالة والباراميترات الخاصة فيها).

4.المتغيرات المحلية(Local Variables): متغيرات الدالة الحالية.

5.السجلات المحفوظة(Saved Registers): هي السجلات بتحافظ على قيم السجلات الخاصة بالدالة المستدعية (الحالية), عند استدعاء دالة جديدة (يعني وقت بيكون عنا دالة متل main() وبدنا تستدعي دالة اخرى موجود جواها فالدالة الجديدة رح تستخدم Registers وممكن يكونوا نفس الي مستخدمينن بالدالة main فلهيك منحفظ قيمن مشان وقت تنتهي الدالة الجديدة ما نفقد القيم إلي كانت جوا هي السجلات).

جدول التخطيط للx86:

2025-06-17 14_17_20-Word‪ - مستند1.webp


ليش وقت منضيف عالستاك منطرح ؟
السبب الاساسي لانو الستاك بينمو للاسفل (يعني لو كان عنا الEBP بيشير لعنوان 1000 بالذاكرة فالمتغير المحلي الأول بيشير إلى العنوان 996
و المتغير التاني بيشير إلى العنوان 992 وهيك ..
طبعاً هاد الحكي بنظام 32-bit

خلينا ناخد مثال يوضح الامور اكتر:

كيف ينشأ الStack frame؟

(Function Prologue): عند استدعاء دالة في لغة C:
C:
#include <stdio.h>

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

int main(){
    int x = 5, y =3;
    add (x, y);
    return 0;
}

رح نشرح شو بصير خطوة بخطوة:
أول شي بتستدعي الدالة main() بينشأ إطار ستاك بيحتوي على متغيراتها المحلية: x, y.

عند استدعاء دالة الadd() إلي داخل دالة main(): بيوقف تنفيذ دالة main() مؤقتاً، وبينشأ إطار لدالة الadd() فوق إطار main() بالستاك وبيحتوي على:
1.باراميترات الدالة: b=3, a=5
2.المتغيرات المحلية: result
3.عنوان الإرجاع: مكان العودة إلى دالة main()

بعد الانتهاء من دالة add() بينحذف إطارها من الستاك. بترجع بتتنفذ دالة main() من عنوان الإرجاع Return Address.

هيك بيكون الستاك:

2025-06-17 22_00_50-Word‪ - مستند1.webp


وهيك بيكون بالاسمبلي:
كود:
; 1. تمرير الباراميترات
push 3    ; b=3
push 5    ; a=5

;2. استدعاء الدالة (بيحط عنوان الارجاع تلقائيا)
call add    ;push EIP + jmp to add

;3. (Prologue)
add:
    push ebp    ;حفظ اطار المتصل (caller's ebp)
    mov ebp, esp    ;تأسيس اطار جديد (هون صار الebp الحالي)
    sub esp, 4    ; تخصيص مساحة للمتغير المحلي result

باستدعاء الدالة add() بكود الاسمبلي شفنا Register محطوط بالكومنت اسمو EIP شو وظيفة هاد الregister ؟
EIP(Instruction Pointer): هاد الريجيستر يستخدم للاحتفاظ بالmemory address الذي يشير إلى التعليمة (instruction) القادمة التي سيقوم الprocessor بتنفيذها.

خلينا نشوف الستاك للدالة add() شلون صار بعد التهيئة (Prologue):

2025-06-17 22_14_10-Word‪ - مستند1.webp


هلأ كيف بينحذف الStack frame ؟ (Function Epilogue):
وقت بتنتهي الدالة بصير التالي ..
كود:
;1. تخزين النتيجة في السجل eax
mov eax [ebp - 4]        ; eax = result

;2. خاتمة الدالة (Epilogue)
mov esp, ebp    ;يعيد الesp ليشير الى نفس موقع الebp (بيمحي المتغيرات المحلية)
pop ebp        ;يستعيد اطار الدالة المتصلة السابقة (caller's frame)
ret        ;يخرج عنوان الارجاع من الستاك ويقفز اليه

الذاكرة بعد الEpilogue هيك بتصير:

2025-06-17 22_21_52-Word‪ - مستند1.webp


طيب شو بصير بعد هيك ؟ هون بدنا ناخد مصطلح جديد وإلي هو 'اتفاقية الاستدعاء' (calling convenition)
خلينا نكفي الفكرة مشان ما تضيع
فرضاً عنا اتفاقية الاستدعاء الافتراضية (_cdecl)
بهي الاتفاقية بيضيف الcaller
8byte إلى ESP (لأن كل معامل 4Byte ) لتنظيف الستاك.
بالاسمبلي هيك:
كود:
add esp, 8        ;تنظيف الباراميترات

الcaller او المتصل بمثالنا هي الدالة main() والمنادى (callee) هي الدالة add()

طيب شو هني اتفاقيات الاستدعاء أو انماط النداء ؟
Calling Convenitions:

هي مجموعة من القواعد المتفق عليها بين الcompiler والassembler وتحدد:
1.كيفية تمرير الباراميترات للدوال.
2.كيف إرجاع القيم Return Vlaues.
3.مسؤوولية تنظيف الستاك بعد الاستدعاء.

4.أي السجلات يجب حفظها

ليش في أنماط مختلفة؟
لنحسن أداء البرامج وللتكامل مع الانظمة واللغات الاخرى ولدعم حالات خاصة متل (الدوال ذات الباراميترات المتغيرة).

1.cdecl(C Declaration):
هاد النمط الافتراضي. رح نحكي عن النقاط الاربعة المتفق عليها:
1.اتجاه الباراميترات: من اليمين إلى اليسار (Right-to-Left).
2.يخزن القيمة المرجعة بسجل eax.
3.المسؤول عن تنظيف الستاك هو الcaller
4.المتصل يحفظ السجلات التي يحتاجها EAX ،و المنادى يحفظ EBP, EBX, EDI, ESI, ESP

خلونا نشرح السجلات إلي ما شرحناهن:
EBX(Base Register): ليس له وظيفة محددة ويمكن استخدامه في سياقات متعددة كـ مكان للاحتفاظ بال.Data

EDI(Destination Register), ESI(Source Register): إذا بدنا ننقل string من مكان لآخر، عندها بنستخدم الESI للاحتفاظ بالmemory address الذي يوجد به الString حالياً ومنستخدم الEDI للاحتفاظ بالmemory address الذي سيتم نقل الstring إليه.

مثال على _cdecl:
C:
// تعريف الدالة
int __attribute__((cdecl)) add(int a, int b){
    return a + b;
}

//الاستدعاء
int result = add(5, 3);

التنفيذ بالاسمبلي:
كود:
; ينفذ المتصل (Caller)
push 3         ; b = 3 (يمين)
push 5        ; a = 5 (يسار)
call _add        ;نداء للدالة (تلقائيا بيحط عنوان الارجاع)
add esp, 8         ; تنظيف الستاك (بعد انتهاء الدالة)

2.stdcall(Standard Call):
1.اتجاه التمرير: من اليمين إلى اليسار
2.تخزن القيمة المرجعة في السجل eax.
3.المسؤول عن التنظيف المنادى (callee).
4.المنادى يحفظ EBP, EBX, EDI, ESI, ESP

مثال:
C:
// تعريف الدالة
int __attribute__((stdcall)) add(int a, int b){
    return a + b;
}

//الاستدعاء
int result = add(5, 3);

بالاسمبلي:
كود:
; ينفذ المتصل (Caller)
push 3        ; b = 3
push 5        ; a = 5
call _add@8        ; هون الاسم يشفر عدد البايتات (@8 = 8 بايت)

; داخل الدالة (ينفذ المنادى)
ret 8        ; تنظيف الستاك والعودة الى عنوان الارجاع

3.fastcall(Fast Call):
1.أول باراميترين في السجلات (ECX, EDX) والباقي في الستاك من اليمين إلى اليسار.
2.يخزن القيمة المرجعة في السجل eax.
3.المسؤول عن التنظيف المنادى (callee).
4.المنادى يحفظ السجلات المستخدمة ECX, EDX

شو وظيفة هي السجلات؟
ECX(Counter Register): يستخدم كـ counter في العمليات الحسابية التي يوجد بها تكرار متل الloop
EDX(Data Register): يعتبر قرين للEAX يستخدم في العمليات الرياضية الضرب والقسمة وكمان للاحتفاظ بالFunction variables

مثال:
C:
int __attribute__((fastcall)) add(int a, int b, int c){
    return a + b + c;
}

int result = add(3, 5, 2);

بالاسمبلي:
كود:
; ينفذ المتصل
mov ecx, 5        ; a في ECX
mov edx, 3        ; b في EDX
push 2         ; c في stack
call @add@12        ; يشفر الاسم و عدد البايتات

; داخل الدالة (االمنادى)
ret 4         ; تنظيف الستاك (هون بس C)

كيف بتختار النمط المناسب لبرنامجك؟
_cdecl: وقت بتكتب دوال ذات باراميترات متغيرة
_stdcall: وقت بدك تستدعي دوال متعلقة بالWindowa API
_fastcall: وقت بدك أداء سريع لانه بيستخدم الريجسترات ECX, EDX

لهون منكون غطينا شرح أهم الأمور الي لازم نعرفها بالستاك وهلأ مننتقل للهيب ..



الكومة (Heap):

ليست منطقة ذاكرة منفصلة بل هي جزء من مساحة عملية الذاكرة (Process Address Space) وعلى عكس الستاك (الذي يدار تلقائياً) الهيب تدار يدوياً من المبرمج. منستخدما لتخصيص المساحة عند بدء تنفيذ البرنامج (Runtime).

ينشىء نظام التشغيل مقطع بيانات (Data segment) بيحتوي على:
.data
.bss
-Heap: بينمو للأعلى عكس الستاك نقطة البداية تسمى (Program Break)

صورة للتوضيح:

WhatsApp Image 2025-06-17 at 5.57.28 AM.webp


شو إلي بيميز الهيب؟
• حجم الهيب متغير (بينمو/بيتقلص حسب الحاجة).
• يبقى التخصيص للبيانات حتى يتم تحريرها يدوياً بـ free()
• حجمها أكبر بكتير من الستاك.

متل ما حكينا عن وحدة البناء الأساسية بالستاك إلي هي (Stack fram) ،هون الوحدة الأساسية هي الكتلة (chunk):
كل ذاكرة مخصصة بالهيب (فرضاً تم تخصيصها عبر malloc() أو غيرها) ،تخزن في وحدة تسمى الكتلة (chunk) ،حتى لو طلبت مساحة صغيرة (متل malloc(1) ) رح يتم تخصيص كتلة كاملة إلها.

كل كتلة بالهيب إلها هاد الهيكل الأساسي:

2025-06-17 22_51_18-Word‪ - مستند1.webp


التركيز بهاد الهيكل بيكون على جزء الheader
شو إلي بيخزنه هاد الجزء ؟ تخزين الهيدر بيكون حسب حالة الكتلة هل هي محجوزة ام حرة:
بس في معلومات أساسية دائماً تخزن لو كانت الكتلة محجوزة أو حرة:
• mchunk_size: حجم الكتلة الكلي ويحوي كمان حجم الهيدر
• flags: 3 بتات تحوي معلومات خاصة
هدول البتات بيعبروا عن الأمور التالية:
البت(0) PREV_INUSE: إذا كان '1' فهو بيدل على انو الكتلة السابقة محجوزة ،واذا كان '0' غير محجوزة.

البت(1) IS_MMAPPED: إذا كان '1' بتكون الكتلة خُصصت عبر mmap ، واذا كان '0' بتكون خُصصت عبر sbrk (رح نحكي عليهن تحت).
البت(2) NON_MAIN_ARENA: عادة بالوضع الأحادي (Single-Threaded) كل عمليات الحجز بتتم داخل الmain-arena بس بـ بنيات الخيوط المتعددة (Multi-Threaded) ينشأ عدد من الarenas الثانوية (secondary arenas)
فإذا كان هاد البت '1' بيكون الchunk إلي عم نتعامل معه من أحد الsecondry-arena واذا كان '0' من الmain-arena

إذا كانت الكتلة حرة تخزن معلومات إضافية داخل الهيدر :
• fd: مؤشر للكتلة التالية الحرة
• bk: مؤشر للكتلة السابقة الحرة

خلونا نحكي عن الsbrk , mmap
شو هدول؟

هني عبارة عن آليات بنظام التشغيل مسؤولين عن كيفية نمو الهيب
1. sbrk():
1.يحتفظ نظام التشغيل بحدود الهيب في مؤشر يسمى (Program Break) وبتكون البداية من عندو.
2.يحرك هذا المؤشر للأعلى بمقدار n بايت sbrk(n).
3.بيرجع عنوان بداية المساحة الجديدة.
القيود:
ـ ما فينا نحرر أجزاء من الوسط (فقط التقلص من الأعلى) لاعتماده على program break.
ـ غير مناسب لبيئات متعددة الخيوط (threads).

2.mmap():
1.ينشىء منطقة ذاكرة جديدة منفصلة عن الheap الرئيسي .
2.يعيد مؤشر مباشر لهي المنطقة.
3.التحرير يتم عبر الدالة munmap(ptr, size)
المزايا:
- مناسب للكتل الكبير > 128kb
- يدعم التخصيص المتوازي (threads).
- عزل أفضل للاخطاء.

شو هي التحولات الهيكلية إلي بتطرأ على الهيب حسب الحالة؟
1.عند التخصيص (يعني الكتلة محجوزة):

2025-06-17 23_09_01-stack vs heap.pdf - Foxit Reader.webp


2.عند التحرير (يعني الكتلة حرة free() متل سوريا :)

2025-06-17 23_10_28-stack vs heap.pdf - Foxit Reader.webp


3.عند الدمج (coalescing):
قبل: كان عنا
[كتلة محجوزة] [A كتلة حرة] [B كتلة حرة]
بعد التحرير: هيك صار
[(الكتلة الجديدة + B + A) كتلة حرة عملاقة]

شو هو الجزء التاني للهيكل الأساسي؟
• User data:
هاد الجزء خاص بالمساحة الفعلية إلي منحصل عليها للتخصيص (يعني وقت استخدام الدوال متل calloc, malloc )، وهي المنطقة الوحيدة إلي بيوصلها المستخدم

الجزء التالت:
• الحشو (padding): وظيفتو تنسيق الكتلة مع نظام الذاكرة (يعني بيملأ الفراغ ليجعل الحجم الكلي من مضاعفات ال16 او 8 حسب المعمارية 16 للـ64-bit

كيف يتم حساب الchunk؟ على نظام 64-bit:
1.حجم الهيدر بيكون ثابت (16 بايت)
2.معادلة الحجم الكلي: (حجم الهيدر + الحجم المخصص) أقرب مضاعف للعدد 16 = الحجم الكلي.
3.معادلة الحشو: الحجم الكلي - (حجم الهيدر + الحجم المخصص) = الحشو

أمثلة:
• malloc(1):
ـ حجم الهيدر : 16 بايت
ـ المجموع : 16+1 = 17 (أقرب مضاعف لـ 16 : 32 = الحجم الكلي
ـ الحشو : 32 - 17 = 15 بايت.

• malloc (100):
ـ حجم الهيدر 16 بايت
ـ المجموع 100 + 16 = 116 أقرب مضاعف للـ16 : 128 = المجموع الكلي
ـ الحشو: 128 - (116) = 12 بايت


ملاحظات هامة:
• الحد الأدنى للحجم الكلي هو 32 بايت ،حتى لو كان malloc(0).
• الكتل الكبيرة أكبر من 128kb نستخدم mmap بدل sbrk ولها هيدر مختلف عادة (32Byte).

إن شاء الله لهون منكون قادرين على البدء بشرح الـDynamic memory allocation, إلي رح يكون بالمقال القادم.

بالنهاية لازم نوضح أه‍م الامور المختلفة بين الheap والstack بالجدول التالي:

2025-06-17 23_12_45-stack vs heap.pdf - Foxit Reader.webp


اعذرونا مشان الجدوال كانت النية يطلعوا افضل من هيك بس اموري بالديزاين تعبانة يمكن يبين معكن من تنسيق الالوان xD اهم شي انو تكون وصلت الفكرة ان شاء الله انتظرونا بالمقال القادم :ninja:
 
التعديل الأخير:

آخر المشاركات

عودة
أعلى