فك رموز C++ المشوهة: دليل شامل لاستخدام c++filt

نظرة عامة

كثيرًا ما يُفاجأ مطورو النظم ومهندسو عكس البرمجيات في بيئات لينكس بظهور سلاسل نصية مبهمة وغامضة عند تتبع وتصحيح الملفات التنفيذية المكتوبة بلغة C++. هذه المعرفات المعقدة ليست شفرات عشوائية، بل هي نتاج آلية تُعرف بتشويه الأسماء (Name Mangling)، والتي يعتمد عليها المترجم لتمييز فضاءات التسمية (Namespaces) والدوال المحملة بشكل زائد (Function Overloading). في هذا المقال، نوضح كيفية تسخير أداة c++filt لتحويل هذه الرموز الملتوية وإعادتها فورًا إلى بنيتها البرمجية الواضحة والأصلية.

تجهيز بيئة العمل والبدء في استعادة المعرفات البسيطة

للحصول على حزمة أدوات معالجة الملفات الثنائية التي تتضمن برنامج c++filt، يكفي تحديث مستودعات النظام وتثبيت الحزمة عبر تنفيذ الأمر التالي:

apt-get update -qq && apt-get install -y binutils

للتحقق من نجاح التثبيت ومعاينة رقم الإصدار قيد التشغيل، يمكن الاستعلام باستخدام الأمر c++filt --version | head -n 2.
تعمل هذه الأداة كأداة ترجمة عكسية للمعرفات (Demangler)، حيث تفك ترميز الرموز المجمعة وتسترجع التواقيع البرمجية الصريحة للدوال والأصناف التي يمكن للبشر قراءتها بسلاسة.
⚙️ [أهم الخيارات والوسائط التشغيلية]

-_, --strip-underscore : يتجاهل الشرطة السفلية (_) الواقعة في بداية المعرف ويبدأ فك الترميز مباشرة.
-n, --no-strip-underscore : يُبقي على الشرطة السفلية البادئة دون اقتطاع أثناء المعالجة.
-p, --no-params : يقتصر الإخراج على اسم الدالة ومسارها فقط مع استبعاد معلمات الدخل (Arguments).
-t, --types : يتيح فك تشفير وتفسير أسماء الأنواع البيانية (Types) وليس الدوال فحسب.
-s, --format : يحدد التنسيق القياسي للمترجم المستهدف (ABI) لضمان دقة التفسير.

c++filt –version | head -n 2

لتجربة استرجاع دالة أولية بدون معلمات، يتم تمرير الرمز المبهم عبر الأمر c++filt _Z1fv.
تُعيد الأداة بناء التوقيع الرياضي للدالة بدقة ليتضح أنها الدالة المجردة f().

c++filt _Z1fv

عند مواجهة رمز ينتمي إلى نطاق تسمية محدد ويستقبل معلمات رقمية، يمكن تنفيذ الأمر c++filt _ZN4Math3addEii.
تقوم الأداة بتفكيك النطاق المتداخل وتحديد المعاملين، لتظهر النتيجة بوضوح بالشكل المألوف: Math::add(int, int).

c++filt _ZN4Math3addEii

استكشاف آلية التشويه عمليًا وربطها بأداة nm

لتجربة هذه الآلية من البداية وبناء ملفات كائنية لاختبارها، نقوم بتجهيز أدوات البناء الأساسية في النظام عبر تنفيذ apt-get update -qq && apt-get install -y build-essential.

apt-get update -qq && apt-get install -y build-essential

سنقوم الآن بصياغة ملف C++ تجريبي يحتوي على دالة مكررة الاسم مع اختلاف نوع المعلمات (Overloading) داخل فضاء تسمية عبر الأمر cat << 'EOF' > /tmp/demo.cpp.

cat « ‘EOF’ > /tmp/demo.cpp
namespace Calculator {
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }
}
EOF

يتم تحويل هذا الكود البرمجي إلى ملف كائن ثنائي غير مربوط (Object File) باستعمال المترجم: g++ -c /tmp/demo.cpp -o /tmp/demo.o.

g++ -c /tmp/demo.cpp -o /tmp/demo.o

لعرض جدول الرموز البرمجية المضمنة واستخراج أسماء الدوال الناتجة عن التجميع، ننفذ الأمر nm /tmp/demo.o | grep add.
تُظهر المخرجات كيف قام المترجم بتحوير الدالتين لتصبحا _ZN10Calculator3addEii و_ZN10Calculator3addEdd بهدف التفريق الحاسم بينهما داخل الجدول الثنائي.

nm /tmp/demo.o | grep add

يمكننا توجيه خرج أداة nm مباشرة عبر أنبوب المعالجة (Pipeline) لمعالجة الرموز دفعة واحدة بالصيغة: nm /tmp/demo.o | c++filt | grep add.
تتولى c++filt قراءة التدفق النصي وترجمة التواقيع المشفرة لحظيًا إلى صيغتها الصريحة المقروءة: Calculator::add(int, int) وCalculator::add(double, double).

nm /tmp/demo.o | c++filt | grep add

التعامل مع القوالب المركبة وفك التشفير عبر خطوط الأنابيب

يزداد تعقيد الرموز المشوهة عند التعامل مع مكتبة القوالب القياسية (STL). سنقوم بتوليد نموذج يعتمد على فئات متداخلة مثل std::vector وstd::string باستخدام الأمر cat << 'EOF' > /tmp/template_demo.cpp.

cat « ‘EOF’ > /tmp/template_demo.cpp
#include 
#include 
template 
class Storage {
public:
void print(T val) {}
};
template class Storage<std::vectorstd::string>;
EOF

نقوم بتجميع هذا النموذج لإنتاج ملف ثنائي تحليلي عبر تنفيذ الأمر g++ -c /tmp/template_demo.cpp -o /tmp/template_demo.o.

g++ -c /tmp/template_demo.cpp -o /tmp/template_demo.o

عند استخراج الرمز الخاص بتابع الطباعة print بواسطة nm /tmp/template_demo.o | grep print، تظهر سلسلة شديدة الطول وغير قابلة للفهم بالعين المجردة نتيجة تشفير تفاصيل القالب ومعاملاته النوعية.

nm /tmp/template_demo.o | grep print

لحل هذه المعضلة في سياق العمل المباشر، نربط المخرجات فورًا بأداة فك التشويه عبر الأمر nm /tmp/template_demo.o | c++filt | grep print.
ينتج عن هذا الإجراء إظهار البنية الهيكلية الكاملة للقالب بوضوح تام، بما يشمل أسماء الأصناف والحاويات والوسائط المرتبطة بالدالة.

nm /tmp/template_demo.o | c++filt | grep print

تخصيص مظهر الدوال ومعالجة ملفات السجلات أثناء التشخيص

في الوضع الاعتيادي، يعرض الأمر c++filt _ZN10Calculator3addEii التوقيع التفصيلي متضمنًا مسارات الفئات وقائمة الأنواع المُمررة بين القوسين.

c++filt _ZN10Calculator3addEii

إذا أردنا التركيز على معرف الدالة ومسارها فقط مع استبعاد أنواع المعاملات لتقليل الضوضاء البصرية، نمرر الخيار -p عبر الأمر c++filt -p _ZN10Calculator3addEii.
يعمل هذا التبديل على تقليص المخرج بدقة ليقتصر على اسم النطاق واسم الدالة: Calculator::add.

c++filt -p _ZN10Calculator3addEii

لتفعيل نمط فحص وتفسير الأنواع المجردة بدقة، يمكن تزويد الأداة بالمعامل -t كما في c++filt -t _ZN10Calculator3addEii.
يحافظ هذا الخيار على سلامة التفسير ويعيد البنية الكاملة Calculator::add(int, int) دون أي تشويه لمعلومات الأنواع.

c++filt -t _ZN10Calculator3addEii

تكمن القوة الحقيقية لـ c++filt في قدرتها على تنقيح سجلات الأخطاء البرمجية تلقائيًا؛ فبتمرير رسالة خطأ تتضمن رمزًا مدمجًا عبر echo 'Fatal error in _ZN10Calculator3addEii at runtime' | c++filt، تلتقط الأداة الرمز المشوه وتستبدله في موقعه الدقيق مع الحفاظ التام على سياق الجملة المحيطة.

echo ‘Fatal error in _ZN10Calculator3addEii at runtime’ | c++filt

بهذا يتضح كيف ترفع آلية تشويه الأسماء (Name Mangling) من كفاءة الربط الثنائي في مترجمات C++، وكيف توفر أداة c++filt وسيلة مرنة لفك طلاسم هذه الرموز. يساعد دمج هذه الأداة في سير عملك على تسريع تتبع مشكلات الربط (Linker Errors) وتشريح تقارير الانهيار في بيئات لينكس بكل سهولة.