فالغريند: أداة مهملة من الظل أم أداة تصحيح أخطاء جادة؟
قبل بضعة أشهر، ألقينا نظرة على معقمات C++ – الأدوات الصغيرة المفيدة التي تساعدنا في الحفاظ على نظافة الشيفرة البرمجية وخالية من أخطاء الذاكرة.
اليوم، سنعود خطوة إلى الماضي ونتحدث عن أداة قديمة لكنها لا تزال قوية جدًا تسمى Valgrind. لقد كانت تخدم المطورين لبعض الوقت الآن لأنها تكتشف مجموعة واسعة من الأخطاء مثل قراءات الذاكرة غير المهيأة، وتجاوزات المخزن المؤقت للكومة، وتسريبات الذاكرة، والأقفال المسدودة، وما إلى ذلك.
بعد أن أصدرت Clang مجموعتها من المطهّرات، دُفعت Valgrind إلى الخلفية، ولكن كما سترى في هذا المنشور في هذه المدونة، هناك بعض الحالات التي تتفوق فيها Valgrind على المطهّرات.
وحش تحت غطاء المحرك
واحدة من أكبر مزايا Valgrind على أدوات التعقيم هي حقيقة أنه لا يتطلب أن يكون البرنامج مُفَعَّلًا قبل فحصه. هذا يعني أنه يمكن استخدامه لتصحيح أخطاء أي نوع من البرامج “خارج الصندوق” دون الحاجة إلى الوصول إلى شيفرتها المصدرية.
لن يكون هذا ممكنًا بدون الوحش الموجود تحت غطاء Valgrind. باختصار، بمجرد تهيئة Valgrind، فإنه يتحكم في برنامجك ويقوم بتشغيله على وحدة معالجة مركزية محاكاة مقدمة من نواة Valgrind. ثم يضيف بعد ذلك شيفرة التثبيت الخاصة به اعتمادًا على نوع الأخطاء التي تتطلع إلى اكتشافها.
على عكس أدوات التعقيم، يستخدم Valgrind التضمين الديناميكي الثنائي (DPI) والتجميع في الوقت المناسب (JIT) لتضمين كود البرنامج مع كود التجهيز، أي اعتراض استدعاءات دالة التخصيص من أجل تخزين بعض المعلومات الإضافية.
إليك كيفية عملها. يتم تعيين كل كتلة مخصصة إلى كتلة ظل حيث يتم تخزين مكدس الاستدعاء في وقت استدعاء الدالة malloc. بمجرد استدعاء الدالة free ، يحاول Valgrind العثور على كتلة الظل المطابقة للعنوان الذي تم تمريره إلى free. إذا لم يتم العثور على الكتلة، يصدر Valgrind رسالة خطأ. خلاف ذلك، تتم إضافة الكتلة إلى قائمة الانتظار ويتم وضع علامة على أنه لا يمكن الوصول إليها. بهذه الطريقة، من الممكن اكتشاف الوصول غير الصالح إلى الذاكرة المحررة. ومع ذلك، يرجى ملاحظة أنه يمكن إزالة الكتل من قائمة الانتظار بمجرد نفاد المساحة الخالية في النظام.
المرونة التي توفرها البنية متعددة الطبقات
كما هو موضح في الشكل أدناه، يتكون Valgrind من طبقتين: نواة Valgrind الأساسية والأداة الإضافية التي يمكن أن تكون أيًا من الأدوات الموجودة في مجموعة أدوات Valgrind، بما في ذلك:
- Memcheck – تتبع عمليات تخصيص الذاكرة والإبلاغ عن تسرب الذاكرة
- Helgrind – يكتشف المشكلات المتعلقة بتعدد الخيوط (مثل حالات الجمود وسباقات البيانات وغيرها).
- Cachegrind – يعمل كمحلل للتخزين المؤقت والتنبؤ بالفروع
- ماسيف – يحلل استخدام ذاكرة الكومة
ضع في اعتبارك أن Valgrind مفتوح المصدر ويمكنك كتابة أداتك الخاصة بك إذا أردت.
لكل من هاتين الطبقتين دورهما الخاص. يقوم الجزء الأساسي بتحميل البرنامج قيد الاختبار في العملية وتفكيك شفرته البرمجية. وبمجرد الانتهاء من ذلك، يتم تمرير أجزاء التعليمات البرمجية إلى الأداة الإضافية التي تضيف الأجهزة إلى التعليمات البرمجية، وأخيرًا، تقوم بتجميعها مرة أخرى.
طريقة أسهل لتصحيح الأخطاء
كما ذكرنا في منشور المدونة السابقة، تتطلب معقمات C++ إعادة تجميع الشيفرة البرمجية الخاصة بك. قد يكون هذا غير مريح عندما تريد اختبار الشيفرة الخاصة بك مع كل من AddressSanitizer و MemorySanitizer. نظرًا لأن الاثنين لا يمكنهما العمل معًا، فستحتاج إلى إجراء اختبارات متعددة لاكتشاف قراءات الذاكرة غير المهيأة والأخطاء الأخرى المتعلقة بالعناوين. من ناحية أخرى، يمكن ل Valgrind تشغيل أي برنامج تقريبًا كما هو. الشيء الوحيد الذي يحتاجه هو دعم جميع التعليمات التي يستخدمها برنامجنا.
لنفترض أنك تريد تصحيح أخطاء مكتبة لا يمكن الوصول إلى شيفرتها المصدرية. لن يسفر استخدام أدوات التعقيم عن أي نتائج لأنها، على عكس Valgrind، تعمل على مستوى المحول البرمجي. باستخدام Valgrind، يمكنك التعامل مع هذه الحالات بشكل افتراضي، ولكن ضع في اعتبارك أنه قد ينتهي بك الأمر برسائل خطأ لا تعني لك الكثير لأنك لا تملك أي تحكم في هذه الشيفرة. ومع ذلك، أنت حر في تصفية هذه الرسائل عن طريق كتابتها في ملف قمع تتم قراءته عند بدء تشغيل Valgrind.
إلى جانب عملية تصحيح أخطاء أكثر انسيابيةً، فالجريند بديل رائع على المنصات التي لا تدعم أدوات التعقيم. على سبيل المثال، لا تُشحن Apple Clang مع معالج التسريبات، مما يجعل Valgrind أفضل بديل لك إلا إذا كنت على استعداد للتبديل إلى مترجم آخر.
الأداء باعتباره العيب الأكبر
دعونا نلقي نظرة على نسخة معدلة قليلًا من المثال المثال من منشورنا الأول عن معقمات C++:
char const * src{ "Hello world!" };
auto const dst{ std::make_unique< char[] >( std::strlen( src ) ) };
for ( auto i{ 0ul }; i < 1000000; ++i )
{
std::strcpy( dst.get(), src );
}
std::puts( dst.get() );
تمت إضافة for loop للتأكيد على الفرق في الأداء بين AddressSanitizer و Valgrind. في كلتا الحالتين، يتم تجميع الكود باستخدام الأمر التالي (يتطلب تشغيل ASan أيضًا إضافة -fsanitize=address):
clang++ example.cpp -g -o example.out
تكشف نظرة سريعة على الرسم البياني أدناه عن وجود فجوة كبيرة في الأداء بين Valgrind وASan، وهذا هو السبب في أن الكثير من المطورين قد يترددون في استخدام الأداة في المقام الأول.
مقارنة جنباً إلى جنب
من أجل الحصول على فهم أفضل للأخطاء التي يستطيع Valgrind والمطهرات اكتشافها، دعنا نلقي نظرة على الجدول التالي:
* يكتشف برنامج MSan قراءات الذاكرة غير المهيأة
** يكتشف UBSan السلوك غير المعرف
كما ترى، لن يساعدك Valgrind في اكتشاف التدفقات الزائدة في المكدس والمتغيرات العامة. هذا لأن لديه فقط إمكانية الوصول إلى تخصيصات الكومة التي تقوم بها الدالة malloc. أيضًا، لا تعتمد عليه في اكتشاف أي سلوك غير معرّف في شفرتك – على الرغم من أنه سيتم تحذيرك بشأن محاولة الوصول إلى الذاكرة الناتجة عن سلوك غير معرّف.
في الوقت نفسه، ASan ليس مثاليًا أيضًا. فهو لا يكتشف قراءات الذاكرة غير المهيأة أو السلوك غير المعرف ولكن MSan و UBSan يكتشفان ذلك.
فالجريند أم المطهرات: ما الذي يجب أن نستخدمه؟
يبقى السؤال إذن: هل يجب علينا استخدام Valgrind أم أن المطهرات هي البديل الأفضل؟ لا توجد إجابة صحيحة على هذا السؤال لأن هاتين الأداتين تعملان بطرق مختلفة تمامًا. من الناحية المثالية، يجب عليك استخدام كليهما اعتمادًا على بيئتك والأخطاء التي تريد اكتشافها.
على الرغم من أن أدوات التعقيم اليوم تفرض عبئًا أقل بكثير على وحدة المعالجة المركزية وتوفر نطاقًا أوسع من الأخطاء المكتشفة، إلا أنها لا تزال لها عيوبها. فهي تعمل على مستوى المحول البرمجي مما يعني أنك بحاجة إلى الشيفرة المصدرية. كما أنها تتطلب منك إعادة تجميع الشيفرة البرمجية الخاصة بك مع كل اختبار تشغيل، وتستغرق وقتًا أطول للتكامل وتفتقر إلى الدعم على منصات معينة.
إذا كان لديك مشروع كبير في متناول يدك ولا تريد أن تقلق بشأن إعادة تجميع الشيفرة الخاصة بك، فقد يكون استخدام Valgrind أكثر منطقية. لقد وجدنا أنه مفيد بشكل خاص في تصحيح أخطاء المكتبات المغلقة المصدر واكتشاف أشياء مثل أخطاء ما بعد الاستخدام الخالي من الأخطاء بسهولة أكبر. على الرغم من أن أدوات التعقيم تتحسن أكثر فأكثر، إلا أننا نميل إلى تفضيلها على Valgrind.
في النهاية، وبغض النظر عن الأدوات التي تستخدمها، فإن أهم شيء هو أن تجعل شفرتك آمنة قدر الإمكان. مع وضع ذلك في الاعتبار، ترقبوا المزيد من المنشورات المثيرة للاهتمام حول تصحيح الأخطاء في C++.