تصحيح الأخطاء وأمن الذاكرة
درس 6 من 8
6تصحيح الأخطاء وأمن الذاكرة
الآن بعد أن تتعامل مع الذاكرة يدويًا، تحتاج أدوات احترافية تكشف أخطاءها قبل أن تتحوّل لثغرات أو انهيارات في الإنتاج.
ماذا تشاهد في الرسم: تتحقّق من المدخلات أولًا، ثم توقف البرنامج في gdb لتراه من الداخل، ثم يكشف valgrind أخطاء الذاكرة قبل الإصلاح.
خارطة هذا الجزء
المفاهيم بالترتيب، اضغط أيًّا منها للقفز إليه:
طريقان إلى نفس الفكرة
الأخطاء في الذاكرة مثل تسرّب ماء صغير خلف الجدار: لا تراه اليوم، لكنه يخرّب البيت بعد شهر. تحتاج أجهزة كشف تسمع ما لا تراه العين، تخبرك أين الجدار المبتل قبل أن ينهار. وفي البرامج بعض هذه التسربات تصير ثغرات يستغلها المهاجمون.
gdb يوقف البرنامج ويفحص المكدّس والمتغيرات، وvalgrind يكشف التسرّب والقراءة من ذاكرة غير مهيأة، و-fsanitize=address يلتقط تجاوز الحدود لحظة حدوثه. وفيض المخزن المؤقت buffer overflow يتحول إلى ثغرة أمنية، فتستعمل دوال محدودة الطول مثل snprintf.
الفكرة الجوهرية: ثلاث طبقات حماية
1التحقق من المدخلات (Secure Input)
أي بيانات تدخل برنامجك من مصدر خارجي (مستخدم، ملف، شبكة) يجب اعتبارها غير موثوقة حتى تتحقق منها: هل الطول ضمن الحدود المتوقعة؟ هل النوع صحيح؟ لا تفترض أبدًا أن المدخل "سيكون سليمًا".
2GDB: مصحّح تفاعلي
يوقف تنفيذ برنامجك عند نقطة محددة (Breakpoint) ويتيح لك فحص قيمة كل متغيّر، والتقدّم سطرًا سطرًا (Step)، لمشاهدة اللحظة بالضبط التي ينحرف فيها البرنامج عن توقعك.
3Valgrind: كاشف أخطاء الذاكرة
يشغّل برنامجك داخل بيئة مراقبة تكتشف تسريبات الذاكرة، القراءة/الكتابة خارج الحدود المخصَّصة، واستخدام ذاكرة غير مُهيّأة، أخطاء قد "تعمل بالصدفة" أثناء الاختبار لكنها كارثية لاحقًا.
كيف يعمل، خطوة بخطوة: جلسة تصحيح نموذجية
| الأمر | ماذا يفعل |
|---|---|
gdb ./program | يفتح البرنامج داخل GDB دون تشغيله بعد |
break main | يضع نقطة توقّف عند بداية main |
run | يبدأ التنفيذ الفعلي حتى أول نقطة توقّف |
next / step | ينفّذ السطر الحالي وينتقل للتالي (step يدخل داخل الدوال المستدعاة، next يتخطاها) |
print variable | يعرض القيمة الحالية لمتغيّر معيّن في تلك اللحظة بالضبط |
# تشغيل Valgrind لفحص تسريبات الذاكرة valgrind --leak-check=full ./program # جزء نموذجي من الناتج عند وجود تسريب: # ==12345== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1 # ==12345== at 0x...: malloc # ==12345== by 0x...: create_array (main.c:12)
شكل 1، "definitely lost" يعني بالضبط ذاكرة حُجزت بـmalloc ولم يعد أي مؤشر في البرنامج يشير إليها، ولم تُحرَّر أبدًا: Valgrind حتى يخبرك رقم السطر بالضبط الذي حجزها.
| فئة التسريب في Valgrind | تعني |
|---|---|
| definitely lost | ذاكرة لا يوجد أي مؤشر في البرنامج يشير إليها إطلاقًا، تسريب مؤكّد 100%. |
| indirectly lost | ذاكرة فقدتَ الوصول إليها لأن الكتلة التي كانت تشير إليها (مثل عقدة في قائمة) هي نفسها "definitely lost"، تسريب تابع لتسريب آخر. |
| possibly lost | يوجد مؤشر يشير لمكان داخل الكتلة لا بدايتها تمامًا، غالبًا تسريب حقيقي، لكن Valgrind غير متأكد 100%. |
| still reachable | ذاكرة لم تُحرَّر لكن يوجد مؤشر صالح يصل إليها حتى نهاية البرنامج، ليست خطأ بالضرورة (مثل ذاكرة عامة تحتاجها طوال عمر البرنامج). |
تعمّق: فئات أخطاء الذاكرة وقراءة Valgrind و GDB
أخطاء الذاكرة ليست مجرد أخطاء برمجية بل أبواب أمنية. نفصّل هنا فئاتها، وكيف تقرأ تقرير Valgrind، وكيف تفسّر الانهيار (Segmentation Fault) سببيًا، وكيف تستخدم GDB منهجيًا، وكيف تتعامل مع الأدوات الذكية بعين ناقدة.
أولًا: ستّ فئات من أخطاء الذاكرة
| الفئة | ماذا يحدث | الأثر |
|---|---|---|
| تسريب (Leak) | حجزت كتلة وفقدت كل المؤشرات إليها دون تحريرها | استنزاف الذاكرة مع الوقت |
| مؤشر ضائع (Lost pointer) | إسناد مؤشر جديد فوق القديم قبل تحرير ما يشير إليه | تسريب لا يمكن إصلاحه لاحقًا |
| قراءة أو كتابة خارج الحدود | الوصول إلى ما بعد نهاية الكتلة أو قبل بدايتها | إفساد بيانات مجاورة أو تسريب أسرار |
| استخدام بعد التحرير (Use-after-free) | استعمال مؤشر لكتلة حُررت | قراءة بيانات غريبة أو انهيار |
| تحرير مزدوج (Double free) | استدعاء free مرتين على الكتلة نفسها | إفساد مدير الذاكرة |
| متغير غير مهيّأ | قراءة قيمة لم تُسنَد إليها قيمة | سلوك غير متوقع ومتغير |
شكل 2، دورة حياة الكتلة وأين تنشأ الأخطاء منها.
ثانيًا: مؤشر متدلٍّ يسبب فوضى: تتبّع السبب
بعد free(p) يبقى المتغير p يحمل العنوان القديم ذاته، لكن الكتلة عادت إلى النظام الذي قد يعيد منحها لحجز آخر. فإذا كتبت عبر p فقد تفسد بيانات جزء آخر من برنامجك لا علاقة له بالأمر، وتظهر المشكلة بعيدًا عن مكانها الحقيقي. لذلك تعوّد على تصفير المؤشر بعد التحرير (p = NULL) لأن تحرير NULL آمن وقراءته تنهار فورًا بشكل واضح بدل أن تفسد بصمت.
ثالثًا: كيف تقرأ تقرير Valgrind
تقرير Valgrind يخبرك بثلاثة أشياء في كل خطأ: ماذا حدث (مثلًا Invalid write of size 4)، وأين (سلسلة النداءات ببعض أسطر الملف إن ترجمت بالعلم -g)، ومن أين جاءت الكتلة (كتلة حُجزت أو حُررت هنا). اقرأ أول خطأ فقط أولًا: كثير مما يليه نتيجة له. وفي النهاية يلخّص التسريبات في ثلاث فئات: definitely lost (تسريب مؤكد)، و indirectly lost (تسريب لأن الكتلة التي تحمل مؤشرها ضاعت)، و still reachable (ما زال مرجعه في نهاية البرنامج).
| عبارة في التقرير | معناها | أين تنظر |
|---|---|---|
| Invalid read/write | وصول خارج كتلة صالحة | الفهرس أو الحلقة التي تتجاوز الحجم |
| Address is 0 bytes after a block | كتبت بعد نهاية الكتلة مباشرة | خطأ بفارق واحد أو نسيان حرف الصفر |
| Address … inside a block … free'd | استخدام بعد التحرير | المكان الذي حُررت فيه الكتلة |
| Conditional jump depends on uninitialised value | شرط يعتمد على متغير لم يُهيَّأ | تهيئة المتغير أو المصفوفة |
| definitely lost | تسريب مؤكد | سطر الحجز الظاهر في التقرير |
رابعًا: تفسير الانهيار تفسيرًا سببيًا
عبارة مثل "ينهار لأن المؤشر خاطئ" ليست تفسيرًا. التفسير الصحيح سلسلة سببية كاملة تمكّنك من التنبؤ بالنتيجة دون إعادة تشغيل البرنامج. مثال: دالة تحرر كتلة، ثم يستدعيها كود آخر يقرأ من المؤشر نفسه، فيحاول المعالج قراءة عنوان لم يعد مسموحًا، فتقطع النواة العملية بإشارة SIGSEGV.
شكل 3، السلسلة السببية لانهيار الذاكرة، من الشيفرة إلى الإشارة.
خامسًا: GDB بمنهج
المنهج الجيد في GDB أن تفترض شيئًا ثم تتحقق: قبل أن تشغّل، اكتب ما تتوقعه من المتغيرات عند نقطة معينة، ثم راقب. رتّب الأمور كالتالي: ترجم بالعلم -g لتحصل على أسماء المتغيرات وأرقام الأسطر، حدد نقطة توقف (Breakpoint) قبل الموضع المشتبه، شغّل، ثم تقدّم خطوة بخطوة وافحص القيم. وفي التحليل الديناميكي تستنتج خوارزمية دالة غامضة بمراقبة مدخلاتها ومخرجاتها وتدفق القرارات داخلها بدل التخمين من الشكل.
| الأمر | الفائدة |
|---|---|
break file.c:25 | نقطة توقف عند سطر |
run | بدء التنفيذ |
next / step | خطوة تتخطى الدالة / تدخل فيها |
print x | عرض قيمة |
backtrace | سلسلة النداءات الحالية |
watch x | توقف عندما يتغير المتغير |
شكل 4، حلقة التصحيح المنهجية في GDB: توقع، توقف، افحص، قارن.
سادسًا: إدخال آمن، وأدوات ذكية بعين ناقدة
الإدخال هو أكثر ما يستغله المهاجمون. دالة gets حُذفت من المعيار لأنها لا تقبل حجمًا. أما scanf("%s") بلا عرض فتقرأ بلا حد. والنهج الآمن: اقرأ بدالة تقبل حجم المخزن مثل fgets أو getline، وتحقق من القيمة المُعادة، واعتبر كل مدخل غير موثوق حتى تتحقق من طوله ونوعه ومداه.
وفي المشاريع التي تسمح بالأدوات الذكية يُطلب منك أن تعتبر شرحها فرضية لا حقيقة: قد تخطئ في ملكية المؤشر، أو تغفل مسار خروج مبكرًا، أو تقترح إصلاحًا يُدخل خطأ جديدًا. احكم عليها بالأدلة: تتبّع يدوي، تقرير Valgrind، أو جلسة GDB. وإذا لم تستطع شرح خطأ الذاكرة دون إعادة تشغيل البرنامج فتلك علامة أن فهمك لم يكتمل بعد.
مشروع «AI Memory Visualizer + Valgrind Tracer»: ما المطلوب منك؟
المشروع لا يطلب منك أن ترى أن الذاكرة خاطئة، بل أن تشرح لماذا هي خاطئة بالتفصيل: أين تعيش البيانات (المكدس أم الكومة)، وكم تبقى صالحة، ومن المؤشر المسؤول عن تحريرها. تحلّل برامج صغيرة فيها تسرّب وقراءة غير صالحة وتداخل مؤشرات وانهيار، وتستعين بالذكاء الاصطناعي كمساعد تحليل فقط.
| المهمة | الفكرة التي تتدرب عليها |
|---|---|
| خرائط الذاكرة | تحويل التفكير الضمني إلى خريطة: الإطارات والمتغيرات المحلية والكومة والمؤشرات المتداخلة، مع عمر كل شيء. |
| Valgrind وتتبع الذاكرة | التمييز بين التسرّب والقراءة أو الكتابة غير الصالحة والاستخدام بعد التحرير، وربط كل تقرير بسطر وحالة ذاكرة. |
| تقرير الانهيار | شرح خطأ التقسيم (segfault) كسلسلة سببية: من الكود إلى سوء استخدام الذاكرة إلى العطل. |
| جاهزية المراجعة | مراجعة الأقران: هل تشرح من دون تشغيل البرنامج مرة أخرى؟ |
شروح الذكاء الاصطناعي فرضيات لا حقائق، وهي كثيرًا ما تبدو معقولة لكنها ناقصة أو خاطئة في سلوك الذاكرة. المطلوب مثال موثّق واحد على الأقل تكتشف فيه خطأ المساعد وتشرح لماذا هو خطأ.
«ينهار لأن المؤشر خاطئ» لا تُقبل. قل أي وصول بعينه كان غير صالح، ولماذا لم تعد تلك الذاكرة ملكًا للمؤشر.
جرّب بنفسك
أي أداة تكتشف تسريبات الذاكرة والقراءة خارج الحدود حتى لو «عمل» البرنامج بالصدفة؟
Valgrind يشغّل برنامجك داخل بيئة مراقبة. أما GDB فمصحّح تفاعلي يوقف التنفيذ عند نقاط التوقّف.
لماذا يجب اعتبار أي بيانات خارجية غير موثوقة حتى تتحقق منها؟
التحقق من الحدود والنوع هو خط الدفاع الأول ضد أخطاء الذاكرة التي تتحوّل إلى ثغرات.
ماذا يتيح لك GDB عند نقطة توقّف (Breakpoint)؟
هذه الطريقة تُظهر اللحظة التي ينحرف فيها البرنامج عمّا تتوقعه.
بطاقات مراجعة سريعة
تحقق من فهمك
لو أخبرك Valgrind أن هناك تسريب ذاكرة في سطر يحتوي malloc، لكن البرنامج "يعمل بشكل طبيعي" ظاهريًا، هل تتجاهل التحذير؟
لا، "يعمل ظاهريًا" لا يعني أن لا مشكلة؛ تسريب الذاكرة يتراكم بصمت (خصوصًا في برنامج يعمل لفترة طويلة أو يُستدعى ملايين المرات) حتى يستنفد ذاكرة النظام فعليًا لاحقًا.
بعد free(p) استعملت p فظهر خطأ بعيد عن موضعه، لماذا؟
لأن النظام قد يعيد منح الكتلة لحجز آخر فتفسد بيانات غير ذات صلة.
أي خطأ في Valgrind تقرؤه أولًا؟
الأول، فكثير مما يليه نتيجة له.
مصادر هذا الدرس
مرتّبة بالنوع. ابدأ بالتوثيق الرسمي إذا تبي المرجع الدقيق، وبالمقال أو الفيديو إذا تبي الشرح أولًا.
توثيق رسمي2
خلّصت الدرس؟
سجّل الدخول ليُحفظ تقدّمك.