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

[ شرح ] لغة C&ASM | الدوال الجزء التاني 0x14

N0Tb1t

./عضو
>:: v1p ::<

firefox
linux

السمعة:

بسم الله

بعد ما تكلمنا بالمقالات السابقة وبنينا حجر اساس بلغة C وتقسيم الذاكرة وتعرفنا عالمؤشرات واخدنا مقدمة عن الاسمبلي ان شاء الله نحنا ماشيين على الطريق الصح لناخد بنية معرفية كاملة مع لغة C. وهذه المقالات ان شاء الله اذا سئلت عنها امام رب العالمين باذن الله سوف اكون مؤدي واجبي الكامل تجاه هذه المقالات.

في هذا المقال رح نحكي عن الدوال بس بطريقة advanced ورح نحكي عن شوي مصطلحات مرجعها الرئيسي كتاب k&r بحيث يكون عنا المام فهم بكل شي عم نشوفو بالكود ومتل ما حكينا المقال السابق رح نجيب كود assembly ونفصلو حبة حبة وبنزيد بهاد المقال انو رح نتتبع السجلات لتوصل الفكرة بالزبط ويكون عنا تصور انو الكود كيف بيمشي بالـlow-level.


وخليني حط فهرس للي رح نتعملو بحيث اذا كنت حابب تطلع على موضوع معين من الموضوعات الي رح نحكي فيها او عندك نقص معرفة بس بهالجزئية توصل الفكرة باذن الله.

1.Basics of Functions (اساسيات الدوال)​

1.1 variables scope (نطاق المتغيرات)

1.2 Header Files (ملفات الـهيدر)

1.3 static variables (المتغيرات الساكنة)

1.4 recursion (العودية)


متل ما حكينا من قبل انو الدوال الها مكونات اساسية بتكون موجودة بكل دالة وحكينا عن انواع واشكال الدوال.

اليوم رح نفوت بالجزء العملي والخفايا إلي بتصير بتنفيذ البرنامج إلي للاسف اغلب العرب بيجهلوها بسبب القاعدة إلي سيرنا عليها الحياة (١+١ = ٢).

المهم هاد النقد اليوم ما رح يفيدنا بس لازم كل شخص يعمل على رفع هذا الجهل إن كان بإمكانو.

المكونات الاساسية للدوال:

•التوقيع (signature): إلي منشوف فيه تعريف الدالة:
كود:
returnType funcName(Parameters) {}
•الجسم(body): الوظيفة إلي بتدها تقوم فيها هي الدالة.
•الباراميترات: قيم الإدخال لهي الدالة.
•قيمة الإرجاع (Return value): الناتج الرئيسي للدالة.

حكينا اليوم مش نظري وهي الموضوعات مشروحة بشكل نظري اكتر بهاد المقال:

خلينا ناخد نموذج لتنفيذ دالة:
كود:
int add(int a, int b) {
int result = a + b;
return result;
}

هي أبسط دالة ممكن تمر معك (دالة الجمع عبارة عن دالة بتاخد باراميترين وبتجمعن بعملية الجمع العادة وبترجع الناتج (result). وفرضاً بعدها بيتم استدعائها بدالة main().

كيف بتتنفذ هي الدالة بأسمبلي وكيف بيكون تخطيط الذاكرة؟ خلينا نفصل خطوة بخطوة.

assembly code:

كود:
add:
    push    ebp
    mov    ebp, esp
    sub    esp, 16
    mov    edx, DWORD PTR [ebp+8]
    mov    eax, DWORD PTR [ebp+12]
    add    eax, edx
    mov    DWORD PTR [ebp-4], eax
    mov    eax, DWORD PTR [ebp-4]
    leave
    ret
main:
    push    ebp
    mov    ebp, esp
    push    3
    push    5
    call    add
    add    esp, 8
    mov    eax, 0
    leave
    ret

تحويل الكود بالامر التالي:
Bash:
gcc -S -masm=intel -m32 -fno-asynchronous-unwind-tables -fno-pie -O0 fileName.c -o fileName.S

ممكن تواجهك مشكلة بمكتبة m32 ابحث عن كيف بتنزل هي المكتبة على نظامك
بما اني بشتغل على arch هاد حل المشكلة على توزيعة archlinux:
Bash:
sudo pacman -S gcc-multilib lib32-gcc-libs lib32-glibc

#وهاد حل المشكلة على كالي الاكثر شيوعا
sudo apt  install gcc-multilib libc6-dev-i386 libc6:i386

تمام منرجع لكود الاسمبلي:
1.انشاء اطار المكدس لدالة add():

كود:
add: 
    push    ebp        ; يحفظ قيمة ebp الحالية (الخاص بدالة main())
    mov    ebp, esp        ; انشاء اطار جديد لدالة add()
هون بيتم انشاء stack frame جديد لدالة add() وغالبا هدول التعليمتين بكونوا اول تعليمتين بمعظم الدوال.

2.تخصيص مساحة للمتغيرات المحلية:
كود:
sub    esp, 16
بيخصص مساحة 16 بايت للمتغيرات المحلية (مع العلم ما عنا غير متغير يعني 4 بايت بس ال12 بايت الزيادة بتكون للمحاذاة) للـ padding.

3.قراءة المدخلات واجراء العمليات:
كود:
    mov    edx, DWORD PTR [ebp+8]        ; edx = (value 5) اول باراميتر
    mov    eax, DWORD PTR [ebp+12]        ; eax = (value 3) تاني باراميتر
    add    eax, edx        ; eax = 5 + 3 = 8

4.حفظ الناتج وارجاعه:
كود:
    mov    DWORD PTR [ebp-4], eax        ; يخزن الناتج (8) في المتغير المحلي
    mov    eax, DWORD PTR [ebp-4]        ; يحمل الناتج الى eax (لتكون قيمة الارجاع)
    leave        ; (mov    esp, ebp + pop ebp) تعادل هي العبارة لتعيد ضبط المكدس واستعادة ebp الخاص بالـmain
    ret        ; يخرج عنوان الرجوع من المكدس ويعود الى الدالة main()

شرح الدالة main():
1. انشاء اطار المكدس:
كود:
main:
    push    ebp
    mov    ebp, esp        ; متل الي بدالة add()

2. تمرير المدخلات للدالة add():
كود:
    push    3        ; يمرر القيمة التانية 3
    push    5        ; يمرر القيمة الاولى 5
    call    add        ; استدعاء الدالة add()

3. تنظيف المكدس بعد الرجوع من الاستدعاء:
كود:
add    esp, 8        ; يزيل المدخلات من المكدس (5و 3) و يحتفظ بالقيمة 8

4. انهاء البرنامج:
كود:
    mov    eax, 0        ; قيمة الارجاع 0 (يعني البرنامج تم تنفيذو بنجاح)
    leave        ; mov    esp, ebp + pop    ebp
    ret        ; العودة الى نظام التشغيل

تخطيط الذاكرة وتتبع المؤشر esp:
1.عند بدء تنفيذ main() بداية البرنامج (بعد mov ebp, esp داخل main):

كود:
[ret address]    : عنوان الرجوع للنظام
[saved ebp]    : ebp/esp الحالي

2.بعد push 3+ push 5:
كود:
[ret address]
[saved ebp]
3
5    : هون صار الـesp

3.بعد (call add):
كود:
[ret address]
[saved ebp]
3
5
[ret address]    : عنوان الرجوع لـmain

4.داخل دالة add() بعد (mov ebp, esp):
كود:
[ret address]
[saved ebp]
3
5
[ret address]
[saved new ebp]    : ebp/esp الخاص بدالة add()

5.بعد (sub esp, 16):
كود:
[ret address]
[saved ebp]
3 : [ebp + 12]
5 : [ebp + 8]
[ret address] : [ebp + 4]
[saved new ebp] : [ebp]
[local variable result] : [ebp -4]
[مساحة فارغة للمحاذاة] : [esp]
// هون هلأ بيساوي العمليات وبيخزن الناتج بالمتغير result.

6.بعد leave:
بيرجع للـ [new ebp]
كود:
[ret address]
[saved ebp]
3
5
[ret address]
[saved new ebp] -> esp

7.بعد ret:
بيرجع للدالة main() لعنوان الارجاع الخاص فيها
كود:
[ret address]
[saved ebp]
3
5
[ret address] -> esp

8.بعد add esp,8 بالدالة main():
بينظف الـ3 و5 وبيرجع لعنوان إرجاع النظام وبيخرج من البرنامج.


Vars Scope 1.1:

بلغة C نطاق المتغيرات (scope) يحدد المكان إلي بيقدر يوصلو المتغير بالكود.

1.النطاق المحلي(Block scope):
المكان
: داخل الكتلة {} (سواءً كانت دالة، حلقة، شرط ... الخ).
مدى الوصول: المتغير بس بنقدر نشوفو داخل هي الكتلة وغير مرئي خارجها.
مثال:
C:
void example() {
    int x = 10; // نطاقها محلي داخل الدالة
    if (x > 5) {
        int y = 20; // نطاقها داخل دالة if
        printf("%d", y); // يطبع
    }
    printf("%d", y); // لا يطبع وبطالع خطأ
}

2.النطاق العام (file scope):
المكان
: خارج اي دالة أو كتلة (على مستوى الملف الحالي).
مدى الوصول: منقدر نوصلو من أي مكان بالملف لأن عام لجميع الملف.
مثال:
C:
int global = 50; // نطاق عام داخل الملف

void func1() {
    global ++; // الوصول مسموح
}

void func2() {
    printf("%d", global); // يطبع 51
}

3.نطاق المعاملات (function arguments scope):
المكان
: داخل أقواس () الدالة.
مدى الوصول: المتغيرات المحلية لهي الدالة(يعني وبس هي بتتمرر داخل الدالة).
مثال:
C:
int sum(int a, int b) { // a, b نطاقهما داخل هذه الدالة فقط
    return a + b;
}

4.ظاهرة الاخفاء (shadowing):
وقت منعرّف متغير محلي باسم متغير عام يُحجب المتغير العام ويتم استخدام المتغير المحلي لهي الكتلة.
مثال:
C:
int x = 100; // متغير عام

void demo() {
    int x = 200; //تخفي المتغير العام
    printf("%d", x); // يطبع 200
}

ملاحظات مهمة:
1.المتغيرات تعرف عن بداية الكتلة {}.
2.المتغيرات العامة تعرّف خارج الدوال.
3.نطاق المتغير يبدأ من مكان تعريفه حتى نهاية الكتلة إذا كان (local) ,او نهاية الملف اذا كان (global).
4.الكتل المتداخلة قادرة على الوصول إلى متغيرات الكتل الخارجية اذا ما صار shadowing.




1.2 Header files:

ملفات الرأس (.h) وتسمى أيضاً (ملفات الـinclude) هي ملفات بتحتوي على إعلانات (declarations) يمكن مشاركتها بين عدة ملفات مصدرية (source files) (.c).
منستخدما لتجنب تكرار كتابة الإعلانات بكل ملف.
وبتفصل الواجهة (interface) عن التنفيذ (implementation). هاد الشي بيسهل الصيانة واعادة استخدام الكود.

شو محتويات الـHeader files:
1.إعلانات الدوال (functions declarations)
متل:
C:
 int add(int a, int b);
طبعاً هون بس منحط الاعلان ما منحط جسم الدالة.

2.المتغيرات الخارجية(external variables): هي متغيرات يتم مشاركتها عبر الملفات. وتعرف بكلمة extern
متل:
C:
extern int counter;

3.الماكروز (MACROS):
متل:
C:
#define MAX_SIZE 100

4.تعريفات الهياكل والاتحادات (structs and unions).
(رح نحكي عليهن بالمقالات الجاية ان شاء الله).

5.تعريفات النماذج typedef.
(برضو رح نحكي عليهن).

6.تضمينات مكتبات النظام.

متل:
C:
#include <stdio.h>

كيف منستخدمن؟
مثال:
انشاء ملف رأس (مثال: calc.h):
C:
//اعلان الدالة
int add(int a, int b);

//اعلان متغير خارجي
extern int result;

تضمينه بملف المصدر (مثال: main.c):
C:
#include "clac.h"

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

تنفيذ الدوال في ملف منفصل (مثال: math.c):
C:
#include "calc.h"

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

الفروق الاساسية بالتضمين:
WhatsApp Image 2025-06-29 at 10.24.40 PM.webp


في شي اسمو حماية التضمين المتكرر (include guards) شو بفيد؟
بيمنع تكرار التضمينات اذا تم تضمين الملف اكتر من مرة (مشان ما يصير تضارب وهاد الشي بيأدي لاخطاء).
مثال:
C:
#ifndef MY_HEADER_H  //اذا لم يعرف هذا الرمز
#define MY_HEADER_H //عرفه الان

// محتويات الملف

#endif // MY_HEADER_H انهاء


تحذيرات هامة:
-لا تحط تعريفات (definitions) بملفات الرأس
متل:
int x;

-استخدام extern للمتغيرات المشتركة.

-لا تحط كود الدوال (الـBody) بيكون موجود بملف التنفيذ فقط.



1.3 المتغيرات والدوال الثابتة (static):

بلغة C يتم استخدام كلمة static بثلاث سياقات:
1.المتغيرات الثابتة في نطاق الملف (file scope):
عندما بتم تعريف متغير خارج الدوال (أي في مستوى الملف) باستخدام static فهاد يعني ان هاد المتغير له نطاق رؤية محصور بالملف الحالي فقط. أي لا يمكن الوصول له من ملفات اخرى حتى مع استخدام كلمة extern وهاد الشي مفيد لاخفاء المتغيرات عن بقية ملفات البرنامج
مثال:
C:
// in the file1.c
static int internal_var; // متغير ثابت عام (غير مرئي خارج الملف)

void func() {
    internal_var = 42; // استخدام محلي
}
internal_var لا يمكن الوصول اليه من ملف آخر متل file2.c حتى مع استخدام extern

2.المتغيرات الثابتة داخل الدوال(Inside function):
عندما يتم تعريف متغير ثابت باستخدام static فإن هذا المتغير يحتفظ بقيمته بين استدعاءات الدالة.
عادةً المتغيرات المحلية تفقد قيمتها بمجرد الخروج من الدالة، ولكن المتغيرات الثابتة داخل الدالة تبقى موجودة طوال عمر البرنامج ويتم تهيئته مرة واحدة فقط (بأول مرة يتم استدعاء الدالة فيها).
مثال:
C:
void counter() {
    static int count = 0; // تهيئة مرة واحد فقط
    count ++;
    printf("Count: %d\n", count);
}
عند استدعاء counter() أول مرة بيكون count=1, وعند استدعائها تاني مرة بيصير count=2 (لأن متل ما قلنا بتنحفظ القيمة).

3.الدوال الثابتة(static function):
يمكن استخدام static ايضا مع الدوال.
والدالة الثابتة تكون مرئية فقط داخل الملف الذي تم تعريفها به. أي لا يمكن استدعاؤها من ملفات اخرى. هذا يساعد على تنظيم الكود واخفاء تفاصيل البرنامج.
مثال:
C:
static void helper() { // دالة ثابتة محلية (فقط لهاد الملف)
    // ...
}

التهيئة التلقائية للدوال الثابتة:
المتغيرات الثابتة غير المهيئة تهيىء تلقائياً إلى صفر
مثال:
C:
static int x;  // x = 0
static int *p;  // p = NULL

وموقع تخزين الدوال الثابتة يكون في .data segment

وهذا جدول يوضح الفرق بين الدوال الثابتة والدوال الخارجية:

WhatsApp Image 2025-06-29 at 11.15.14 PM.webp




1.4 العودية (recursion):

عبارة عن اسلوب برمجي تستدعي فيه الدالة نفسها لحل المشكلة.
(يعني نحنا منقسم المشكلة الاساسية لمسائل فرعية أصغر من نفس نوع المشكلة حتى تصير هي المسائل الفرعية بسيطة ويمكن حلها بطريقة رياضية مثلاً).

وبتعتمد العودية على مبدأين اساسيين:
1.الحالة الاساسية (base case):
هي بتكون حالة توقف العودية ، عادة بتكون هي ابسط حالة يمكن حلها مباشرة متل (0! = 1).

2.الخطوة التكرارية (recursive step):
هون بيتم تقسيم المشكلة لجزء بسيط + استدعاء أصغر لنفس الدالة.

مثال حساب العاملي (factirial):
وخلينا نتفق انو قاعدة العاملي الاساسية هي 0! = 1 والـ(!) هي اشارة العاملي.
C:
#include <stdio.h>

int factorial(int n) {
    if (n == 0) // الحالة الاساسية
        return 1;
    else // الخطوة التكرارية
        return n * factorial(n-1); //عودية
}

int main() {
    printf("%d! = %d\n", 3, factorial(3)); // يطبع 6
    return 0;
}

كيف عم تعمل خطوة بخطوة:
كود:
1.factorial(3): 3!=0 -> 3 * factorial(2)
2.factorial(2): 2!=0 -> 2 * factorial(1)
3.factorial(1): 1!=0 -> 1* factorial(0)
4.factorial(0): 0==0 -> 1

بالرجعة لهي الخطوات:
1*1 = 1-> 1*2 = 2 -> 2*3 = 6.

ان شاء الله تكون وضحت هي المفاهيم الي رح تكون معنا بكل برنامج رح نكودو اعتذر اذا عم تكون المقالات طويلة ولكن هذا الحق العلم ويجب ان نوفيه حقه.

لا تنسونا من صالح دعائكم وصلوا عالنبي السلام عليكم.
 
التعديل الأخير:

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

عودة
أعلى