ما هو داخل ملف wasm؟ إدخال wasm-decompile

لدينا العديد من المترجمين والأدوات الأخرى تحت تصرفنا لإنشاء ملفات .wasm والعمل عليها. عدد هذه الأدوات يتزايد باستمرار. في بعض الأحيان تحتاج إلى النظر في ملف .wasm ومعرفة ما بداخله. ربما أنت مطور إحدى أدوات Wasm ، أو ربما أنت مبرمج يكتب رمزًا مصممًا ليتم تحويله إلى Wasm ويهتم بكيفية ظهوره إلى ما سيتحول إليه رمزه. قد ينشأ مثل هذا الاهتمام ، على سبيل المثال ، من خلال اعتبارات الأداء.



تكمن المشكلة في أن ملفات .wasm تحتوي على كود منخفض المستوى يشبه إلى حد كبير كود المجمع الحقيقي. على وجه الخصوص ، على عكس ، على سبيل المثال ، JVM ، يتم تجميع جميع هياكل البيانات في مجموعات من عمليات التحميل / التخزين ، وليس في شيء يحتوي على أسماء واضحة للحقول والفئات. يمكن للمجمعين ، مثل LLVM ، تغيير رمز الإدخال بحيث لا يبدو ما يحصلون عليه قريبًا منه. 

وماذا عن من يريد أخذ ملف wasm لمعرفة ما يحدث فيه؟

تفكيك أو ... فك؟


لتحويل ملفات .wasm إلى ملفات .wat تحتوي على تمثيل نصي قياسي لكود Wasm ، يمكنك استخدام أدوات مثل wasm2wat (هذا جزء من مجموعة أدوات WABT ). نتائج هذا التحويل دقيقة للغاية ، ولكن قراءة الكود الناتج ليس ملائمًا بشكل خاص.

هنا ، على سبيل المثال ، هي وظيفة بسيطة مكتوبة في C:

typedef struct { float x, y, z; } vec3;

float dot(const vec3 *a, const vec3 *b) {
    return a->x * b->x +
           a->y * b->y +
           a->z * b->z;
}

يتم تخزين الكود في ملف dot.c.

نستخدم الأمر التالي:

clang dot.c -c -target wasm32 -O2

بعد ذلك ، لتحويل ما حدث إلى ملف .wat ، نطبق الأمر التالي:

wasm2wat -f dot.o

إليك ما سيعطينا:

(func $dot (type 0) (param i32 i32) (result f32)
  (f32.add
    (f32.add
      (f32.mul
        (f32.load
          (local.get 0))
        (f32.load
          (local.get 1)))
      (f32.mul
        (f32.load offset=4
          (local.get 0))
        (f32.load offset=4
          (local.get 1))))
    (f32.mul
      (f32.load offset=8
        (local.get 0))
      (f32.load offset=8
        (local.get 1))))))

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

دعنا نحاول ، بدلاً من استخدام wasm2wat ، قم بتشغيل الأمر التالي:

wasm-decompile dot.o

إليك ما ستقدمه لنا:

function dot(a:{ a:float, b:float, c:float },
             b:{ a:float, b:float, c:float }):float {
  return a.a * b.a + a.b * b.b + a.c * b.c
}

يبدو بالفعل أفضل بكثير. بالإضافة إلى استخدام التعبيرات التي تذكرنا بلغة برمجة معروفة بالفعل لك ، تقوم أداة فك الشفرة بتحليل الأوامر التي تهدف إلى العمل مع الذاكرة وتحاول إعادة إنشاء هياكل البيانات التي تمثلها هذه الأوامر. يقوم النظام بعد ذلك بوضع تعليقات توضيحية على كل متغير ، يتم استخدامه كمؤشر بإعلان بنية "مدمج". لا يقوم المترجم بإنشاء إعلان بنية مسمى ، لأنه لا يعرف ما إذا كان هناك شيء مشترك بين الهياكل التي تستخدم 3 قيم عائمة لكل منها.

كما ترون ، تبين أن نتائج التفكيك أكثر قابلية للفهم من نتائج التفكيك.

ما هي اللغة التي يكتبها المحلل اللغوي؟


تقوم أداة wasm-decompile بإخراج الكود ، في محاولة لجعل هذا الكود يبدو وكأنه نوع من لغة البرمجة "المتوسطة". في الوقت نفسه ، تحاول هذه الأداة ألا تبتعد كثيرًا عن Wasm.

كان الهدف الأول من wasm-decompiler هو إنشاء كود قابل للقراءة. هذا هو - مثل هذا الرمز الذي سيسمح للقارئ بفهم ما يحدث بسهولة في ملف wasm المتحلل. الغرض الثاني من هذه الأداة هو توفير التمثيل الأكثر دقة لملف .wasm ، من خلال إنشاء رمز يمثل بالكامل ما يحدث في الملف المصدر. من الواضح أن هذه الأهداف بعيدة كل البعد عن التوافق الجيد مع بعضها البعض.

لم يتم تصور مخرجات wasm-decompiler في الأصل على أنها رمز يمثل بعض لغة البرمجة الحقيقية. لا توجد طريقة حاليًا لتجميع هذا الرمز في Wasm.

أوامر لتحميل وحفظ البيانات


كما هو موضح أعلاه ، يبحث wasm-decompile عن أوامر التحميل والحفظ المرتبطة بمؤشر معين. إذا كانت هذه الأوامر تشكل تسلسلاً مستمرًا ، فإن أداة فك الشفرة تعرض أحد إعلانات بنية البيانات "المضمنة".

إذا لم يتم الوصول إلى جميع "الحقول" ، لا يمكن لمحلل الشفرة تمييز الهيكل بشكل موثوق من تسلسل معين من العمليات مع العمل مع الذاكرة. في هذه الحالة ، يستخدم wasm-decompile خيار العودة ، باستخدام أنواع أبسط مثل float_ptr(إذا كانت الأنواع هي نفسها) ، أو ، في أسوأ الحالات ، يولد كودًا يوضح كيفية العمل مع مصفوفة ، مثل o[2]:int. يخبرنا هذا الرمز أنه oيشير إلى عناصر من النوع int، وننتقل إلى العنصر الثالث من هذا النوع.

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

يسعى المترجم إلى اتباع نهج ذكي للفهرسة وقادر على تحديد أنماط مثل (base + (index << 2))[0]:int. مصدر هذه الأنماط هو عمليات الفهرسة المعتادة لـ C ، مثل base[index]حيث baseتشير إلى نوع 4 بايت. هذا أمر شائع جدًا في التعليمات البرمجية ، نظرًا لأن Wasm ، في أوامر تحميل البيانات وحفظها ، لا يدعم سوى الإزاحات المحددة على أنها ثوابت. في الكود الذي تم إنشاؤه بواسطة wasm-decompile ، يتم تحويل هذه البنيات إلى نوع base[index]:int.

بالإضافة إلى ذلك ، يعرف المترجم عندما تشير العناوين المطلقة إلى قسم البيانات.

التحكم في تدفق البرنامج


إذا تحدثنا عن بنيات التحكم ، فإن أشهرها هو بناء if-ثم Wasm ، والذي يتحول if (cond) { A } else { B }، مع إضافة حقيقة أن مثل هذا البناء في Wasm يمكن أن يعيد قيمة ، لذلك يمكن أن يمثل أيضًا عاملًا ثلاثيًا ، مثل cond ? A : B، في بعض اللغات.

هياكل المراقبة الوسم الأخرى على أساس كتلة blockو loop، وكذلك التحولات br، br_ifو br_table. يحاول المترجم البقاء بالقرب من هذه الهياكل قدر الإمكان. لا يسعى إلى إعادة البناء أثناء / من أجل / تبديل الهياكل التي يمكن أن تكون بمثابة الأساس لهم. والحقيقة هي أن هذا النهج يظهر نفسه بشكل أفضل عند معالجة التعليمات البرمجية المحسنة. على سبيل المثال ، تصميم تقليديloop قد تبدو في الرمز الذي تم إرجاعه بواسطة wasm-decompile مثل هذا:

loop A {
  //    .
  if (cond) continue A;
}

فيما يلي Aتسمية تسمح لك ببناء هياكل متداخلة في بعضها البعض loop. والحقيقة أن هناك أوامر ifو continueتستخدم للسيطرة على دورة قد تبدو إلى حد ما غريبة بينما الحلقات، لكنها تتوافق مع الوسم البناء br_if.

يتم رسم الكتل بطريقة مماثلة ، ولكن هنا الشروط في البداية ، وليس في النهاية:

block {
  if (cond) break;
  //    .
}

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

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

br_table[A, B, C, ..D](a);
label A:
return 0;
label B:
return 1;
label C:
return 2;
label D:

هذا يذكرنا بالاستخدام switchللتحليل aعندما يكون الخيار الافتراضي هو D.

ميزات أخرى مثيرة للاهتمام


فيما يلي بعض الميزات الإضافية لـ wasm-decompile:

  • , . C++-.
  • , , , . . , .
  • .
  • Wasm-, . , wasm-decompile , , , .
  • , ( , C- ).

 


فك شفرة Wasm هي مهمة أكثر تعقيدًا من فك شفرة JVM بايت.

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

على عكس JVM bytecode ، تم تحسين الرمز الذي يدخل إلى ملفات .wasm إلى حد كبير بواسطة LLVM. ونتيجة لذلك ، غالبًا ما يفقد مثل هذا الرمز معظم هيكله الأصلي. يختلف كود الإخراج كثيرًا عما يكتبه المبرمج. هذا يعقد إلى حد كبير مهمة فك شفرة Wasm مع إخراج النتائج التي يمكن أن تجلب فوائد حقيقية للمبرمجين. لكن هذا لا يعني أننا لا يجب أن نسعى لحل هذه المشكلة!

ملخص


إذا كنت مهتمًا بموضوع فك شفرة Wasm ، فربما تكون أفضل طريقة لفهم هذا الموضوع هي أخذ مشروع wasm. بالإضافة إلى ذلك ، يمكنك العثور هنا على إرشادات أكثر تفصيلاً حول فك الضغط. يمكن العثور على كود المحلل اللغوي في ملفات هذا المستودع ، والتي تبدأ أسماؤها بـ decompile(إذا كنت ترغب ، انضم إلى العمل على المحلل). هنا يمكنك العثور على اختبارات توضح أمثلة إضافية للاختلافات بين ملفات .wat ونتائج فك التجميع.

وبأي أدوات تبحث عن ملفات .wasm؟

, , iPhone. , .


All Articles