> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# أخطاء شائعة في C وكشفها بالأدوات Sanitizers
- URL: https://arabroot.io/articles/اخطاء-شائعة-في-c-وكشفها-بالادوات-sanitizers/
- Published: 2026-10-31T08:06:15.000Z
- Updated: 2026-10-31T09:05:38.000Z
- Description: أربعة عشر خطأً يقع فيها من يأتي إلى C من لغة أخرى، شغلنا كلاً منها ورأينا أي أداة كشفته وأيها فاتها، ثم جربنا التغذية العشوائية على قارئ الطلبات، وختمنا بخطة قراءة من المصادر الرسمية.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, لغات البرمجة عملياً

إذا قرأت مقالات [مرحلة لغة C](https://arabroot.io/start/#stage-lang-c) حتى هنا، فقد بنيت مكتبة ضريبة لنقطة البيع في شركة ناشئة تبيع القهوة والتمور، ثم خدمة طلبات كاملة بقاعدة بيانات وخيوط. والآن لنفرض أن الشركة وظفت مطوراً قضى عشر سنوات في كتابة C لأنظمة مدمجة، وطلبت منه أن يراجع كل ما كتبه الفريق قبل أن تعتمد عليه نقطة البيع في كل الفروع. وأول ما سيقوله لك بعد المراجعة أن أخطاءك ليست أخطاء من لا يعرف البرمجة، وإنما أخطاء من يعرف لغة أخرى جيداً: أنت تقارن النصوص كما في Java، وتعد الحروف كما في Python، وتفترض أن الفحص الذي كتبته سيبقى في البرنامج كما كتبته، وتتوقع أن الخطأ سيوقف البرنامج ويخبرك. وC لا تفعل أياً من هذا.

وفي هذا المقال جمعنا أربعة عشر خطأً من هذا النوع، وكل خطأ في ملف صغير في `lab/m/` بنسختين: النسخة الخاطئة، والنسخة الصحيحة بالخيار `-DFIX`. وشغلنا كل نسخة خاطئة بالمترجم GCC 16.2 ثم مررناها على الأدوات التي تكشف هذا النوع من الأخطاء، وسجلنا ما رأته كل أداة وما فاتها، لأن السؤال العملي ليس «ما الأخطاء الممكنة في C؟»، فهي كثيرة، وإنما «أي أداة أشغل لكي لا يصل هذا الخطأ إلى الإنتاج؟». ثم نختم بتجربة تغذية عشوائية Fuzzing على قارئ الطلبات من مقال الخدمة، وبخطة قراءة من المصادر الرسمية.

وسوف نناقش في هذا المقال ما يلي:

- طبقات الأدوات Tools: تحذيرات المترجم، والتحليل الساكن Static Analysis، والفحص وقت التشغيل Sanitizers وValgrind، والتغذية العشوائية Fuzzing، ومن كشف ماذا في المختبر.
- أربعة عشر خطأً، لكل منها التشغيل الخاطئ الحقيقي والإصلاح: النصوص، والذاكرة، والأعداد، والماكرو، والحروف العربية، والحالة المخفية في الدوال.
- تجربة Fuzzing بالأداة libFuzzer لثلاثة ملايين ونصف مدخل عشوائي.
- إعدادات البناء التي ننصح بها لكل مرحلة، وخطة قراءة من المصادر الرسمية.

## لماذا تهم هذه الأخطاء؟ أرقام من مشروع curl Memory Safety

قد يبدو بعض ما سيأتي أخطاءً صغيرة لا تحدث لمبرمج منتبه، ولكن الأرقام من أكثر مشاريع C مراجعة في العالم تقول غير ذلك. ففي مارس 2021 حلل Daniel Stenberg، مؤسس مشروع curl، كل الثغرات الأمنية في تاريخ المشروع، ونشر النتيجة في مقاله [نصف ثغرات curl أخطاء C](https://daniel.haxx.se/blog/2021/03/09/half-of-curls-vulnerabilities-are-c-mistakes/?ref=arabroot.io): من 98 ثغرة كانت 51 ثغرة، أي 52%، بسبب أخطاء خاصة بلغة C، مثل تجاوز المخزن والاستخدام بعد التحرير والقراءة خارج الحدود، وفي السنوات الثلاث التي سبقت المقال كانت النسبة أعلى، 18 من 26 ثغرة. وهذا مشروع يكتبه مطورون محترفون منذ 1998، ويمر كوده على مراجعات واختبارات وأدوات فحص مستمرة. وأشهر مثال على ما يمكن أن يفعله خطأ واحد من هذا النوع ثغرة [Heartbleed](https://heartbleed.com/?ref=arabroot.io) في OpenSSL عام 2014: المكتبة صدقت طولاً جاء في رسالة من العميل ونسخت من الذاكرة بقدره، فكان المهاجم يقرأ حتى 64 KB من ذاكرة السيرفر في كل طلب، وفيها مفاتيح وكلمات مرور. وقد شرحنا في مقال [أمان الذاكرة Memory Safety](https://arabroot.io/articles/%D8%A7%D9%85%D8%A7%D9%86-%D8%A7%D9%84%D8%B0%D8%A7%D9%83%D8%B1%D8%A9-memory-safety-%D9%84%D9%85%D8%A7%D8%B0%D8%A7-%D8%AA%D8%AF%D9%81%D8%B9-%D8%A7%D9%84%D8%AD%D9%83%D9%88%D9%85%D8%A7%D8%AA-%D9%86%D8%AD%D9%88-rust/) لماذا صار هذا النوع من الأخطاء قضية تتكلم عنها الحكومات. والدرس الذي يهمنا هنا أن الانتباه وحده لا يكفي، وأن المشاريع الجادة بلغة C تعتمد على طبقات من الأدوات، وهذا ما سنقيسه.

## طبقات الأدوات Tools: من يكشف ماذا؟

الأدوات التي تكشف أخطاء C أربع طبقات، وكل طبقة تعمل في وقت مختلف وترى نوعاً مختلفاً:

- تحذيرات المترجم Compiler Warnings: الخيارات `-Wall -Wextra` في GCC وClang، وهي أرخص طبقة لأنها تعمل مع كل بناء، وتكشف ما يمكن معرفته من النص وحده، مثل مقارنة عدد بلا إشارة بصفر.
- التحليل الساكن Static Analysis: المحلل `-fanalyzer` في GCC، وclang-tidy وCppcheck اللتان رأيناهما في المقال الأول. وهي تتبع مسارات التنفيذ الممكنة دون تشغيل البرنامج، فتجد مثلاً أن مؤشراً قد يكون `NULL` في أحد الفروع، أو أن ذاكرة استخدمت بعد تحريرها.
- الفحص وقت التشغيل Sanitizers وValgrind: AddressSanitizer للذاكرة، وUndefinedBehaviorSanitizer للسلوك غير المعرف، وThreadSanitizer للخيوط، وMemorySanitizer للقيم التي لم تعط قيمة. وهي ترى ما يحدث فعلاً، ولكن فقط في المسارات التي شغلتها الاختبارات. وValgrind تفعل جزءاً كبيراً من ذلك دون إعادة البناء، بثمن بطء كبير.
- التغذية العشوائية Fuzzing: أداة تولد ملايين المدخلات وتشغل الكود بها تحت Sanitizers، فتصل إلى مسارات لم يكتب لها أحد اختباراً.

والصورة التالية تجمع نتيجة المختبر: لكل خطأ من الأربعة عشر، ماذا حدث في التشغيل العادي، وأي أداة كشفته. ولاحظ قبل أن تقرأ التفاصيل شيئين: لا توجد أداة كشفت كل الأخطاء، وخمسة أخطاء على الأقل لم تكشفها إلا أداة واحدة. وقد شغلنا تحذيرات `-Wall -Wextra` والمحلل `-fanalyzer` على الملفات الأربعة عشر كلها، والأدوات وقت التشغيل على الأخطاء التي تخصها، والمخرج كاملاً في `lab/run-output.txt` و`run-output2.txt` و`run-output3.txt`.

![جدول الأخطاء الأربعة عشر بترتيب المقال وما كشفها: مقارنة النصوص بـ == كشفها -Wall، وstrncpy كشفها -Wall وASan، وchar بإشارة كشفه -Wall و-fanalyzer وASan وUBSan، وstrlen للعربي رأت منه -Wall القطع فقط، وstrtok لم تكشفه أي أداة، وmalloc بحجم المؤشر كشفه -Wall و-fanalyzer وASan، وتحرير القائمة كشفه -Wall و-fanalyzer وASan، وrealloc كشفه -fanalyzer وValgrind، وفحص التجاوز كشفته UBSan وحدها، والعد التنازلي وصيغة printf ونسيان break كشفها -Wall -Wextra، والمتغير بلا قيمة كشفه -Wall و-fanalyzer وValgrind وMemorySanitizer، والماكرو لم تكشفه أي أداة](https://arabroot.io/content/images/2026/10/c-idioms-01-matrix.webp)

كل أداة ترى نوعاً من الأخطاء، وخطآن لم ترهما أي أداة

وقد يتساءل البعض: إذا كانت كل هذه الأدوات موجودة، فلماذا ما زالت أخطاء C تصل إلى الإنتاج في مشاريع مثل curl وOpenSSL؟ والإجابة أن كل أداة وقت التشغيل لا ترى إلا المسارات التي شغلتها، وأغلب الثغرات تعيش في مسارات لا يصل إليها الاختبار العادي: رسالة بطول غريب، أو ترميز نادر، أو خطأ في الشبكة في لحظة معينة. وثغرة Heartbleed نفسها كانت في مسار يعمل كل يوم، ولكن بطول يكذب فيه المرسل، وهو مدخل لا يكتبه أحد في اختبار. ولهذا جاءت التغذية العشوائية بعد أن صارت Sanitizers رخيصة، فالأولى تولد المسارات الغريبة، والثانية تكشف ما يحدث فيها، وكل منهما وحدها أضعف بكثير. وهذا أيضاً سبب أن الحل الذي تدفع نحوه الحكومات والشركات الكبيرة اليوم ليس أدوات أفضل لـ C، وإنما لغات تمنع هذه الفئة من الأخطاء من البداية، كما شرحنا في مقال أمان الذاكرة.

### كيف تعمل هذه الأدوات من الداخل؟ Shadow Memory and Instrumentation

معرفة طريقة عمل كل أداة تشرح لك لماذا تكشف خطأً وتفوت آخر، ولماذا لا تجمع بعضها في بناء واحد. فـ AddressSanitizer تضيف المترجم حول كل قراءة وكتابة فحصاً صغيراً في ذاكرة ظل Shadow Memory، كل بايت فيها يصف ثمانية بايتات من الذاكرة الحقيقية، وتضع حول كل متغير وكل قطعة محجوزة مناطق محرمة Redzones، وتؤخر إعادة استخدام القطع المحررة في حجر صحي Quarantine لكي يظهر الاستخدام بعد التحرير. ولهذا تكشف القراءة بعد نهاية القطعة بايتاً واحداً، ولا تكشف التجاوز بين حقلين داخل نفس الهيكل، لأنه لا توجد منطقة محرمة بينهما، وهو ما رأيناه في مقال الأنواع والذاكرة. وثمنها بحسب [توثيقها](https://clang.llvm.org/docs/AddressSanitizer.html?ref=arabroot.io) بطء بمقدار الضعف تقريباً. أما UndefinedBehaviorSanitizer فتضيف فحصاً حول العمليات التي قد تكون غير معرفة فقط، مثل الجمع بإشارة والإزاحة والفهرسة في مصفوفة معروفة الحجم، فهي أرخص بكثير، ويمكن جمعها مع ASan في نفس البناء كما فعلنا. وMemorySanitizer تتبع لكل بت في الذاكرة هل أعطي قيمة أم لا، وثمنها بطء ثلاثة أضعاف كما في [توثيقها](https://clang.llvm.org/docs/MemorySanitizer.html?ref=arabroot.io)، وتحتاج أن تبنى كل المكتبات التي يستخدمها البرنامج بها لكي لا تعطي إنذارات خاطئة، ولهذا هي أقل استخداماً. وThreadSanitizer تسجل كل وصول للذاكرة مع الأقفال التي حوله، وثمنها بطء بين خمسة وخمسة عشر ضعفاً وذاكرة بين خمسة وعشرة أضعاف بحسب [توثيقها](https://clang.llvm.org/docs/ThreadSanitizer.html?ref=arabroot.io)، ولا تجتمع مع ASan في بناء واحد لأن كلاً منهما تحتاج ذاكرة الظل بطريقتها.

أما Valgrind فمختلفة تماماً: لا تحتاج إعادة بناء، وإنما تشغل البرنامج الجاهز على معالج افتراضي تترجم فيه كل تعليمة وتضيف إليها الفحص وقت التشغيل. ولهذا تعمل على أي ملف تنفيذي، حتى المكتبات التي لا تملك كودها، وهي الأداة التي وجدت تسرب اتصالات قاعدة البيانات في مقال الخدمة. وثمنها كبير، فـ [دليل البداية](https://valgrind.org/docs/manual/quick-start.html?ref=arabroot.io) يقول إن البرنامج يصبح أبطأ عشرين إلى ثلاثين مرة، وفي مقال الأنواع والذاكرة رأينا برنامجاً يأخذ ثانية ونصفاً تقريباً يصبح 77 ثانية تحت Valgrind. فالقاعدة العملية: Sanitizers في كل تشغيل للاختبارات لأنها رخيصة نسبياً، وValgrind في تشغيل ليلي على سيناريو أطول، أو عندما تحتاج فحص مكتبة لا تستطيع إعادة بنائها.

## أخطاء النصوص Strings

### 1\. مقارنة النصوص بالرمز == String Comparison

في Java تعلمت أن `==` للنصوص خطأ، ثم نسيت ذلك في Python وJavaScript حيث تعمل. وفي C الخطأ أوضح: النص مصفوفة، واسم المصفوفة عنوان، فـ `cmd == "pay"` تقارن عنوانين لا محتوى:

```c
    const char *cmd = argc > 1 ? argv[1] : "pay";
    if (cmd == "pay") puts("paying");            // compares two addresses
```

```text
$ /tmp/b/m01 pay
unknown command
exit 0
```

المستخدم كتب `pay` بالضبط، والبرنامج قال «أمر غير معروف»، لأن النص الذي جاء من سطر الأوامر في عنوان، والنص الثابت `"pay"` في عنوان آخر. وبدون خيارات لم يقل المترجم شيئاً، ومع `-Wall` ظهر التحذير الذي يكفي لكشفه: `comparison with string literal results in unspecified behavior [-Waddress]`. والإصلاح `strcmp(cmd, "pay") == 0`، والنسخة المصلحة طبعت `paying`. ولاحظ الشكل `== 0`: الدالة `strcmp` ترجع صفراً عند التساوي، فكتابة `if (strcmp(a, b))` تعني «إذا كانا مختلفين»، وهذا خطأ آخر يقع فيه كثيرون.

### 2\. الدالة strncpy لا تضع نهاية النص Null Terminator

من سمع أن `strcpy` خطرة ينتقل إلى `strncpy` لأن فيها حداً، وهذا انتقال من خطأ إلى خطأ. فرمز المنتج `SKU-12345` تسعة حروف، والمخزن ثمانية بايتات:

```c
    char next[8] = "XXXXXXX";
    char sku[8];
    const char *input = "SKU-12345";
    strncpy(sku, input, sizeof sku);            // copies 8 bytes, no terminator
```

```text
$ /tmp/b/m02
sku=[SKU-1234XXXXXXX] (strlen 15) next=XXXXXXX
exit 0
```

الدالة نسخت ثمانية بايتات بالضبط ولم تضع الصفر في النهاية، لأن صفحة [strncpy(3)](https://man7.org/linux/man-pages/man3/strncpy.3.html?ref=arabroot.io) تقول إنها لا تضعه إذا كان النص بطول الحد أو أطول. فلما طبعنا الرمز أكمل `printf` القراءة بعد نهاية المخزن حتى وجد صفراً، فطبع الرمز ومعه محتوى المتغير المجاور، و`strlen` قالت 15 لمخزن حجمه 8\. وفي برنامج حقيقي قد تكون البايتات التالية كلمة مرور أو رمزاً سرياً. والمترجم حذر هنا مع `-Wall -Wextra`: `'strncpy' output truncated copying 8 bytes from a string of length 9 [-Wstringop-truncation]`، وASan أوقفت البرنامج بخطأ `stack-buffer-overflow` عند القراءة. والإصلاح `snprintf(sku, sizeof sku, "%s", input)` التي تقطع وتضع النهاية دائماً، فطبعت النسخة المصلحة `SKU-123` بطول 7، ومع ذلك حذر المترجم بأن النص قطع `[-Wformat-truncation=]`، وهو تحذير مفيد: القطع الصامت لرمز منتج خطأ في المنطق حتى لو لم يكن خطأ ذاكرة، فإما أن تكبر المخزن أو ترفض الرمز الطويل.

### 3\. الحرف char بإشارة والنص العربي Signed char

هذا الخطأ لن تراه في الأمثلة الإنجليزية في الكتب، وستراه في أول يوم تعالج فيه أسماء المنتجات العربية. البرنامج يعد تكرار كل بايت في اسم المنتج في جدول من 256 خانة:

```c
static int counts[256];
    const char *name = "قهوة";
    for (const char *p = name; *p; p++) {
        counts[*p]++;                       // *p is negative for bytes 0x80..0xFF
    }
```

```text
$ /tmp/b/m10
exit 139
Segmentation fault (core dumped)
m10_signedchar.c:10:15: runtime error: index -39 out of bounds for type 'int [256]'
==203==ERROR: AddressSanitizer: global-buffer-overflow on address 0x0000004048c4 at pc 0x00000040133d bp 0x7ffe1a69b3b0 sp 0x7ffe1a69b3a8
    #0 0x00000040133c in main /lab/m/m10_signedchar.c:10
```

الحروف العربية في UTF-8 بايتان، وكلاهما أكبر من 127، فالقاف مثلاً تبدأ بالبايت 0xD9\. ولأن `char` بإشارة على x86 كما رأينا في مقال الأنواع، صار 0xD9 العدد 39-، فكتب البرنامج قبل بداية الجدول، وانهار برمز 139\. وUBSan قالت ذلك بالرقم: `index -39 out of bounds`. والمترجم حذر مع `-Wall`: `array subscript has type 'char' [-Wchar-subscripts]`. والإصلاح أن تحول البايت إلى `unsigned char` قبل استخدامه فهرساً أو تمريره إلى دوال مثل `isalpha` و`toupper`، فطبعت النسخة المصلحة `byte 0xD9 seen 3 times`، وهي القاف والهاء والواو، أما التاء المربوطة فتبدأ بالبايت 0xD8.

### 4\. الدالة strlen تعد البايتات لا الحروف UTF-8

القادم من Python حيث `len("قهوة عربية")` تعطي 10 سوف يحسب طول الاسم بالدالة `strlen` ويعرضه في واجهة نقطة البيع أو يقطعه ليتسع في سطر الفاتورة:

```text
$ /tmp/b/m11
strlen = 19 bytes
first 7 bytes: [قهو�]
$ /tmp/b/m11f
strlen = 19 bytes
mbstowcs = 10 characters
first 7 bytes: [قهو�]
```

الاسم عشرة حروف بالمسافة، ولكنه 19 بايتاً، تسعة حروف عربية في بايتين لكل منها ومسافة في بايت. وعندما قطعنا الاسم عند سبعة بايتات لكي يتسع في مخزن من ثمانية، انقطعت التاء المربوطة في منتصفها بعد أن أخذت القاف والهاء والواو ستة بايتات، فظهر في الطرفية رمز الاستبدال �. والمترجم مع `-Wall` حذر من القطع نفسه: `'%s' directive output truncated writing 19 bytes into a region of size 8`، ولكنه لا يعرف شيئاً عن الحروف، فلا أداة تقول لك إن القطع قسم حرفاً أو إن الطول عشرة لا تسعة عشر، لأن هذا ليس خطأ في نظر اللغة، فكل بايت في مكانه. والنسخة المصلحة تعد الحروف بالدالة [mbstowcs](https://man7.org/linux/man-pages/man3/mbstowcs.3.html?ref=arabroot.io) بعد ضبط اللغة على `C.UTF-8` فأعطت 10، ولكن لاحظ أن القطع ما زال خاطئاً في النسخة المصلحة نفسها، لأن إصلاحه يحتاج قطعاً على حدود الحروف لا البايتات. والقاعدة: في C النص بايتات، وكل ما يخص الحروف، من الطول إلى القطع إلى المقارنة دون حالة الأحرف، يحتاج مكتبة تفهم Unicode مثل ICU، أو كوداً تكتبه وتختبره بنصوص عربية حقيقية.

![النص العربي في C: كلمة قهوة ثمانية بايتات بترميز UTF-8 تبدأ القاف بالبايت 0xD9 الذي يصبح -39 في char بإشارة فيكتب البرنامج خارج الجدول وينهار برمز 139 وتقول UBSan index -39، وقهوة عربية 19 بايتاً و10 حروف بحسب mbstowcs، والقطع عند سبعة بايتات يقسم الواو فيظهر رمز الاستبدال](https://arabroot.io/content/images/2026/10/c-idioms-03-arabic.webp)

في C النص بايتات، والحرف العربي بايتان كلاهما فوق 127

### 5\. الدالة strtok والحالة المخفية Hidden State

ملف الطلبات من نقطة البيع يفصل الطلبات بفاصلة منقوطة، وسطور الطلب الواحد بفاصلة. والحل الذي يكتبه كل مبتدئ حلقتان بالدالة `strtok`:

```c
    char orders[] = "1:990,2:2500;3:1575";
    for (char *o = strtok(orders, ";"); o; o = strtok(NULL, ";"))
        for (char *l = strtok(o, ","); l; l = strtok(NULL, ",")) { printf("line %s\n", l); lines++; }
```

```text
$ /tmp/b/m13
line 1:990
line 2:2500
2 lines (expected 3)
```

ضاع الطلب الثاني كله دون أي رسالة. والسبب أن `strtok` تحفظ مكانها في النص في متغير داخلي مخفي، كما تشرح صفحة [strtok(3)](https://man7.org/linux/man-pages/man3/strtok.3.html?ref=arabroot.io)، فلما استخدمتها الحلقة الداخلية على الطلب الأول نسيت الحلقة الخارجية أين كانت، و`strtok(NULL, ";")` أكملت من حيث توقفت الداخلية فلم تجد شيئاً. ولم تكشفه أي أداة، لأنه خطأ في المنطق لا في الذاكرة. والإصلاح `strtok_r` التي تأخذ مكانها في متغير تمرره أنت، فطبعت النسخة المصلحة السطور الثلاثة. ونفس الحالة المخفية تجعل `strtok` غير آمنة مع الخيوط، ومثلها دوال قديمة أخرى ترجع مؤشراً إلى مخزن داخلي ثابت مثل `strerror` و`localtime`، ولكل منها نسخة `_r` تأخذ مخزنك أنت. والقاعدة: إذا كانت الدالة تتذكر شيئاً بين استدعاءين، فابحث عن نسختها التي لا تتذكر.

## أخطاء الذاكرة Memory

### 6\. حجم خاطئ في malloc: حجم المؤشر بدلاً من حجم البيانات sizeof

```c
typedef struct { long id; long price; long qty; } Line;
    Line *lines = malloc(n * sizeof(Line *));   // 4 * 8 = 32 bytes for 4 * 24 bytes of data
```

```text
m03_mallocsize.c:15:37: warning: array subscript 3 is outside array bounds of 'Line[1]' [-Warray-bounds=]
$ /tmp/b/m03
last price 990
exit 0
==61==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7c23bc7e0060 at pc 0x00000040123e bp 0x7ffe5b5b31e0 sp 0x7ffe5b5b31d8
WRITE of size 8 at 0x7c23bc7e0060 thread T0
    #0 0x00000040123d in main /lab/m/m03_mallocsize.c:14
0x7c23bc7e0060 is located 0 bytes after 32-byte region [0x7c23bc7e0040,0x7c23bc7e0060)
```

نجمة واحدة زائدة في `sizeof(Line *)` جعلت الحجز 32 بايتاً لبيانات حجمها 96، والبرنامج كتب فوق ذاكرة ليست له، ومع ذلك طبع النتيجة الصحيحة وخرج برمز 0، لأن ما كتب فوقه لم يستخدمه أحد بعد. وهذا أخطر ما في هذا النوع: يعمل في الاختبار، ويفسد بيانات الحجز التالي في الإنتاج. والمترجم انتبه هذه المرة مع `-Wall` لأن العدد ثابت والحلقة صغيرة، والمحلل `-fanalyzer` قال `heap-based buffer overflow [CWE-122]`، وASan أعطت التفاصيل كاملة: كتابة بحجم 8 بايت مباشرة بعد منطقة حجمها 32 بايتاً. والإصلاح الذي يعتمده كثير من مطوري C أسلوب لا يمكن أن يخطئ في النوع: `malloc(n * sizeof *lines)`، فالحجم يتبع نوع المتغير نفسه، وإذا تغير النوع يوماً تغير الحجم معه.

### 7\. تحرير قائمة مترابطة: القراءة بعد free Use After Free

```c
    for (Node *n = head; n; n = n->next) free(n);          // n->next is read from freed memory
```

```text
$ /tmp/b/m12
Segmentation fault (core dumped)
exit 139
==235==ERROR: AddressSanitizer: heap-use-after-free on address 0x7bdaa84e0058 at pc 0x00000040129a bp 0x7ffc14d74580 sp 0x7ffc14d74578
READ of size 8 at 0x7bdaa84e0058 thread T0
    #0 0x000000401299 in main /lab/m/m12_listfree.c:16
m12_listfree.c:11:15: warning: dereference of possibly-NULL 'n' [CWE-690] [-Wanalyzer-possible-null-dereference]
m12_listfree.c:16:31: warning: use after 'free' of 'n' [CWE-416] [-Wanalyzer-use-after-free]
```

الحلقة تبدو طبيعية جداً، ولكن خطوتها `n = n->next` تعمل بعد `free(n)`، أي أنها تقرأ المؤشر التالي من عقدة محررة. وفي مقال إدارة الذاكرة رأينا أن قراءة ذاكرة محررة قد تعطي قيمة خاطئة بصمت، وهنا كانت النتيجة انهياراً برمز 139 في البناء المحسن، لأن glibc تكتب بياناتها الإدارية في أول ستة عشر بايتاً من القطعة المحررة، والحقل `next` يقع فيها بعد الرقم والحشو، فقرأت الحلقة عنواناً لا معنى له. وكشفته ثلاث أدوات: ASan وقت التشغيل بالسطر 16، والمحلل الساكن `-fanalyzer` قبل التشغيل برقم CWE-416، وحتى `-Wall` وحده في GCC 16 بالتحذير `pointer 'n' used after 'free' [-Wuse-after-free=]`. ولاحظ أن المحلل وجد خطأً ثانياً لم نقصده: في السطر 11 لا نتحقق من أن `malloc` لم ترجع `NULL`، وهو محق. والإصلاح أن تحفظ التالي قبل التحرير: `for (Node *n = head, *next; n; n = next) { next = n->next; free(n); }`.

### 8\. السطر p = realloc(p, n): تسرب Memory Leak عند الفشل

```c
static char *grow(char *buf, size_t n) {
    buf = realloc(buf, n);              // on failure buf becomes NULL and the old block leaks
    return buf;
}
```

```text
buf=(nil)
==268== 16 bytes in 1 blocks are definitely lost in loss record 1 of 1
==268==    by 0x401194: main (m14_realloc.c:18)
==268==    definitely lost: 16 bytes in 1 blocks
m14_realloc.c:19:5: warning: use of possibly-NULL 'buf' where non-null expected [CWE-690] [-Wanalyzer-possible-null-argument]
```

طلبنا حجماً مستحيلاً لكي تفشل `realloc`، فرجعت `NULL` وتركت الكتلة القديمة كما هي، كما تقول صفحة [realloc](https://man7.org/linux/man-pages/man3/realloc.3p.html?ref=arabroot.io). ولأننا كتبنا النتيجة فوق المؤشر الوحيد إلى الكتلة، صارت 16 بايتاً لا يصل إليها أحد، وValgrind سمتها `definitely lost`. وفي الإنتاج يحدث هذا عندما يأتي الحجم من مدخل خاطئ، أو في Container يقترب من حد ذاكرته. والإصلاح الذي رأيناه في مقال الأنواع والذاكرة: متغير مؤقت، ثم قرار واضح عند الفشل، فخرجت النسخة المصلحة تحت Valgrind بالسطر `All heap blocks were freed -- no leaks are possible`.

![أربعة أخطاء ذاكرة من المختبر: strncpy طبعت SKU-1234XXXXXXX بطول 15 لمخزن من 8 وكشفتها ASan، وmalloc بحجم المؤشر حجز 32 بايتاً لـ 96 وطبع النتيجة الصحيحة برمز 0 وكشفته ASan heap-buffer-overflow، وتحرير القائمة انهار برمز 139 وكشفته ASan و-fanalyzer CWE-416، وrealloc الفاشلة سربت 16 بايتاً كشفتها Valgrind](https://arabroot.io/content/images/2026/10/c-idioms-04-memory.webp)

ثلاثة من الأربعة خرجت بنجاح أو بنتيجة صحيحة في التشغيل العادي

## أخطاء الأعداد Integers

### 9\. فحص التجاوز بعد حدوثه: المترجم حذف الفحص Signed Overflow

هذا أهم خطأ في المقال، لأنه يصيب المبرمج الحريص بالذات. الدالة تضيف كمية إلى المخزون، والمبرمج يعرف أن الجمع قد يتجاوز الحد، فكتب فحصاً يبدو منطقياً تماماً: إذا صار المجموع أصغر من المخزون الأصلي فقد التف العدد:

```c
static int add_stock(int stock, int delta, int *out) {
    if (stock + delta < stock) return -1;   // overflow is undefined, so the compiler may delete this
    *out = stock + delta;
    return 0;
}
    if (add_stock(INT_MAX, delta, &r)) puts("overflow detected");
    else printf("new stock %d\n", r);
```

```text
$ /tmp/b/m04o0
new stock -2147483648
exit 0
$ /tmp/b/m04o2
new stock 2147483647
exit 0
```

أضفنا 1 إلى أكبر عدد ممكن. والفحص لم يكتشف التجاوز في البناءين، والأغرب أن البناءين أعطيا نتيجتين مختلفتين: بدون تحسين التف العدد إلى أصغر عدد سالب، ومع `-O2` طبع البرنامج الحد الأقصى نفسه، كأن الجمع لم يحدث. ولكي نفهم نظرنا في ما أنتجه المترجم. بدون تحسين، هذه مقارنة الدالة:

```text
add_stock:
	cmpl	$0, -8(%rbp)
	jns	.L2
```

المترجم لم يحسب `stock + delta` ثم يقارن، وإنما قارن `delta` بالصفر مباشرة. فهو حول الشرط `stock + delta < stock` إلى `delta < 0`، وهذا التحويل صحيح رياضياً في الأعداد التي لا تلتف، والمعيار يقول إن التفاف الأعداد بإشارة سلوك غير معرف، أي أن المترجم يحق له أن يفترض أنه لا يحدث. فصار الفحص «هل الكمية سالبة؟»، وهو فحص لا علاقة له بالتجاوز. ومع `-O2` ذهب المترجم أبعد، وهذا جزء من `main` بعد التحسين:

```text
	call	__isoc23_strtol
	testl	%eax, %eax
	js	.L7
	movl	$2147483647, %esi
	movl	$.LC0, %edi
	xorl	%eax, %eax
	call	printf
```

بعد قراءة الكمية بالدالة `strtol`، يفحص المترجم هل هي سالبة، وإذا لم تكن يطبع الرقم الثابت 2147483647 مباشرة. والمنطق من وجهة نظره: المخزون هو `INT_MAX`، والكمية ليست سالبة، والجمع لا يمكن أن يتجاوز الحد لأن ذلك غير معرف، فالكمية لا بد أن تكون صفراً، والنتيجة لا بد أن تكون `INT_MAX`. فالمترجم لم يحذف الفحص فقط، وإنما استخدم افتراض «لا تجاوز» ليحذف الجمع كله. ولم يحذر أي خيار تحذير من هذا، وكشفه UndefinedBehaviorSanitizer فقط:

```text
m04_overflowcheck.c:9:15: runtime error: signed integer overflow: 1 + 2147483647 cannot be represented in type 'int'
overflow detected
```

والإصلاح ألا تفحص بعد العملية أبداً، وإنما أن تستخدم عملية تخبرك بالتجاوز قبل حدوثه. وفي C23 صار لذلك ترويسة قياسية `stdckdint.h` فيها `ckd_add` و`ckd_sub` و`ckd_mul`، وهي مبنية على الدوال المدمجة التي يوفرها GCC منذ زمن، كما في [توثيق دوال فحص التجاوز في GCC](https://gcc.gnu.org/onlinedocs/gcc/Integer-Overflow-Builtins.html?ref=arabroot.io):

```c
#include <stdckdint.h>
    return ckd_add(out, stock, delta) ? -1 : 0;   // C23 <stdckdint.h>: checked arithmetic
```

والنسخة المصلحة طبعت `overflow detected` في كل مستوى تحسين. وتذكر هذا المثال كلما قرأت أن السلوك غير المعرف «يعطي قيمة غريبة»، فهو قد يعطي قيمة صحيحة المظهر، أو يحذف الكود الذي كتبته لتحمي نفسك.

![فحص التجاوز stock + delta أصغر من stock: بدون تحسين حوله المترجم إلى cmpl $0 أي delta أصغر من صفر وطبع -2147483648، ومع -O2 طبع الثابت 2147483647 مباشرة بعد فحص إشارة الكمية، وUBSan وحدها قالت signed integer overflow: 1 + 2147483647، والإصلاح ckd_add من stdckdint.h في C23 طبع overflow detected](https://arabroot.io/content/images/2026/10/c-idioms-02-overflow.webp)

الفحص الذي يعتمد على سلوك غير معرف لا يبقى في البرنامج

### 10\. العد التنازلي بعدد بلا إشارة Unsigned Loop

```c
    for (size_t i = n - 1; i >= 0; i--) {       // size_t is never negative: 0 - 1 wraps around
```

```text
m05_unsignedloop.c:9:30: warning: comparison of unsigned expression in '>= 0' is always true [-Wtype-limits]
$ /tmp/b/m05
i=2
i=1
i=0
i=18446744073709551615
i=18446744073709551614
... still looping
```

الحلقة طبعت 2 ثم 1 ثم 0، ثم بدل أن تتوقف طرحت 1 من الصفر فصار العدد بلا إشارة أكبر عدد ممكن، والشرط `i >= 0` صحيح دائماً لعدد بلا إشارة، فلن تتوقف أبداً، وأوقفناها نحن بعداد بعد خمس دورات. وفي الكود الحقيقي تقرأ الحلقة `prices[18446744073709551615]` في الدورة الرابعة وتنهار. وعلى عكس الأعداد بإشارة، التفاف الأعداد بلا إشارة سلوك معرف في المعيار، ولهذا لا تنفع فيه UBSan، ولكن `-Wextra` يكشفه وقت الترجمة بالتحذير `-Wtype-limits`. والإصلاح الذي يستخدمه مطورو C هو الشكل `for (size_t i = n; i-- > 0;)`، الذي يفحص قبل أن يطرح، وطبعت النسخة المصلحة العناصر الثلاثة بترتيب عكسي.

### 11\. صيغة printf الخاطئة Format String

```c
    int64_t total = 5000000123LL;
    size_t items = 3;
    printf("total %d halalas, items %d\n", total, items);
```

```text
$ /tmp/b/m09
total 705032827 halalas, items 3
```

خمسة مليارات هللة طبعت 705 ملايين، لأن `%d` تقرأ أربعة بايتات من قيمة حجمها ثمانية، فأخذت النصف الأسفل. وبدون خيارات لم يقل المترجم شيئاً، ومع `-Wall` حذر مرتين، مرة لكل معامل: `format '%d' expects argument of type 'int', but argument 2 has type 'int64_t'`، لأن GCC يعرف صيغ `printf` ويقارنها بأنواع المعاملات. ولكن التحذير لا يظهر إذا كانت الصيغة متغيراً لا نصاً ثابتاً، ولهذا السبب نفسه لا تمرر أبداً نصاً من مستخدم كصيغة، فـ `printf(user_input)` ثغرة معروفة تسمح بقراءة الذاكرة والكتابة فيها. والإصلاح الصيغ الصحيحة: `%zu` لـ `size_t`، و`PRId64` من `inttypes.h` لـ `int64_t`، لأن `%ld` تصلح على Linux وتخطئ على Windows حيث `long` أربعة بايتات. والنسخة المصلحة طبعت `total 5000000123 halalas, items 3`.

## أخطاء في اللغة نفسها Language Traps

### 12\. نسيان break في switch: الانتقال إلى الحالة التالية Fall Through

```c
    switch (zone) {
    case 1: fee = 1500;
    case 2: fee = 2500; break;
    default: fee = 4000;
    }
```

```text
$ /tmp/b/m06
zone 1 fee: 2500
m06_fallthrough.c:7:17: warning: this statement may fall through [-Wimplicit-fallthrough=]
```

رسوم الشحن للمنطقة الأولى 15 ريالاً، والبرنامج أخذ 25، لأن الحالة الأولى أكملت إلى الثانية بعد أن نسي المبرمج `break`. والخيار `-Wextra` يحذر من ذلك بالتحذير `-Wimplicit-fallthrough`. وإذا كان الانتقال مقصوداً فاكتب ذلك صراحة بالخاصية `[[fallthrough]];` من C23، فيسكت التحذير ويعرف من يقرأ الكود بعدك أنك قصدته.

### 13\. متغير بلا قيمة أولى Uninitialized Variable

```c
static int total_of(const int *prices, int n) {
    int total;                  // whatever was on the stack
    for (int i = 0; i < n; i++) total += prices[i];
    return total;
}
```

```text
$ /tmp/b/m07o0
total 3490
$ /tmp/b/m07o2
total 3490
m07_uninit.c:10:39: warning: 'total' is used uninitialized [-Wuninitialized]
==154== Conditional jump or move depends on uninitialised value(s)
==154==    at 0x4011A5: main (m07_uninit.c:17)
```

النتيجة صحيحة في البناءين، 3490، والسبب صدفة: المكان في ال Stack كان صفراً في هذا التشغيل. وهذا ما يجعل هذا الخطأ مزعجاً، فهو يعمل حتى يتغير شيء بعيد، كدالة استدعيت قبله أو مستوى تحسين أو مترجم. والمترجم حذر مع `-Wall`، والمحلل `-fanalyzer` برقم CWE-457، وValgrind أشارت إلى السطر 17 حيث استخدمت القيمة في شرط. ثم جربنا MemorySanitizer، الأداة المتخصصة في هذا النوع، وهي في Clang فقط، فواجهتنا مشكلتان حقيقيتان: الأولى أن الرابط فشل بالرسالة `cannot find .../libclang_rt.msan.a`، لأن مكتبة الأداة في Debian في حزمة منفصلة اسمها `libclang-rt-19-dev`، وبعد تثبيتها عملت وطبعت `WARNING: MemorySanitizer: use-of-uninitialized-value`. والثانية أنها لم تطبع اسم الملف ورقم السطر، وإنما `invalid path to external symbolizer!`، لأنها تبحث عن الأداة `llvm-symbolizer` باسمها العادي، وDebian يثبتها باسم فيه الإصدار، والحل متغير البيئة `MSAN_SYMBOLIZER_PATH` الذي يشير إلى مكانها. والإصلاح الحقيقي للخطأ بسيط: أعط كل متغير قيمة عند تعريفه، `int total = 0;`.

### 14\. الماكرو ليس دالة Macros

```c
#define SQUARE(x) x * x
#define MAX(a, b) ((a) > (b) ? (a) : (b))
    printf("SQUARE(1 + 2) = %d\n", SQUARE(1 + 2));
    int m = MAX(next_id++, 3);
```

```text
$ /tmp/b/m08
SQUARE(1 + 2) = 5
MAX(next_id++, 3) = 6, next_id is now 7
$ /tmp/b/m08f
SQUARE(1 + 2) = 9
MAX(next_id++, 3) = 5, next_id is now 6
```

مربع 3 صار 5، لأن المعالج المسبق ينسخ النص كما هو، فصار `1 + 2 * 1 + 2`، والضرب قبل الجمع. والماكرو الثاني مكتوب بالأقواس الصحيحة، ومع ذلك أخطأ: `next_id++` ظهرت مرتين في النص بعد الاستبدال، فزاد الرقم مرتين، ورجع الماكرو 6 بدلاً من 5، وأحرق الرقم 6 من الأرقام التسلسلية. ولم تكشف أي أداة الخطأين، لأن الكود بعد الاستبدال صحيح تماماً في نظر المترجم. والإصلاح في النسخة المصلحة دوال `static inline`، وهي بنفس سرعة الماكرو بعد التحسين، ولها أنواع، وتحسب كل معامل مرة واحدة. والقاعدة: الماكرو للثوابت التي لا تستطيع أن تكون `constexpr`، وللتجميع الشرطي `#ifdef`، وليس لما يمكن أن تكتبه دالة.

## التغذية العشوائية Fuzzing: ثلاثة ملايين ونصف مدخل على قارئ الطلبات

الأخطاء السابقة صنعناها بأيدينا، فعرفنا أين نبحث. ولكن كيف تجد أخطاءً لا تعرف بوجودها في كود حقيقي؟ هنا تأتي التغذية العشوائية: أداة تولد مدخلات، وتراقب أي فرع في الكود يصل إليه كل مدخل، وتحتفظ بالمدخلات التي تصل إلى فروع جديدة وتعدلها، وكل ذلك تحت ASan وUBSan، فأي تجاوز أو سلوك غير معرف يوقف التشغيل ويحفظ المدخل الذي سببه. وهذه الطريقة هي التي وجدت آلاف الأخطاء في مشاريع C المفتوحة عبر مشروع [OSS-Fuzz](https://github.com/google/oss-fuzz?ref=arabroot.io). وجربنا [libFuzzer](https://llvm.org/docs/LibFuzzer.html?ref=arabroot.io)، المدمجة في Clang، على الدالة `parse_order` من مقال الخدمة، وهي الدالة التي تستقبل جسم الطلب كما يرسله أي شخص على الإنترنت، فهي أول ما يجب أن يتحمل المدخلات الغريبة. ويكفي لذلك ملف صغير فيه دالة باسم محدد تستدعيها الأداة بكل مدخل:

```c
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
    OrderRequest o;
    const char *err;
    parse_order((const char *)data, size, &o, &err);
    return 0;
}
```

```bash
clang -g -O1 -fsanitize=fuzzer,address,undefined fuzz_parse.c src/orders.c src/money.c -o fuzz -lpq -lcjson
./fuzz -max_total_time=60 -print_final_stats=1 corpus/
```

```text
#524288	pulse  cov: 66 ft: 74 corp: 10/197b lim: 4096 exec/s: 58254 rss: 294Mb
#2097152	pulse  cov: 66 ft: 74 corp: 10/197b lim: 4096 exec/s: 55188 rss: 499Mb
#3418267	DONE   cov: 66 ft: 74 corp: 10/197b lim: 4096 exec/s: 56037 rss: 499Mb
Done 3418267 runs in 61 second(s)
stat::number_of_executed_units: 3418267
stat::average_exec_per_sec:     56037
stat::peak_rss_mb:              499
```

في دقيقة واحدة جربت الأداة 3418267 مدخلاً، حوالي 56 ألفاً في الثانية، دون أي انهيار أو تقرير من ASan أو UBSan. والرقم `cov: 66` عدد الفروع التي وصلت إليها في كودنا، وقد توقف عن الزيادة مبكراً، و`corp: 10/197b` يعني أنها احتفظت بعشرة مدخلات مجموعها 197 بايتاً تغطي هذه الفروع. وبدأنا من مدخل واحد صحيح هو `{"items":[{"product_id":1,"qty":3}]}`. وهل تعني هذه النتيجة أن الدالة بلا أخطاء؟ لا، والتفصيل أن الفروع توقفت عند 66 لأن مكتبة cJSON نفسها لم تبن بأدوات الأداة، فالأداة لا ترى فروعها، ولا تعرف كيف تولد JSON صحيحاً أعمق. والنتيجة الصحيحة أن الدالة تحملت ثلاثة ملايين ونصف مدخل عشوائي دون خطأ ذاكرة، وهذا مختلف عن «لا أخطاء فيها». والخطوة التالية في مشروع حقيقي أن تبني cJSON من المصدر مع نفس الخيارات، وأن تعطي الأداة قاموساً بكلمات JSON، وأن تتركها تعمل ساعات في خط الاختبارات الليلي لا دقيقة.

![التغذية العشوائية على parse_order: libFuzzer مع ASan وUBSan جربت 3418267 مدخلاً في 61 ثانية بمعدل 56037 في الثانية وذاكرة 499 MB، ووصلت إلى 66 فرعاً واحتفظت بعشرة مدخلات مجموعها 197 بايتاً، دون أي انهيار، مع ملاحظة أن cJSON غير مبنية بأدوات الأداة فلا ترى فروعها](https://arabroot.io/content/images/2026/10/c-idioms-05-fuzz.webp)

ثلاثة ملايين ونصف مدخل في دقيقة، والنتيجة تحمل لا براءة

## كيف تبدأ في مشروع قائم؟ Adopting the Tools

إذا شغلت `-Wall -Wextra` لأول مرة على مشروع C عمره سنوات، فقد تظهر لك مئات التحذيرات، والرد الطبيعي أن تطفئها وتنسى الأمر. والطريقة التي تعمل في الواقع تدريجية: ابدأ بجعل التحذيرات أخطاءً في الملفات الجديدة فقط، أو في المجلد الذي تعمل فيه، ثم اختر نوعاً واحداً من التحذيرات في كل مرة وأصلحه في كل المشروع، وابدأ بالأنواع التي تعني أخطاء حقيقية مثل `-Wformat` و`-Wuninitialized` و`-Wuse-after-free`، قبل الأنواع الأسلوبية مثل المتغيرات غير المستخدمة. وفي Sanitizers ابدأ ببناء الاختبارات الموجودة بـ ASan وUBSan كما هي، فكل تقرير يظهر هو خطأ حقيقي في مسار تختبره فعلاً، وإذا جاء التقرير من مكتبة خارجية لا تملكها، فلكل أداة طريقة لكتمان تقرير محدد Suppressions بملف تكتب فيه اسم الدالة، بدلاً من إطفاء الأداة كلها. والقاعدة التي ننصح بها أن يكون الهدف ألا يزيد العدد، لا أن يصل إلى صفر في أسبوع: خط الاختبارات الآلية يفشل إذا ظهر تحذير جديد أو تقرير جديد، والقديم يصلح تدريجياً.

## إعدادات البناء Build Profiles: أي أداة في أي وقت؟

بعد كل ما سبق، هذا ما ننصح به لأي مشروع C، وهو ما استخدمناه في خدمة الطلبات:

| متى                     | الخيارات                                                                  | ماذا تكشف                                                                     |
| ----------------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| كل بناء على جهاز المطور | \-std=c23 -Wall -Wextra -Wpedantic -Wconversion -g                        | الأخطاء 1 و2 و3 و6 و7 و10 و11 و12 و13 من هذا المقال وقت الترجمة               |
| خط الاختبارات الآلية    | نفس الخيارات مع \-Werror، وبناء ثان بـ \-fsanitize=address,undefined      | كل أخطاء الذاكرة والتجاوز في المسارات التي تختبرها                            |
| كود فيه خيوط            | بناء ثالث بـ \-fsanitize=thread                                           | سباق البيانات وترتيب الأقفال المعكوس                                          |
| تحليل ساكن قبل الدمج    | \-fanalyzer وclang-tidy وCppcheck                                         | مسارات NULL والاستخدام بعد التحرير وتجاوز الحدود دون تشغيل، ومنها الخطآن 6 و8 |
| كل ليلة                 | Valgrind على سيناريو طويل، وFuzzing لساعات على كل دالة تقرأ مدخلات خارجية | التسرب والمسارات التي لم يكتب لها اختبار                                      |
| الإنتاج                 | \-O2 -D\_FORTIFY\_SOURCE=3 -fstack-protector-strong                       | جزء من تجاوزات المخزن يوقف البرنامج بدلاً من أن يكمل                          |

ولاحظ أن الخطأين 5 و14 لم تكشفهما أي أداة، وأن الأدوات لم تر من الخطأ 4 إلا القطع، فهي أخطاء منطق في النصوص والماكرو، وعلاجها مراجعة الكود والاختبارات بنصوص عربية حقيقية، والقواعد التي تمنعها من البداية: لا ماكرو حيث تصلح دالة، ولا دوال بحالة مخفية، ولا قطع للنصوص على البايتات.

## خطة القراءة Reading Plan: المصادر الرسمية

لا تملك C كتاباً رسمياً واحداً مثل كتاب Rust أو جولة Go، لأن اللغة معيار لا مشروع. ولكن هناك مصادر هي الأصل، وما سواها شرح لها، وهذا ترتيب نقترحه لمن انتهى من هذه المرحلة:

1. توثيق المترجم الذي تستخدمه: [توثيق GCC](https://gcc.gnu.org/onlinedocs/?ref=arabroot.io)، وبالذات صفحة [خيارات التحذير](https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html?ref=arabroot.io) وصفحة [خيارات الفحص](https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html?ref=arabroot.io). اقرأ وصف كل تحذير في `-Wall` و`-Wextra` مرة واحدة، وستعرف أنواع الأخطاء التي يعرفها المترجم.
2. توثيق أدوات Clang: [AddressSanitizer](https://clang.llvm.org/docs/AddressSanitizer.html?ref=arabroot.io) و[UndefinedBehaviorSanitizer](https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html?ref=arabroot.io) و[ThreadSanitizer](https://clang.llvm.org/docs/ThreadSanitizer.html?ref=arabroot.io) و[MemorySanitizer](https://clang.llvm.org/docs/MemorySanitizer.html?ref=arabroot.io)، ففيها ما تكشفه كل أداة وما لا تكشفه، وهو ما رأيناه عندما فاتت ASan التجاوز داخل الهيكل.
3. صفحات الدليل man من مشروع man-pages على [man7.org](https://man7.org/linux/man-pages/man7/pthreads.7.html?ref=arabroot.io): لكل دالة من مكتبة C قسم `RETURN VALUE` وقسم `ERRORS`، وهما ما تحتاجه لمعالجة الأخطاء بشكل صحيح. ومعها [دليل glibc](https://sourceware.org/glibc/manual/?ref=arabroot.io) لما يخص المكتبة نفسها.
4. المعيار نفسه: آخر مسودة لـ C23 متاحة مجاناً من [لجنة WG14](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3220.pdf?ref=arabroot.io). لا تقرأه من أوله، وإنما ارجع إليه عندما تسأل «هل هذا معرف؟»، وابحث فيه عن الكلمة undefined.
5. قواعد البرمجة الآمنة: [معيار SEI CERT C](https://wiki.sei.cmu.edu/confluence/display/c/SEI+CERT+C+Coding+Standard?ref=arabroot.io) من جامعة Carnegie Mellon، وهو قائمة قواعد لكل منها مثال خاطئ ومثال صحيح، والفحوص `cert-` في clang-tidy مبنية عليه.
6. كتاب حديث مجاني: [Modern C](https://gustedt.gitlabpages.inria.fr/modern-c/?ref=arabroot.io) من Jens Gustedt، أحد أعضاء لجنة المعيار، ونسخته الإلكترونية متاحة مجاناً من موقعه، ويشرح C كما تكتب اليوم لا كما كتبت في الثمانينيات.
7. كود حقيقي منضبط: اقرأ أسلوب نواة Linux في [دليل أسلوب الكود](https://www.kernel.org/doc/html/latest/process/coding-style.html?ref=arabroot.io)، ثم اقرأ ملفاً من curl أو PostgreSQL، ولاحظ كيف يعالجان الأخطاء والذاكرة في كل سطر.

وللمرجع السريع أثناء الكتابة يستخدم كثيرون موقع [cppreference](https://en.cppreference.com/w/c?ref=arabroot.io)، وهو مرجع يكتبه المجتمع وليس رسمياً، ولكنه دقيق ويذكر لكل دالة المعيار الذي أضافها.

## قائمة تحقق Checklist لمراجعة كود C

- كل مقارنة نصوص بـ `strcmp` مع `== 0`، ولا `strncpy` ولا `strcpy` مع نص لا تعرف طوله.
- كل فهرس من بايت نص يمر عبر `unsigned char`، وكل ما يخص الحروف العربية يختبر بنصوص عربية حقيقية.
- كل `malloc` بالشكل `n * sizeof *p` ويفحص `NULL`، وكل `realloc` في متغير مؤقت.
- لا فحص تجاوز بعد العملية، و`ckd_add` وأخواتها لكل حساب قد يكبر.
- لا عد تنازلي بشرط `>= 0` على عدد بلا إشارة.
- كل متغير له قيمة عند تعريفه، وكل `case` ينتهي بـ `break` أو `[[fallthrough]]`.
- لا ماكرو حيث تصلح دالة `static inline`، ولا دوال بحالة مخفية مثل `strtok` في الكود الجديد.
- صيغ `printf` ثابتة وصحيحة: `%zu` و`PRId64`.
- البناء بالإعدادات في جدول إعدادات البناء، وFuzzing لكل دالة تقرأ مدخلات من خارج البرنامج.

## تدريب عملي Practice

- اكتب دالة `truncate_utf8` تقطع نصاً عند عدد من البايتات دون أن تقسم حرفاً، واختبرها بالاسم `قهوة عربية` وحد سبعة بايتات.
- ابن cJSON من المصدر مع `-fsanitize=fuzzer-no-link,address`، وأعد تجربة التغذية العشوائية، وقارن رقم `cov` بما رأيناه.
- شغل `m04_overflowcheck.c` بـ Clang بدلاً من GCC بمستويات التحسين المختلفة، ولاحظ هل يتصرف المترجم الآخر بنفس الطريقة.

## الخلاصة

وصلنا لنهاية الموضوع ونهاية مرحلة لغة C، وأهم ما فيه:

- أخطاء القادمين من لغات أخرى ليست أخطاء مبتدئين، وإنما افتراضات من لغة أخرى: مقارنة النصوص، وطول النص، والفحص الذي يبقى في البرنامج، والخطأ الذي يوقفه.
- في curl كان 51 من 98 ثغرة أخطاء خاصة بـ C، فالانتباه وحده لا يكفي، والمشاريع الجادة تعتمد على طبقات من الأدوات.
- لا توجد أداة كشفت الأخطاء الأربعة عشر كلها، واثنان منها لم تكشفهما أي أداة، الماكرو و`strtok`، وفي ثالث، طول النص العربي، رأت الأدوات القطع فقط ولم تر الحرف المقسوم.
- فحص التجاوز بعد حدوثه حوله المترجم إلى فحص لإشارة الكمية بدون تحسين، وحذف الجمع كله مع `-O2`، وكشفه UBSan وحدها، والإصلاح `ckd_add` من C23.
- ثلاثة أخطاء ذاكرة من أربعة خرجت بنجاح أو بنتيجة صحيحة في التشغيل العادي، وكشفتها ASan وValgrind و`-fanalyzer`.
- الحرف العربي بايتان فوق 127، فيصبح عدداً سالباً في `char`، و`strlen` تعد 19 لعشرة حروف.
- كل أداة لها نموذج لما تراقبه وثمن: ASan ضعف الوقت ولا ترى ما بين حقول الهيكل، وMSan ثلاثة أضعاف وتحتاج بناء كل المكتبات بها، وTSan حتى خمسة عشر ضعفاً، وValgrind لا تحتاج إعادة بناء وثمنها عشرون إلى ثلاثون ضعفاً.
- في مشروع قائم لا تحاول الوصول إلى صفر تحذيرات في أسبوع، وإنما امنع الجديد وأصلح القديم نوعاً بعد نوع.
- التغذية العشوائية جربت 3418267 مدخلاً على قارئ الطلبات في دقيقة دون خطأ، وهذه نتيجة تحمل لا ضمان.
- المصادر الرسمية: توثيق GCC وClang وصفحات man ومسودة المعيار، ومعها معيار CERT C وكتاب Modern C.

وإذا وصلت إلى هنا فقد رأيت لماذا تبقى C في نواة النظام وقواعد البيانات، ولماذا تدفع الحكومات نحو لغات آمنة الذاكرة. وفي مرحلة لغة Rust من نفس المسار ترى كيف تمنع لغة أخرى أغلب أخطاء هذا المقال وقت الترجمة:

[![](https://arabroot.io/content/images/2026/10/c-for-devs-cover-cover.webp)دليللغة C لمن يبرمج بلغة أخرى: المترجم GCC والأدواتنبني مكتبة صغيرة تحسب ضريبة الفاتورة بالهللات بأحدث GCC، ونمر على ما يختلف في C عما تعرفه، ومراحل الترجمة، والتحذيرات، وCMake، وأدوات التنسيق والفحص والاختبار والتصحيح.](https://arabroot.io/articles/%D9%84%D8%BA%D8%A9-c-%D9%84%D9%85%D9%86-%D9%8A%D8%A8%D8%B1%D9%85%D8%AC-%D8%A8%D9%84%D8%BA%D8%A9-%D8%A7%D8%AE%D8%B1%D9%89-%D8%A7%D9%84%D9%85%D8%AA%D8%B1%D8%AC%D9%85-gcc-%D9%88%D8%A7%D9%84%D8%A7%D8%AF%D9%88%D8%A7%D8%AA/)[![](https://arabroot.io/content/images/2026/10/c-types-memory-cover-cover.webp)دليلالمؤشرات Pointers والذاكرة والأخطاء في لغة Cنقرأ ملف طلبات نقطة البيع بلغة C ونقف عند كل ما تسمح به اللغة دون أن تخبرك: تحويلات الأنواع، والحشو، وتجاوز المخزن، ومن يحرر الذاكرة، وكيف تصل الأخطاء بقيمة الرجوع وerrno.](https://arabroot.io/articles/%D8%A7%D9%84%D9%85%D8%A4%D8%B4%D8%B1%D8%A7%D8%AA-pointers-%D9%88%D8%A7%D9%84%D8%B0%D8%A7%D9%83%D8%B1%D8%A9-%D9%88%D8%A7%D9%84%D8%A7%D8%AE%D8%B7%D8%A7%D8%A1-%D9%81%D9%8A-%D9%84%D8%BA%D8%A9-c/)[![](https://arabroot.io/content/images/2026/10/c-concurrency-cover-cover.webp)دليلالتزامن في C: الخيوط pthreads والأقفال Locksنقيس بلغة C مئة طلب شبكة وألفي ختم SHA-256 بالتسلسل وبالخيوط وبحلقة أحداث وOpenMP، ثم نصنع سباق بيانات وجموداً حقيقيين ونكشفهما بـ ThreadSanitizer ونصلحهما.](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%B2%D8%A7%D9%85%D9%86-%D9%81%D9%8A-c-%D8%A7%D9%84%D8%AE%D9%8A%D9%88%D8%B7-pthreads-%D9%88%D8%A7%D9%84%D8%A7%D9%82%D9%81%D8%A7%D9%84-locks/)[![](https://arabroot.io/content/images/2026/10/rust-for-devs-cover-cover.webp)دليلRust للمطورين: الأدوات Cargo والأساسيات في يوم واحدثبتنا Rust 1.99.1 من الصفر وبنينا مكتبة تحسب قيمة الطلب والضريبة بالهللات، ومررنا عليها بكل أداة ستكتبها يومياً مع المخرج الحقيقي، وما يفاجئ القادم من Python وJava وGo.](https://arabroot.io/articles/rust-%D9%84%D9%84%D9%85%D8%B7%D9%88%D8%B1%D9%8A%D9%86-%D8%A7%D9%84%D8%A7%D8%AF%D9%88%D8%A7%D8%AA-cargo-%D9%88%D8%A7%D9%84%D8%A7%D8%B3%D8%A7%D8%B3%D9%8A%D8%A7%D8%AA-%D9%81%D9%8A-%D9%8A%D9%88%D9%85-%D9%88%D8%A7%D8%AD%D8%AF/)[![](https://arabroot.io/content/images/2026/10/memory-safety-rust-cover-cover.webp)دليلأمان الذاكرة Memory Safety: لماذا تدفع الحكومات نحو Rust؟ماذا طلبت الحكومات فعلاً بشأن C وC++، ولماذا تسبب أخطاء الذاكرة أغلب الثغرات الخطيرة، ومتى يكون Rust هو الاختيار الصحيح.](https://arabroot.io/articles/%D8%A7%D9%85%D8%A7%D9%86-%D8%A7%D9%84%D8%B0%D8%A7%D9%83%D8%B1%D8%A9-memory-safety-%D9%84%D9%85%D8%A7%D8%B0%D8%A7-%D8%AA%D8%AF%D9%81%D8%B9-%D8%A7%D9%84%D8%AD%D9%83%D9%88%D9%85%D8%A7%D8%AA-%D9%86%D8%AD%D9%88-rust/)

## سجل التحديثات

- أكتوبر 2026: كتابة المقال، وتنفيذ الأخطاء الأربعة عشر داخل Container مبني على gcc:16.2.0-trixie مع GCC 16.2.0 وClang 19.1.7 وglibc 2.41 وValgrind 3.24.0، والتغذية العشوائية بـ libFuzzer من Clang 19.1.7 مع الحزمة libclang-rt-19-dev، على Docker Engine 29.8.0.