Valgrind: أهي أداة مهملة من عالم الظلال أم أداة جدية لتصحيح الأخطاء؟
قبل بضعة أشهر، ألقينا نظرة على أدوات التنقية في لغة C++ — وهي أدوات صغيرة ومفيدة تساعدنا في الحفاظ على نظافة الكود وخاليًا من أخطاء الذاكرة.
اليوم، سنعود قليلاً إلى الماضي ونتحدث عن أداة قديمة لكنها لا تزال قوية جدًّا تُدعى Valgrind. وقد ظلت هذه الأداة تخدم المطورين منذ فترة طويلة، حيث إنها تكتشف مجموعة واسعة من الأخطاء مثل قراءة الذاكرة غير المُهيأة، وتجاوز سعة مخزن الـ«هيوب»، وتسربات الذاكرة، وحالات التعطل المتبادل، وما إلى ذلك.
بعد أن أطلقت Clang مجموعتها من أدوات التعقيم، تراجع دور Valgrind إلى الخلفية، لكن كما سترون في هذه المدونة، هناك بعض الحالات التي يتفوق فيها Valgrind فعليًّا على أدوات التعقيم بشكل ساحق.
وحش تحت غطاء المحرك
تتمثل إحدى أكبر مزايا Valgrind مقارنةً ببرامج التنقية في أنه لا يتطلب تجهيز البرنامج بأدوات الرصد قبل فحصه. وهذا يعني أنه يمكن استخدامه لتصحيح أخطاء أي نوع من البرامج «فورًا» دون الحاجة إلى الوصول إلى كود المصدر الخاص بها.
ولن يكون هذا ممكنًا لولا «الوحش» الكامن وراء كواليس Valgrind. باختصار، بمجرد تهيئة Valgrind، فإنه يتولى التحكم في برنامجك ويقوم بتشغيله على وحدة معالجة مركزية (CPU) محاكاة يوفرها نواة Valgrind. ثم يضيف كود التجهيز الخاص به اعتمادًا على نوع الأخطاء التي تريد الكشف عنها.
على عكس أدوات الفحص، يستخدم Valgrind تقنية «التجهيز الثنائي الديناميكي» (DPI) والتجميع «في الوقت المناسب» (JIT) لدمج كود برنامجنا مع كود التجهيز، أي لاعتراض استدعاءات وظائف التخصيص بهدف تخزين بعض المعلومات الإضافية.
Here’s how it works. Each allocated block maps to a shadow block where the call stack at the time of the malloc function call is stored. Once the free function is called, Valgrind tries to find the shadow block corresponding to the address passed to free. If the block is not found, Valgrind emits an error message. Otherwise, the block is added to the queue and gets marked as inaccessible. This way, it is possible to detect invalid access to freed memory. However, please note that blocks can be removed from the queue once the system runs out of free space.
المرونة التي توفرها البنية الطبقية
كما هو موضح في الشكل أدناه، يتكون Valgrind من طبقتين: نواة Valgrind ومكوّن الأداة الإضافي الذي يمكن أن يكون أيًا من الأدوات الموجودة في مجموعة أدوات Valgrind، بما في ذلك:
- Memcheck – يتتبع عمليات تخصيص الذاكرة ويبلغ عن تسربات الذاكرة
- Helgrind – يكتشف المشكلات المتعلقة بالتعدد الخيطي (مثل حالات التعطل المتبادل، وتنافس البيانات، وما إلى ذلك)
- Cachegrind – يعمل كأداة تحليل أداء ذاكرة التخزين المؤقت وتوقع الفروع
- Massif – يحلل استخدام ذاكرة الكومة
ضع في اعتبارك أن Valgrind هو برنامج مفتوح المصدر، ويمكنك كتابة أداة خاصة بك إذا أردت.
لكل من هاتين الطبقتين دورها الخاص. حيث يقوم الجزء الأساسي بتحميل البرنامج الخاضع للاختبار إلى العملية وتفكيك كوده. وبمجرد الانتهاء من ذلك، تُمرر أجزاء الكود إلى المكون الإضافي للأداة الذي يضيف أدوات القياس إلى الكود، ثم يقوم في النهاية بإعادة تجميعه.
طريقة أسهل لتصحيح الأخطاء
كما ذكرنا في منشور المدونة السابق، تتطلب أدوات التنقية في لغة C++ إعادة ترجمة الكود. وقد يشكل ذلك عائقًا عندما ترغب في اختبار الكود باستخدام كل من AddressSanitizer وMemorySanitizer. ونظرًا لأن هذين البرنامجين لا يمكنهما العمل معًا، فستحتاج إلى إجراء عدة جولات اختبار للكشف عن عمليات قراءة الذاكرة غير المُهيأة والأخطاء الأخرى المتعلقة بالعناوين. من ناحية أخرى، يمكن لـ Valgrind تشغيل أي برنامج تقريبًا كما هو. والشيء الوحيد الذي يحتاجه هو دعم جميع التعليمات التي يستخدمها برنامجنا.
لنفترض أنك تريد تصحيح أخطاء مكتبة لا يمكن الوصول إلى شفرة مصدرها. لن يؤدي استخدام أدوات التنقية إلى أي نتائج لأنها، على عكس Valgrind، تعمل على مستوى المُجمِّع. مع Valgrind، يمكنك التعامل مع هذه الحالات بشكل افتراضي، لكن ضع في اعتبارك أنك قد تحصل في النهاية على رسائل خطأ لا تعني لك الكثير لأنك لا تملك أي سيطرة على تلك الشفرة. ومع ذلك، يمكنك تصفية هذه الرسائل عن طريق تدوينها في ملف «suppressions» الذي يتم قراءته عند بدء تشغيل Valgrind.
إلى جانب عملية تصحيح الأخطاء الأكثر سلاسة، يُعد Valgrind بديلاً ممتازًا على الأنظمة الأساسية التي لا تدعم أدوات التنقية. فعلى سبيل المثال، لا يأتي Apple Clang مزودًا بـ LeakSanitizer، مما يجعل 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() );
A for loop has been added to emphasize the performance difference between AddressSanitizer and Valgrind. In both cases, the code is compiled using the following command (running ASan also requires us to add -fsanitize=address):
clang++ example.cpp -g -o example.out
تكشف نظرة سريعة على الرسم البياني أدناه عن فجوة هائلة في الأداء بين Valgrind و ASan، وهذا هو السبب الذي قد يجعل الكثير من المطورين يترددون في استخدام هذه الأداة منذ البداية.
مقارنة جنبًا إلى جنب
من أجل فهم أفضل للأخطاء التي يمكن لـ Valgrind وأدوات التنقية (sanitizers) اكتشافها، دعونا نلقي نظرة على الجدول التالي:
* يكتشف MSan عمليات
قراءة الذاكرة غير المُهيأة** يكتشف UBSan السلوك غير المُعرَّف
As you can see, Valgrind won’t help you catch overflows in stack and global variables. This is because it only has access to the heap allocations performed by malloc function. Also, don’t count on it detecting any undefined behavior in your code — though you’ll get warned about trying to access memory that resulted from undefined behavior.
وفي الوقت نفسه، فإن ASan ليست مثالية هي الأخرى. فهي لا تكتشف عمليات قراءة الذاكرة غير المُهيأة ولا السلوك غير المُعرَّف، في حين أن MSan وUBSan تقومان بذلك.
«فالغريند» أم أدوات التنقية: أيهما ينبغي أن نستخدم؟
يبقى السؤال إذن: هل ينبغي لنا استخدام Valgrind أم أن أدوات التنقية (sanitizers) تشكل بديلاً أفضل؟ لا توجد إجابة صحيحة على هذا السؤال، حيث إن هاتين الأداتين تعملان بطرق مختلفة تمامًا. ومن الناحية المثالية، ينبغي عليك استخدام كليهما اعتمادًا على بيئتك والأخطاء التي ترغب في اكتشافها.
على الرغم من أن أدوات التصحيح (sanitizers) الحالية تفرض عبئًا أقل بكثير على وحدة المعالجة المركزية (CPU) وتوفر نطاقًا أوسع من الأخطاء المكتشفة، إلا أنها لا تزال تنطوي على عيوب. فهي تعمل على مستوى المُترجم، مما يعني أنك بحاجة إلى الكود المصدري. كما أنها تتطلب إعادة ترجمة الكود مع كل عملية اختبار، وتستغرق وقتًا أطول في عملية الدمج، وتفتقر إلى الدعم على بعض المنصات.
إذا كان لديك مشروع كبير بين يديك ولا تريد أن تقلق بشأن إعادة ترجمة الكود الخاص بك، فقد يكون استخدام Valgrind خيارًا أكثر منطقية. لقد وجدنا أنه مفيد بشكل خاص في تصحيح أخطاء المكتبات ذات المصدر المغلق واكتشاف مشكلات مثل أخطاء «الاستخدام بعد التحرير» (use-after-free) بسهولة أكبر. ومع ذلك، وبما أن أدوات التنقية (sanitizers) تتحسن باستمرار، فإننا نميل إلى تفضيلها على Valgrind.
في النهاية، وبغض النظر عن الأدوات التي تستخدمها، فإن الأهم هو أن تجعل كودك آمنًا قدر الإمكان. وبناءً على ذلك، تابعونا لمزيد من المنشورات الشيقة حول تصحيح الأخطاء في لغة C++.