كن حكيماً وعقّم Keeping your C++ code free from bugs

Illustration representing clean and bug-free C code, with icons of code, checkmarks, and a bug being eliminated,

على الرغم من كل الخسائر التي ألحقتها بنا هذه الجائحة، إلا أنها جعلتنا على الأقل أكثر وعيًا بنظافتنا الشخصية.

نحن نقوم برش المساحات والأسطح وأيدينا في كثير من الأحيان، فلماذا لا نقوم بتعقيم أكوادنا البرمجية أثناء قيامنا بذلك؟ ففي نهاية المطاف، البرمجيات تدير العالم، والأخطاء التي تتسبب في تعطل البرامج يمكن أن تسبب أضراراً جسيمة – مثلها مثل نظيراتها الفيروسية.

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

يمكن أن يساعدك استخدام أدوات التعقيم أثناء بناء برنامجك واختباره في اكتشاف الكثير من المشكلات في شفرتك المصدرية في وقت مبكر، بما في ذلك تسرب الذاكرة، وتجاوزات المخزن المؤقت، والسلوك غير المعرّف.

اليوم، سنلقي نظرة على ثلاثة أنواع من معقمات كلانج وكيفية استخدامها وما هي الحشرات التي يمكن أن تساعدنا في القضاء عليها في مهدها.

دعونا نرش بعيداً!

تنظيف مساحة العنوان الخاص بك باستخدام AddressSan Sanitizer (ASan)

يُستخدم AddressSanitizer (اختصارًا ASan) للكشف عن التدفقات الزائدة والقصيرة في المخزن المؤقت (المكدس والكومة والمخزن المؤقت العام) للاستخدام بعد الفراغ، والمخزن المؤقت المزدوج، والمخزن المؤقت (المكدس والكومة والمخزن المؤقت العام)، بالإضافة إلى أخطاء الذاكرة الأخرى.

وهو يتكون من وحدة أدوات المحول البرمجي ومكتبة وقت التشغيل التي تقوم بإدراج مناطق حمراء حول كل مجموعة من البايتات المخصصة مع الدالة malloc. كما تقوم بتسميم البايتات المحررة وتتبع مكدس الاستدعاء لكل malloc/free pair.

هذا هو الشكل الذي يبدو عليه رمزنا بدون ASAN:

وإليك ما يقوم به ASAN من أجل اكتشاف الأخطاء المتعلقة بالعناوين:

إن استخدام ASAN بسيط مثل إضافة -fsanitize=address كعلامة للمترجم والرابط. يمكنك أيضًا تعيين عدد كبير من خيارات وقت التشغيل عبر متغير البيئة ASAN_OPTIONS (هنا قائمة كاملة بعلامات ASAN التي يمكنك استخدامها).

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

char const * src{ "Hello world!" };
auto const dst{ std::make_unique< char[] >( std::strlen( src ) ) };

std::strcpy( dst.get(), src );
std::puts( dst.get() );

الأمر صعب، أليس كذلك؟ والآن، تخيل أن هذا الخطأ كان جزءًا من قاعدة شفرات أكبر بكثير. سيستغرقنا الأمر وقتًا طويلاً لتصحيحه يدويًا، بينما مع تشغيل ASAN، يمكننا تحديد تجاوز سعة المخزن المؤقت المخفي في ثوانٍ (انظر العرض التوضيحي):

==1==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60200000001c at pc 0x00000044b754 bp 0x7ffc9ea586d0 sp 0x7ffc9ea57e80
WRITE of size 13 at 0x60200000001c thread T0
#0 0x44b753 (/app/output.s+0x44b753)

سيحذرك ASAN أيضًا إذا استمر برنامجك في استخدام مؤشر بعد تحريره (وهي ثغرة أمنية شائعة تسمى خطأ الاستخدام بعد التحرر). إليك مثال مثال لبرنامج يحتوي على الثغرة:

constexpr std::size_t bodyOffset{ 96u };
char * message{ new char[ 1024 ] };

// fill in the message

char * bodyBegin{ message + bodyOffset };

delete [] message;

// many lines of code ...

std::puts( bodyBegin ); // heap-use-after-free bug

وإليك التقرير الذي تحصل عليه من ASAN بعد اختبار الكود:

==1==ERROR: AddressSanitizer: heap-use-after-free on address 0x6190000000e0 at pc 0x000000470a69 bp 0x7ffe047e6b90 sp 0x7ffe047e6340
READ of size 929 at 0x6190000000e0 thread T0

أنيق جداً، أليس كذلك؟

الآن، قد تسأل نفسك: كم يكلف هذا؟ حسنًا، لا شيء يأتي مجانًا في عالم اليوم، وللأسف، ينطبق الأمر نفسه على ASAN.

ومع ذلك، إذا كنت تفكر في مقدار الوقت الذي يمكن أن توفره لك هذه الأداة، فهي ليست مكلفة على الإطلاق. يبلغ متوسط النفقات العامة حوالي 2 ضعف من حيث الأداء واستخدام الذاكرة.

بالحديث عن الأداء، المنافس الرئيسي لـ ASAN فالغريندسيفرض تباطؤًا أعلى من 10 إلى 100 مرة على برنامجك. هذا تحسن قوي في كتابنا.

الآن بعد أن نظفنا مساحة العنوان لدينا، دعنا نقوم ببعض التعقيم لذاكرتنا.

الكشف عن قراءات الذاكرة غير المهيأة باستخدام MemorySanitizer (MSan)

قد تظن أن برنامج AddressSanitizer يغطي جميع الأخطاء المتعلقة بالذاكرة، لكن الأمر ليس كذلك. فهو لا يتعامل مع قراءات الذاكرة غير المهيأة، وهنا يأتي دور معقّم آخر يُدعى MemorySanitizer (اختصارًا MSan).

سيسمح لك MSan بنسخ الذاكرة غير المهيأة وإجراء عمليات منطقية وحسابية بسيطة عليها، ولكن عندما تحاول استخدامها في بيان اتخاذ القرار، ستحصل على علامة حمراء. لفهم ذلك بشكل أفضل، ألقِ نظرة على مقتطف الشيفرة التالي (التجريبي):

int *src{ new int[ 5 ] };
int dst[ 5 ];

std::memcpy( dst, src, 5 ); // copying uninitialized memory, OK

if ( src[ 0 ] ) // MSan warning: use-of-uninitialized-value
{
// more code ...
}

كما ترى، سيحذرنا MSan من أي قيمة غير مهيأة قيد الاستخدام. لن يتم الإبلاغ عن تسرب الذاكرة الذي قد تلاحظه لأن هذا جزء مما يتعامل معه ASan – من المهم ألا نختلط علينا الأمر.

للوصول إلى أصل هذه القيمة، استخدم العلامة -fsanitize-memory-track-origins مع -fsanitize=memory.

والآن، لنتحدث قليلًا عن كيفية عمل MSan فعليًا. إنه يطبق تخطيط الظل من بت إلى بت، كما هو موضح في الشكل أدناه، حيث 1 يعني “مسموم” أو بت غير مهيأ. يسمح هذا بحساب عنوان ذاكرة الظل بكفاءة عالية جدًا. وبالنظر إلى عنوان ذاكرة التطبيق ProductAddr ، فإن حساب ShaddowAddr هو ProductAddr و ShadowMask ، حيث ShadowMask هو ثابت خاص بالمنصة.

كلما كان الوصول إلى أحد البتات المسمومة له أي تأثير جانبي (على سبيل المثال في التفرع)، سيتم رفع تحذير. يقدم هذا البت الإضافي 2.5 ضعف على وحدة المعالجة المركزية و2 ضعف على الذاكرة. ستكون النفقات الزائدة أعلى قليلاً إذا تم تتبع أصول الذاكرة أيضًا، 5 أضعاف على وحدة المعالجة المركزية و3 أضعاف على الذاكرة، على وجه التحديد.

التقاط السلوك غير المعرّف باستخدام UndefinedBehaviorSanitizer (UBSan)

أخيرًا وليس آخرًا، UndefinedBehaviorSanitizer (اختصارًا UBSan).

سيكتشف UBSan تجاوز الأعداد الصحيحة الموقّعة واستخدام المؤشرات الفارغة والقسمة على صفر وغيرها من السلوكيات غير المعرّفة أثناء تنفيذ برنامجك. بصرف النظر عن علامة المحول البرمجي -fsanitize=undefined ، التي تتحقق من جميع أنواع الأخطاء، هناك العديد من العلامات الإضافية التي يمكن أن تكون مفيدة في العثور على أخطاء أكثر تحديدًا، بما في ذلك:

-fsanitize=bounds
-fsanitize=vptr
-fsanitize=enum
-fsanitize=signed-integer-overflow
-fsanitize=null
-fsanitize=unsigned-integer-overflow
-fsanitize=return
-fsanitize=integer-divide-by-zero
-fsanitize=unreachable
-fsanitize=alignment

كما ترى، يتعامل UBSan مع الأخطاء البسيطة مثل تلك الموضحة في المثال التالي بشكل جيد (العرض التوضيحي):

int main()
{
      int m = std::numeric_limits< int >::max();
      return m + 1;
}

وإليك التقرير الذي حصلنا عليه

runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'

سيُظهر UBSan قوته بشكل أفضل في التطوير اليومي وكذلك في قواعد الشفرات الكبيرة. فيما يتعلق بالأداء، هناك ما يقرب من 1.25 ضعفًا من النفقات الزائدة لوحدة المعالجة المركزية ولا تأثير على استخدام الذاكرة على الإطلاق (لا يؤثر UBSan على تخطيط مساحة العنوان).

الجوانب السلبية التي يجب مراعاتها

والآن بعد أن رأينا كيف يمكن للمطهرات أن تساعدنا في بناء شيفرة أفضل، لا يزال علينا أن نذكر بعض الجوانب السلبية لاستخدامها.

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

الجانب السلبي الثاني (والأكثر أهمية)، هو حقيقة أن ASan و MSan لا يمكنهما العمل معًا. هذا يعني أنك ستحتاج إلى إجراء عمليات تشغيل متعددة لاختبار برنامجك، وهو ما قد يستغرق بعض الوقت. ربما هذا هو السبب في أن المطهرات لا تزال لا تحظى بالكثير من الحب من مجتمع C++.

اجتماع C++C تُظهر نتائج الاستطلاع أن عددًا كبيرًا من المطورين لا يستخدمون أدوات التعقيم في عمليات الإنشاء الخاصة بهم على الإطلاق.

على الرغم من عيوبها، فإننا نوصي بشدة باستخدام أدوات التعقيم. فهي ستلتقط المشكلات التي قد تبدو آمنة بالنسبة للأدوات المنافسة، ولن تعطل سير عملك مع التباطؤ الشديد.

من المنطقي تمامًا إذن أن تدمجها مع عمليات التطوير “السريعة” مثل التكامل المستمر أو خطوط أنابيب طلبات السحب.

ومع ذلك، دعنا لا نصرف النظر عن Valgrind بعد لأنه لا يزال بإمكاننا الاستفادة منه كثيرًا. سنتحدث عن مزاياه في إحدى المنشورات المستقبلية.

هذا كل ما لدينا في الوقت الحالي – ترقبوا المزيد من المنشورات الشيقة القادمة وتذكروا أن بناء برمجيات موثوقة يبدأ بكود نظيف.

28 مايو، 2021

اكتشف حلولنا

استكشاف حلولنا على بُعد نقرة واحدة فقط. جرّب منتجاتنا أو تحدث معنا مع أحد خبرائنا للتعمق أكثر في ما نقدمه.