
إذا لم تكن خائفًا من الصورة أعلاه ، إذا كنت تعرف مدى اختلاف endian عن endian الصغيرة ، إذا كنت مهتمًا دائمًا بكيفية "ترتيب" الملفات الثنائية ، فهذا المقال مخصص لك!
مقدمة
كان هناك بالفعل العديد من المقالات حول هبر حول الهندسة العكسية للتنسيقات الثنائية وحول دراسة بنية الرمز الفرعي لملف .class:
مجموعة من الثوابت
أساسيات Bytecode جافا ،
جافا بايت "مرحبا العالم" ،
مرحبا العالم من bytecode ل JVM الخ
الباحث لديه مهمة إما التعامل مع بروتوكول ثنائي غير معروف أو حفر هيكل ثنائي له مواصفات.
نشأ اهتمامي بالتنسيقات الثنائية حتى عندما كنت طالبًا وكتب ورقة بحثية عن تطوير برنامج تشغيل نظام ملفات Linux. بعد ذلك بسنوات قليلة ، ألقيت محاضرات حول أساسيات Linux لخبراء الطب الشرعي - في الأيام الخوالي ، كان Linux جديدًا ومتخصصًا شابًا بعد الجامعة يمكنه إخبار خبراء بالغين بالكثير من الأشياء الجديدة. أخبرني بكيفية إزالة ملف تفريغ من قرص باستخدام dd ، وبعد توصيل الصورة بجهاز كمبيوتر آخر للدراسة ، أدركت أن صورة القرص تحتوي على الكثير من المعلومات المثيرة للاهتمام. يمكن استخراج هذه المعلومات حتى بدون تحميل الصورة (هاه ، حلقة -o حلقة ...) إذا كنت تعرف مواصفات تنسيق نظام الملفات وكان لديك الأدوات المناسبة. لسوء الحظ ، لم يكن لدي مثل هذه الأدوات.
بعد بضع سنوات ، كنت بحاجة إلى فك تشفير مكتبة Java. لم يكن هناك واجهة المستخدم الرسومية JD في تلك الأيام ، وكذلك أداة فك تشفير أيديولوجية ، ولكن كان هناك JAD. بالنسبة لمكتبتي ، أنتجت JAD خليطًا من أكواد جافا البرمجية مع رسائل خطأ. بالإضافة إلى ذلك ، لم تدعم JAD التعليقات التوضيحية ، وفي Java 6 ، التي ظهرت بعد ذلك ، كانت تستخدم بشكل كامل. المسلحة مع مواصفات الجهاز الظاهري جافا ، لقد بدأت ...
فكرة
كنت بحاجة إلى آلية عالمية لوصف الهياكل الثنائية ومحمل عالمي. سيقوم المُحمل ، باستخدام الوصف ، بقراءة البيانات الثنائية في الذاكرة. عادة ما يتعين عليك التعامل مع الأرقام والسلاسل وصفائف البيانات والهياكل المركبة. كل شيء بسيط مع الأرقام - لها طول ثابت - 1 أو 2 أو 4 أو 8 بايت ويمكن تعيينها على الفور لأنواع البيانات المتاحة في اللغة. على سبيل المثال: بايت ، قصيرة ، كثافة العمليات ، طويلة لجافا. بالنسبة للأنواع الرقمية التي تزيد عن بايت واحد ، يجب توفير علامة ترتيب البايت (ما يسمى تمثيل BigEndian / LittleEndiang).
السلاسل أكثر تعقيدًا - يمكن أن تكون بترميزات مختلفة (ASCII ، UNICODE) ، ذات طول ثابت أو متغير. يمكن اعتبار سلسلة ذات طول ثابت صفيف بايت. بالنسبة للسلاسل ذات الطول المتغير ، يمكنك استخدام خيارين للتسجيل - الإشارة إلى طوله في بداية السطر (سلاسل Pascal أو بادئة الطول) أو وضع حرف خاص في نهاية السطر للإشارة إلى نهاية السطر. على هذا النحو ، يتم استخدام بايت بقيمة صفر (ما يسمى بالنواب المنتهية بإلغاء null). كلا الخيارين لهما مزايا وعيوب ، نقاش يتجاوز نطاق هذا المقال. إذا تم تحديد الحجم في البداية ، فعند تطوير التنسيق ، ستحتاج إلى تحديد الحد الأقصى لطول السلسلة: يعتمد عدد البايتات التي يتعين علينا تخصيصها على علامة الطول على هذا: 2 8 - 1 بايت واحد ، 2 16 - 1 بايت اثنين ، إلخ.
سنقوم بتمييز بنيات البيانات المركبة إلى فئات منفصلة ، مع الاستمرار في تحلل الأرقام والسلاسل.
هيكل ملف .class
نحتاج إلى وصف بنية ملف Java .class بطريقة أو بأخرى. كنتيجة لذلك ، أرغب في الحصول على مجموعة من فئات Java ، حيث يحتوي كل فصل فقط على حقول تتوافق مع بنية البيانات قيد الدراسة ، وربما ، أساليب إضافية لعرض الكائن في نموذج يمكن قراءته بواسطة الإنسان عند استدعاء طريقة toString (). بشكل قاطع ، لا أريد أن يكون لديّ منطق داخله مسؤول عن قراءة أو كتابة ملف.
نأخذ مواصفات آلة جافا الافتراضية ،
مواصفات JVM ، Java SE 12 Edition .
سنهتم بالقسم 4 "تنسيق ملف الفصل".
لتحديد الحقول التي سيتم تحميلها ، سنقدم التعليق التوضيحيFieldOrder (index = ...). نحتاج إلى الإشارة صراحة إلى ترتيب الحقول الخاصة بالمحمّل ، نظرًا لأن المواصفات لا تمنحنا ضمانًا بالترتيب الذي سيتم حفظها به في ملف ثنائي.
يبدأ ملف Java .class بـ 4 بايت من الرقم السحري ، واثنين من بايت من الإصدار الثانوي من Java ، واثنين من بايت من الإصدار الرئيسي. نقوم بتعبئة الرقم السحري في المتغير int ، وأرقام الإصدارات الثانوية والقصيرة قصيرة:
@FieldOrder(index = 1) private int magic; @FieldOrder(index = 2) private short minorVersion; @FieldOrder(index = 3) private short majorVersion;
كذلك في ملف .class هو حجم التجمع الثابت (متغير ثنائي البايت) والتجمع الثابت نفسه. نقدم التعليق التوضيحيContainerSize للإعلان عن حجم الصفائف وهياكل القائمة. يمكن إصلاح الحجم (سنقوم بتعيينه من خلال سمة القيمة) أو يكون له طول متغير ، يتم تحديده بواسطة المتغير الذي تم قراءته مسبقًا. في هذه الحالة ، سوف نستخدم سمة "fieldName" ، والتي تشير إلى أي متغير سنقرأ حجم الحاوية. وفقًا للمواصفات (القسم 4.1 ،
"بنية ClassFile") ، يختلف الحجم الفعلي للتجمع الثابت بمقدار 1 عن القيمة
التي تتم كتابتها إلى constant_pool_count:
u2 constant_pool_count; cp_info constant_pool[constant_pool_count-1];
لمراعاة مثل هذه التصحيحات ، نقدم سمة مصحح إضافية في التعليقات التوضيحيةContainerSize.
الآن يمكننا إضافة وصف للتجمع المستمر:
@FieldOrder(index = 4) private short constantPoolCount; @FieldOrder(index = 5) @ContainerSize(fieldName = "constantPoolCount", corrector = -1) private List<ConstantPoolItem> constantPoolList = new ArrayList<>();
في حالة العمليات الحسابية الأكثر تعقيدًا ، يمكنك ببساطة إضافة طريقة الحصول على القيمة المرجوة: @FieldOrder(index= 1) private int containerSize; @FieldOrder(index = 2) @ContainerSize(filed="actualContainerSize") private List<ContainerItem> containerItems; public int getActualContainerSize(){ return containerSize * 2 + 3; }
تجمع ثابت
كل عنصر في مجموعة الثوابت هو إما وصف ثابت ثابت من النوع int أو long أو float أو double أو String أو وصف لأحد مكونات حقول فئة فئة Java (الحقول) والطرق وتوقيعات الطريقة ، إلخ. يعني المصطلح "ثابت" هنا قيمة غير مسماة مستخدمة في الكود:
if (intValue > 100500)
سيتم تمثيل قيمة 100500 في التجمع الثابت كمثيل CONSTANT_Integer. تحدد مواصفات JVM لـ Java 12 17 نوعًا يمكن أن تكون في مستودع ثابت.
الحالات المحتملة لعناصر تجمع const في تطبيقنا ، سنقوم بإنشاء فئة ConstantPoolItem والتي سيكون فيها علامة حقل أحادية البايت ، والتي تحدد البنية التي نقرأها في الوقت الحالي. لكل عنصر في الجدول أعلاه ، قم بإنشاء فئة Java ، سليل ConstantPoolItem. يجب أن يكون محمل الملفات الثنائية العالمي قادرًا على تحديد الفئة التي يجب استخدامها بناءً على علامة قراءة بالفعل.
(بشكل عام ، يمكن أن تكون العلامة متغيرًا من أي نوع). لهذا الغرض ، حدد واجهة HasInheritor وتطبيق هذه الواجهة في فئة ConstantPoolItem:
public interface HasInheritor<T> { public Class<? extends T> getInheritor() throws InheritorNotFoundException; public Collection<Class<? extends T>> getInheritors(); }
public class ConstantPoolItem implements HasInheritor<ConstantPoolItem> { private final static Map<Byte, Class<? extends ConstantPoolItem>> m = new HashMap<>(); static { m.put((byte) 7, ClassInfo.class); m.put((byte) 9, FieldRefInfo.class); m.put((byte) 10, MethodRefInfo.class); m.put((byte) 11, InterfaceMethodRefInfo.class); m.put((byte) 8, StringInfo.class); m.put((byte) 3, IntegerInfo.class); m.put((byte) 4, FloatInfo.class); m.put((byte) 5, LongInfo.class); m.put((byte) 6, DoubleInfo.class); m.put((byte) 12, NameAndTypeInfo.class); m.put((byte) 1, Utf8Info.class); m.put((byte) 15, MethodHandleInfo.class); m.put((byte) 16, MethodTypeInfo.class); m.put((byte) 17, DynamicInfo.class); m.put((byte) 18, InvokeDynamicInfo.class); m.put((byte) 19, ModuleInfo.class); m.put((byte) 20, PackageInfo.class); } @FieldOrder(index = 1) private byte tag; @Override public Class<? extends ConstantPoolItem> getInheritor() throws InheritorNotFoundException { Class<? extends ConstantPoolItem> clazz = m.get(tag); if (clazz == null) { throw new InheritorNotFoundException(this.getClass().getName(), String.valueOf(tag)); } return clazz; } @Override public Collection<Class<? extends ConstantPoolItem>> getInheritors() { return m.values(); } }
سيقوم اللودر العالمي بإنشاء مثيل للصف المطلوب ومتابعة القراءة. الشرط الوحيد: يجب أن تحتوي الفهارس في الفصول اللاحقة على ترقيم من طرف إلى آخر مع الفئة الأصل. هذا يعني أنه في جميع الفئات الثابتة لـ ConstantPoolItem ، FieldOrder ، يجب أن يحتوي التعليق التوضيحي على فهرس أكبر من واحد ، لأننا في الفصل الأصل قرأنا بالفعل حقل العلامة بالرقم "1".
هيكل ملف .class (تابع)
بعد قائمة عناصر التجمع الثابت في ملف .class ، يوجد معرف من وحدتي بايت يحدد تفاصيل هذه الفئة - هل الفصل عبارة عن تعليق توضيحي ، واجهة ، فئة مجردة ، هل تحتوي على علامة أخيرة ، إلخ. يتبع ذلك معرف اثنين بايت (مرجع إلى عنصر في التجمع الثابت) يحدد هذه الفئة. يجب أن يشير هذا المعرف إلى عنصر من النوع ClassInfo. يتم تعريف الطبقة الفائقة لفئة معينة بطريقة مماثلة (ما يشار إليه بعد كلمة "يمتد" في تعريف الفئة). بالنسبة للفئات التي ليس لديها فئات فائقة المعرفة بوضوح ، يحتوي هذا الحقل على مرجع إلى فئة الكائن.
في Java ، يمكن أن تحتوي أي فئة على فئة فائقة واحدة فقط ، ولكن العدد
يمكن أن يكون هناك عدة واجهات تنفذ هذه الفئة:
@FieldOrder(index = 9) private short interfacesCount; @FieldOrder(index = 10) @ContainerSize(fieldName = "interfacesCount") private List<Short> interfaceIndexList;
يمثل كل عنصر في interfaceIndexList رابطًا لعنصر في التجمع الثابت (كما هو محدد
يجب أن يكون الفهرس عنصرًا بنوع ClassInfo).
يتم تمثيل متغيرات الفئة (الخصائص والحقول) والأساليب بالقوائم المقابلة:
@FieldOrder(index = 11) private short fieldsCount; @FieldOrder(index = 12) @ContainerSize(fieldName = "fieldsCount") private List<Field> fieldList; @FieldOrder(index = 13) private short methodsCount; @FieldOrder(index = 14) @ContainerSize(fieldName = "methodsCount") private List<Method> methodList;
العنصر الأخير في وصف ملف Java .class هو قائمة سمات الفئة. يمكن سرد السمات التي تصف الملف المصدر المتعلق بالصف ، والفئات المتداخلة ، وما إلى ذلك.
تعمل Java bytecode مع البيانات الرقمية في تمثيل endian الكبير ، وسوف نستخدم هذا التمثيل بشكل افتراضي. بالنسبة إلى التنسيقات الثنائية التي تحتوي على أرقام endian الصغيرة ، سنستخدم الشرح LittleEndian . للسلاسل التي ليس لها طول محدد مسبقًا ، ولكن
تتم قراءة قبل حرف المحطة الطرفية (مثل السلاسل منتهية بقيمة خالية C- مثل) سوف نستخدم
@ StringTerminator التعليق التوضيحي:
@FieldOrder(index = 2) @StringTerminator(0) private String nullTerminatedString;
في بعض الأحيان ، في الفصول الأساسية ، تحتاج إلى إعادة توجيه المعلومات من مستوى أعلى. لا يحتوي كائن الأسلوب في methodList على معلومات حول اسم الفئة التي يوجد بها ؛ علاوة على ذلك ، لا يحتوي كائن الأسلوب على اسمه وقائمة المعلمات. يتم تقديم كل هذه المعلومات كمؤشرات على العناصر الموجودة في المجموعة الثابتة. هذا يكفي لجهاز ظاهري ، لكننا نرغب في تطبيق أساليب toString () بحيث تعرض معلومات حول الطريقة في نموذج صديق للإنسان ، وليس في شكل فهارس على عناصر في التجمع الثابت. للقيام بذلك ، يجب أن تحصل فئة Method على مرجع إلى ConstantPoolList وإلى متغير بقيمة thisClassIndex. لتتمكن من تمرير الارتباطات إلى المستويات الأساسية للتداخل ، سنستخدم التعليق التوضيحي Inject :
@FieldOrder(index = 14) @ContainerSize(fieldName = "methodsCount") @Inject(fieldName = "constantPoolList") @Inject(fieldName = "thisClassIndex") private List<Method> methodList;
في الفئة الحالية (ClassFile) ، سيتم استدعاء الأساليب الخاصة بمتغير ثابتPoolList و thisClassIndex ، وفي فئة المتلقي (في هذه الحالة الطريقة) ، سيتم استدعاء أساليب setter (إذا كانت موجودة).
بووتلوأدر العالمي
لذلك ، لدينا واجهة HasInheritor واحدة وخمسة تعليقات توضيحيةFieldOrder وContainerSize و LittleEndian و Inject وStringTerminator ، والتي تسمح لنا بوصف الهياكل الثنائية بمستوى عالٍ من التجريد. بوجود وصف رسمي ، يمكننا نقله إلى أداة التحميل الشاملة ، والتي يمكنها إنشاء مثيل للهيكل الموضح ، وتحليل الملف الثنائي وقراءته في الذاكرة.
نتيجة لذلك ، يجب أن نتمكن من استخدام هذا الرمز:
ClassFile classFile; try (InputStream is = new FileInputStream(inputFileName)) { Loader loader = new InputStreamLoader(is); classFile = (ClassFile) loader.load(); }
لسوء الحظ ، يعد مطورو أنظمة Java متطورًا جدًا لقيم ثمانية بايت في المجموعة.
يتم توفير الثوابت لخليتين ، يجب أن تحتوي الخلية الأولى على قيمة ، وتبقى الثانية
تفريغ. وهذا ينطبق على الثوابت الطويلة والمزدوجة.
وصف من مواصفات JVMجميع الثوابت 8 بايت تناول إدخالات في جدول constant_pool للفئة
الملف. إذا كانت بنية CONSTANT_Long_info أو CONSTANT_Double_info هي الإدخال
عند الفهرس n في الجدول constant_pool ، يكون الإدخال القابل للاستخدام التالي في الجدول هو
تقع في مؤشر ن + 2. يجب أن يكون فهرس constant_pool n + 1 صالحًا ولكن يجب مراعاته
غير صالحة للاستعمال.
على ما يبدو ، أراد مطورو جافا تطبيق نوع من التحسين على مستوى منخفض ، ولكن في وقت لاحق
تم التعرف على أن قرار التصميم هذا قد تحول
غير ناجحة.في الماضي ، جعل ثوابت 8 بايت تأخذ اثنين من إدخالات تجمع ثابت كان خيارا سيئا.
لمعالجة هذه الحالات المحددة ، سنقوم بإضافة تعليق توضيحي علىEntrySize ، والذي سنستخدمه ،
لوضع علامة الثوابت ثمانية بايت:
@EntrySize(value = 2, index = 1) public class EightByteNumberInfo extends ConstantPoolItem { @FieldOrder(index = 2) private int highBytes; @FieldOrder(index = 3) private int lowBytes; }
تشير السمة value إلى عدد الخلايا التي سيشغلها العنصر ، مؤشر - فهرس العنصر ،
الذي يحتوي على القيمة. سيتم توسيع فئات LongInfo و DoubleInfo فئة EightByteNumberInfo.
سوف تحتاج إلى توسيع أداة تحميل التشغيل الشاملة مع دعم وظيفي للتعليق التوضيحيEntrySize.
public ClassFileLoader(String fileName) { try { File f = new File(fileName); FileInputStream fis = new FileInputStream(f); loader = new EntrySizeSupportLoader(fis); } catch (FileNotFoundException e) { throw new RuntimeException(e); } }
بعد تحميل الفئة مع ClassFileLoader ، يمكنك إيقاف المصحح وفحص الفئة المحملة في المفتش متغير في IDE.
سيبدو ملف الفصل كما يلي:

وبركة ثابتة مثل هذا:

استنتاج
قد يرغب أي شخص يمكنه القراءة حتى النهاية في اختيار شفرة جافا البرمجية بأيديهم. لا تتردد في الانتقال إلى github وتنزيل وصف ملف فئة Java كمجموعة من فئات Java: https://github.com/esavin/annotate4j-classfile . المُحمل العام والشروح هنا: https://github.com/esavin/annotate4j-core .
لتنزيل ملف فئة مترجمة ، استخدم أداة التحميل annotate4j.classfile.loader.ClassFileLoader.
لقد كُتبت معظم الشفرة لنظام Java 6 ، حيث قمت بتكييف المجموعة الثابتة فقط مع الإصدارات الحديثة. لم يكن لدي القوة والرغبة في تنفيذ محمل Java بالكامل لأكواد Java البرمجية ، لذلك لا توجد سوى تطورات صغيرة في هذا الجزء.
باستخدام هذه المكتبة (الجزء الأساسي) ، تمكنت من إعادة الملف الثنائي باستخدام بيانات مراقبة هولتر (دراسة تخطيط القلب لنشاط القلب اليومي). من ناحية أخرى ، لم أستطع فك تشفير البروتوكول الثنائي لنظام محاسبة واحد مكتوب في دلفي. لم أفهم كيف يتم نقل التواريخ ، وأحيانًا ينشأ موقف عندما لا تتوافق البيانات الفعلية مع البنية المبنية على القيم السابقة.
حاولت إنشاء نموذج مشابه لملف فئة Java لتنسيق ELF (تنسيق قابل للتشغيل على نظام Unix / Linux) ، لكنني لم أستطع فهم المواصفات تمامًا - لقد أصبح غامضًا جدًا بالنسبة لي. نفس المصير حلت بتنسيقات JPEG و BMP - طوال الوقت واجهت بعض الصعوبات في فهم المواصفات.