وقتی یک فایل باینری کامپایلشده از پایتون، جاوا، سیشارپ یا وباسمبلی را به ابزارهای مهندسی معکوس یا دیکامپایلر میسپارید، مواجهه با خطاهایی مانند کدهای ناشناخته یا مبهمسازی گمراهکننده بسیار رایج است. بسیاری از توسعهدهندگان بدون آگاهی از ساختار درونی هدر و جدولهای این کانتینرها، خروجیهای حدسی دیکامپایلرها را واقعیت محض فرض میکنند؛ در حالی که هر فرمت بخشی از اطلاعات سورسکد را نگه میدارد و بخشی دیگر را دور میریزد.
این درس به شما یاد میدهد که چگونه بایتکد را در چهار اکوسیستم اصلی شامل پایتون (pyc)، جاوا (فایلهای کلاس JVM)، داتنت (متادیتای ECMA-335) و ماژولهای WebAssembly بهطور مستقیم و بدون وابستگی کورکورانه به ابزارهای آماده بخوانید و بررسی کنید. با نوشتن پارسر برای این ساختارها، یاد میگیرید که چه دادههایی مستقیماً در فایل وجود دارند و چه بخشهایی صرفاً استنتاج دیکامپایلر هستند.
یک فایل بایتکد در واقع همان برنامهی شماست بههمراه یادداشتهایی که کامپایلر دربارهی آن ثبت کرده است. در این درس، خوانندههایی (readers) میسازیم که نشان میدهند هر قالب نگهدارنده (container) چه یادداشتهایی را حفظ میکند.
اهداف یادگیری
- خواندن هدر 16 بایتی
.pycبر اساس PEP 552 و تبدیل مستقیم بایتها به شیء کد (unmarshal) بدون استفاده از ماژولmarshal، برای فایلهای تولیدشده با چهار نسخهی مختلف مفسر. - تجزیهی ساختار فایل کلاس JVM بر اساس فصل 4 سند JVMS: جدول مقادیر ثابت (constant pool)، فیلدها، متدها، صفت
Codeو جدولهایی که سوئیچjavac -gاضافه میکند. - دیزاسمبل کردن آپکدهای JVM در فایل
Ledger.classو اثبات درستی فهرست دستورات در برابرjavap -cروی چهار بیلد مختلف از یک کد منبع یکسان. - خواندن بخشبهبخش ماژول WebAssembly، رمزگشایی (decode) بدنهی تمام توابع و اثبات درستی نتیجه در مقایسه با
wasm-objdump -d. - پیمایش متادیتای ECMA-335 در یک اسمبلی .NET از دایرکتوری دادهی PE تا ردیفهای
MethodDefو کدهای CIL هر متد، همراه با اعتبارسنجی با کتابخانهی خوانندهای که در ILSpy به کار رفته است. - سنجش و اندازهگیری اینکه برای یک تابع مشخص در پنج نگهدارنده، هر قالب چه نامها، انواع داده (types) و ساختارهای کنترلی را حفظ میکند و چه مواردی را از دست میدهد.
صورت مسئله
کد ledger.py را کامپایل کردید و فایل ledger.cpython-314.pyc در پوشهی __pycache__ ایجاد شد. از یک ایجنت (agent) خواستید محتوای فایل را بررسی کند. ایجنت یک دیکامپایلر اجرا کرد؛ ابزار خطای Unsupported opcode و پیام # WARNING: Decompyle incomplete را برگرداند. ایجنت بلافاصله نتیجه گرفت که فایل مبهمسازی (obfuscated) شده است.
اما فایل مبهمسازی نشده بود. دو بایت اول آن 2b 0e بود؛ یعنی کلمهی 3627 در قالب لیتلاندین (little-endian). این همان عددی است که CPython 3.14 مینویسد، درحالیکه جدولهای آپکدِ آن دیکامپایلر تا نسخهی 3.13 متوقف شده بود. ابزار قدیمی بود. از آنجا که ایجنت هدر فایل را نخوانده بود، نتوانست تفاوت این دو حالت را تشخیص دهد.
در درس 33 دیدیم که کدهای ماشین بومی (native) چه اطلاعاتی را دور میریزند: نام پارامترها، انواع داده و دستور switch که به یک جدول پرش تبدیل شده است. نگهدارندههای بایتکد اطلاعات کمتری را حذف میکنند، اما هر کدام بخش متفاوتی را دور میریزند. یک فایل pyc تمام نامهای متغیرها را نگه میدارد اما هیچ نوع دادهای را حفظ نمیکند. یک فایل کلاس JVM انواع داده را ثبت میکند، ولی نام متغیرهای محلی را تنها در صورتی که بخواهید نگه میدارد. یک ماژول Wasm ساختار کنترلی را تماموکمال حفظ میکند، اما نامها را میاندازد مگر اینکه یک بخش سفارشی (custom section) آنها را حمل کند. یک اسمبلی .NET نام پارامترها را در یک جدول اختصاصی نگه میدارد اما نام متغیرهای محلی را به فایلی مجزا میسپارد. تا ندانید نگهدارنده در ابتدا چه یادداشتهایی را ذخیره کرده بود، نمیتوانید دربارهی خروجی دیکامپایلر روی هیچکدام از آنها قضاوت کنید.
در این درس چهار خواننده با استفاده از کتابخانهی استاندارد پایتون میسازیم. هر خواننده فایل نگهدارنده را از بایت اول آن میخواند، اطلاعات یک تابع را فهرست میکند و درستی خروجیاش با ابزار رسمی همان قالب روی فیکسچرهایی که ساختهاید ارزیابی و تأیید میشود. در بخش پایانی، وضعیت تابع grade از فایل ledger را در هر پنج نگهدارنده کنار یکدیگر مقایسه خواهیم کرد.
هدر pyc از چهار کلمه تشکیل شده است
استاندارد PEP 552 هدر فایل را بهصورت چهار کلمهی 32 بیتی تعریف میکند. کلمهی اول عدد جادویی (magic number) است. کلمهی دوم یک میدان بیتی (bit field) است: فعال بودن بیت 0 یعنی فایل مبتنی بر هش (hash-based) است؛ فعال بودن بیت 1 حالت check_source را نشان میدهد، یعنی مفسر هنگام وارد کردن ماژول (import)، کد منبع را هش کرده و تطبیق میدهد. اگر بیت 0 صفر باشد، کلمات سوم و چهارم زمان آخرین تغییر (mtime) و اندازهی کد منبع را نگه میدارند. اگر بیت 0 یک باشد، این دو کلمه هش 64 بیتی کد منبع با الگوریتم SipHash را در خود جای میدهند.
مقدار عدد جادویی در مسیر Include/internal/pycore_magic_number.h قرار دارد. از نسخهی 3.11 به این سو، قاعدهی شمارهگذاری این است که Python 3.n با عدد 2900 + 50n شروع میشود و با هر تغییر در بایتکد در طول چرخهی انتشار، یک واحد به آن افزوده خواهد شد. توکن ذخیرهشده یک عدد 16 بیتی است که بلافاصله با \r\n دنبال میشود؛ بنابراین اگر یک فایل pyc در حالت متنی باز شود، عدد جادویی مخدوش و نامعتبر خواهد شد. مقادیر نهایی عبارتاند از: 3495 برای نسخهی 3.11، 3531 برای 3.12، 3571 برای 3.13، 3627 برای 3.14 و 3666 برای 3.15. نسخهی Python 3.15.0 در تاریخ 2026-10-09 منتشر شد و روی سیستمی که این فیکسچرها را ساخته نصب نیست؛ بنابراین عدد مربوط به آن مستقیماً از فایل هدر استخراج شده و از فایلی در اینجا نیامده است.
فیکسچرهای ما برای هر مفسر دو فایل pyc دارند. اسکریپت compile_pyc.py آنها را با دستور py_compile.compile(..., dfile="ledger.py") نوشته است تا نام فایلِ ذخیرهشده صرفاً ledger.py باشد، نه مسیر سیستم سازنده. 16 بایت اول فایلهای کامپایلشده با نسخهی 3.14 به این صورت است:
checked-hash 2b 0e 0d 0a 03 00 00 00 c7 16 c7 fc c5 1d 81 01
timestamp 2b 0e 0d 0a 00 00 00 00 8d 60 cf 6a f1 01 00 00
هدر مبتنی بر برچسب زمانی را کلمهبهکلمه بخوانید: 03 00 00 00 یا 00 00 00 00 کلمهی فلگها است. مقدار 8d 60 cf 6a معادل 0x6acf608d = 1791975565 و نشاندهندهی mtime فایل ledger.py در زمان ساخت فایل است. مقدار f1 01 00 00 عدد 497 یعنی اندازهی کد منبع را نشان میدهد. فایلی که هش آن بررسی میشود، فلگ 3 و به جای آن هشت بایتِ هش دارد. هر چهار مفسر بر سر تمام بخشهای هدر بهجز کلمهی جادویی توافق دارند:
| مفسر | بایتهای جادویی | کلمه | بایتهای فایل | بایتهای grade.co_code |
دستورات grade |
|---|---|---|---|---|---|
| 3.11.14 | a7 0d 0d 0a |
3495 | 1054 | 80 | 31 |
| 3.12.12 | cb 0d 0d 0a |
3531 | 937 | 62 | 26 |
| 3.13.12 | f3 0d 0d 0a |
3571 | 943 | 70 | 26 |
| 3.14.6 | 2b 0e 0d 0a |
3627 | 965 | 96 | 35 |
تابع read_pyc_header() در فایل code/main.py این فیلدها را برمیگرداند و بر اساس کلمه، نسخهی مفسر را مشخص میکند. ویژگی importlib.util.MAGIC_NUMBER همین چهار بایت را برای مفسر در حال اجرا برمیگرداند. اجرای دستور python3 code/main.py compare کد منبع را با تمام مفسرهای موجود در مسیر PATH (از python3.11 تا python3.15) کامپایل میکند؛ سپس برای هر کدام، هدری را که خوانندهی ما میبیند در کنار مشخصاتی که خود مفسر گزارش میدهد چاپ میکند.
ماژول Marshal شیء کد را فیلدبهفیلد مینویسد
بایت شانزدهم در تمام فیکسچرها e3 است؛ یعنی 0x80 | ord('c') که کد نوع دادهی marshal برای یک شیء کد (code object) با بیت فعال FLAG_REF است. این بیت به خواننده میگوید شیء را ذخیره کند تا بعداً بتوان با ارجاعات r به آن بازگشت. مستندات marshal اعلام میکند که این فرمت عمداً مستندسازی نشده و اشیاء کد بین نسخههای مختلف سازگار نیستند. هر دو گزاره درست هستند، اما فرمت آنقدر کوچک است که بشود خواندش. فایل Python/marshal.c این کدهای نوع را فهرست کرده است: N برای None، i برای عدد صحیح 32 بیتی، s برای بایتها، z و Z برای رشتههای اسکی کوتاه، ( و ) برای تاپلها، c برای شیء کد و r برای ارجاع به عقب. ساختارهای نگهدارنده پیش از فرزندانشان شکاف ارجاعی خود را رزرو میکنند؛ خوانندهی ما نیز همین کار را انجام میدهد.
تابع w_complex_object() شیء کد را با ترتیبی ثابت مینویسد. برای نسخههای 3.11 تا 3.15 این ترتیب شامل مقادیر 32 بیتی argcount، posonlyargcount، kwonlyargcount، stacksize و flags بهصورت عدد صحیح 32 بیتی است؛ سپس اشیاء code، consts، names، localsplusnames، localspluskinds، filename، name و qualname، سپس firstlineno و در نهایت linetable و exceptiontable قرار میگیرند. شیء کد ماژول را در فایل نسخهی 3.14 از بایت 17 بهصورت دستی رمزگشایی (decode) کنید:
00 00 00 00 argcount 0
00 00 00 00 posonlyargcount 0
00 00 00 00 kwonlyargcount 0
01 00 00 00 stacksize 1
00 00 00 00 flags 0
f3 22 00 00 00 type 's' with FLAG_REF, 34 bytes of co_code follow
80 00 52 06 74 00 52 01 17 00 74 01 ...
بایتهای کد برای هر دستور دو بایت هستند: 80 00 معادل RESUME 0، 52 06 معادل LOAD_CONST 6، 74 00 معادل STORE_NAME 0 و 17 00 معادل MAKE_FUNCTION. خروجی 3.14 در فیکسچرها دقیقاً همین پنج دستور در سطح ماژول را چاپ میکند. بعد از code، مقادیر ثابت (constants) میآیند که چهار شیء کد تودرتو را در خود دارند، و پس از آنها names قرار میگیرد: ('LABELS', 'percent_change', 'grade', 'total', 'grade_label').
کلاس Unmarshaller در code/main.py هم این ساختار و هم ساختار قدیمیتر در پایتون 3.9 و 3.10 را میخواند؛ در نسخههای قدیمیتر، فیلدهای nlocals، varnames، freevars و cellvars مجزا بودند. این کلاس یک CodeObject با همان نام صفتهای شیء واقعی میسازد. صفت varnames با فیلتر کردن localsplusnames بر اساس بیت CO_FAST_LOCAL در localspluskinds ساخته میشود. این تابع برای تابع grade در هر چهار فایل، مقادیر ('score', 'bucket')، مقدار 10 برای firstlineno و یک linetable غیرخالی را برمیگرداند. تست test_unmarshaller_agrees_with_the_running_interpreter تمام فیلدها را با خروجی تابع marshal.loads() روی فایلی که مفسر فعلی نوشته مقایسه میکند. سه فایل دیگر با کد ما خوانده میشوند، درحالیکه ماژول marshal همین مفسر نمیتواند آنها را بخواند.
چهار مفسر و چهار بایتکد گوناگون برای تابع grade
مستندات ماژول dis در همهی نسخهها یک هشدار مشترک دارد: بایتکد جزئیات پیادهسازی CPython است و هیچ تضمینی برای ثابت ماندن آن وجود ندارد. فیکسچرها نشان میدهند این هشدار برای یازده خط کد پایتون به چه معناست. خروجی python3.1N -m dis ledger.py برای تکتک مفسرها ثبت شده است.
در پایتون 3.11، تابع grade با چنین کدهایی شروع میشود:
11 2 LOAD_FAST 0 (score)
4 LOAD_CONST 1 (10)
6 BINARY_OP 2 (//)
10 STORE_FAST 1 (bucket)
12 12 LOAD_FAST 1 (bucket)
14 LOAD_CONST 2 (9)
16 COMPARE_OP 5 (>=)
22 POP_JUMP_FORWARD_IF_FALSE 2 (to 28)
دستور BINARY_OP در آفست 6 قرار دارد و دستور بعدی در آفست 10 نشسته است. دو بایتِ بین آنها مدخل حافظهی نهان درونخطی (inline cache) مربوط به PEP 659 است که مفسر تطبیقی و تخصصیساز (specializing adaptive interpreter) از آن استفاده میکند. بعد از دستور COMPARE_OP در آفست 16، بلافاصله آفست 22 میآید، یعنی دو مدخل کش به آن اختصاص دارد. این کشها پنهان هستند مگر اینکه آرگومان show_caches=True را ارسال کنید. پایتون 3.12 ترکیب LOAD_CONST و RETURN_VALUE را با دستور RETURN_CONST جایگزین میکند، نام دستور پرش را به POP_JUMP_IF_FALSE تغییر میدهد و حجم کد grade را از 80 به 62 بایت کاهش میدهد. نسخهی 3.13 به جای آفستها برچسبهای L1 تا L4 را چاپ میکند و گزینههای خط فرمان -C (برای کشها) و -O (برای آفستها) را در اختیارتان میگذارد. نسخهی 3.14 مجدداً RETURN_CONST را حذف میکند، دستورات LOAD_SMALL_INT و LOAD_FAST_BORROW را میافزاید و بعد از هر پرش شرطی، یک نشانگر NOT_TAKEN قرار میدهد:
12 20 LOAD_FAST_BORROW 1 (bucket)
22 LOAD_SMALL_INT 9
24 COMPARE_OP 188 (bool(>=))
26 CACHE 0 (counter: 0)
28 POP_JUMP_IF_FALSE 3 (to L1)
30 CACHE 0 (counter: 0)
32 NOT_TAKEN
فهرست بهدستآمده با پرچمهای -C -O تعداد 13 مدخل CACHE را درون grade نشان میدهد: پنج مدخل بعد از BINARY_OP، یکی بعد از هر COMPARE_OP و یکی بعد از هر POP_JUMP_IF_FALSE. فیکسچر تطبیقی نشان میدهد کاربرد این کشها چیست: پس از 200 بار فراخوانی، خروجی dis.dis(grade, adaptive=True) به جای RESUME و COMPARE_OP، مقادیر RESUME_CHECK و COMPARE_OP_INT را چاپ میکند و در تابع total هم دستورات FOR_ITER_LIST، BINARY_OP_ADD_INT و JUMP_BACKWARD_NO_JIT دیده میشوند. این شکلهای تخصصیشده در همان بایتهای co_code مینشینند. فایل pyc هرگز حاوی این دستورات نیست، زیرا تخصصیسازی در حافظه رخ میدهد.
به همین دلیل است که ابزارهای دیکامپایل سقف نسخه دارند. ابزار uncompyle6 نسخهی 3.9.3 اعلام میکند که بایتکدهای پایتون 1.0 تا 3.8 را میپذیرد. ابزار decompyle3 نسخهی 3.9.3 بایتکدهای 3.7 و 3.8 را میخواند. ابزار xdis نسخهی 6.3.0 (دیزاسمبلر چندنسخهای زیربنای هر دو ابزار قبلی) حداکثر تا 3.13 را میخواند. پروژهی pycdc در فایلهای بیلد خود جدولهای آپکد را تا python_3_13.cpp دارد و به محض مواجهه با یک آپکد ناشناخته، عبارت Unsupported opcode را چاپ کرده و خروجی را ناقص علامت میزند. سرویس PyLingual ادعا میکند تمام نسخههای منتشرشده از 3.6 به بعد را پوشش میدهد و برای کامپایل مجدد و اعتبارسنجی خروجیاش از مفسرهای 3.8 تا 3.15 کمک میگیرد. در مواجهه با یک فایل 3.14، سه ابزار اول یا رد میکنند یا شکست میخورند و فقط دو گزینهی آخر به کار میآیند. پیش از انتخاب ابزار، حتماً کلمهی جادویی را بخوانید.
فایل کلاس ساختار درونی خود را در یک دیکشنری نگه میدارد
بخش 4.1 از استاندارد JVMS مربوط به Java SE 25 ساختار فایل کلاس را به شکل یک ساختار واحد معرفی میکند: magic، minor_version، major_version، constant_pool_count، سپس جدول مقادیر ثابت، access_flags، this_class، super_class، اینترفیسها، فیلدها، متدها و صفات. مقدار جادویی 0xCAFEBABE است. ماشین مجازی Java SE 25 نسخههای اصلی (major) از 45 تا 69 را میپذیرد و عدد 65535 برای نسخهی فرعی (minor) نشاندهندهی یک فایل دارای قابلیتهای آزمایشی (preview-feature) است. ده بایت اول Ledger-g.class برابر ca fe ba be 00 00 00 41 00 5e است: نسخهی اصلی 65 نشاندهندهی جاوا 21 است و 0x5e = 94 تعداد مقادیر ثابت موجود در جدول را مشخص میکند. فایل Ledger-jdk25.class که با کامپایلر javac 25.0.2 از همان سورس تولید شده، با ca fe ba be 00 00 00 45 شروع میشود که نشانهی نسخهی اصلی 69 است.
جدول مقادیر ثابت دقیقاً همان جایی است که فایل کلاس دیکشنری اجزای خود را در آن نگهداری میکند. مدخل شمارهی #13 در بیلد -g از نوع Methodref است که به کلاس شمارهی #8 و نام-و-نوع شمارهی #14 اشاره میکند. دنبال کردن این ایندکسها ما را به Ledger.grade:(I)I میرساند؛ یعنی دقیقاً همان متنی که javap -v پس از کاراکترهای // نمایش میدهد. توصیفگر (descriptor) متد (I)I بیانگر یک پارامتر int و یک خروجی int است. توصیفگر ([I)I برای متد total نشان میدهد ورودی آن یک آرایه از int است. هر متد، فیلد و کلاسی که در کد فراخوانی شود چنین مدخلی دارد؛ مدل اعتبارسنج (verifier) در بخش 4.10 فایلی را که دستوراتش با این توصیفگرها همخوانی نداشته باشد رد میکند. کلاس ClassFile در code/main.py تمام 17 برچسب ثابت جدول 4.4 را میخواند و انواع Long یا Double را به عنوان دو اسلات در نظر میگیرد. این کد در صورت مواجهه با برچسب ناشناخته یا بایتهای اضافی پس از آخرین صفت، خطا برمیگرداند.
صفات میتوانند به کلاس، هر فیلد، هر متد و حتی خود صفت Code متصل شوند. سند JVMS تصریح میکند که ماشین مجازی باید از صفاتی که نمیشناسد صرفنظر کند؛ به همین دلیل میتوان جدولهای اشکالزدایی را بدون کوچکترین دستکاری در بدنهی متدها اضافه یا حذف کرد. راهنمای javac توضیح میدهد که سوئیچ -g تمام اطلاعات اشکالزدایی از جمله متغیرهای محلی را درج میکند، حالت پیشفرض فقط شمارهی خطوط و نام فایل منبع را تولید میکند و سوئیچ -g:none هیچ اطلاعاتی تولید نمیکند. این سه بیلد تفاوت را بهخوبی نشان میدهند:
| بیلد | بایتها | تعداد ثابتها | LineNumberTable |
LocalVariableTable |
SourceFile |
بایتهای کد grade |
|---|---|---|---|---|---|---|
javac -g |
1832 | 94 | 7 متد | 6 متد | دارد | 50 |
javac |
1581 | 82 | 7 متد | ندارد | دارد | 50 |
javac -g:none |
1404 | 79 | ندارد | ندارد | ندارد | 50 |
پنجاه بایتِ کد متد grade در هر سه فایل کاملاً یکسان است و تستها این موضوع را بررسی میکنند. چیزی که فلگ -g به متد grade اضافه میکند، تنها یک ردیف در LocalVariableTable است: شروع 0، طول 50، اسلات 0، نام score و توصیفگر I. بدون وجود این ردیف، دیکامپایلر اسلات 0 با نوع دادهی int را میبیند و مجبور است نامی برای آن سرهم کند. جدول StackMapTable در همهی بیلدها باقی میماند چون مدل اعتبارسنج به آن نیاز دارد؛ صفت BootstrapMethods هم حفظ میشود چون متد main رشتهها را با سازوکار invokedynamic به هم متصل میکند.
رمزگشایی دستی دستور tableswitch
فصل ششم JVMS هر دستور را بهصورت یک بایت آپکد و عملوندهایی تعریف میکند که آن آپکد تعیین کرده است. آفستهای انشعاب مقادیری علامتدار و 16 بیتی هستند که نسبت به آدرس همان آپکدِ پرش سنجیده میشوند؛ برخلاف معماری x86-64 که در آن جابهجایی نسبت به دستور بعدی محاسبه میشود. دستورات tableswitch و lookupswitch با صفر تا سه بایت پدینگ (padding) آغاز میشوند تا عملوندهای 32 بیتی آنها حتماً در آدرسی مضرب چهار قرار بگیرند.
بایتهای دستور grade از pc برابر 4 در فایل Ledger-g.class:
aa tableswitch at pc 4
00 00 00 padding to pc 8
00 00 00 2c default: 4 + 44 = 48
00 00 00 06 low 6
00 00 00 0a high 10
00 00 00 2a case 6: 4 + 42 = 46
00 00 00 28 case 7: 4 + 40 = 44
00 00 00 26 case 8: 4 + 38 = 42
00 00 00 24 case 9: 4 + 36 = 40
00 00 00 24 case 10: 4 + 36 = 40
سیوشش بایت، فقط یک دستور، و بازنمایی کل ساختار switch (score / 10) به همراه حالت مشترک آن برای مقادیر 9 و 10. بیلد بومی همین عبارت switch در درس 33 نیازمند بررسی حدود (bounds check)، دستورات lea، movslq، add، یک پرش غیرمستقیم و پنج مورد بازنشانی (relocation) در یک بخش دیگر بود. در اینجا اما تمام مقاصد درون خود دستور جای گرفتهاند و این حقیقت که همگی به هم تعلق دارند نیز ثبت شده است.
تابع disassemble_jvm() تمام 202 آپکد استاندارد را به همراه عرض عملوندهایشان پوشش میدهد، قواعد پدینگ را رعایت میکند و عملوندها را دقیقاً به همان شکلی که javap تولید میکند نمایش میدهد. تابع compare_with_javap() متن استخراجشده از دستور javap -c -p (شامل بلوک چندخطی tableswitch) را تحلیل کرده و آفست، نام نمادین (mnemonic) و عملوند تکتک دستورات را تطبیق میدهد. هر چهار بیلد در تمام 108 دستور بهطور کامل توافق دارند. تست test_branch_offsets_are_relative_to_the_branch_instruction تضمین میکند که دستور ifne در pc برابر 1 به آفست 6 میرسد، if_icmpge در آفست 13 به آفست 33 پرش میکند و goto در آفست 30 به آفست 10 بازمیگردد.
ابزار radare2 6.2.4 که برای این دروس از سورس کامپایل شده، همین فایل را تحلیل میکند. دستور rabin2 -I مشخصات را بهصورت arch java، lang java 21، linenum true و lsyms true گزارش میدهد که برداشت آن از نسخه و دو جدول اشکالزدایی است. دستور pd 14 @ sym.Ledger.grade ساختار tableswitch را همراه با فلشهایی به پنج مقصد آن رسم میکند. این ابزار البته پیام Unable to parse code attribute: 2 را نیز برای این فایل نشان میدهد؛ این پیام محدودیتی است که خود ابزار اعلام میکند و دلیلی است بر اینکه javap را به عنوان مرجع اصلی (oracle) نگه داریم. دیکامپایلرهای این قالب در مقایسه با پایتون بسیار پختهتر هستند. ابزار CFR 0.152 تحت مجوز MIT است. ابزارهای Procyon 0.6.0 و Vineflower 1.12.0 تحت مجوز Apache-2.0 توسعه مییابند (Vineflower یک fork از Fernflower است که جاوا 21 را پشتیبانی میکند). خود Fernflower در مخزن IntelliJ Community نگهداری میشود. ابزار JADX 1.5.6 قالب DEX اندروید را پوشش میدهد و آخرین انتشار ابزار JD-GUI مربوط به سال 2019 بود. هرند، سوتو-والرو، مونپروس و بودری در سال 2020 عملکرد هشت دیکامپایلر مختلف را سنجیدند: بهترین آنها توانست برای 84 درصد از کلاسها خروجی از نظر سینتکسی درست و برای 78 درصد خروجی با رفتار معنایی یکسان تولید کند. دیکامپایلر ترکیبی (meta-decompiler) آنها موفق شد 37.6 درصد از کلاسهایی را که هیچ ابزار تکی قادر به بازسازیشان نبود احیا کند. حضور انواع داده، نامها و استکهای اعتبارسنجیشده این فرمت را کاملاً قابل دیکامپایل میکند؛ اما تضمین نمیکند که یک دیکامپایلر تکی کامل و بینقص باشد.
ماژول Wasm آرایهای از بخشها است
مشخصات هستهی استاندارد WebAssembly، یک ماژول را با پیشوندِ 00 61 73 6d 01 00 00 00 (شامل رشتهی \0asm و نسخهی 1) تعریف میکند که به دنبال آن بخشهای مختلف (sections) قرار میگیرند. هر بخش شامل یک بایت شناسه، یک فیلد اندازهی u32 در قالب LEB128 و محتوای اصلی آن است. شناسههای 1 تا 13 به بخشهای استاندارد اختصاص دارند که هر کدام حداکثر یک بار و با ترتیبی مشخص ظاهر میشوند. بخش با شناسهی 0 یک بخش سفارشی دارای نام است که میتواند در هر نقطهای قرار گیرد. تمام اعداد صحیح با فرمت LEB128 کدگذاری میشوند، بنابراین e4 00 برابر 100 و 80 08 برابر 1024 است. اجرای wasm-objdump -h روی ledger.wasm هفت بخش را لیست میکند و کلاس WasmModule در کد ما دقیقاً همین هفت بخش را با همان آفستهای محتوا شناسایی میکند:
| بخش | محتوا | تعداد | محتوای بخش برای ledger |
|---|---|---|---|
| Type | 10 تا 26 | 3 | (i32) -> nil، (i32, i32) -> i32، (i32) -> i32 |
| Import | 28 تا 39 | 1 | تابع env.log با نوع 0 |
| Function | 41 تا 46 | 4 | ایندکس نوع برای هر تابع تعریفشده |
| Memory | 48 تا 51 | 1 | یک صفحه، بدون حداکثر |
| Export | 53 تا 110 | 5 | شیء memory و نام چهار تابع |
| Code | 113 تا 271 | 4 | بدنهی توابع با طولهای 23، 63، 44 و 23 بایت |
| Data | 273 تا 290 | 1 | رشتهی F\0D\0C\0B\0A\0 در آفست 1024 |
دلیل تفکیک بخش Function از Code این است که مرورگر بتواند پیش از دریافت کل فایل، عملیات کامپایل را آغاز کند. هر مدخل در بخش Code شامل یک فیلد اندازه، برداری از تعاریف متغیرهای محلی و یک عبارت (expression) است. برای متد grade بردار متغیرهای محلی بهصورت 01 7f ذخیره شده است: یک گروه شامل یک متغیر i32. این متغیر همان bucket است؛ همراه با نوع آن و بدون نامش.
جریان کنترل ذ Keep ساختاریافته است. مشخصات استاندارد حکم میکند که پرشها فقط میتوانند ساختارهای دربرگیرندهی block، loop یا if را هدف بگیرند. برچسبها با عمق تودرتویی ایندکس میشوند. دستور br_table یک پرش غیرمستقیم را از طریق عملوندی که فهرستی از برچسبها را پیمایش میکند انجام میدهد. دستور سوئیچ تابع grade را در آفست 0xa3 رمزگشایی کنید:
0e br_table
0b 11 labels follow
04 04 04 04 04 04 buckets 0 to 5: label 4, the outermost block, returns 0
03 02 01 00 buckets 6, 7, 8, 9: labels 3, 2, 1, 0
00 bucket 10: label 0, shared with 9
04 default label 4
هر برچسب نشاندهندهی یک عمق است. برچسب 0 درونیترین block را نشان میدهد و کدهای بعد از end آن، ابتدا i32.const 4 و سپس br 4 برای خروج از بلوک نتیجه است. شش دستور block پیش از جدول، یازده برچسب و پنج دستور br بعد از آن، کل ساختار دستور switch را شکل میدهند؛ یک اعتبارسنج هر شاخهای را که خارج از این ساختار بپرد رد خواهد کرد. تابع disassemble_wasm() عمق را دنبال میکند تا کدهای خروجی همانند wasm-objdump دندانهگذاری شوند، تمام آپکدهای تکبایتی از unreachable تا i64.extend32_s را پوشش میدهد و در هر دو بیلد، تطابق کامل 77 دستور از 77 دستور را با خروجی ثبتشدهی wasm-objdump -d محقق میکند.
تنها چیزی که یک ماژول ساده دور میاندازد، نامها هستند. با این حال wasm-objdump عبارت <grade> را چاپ میکند زیرا بخش Export این رشته را به تابع شمارهی 2 نگاشت کرده است؛ خوانندهی ما نیز همین کار را انجام میدهد. نامهای محلی در هیچ جای این فایل 290 بایتی وجود ندارند. دستور wat2wasm --debug-names یک بخش سفارشی به نام name به فایل میافزاید که شامل زیربخشهایی برای نام ماژول، نام توابع و نامهای محلی است. با این کار حجم فایل به 425 بایت میرسد. خوانندهی ما مقادیر {0: 'score', 1: 'bucket'} را برای تابع 2 و {0: 'values', 1: 'count', 2: 'sum', 3: 'i'} را برای تابع 3 پیدا میکند. اجرای ابزار wasm-strip این بخش را مجدداً حذف میکند. مخزن قراردادهای ابزارها (tool-conventions) دو حامل دیگر را شرح میدهد: دادههای DWARF میتوانند در قالب بخشهای سفارشی .debug_* جاسازی شوند، و یک بخش سفارشی با نام sourceMappingURL میتواند به یک source map اشاره کند که شماره ستونهای آن در واقع آفست بایتها در فایل باینری است.
ابزارها در طول سال 2026 تغییراتی داشتند. ابزار wabt در نسخهی 1.0.42 (منتشرشده در 2026-09-15) ابزار wasm-decompile را حذف کرد و بیلد Homebrew روی این سیستم دقیقاً یازده ابزار باقیمانده را دارد. پکیج wasm-tools نسخهی 1.261.0 ابزارهای print، objdump و demangle را عرضه میکند. کتابخانهی Binaryen در نسخهی version_133 ابزار wasm-dis را دارد. نرمافزار radare2 6.2.4 نیز این ماژول را باز میکند: دستور rabin2 -I معماری را arch wasm تشخیص میدهد. نماد متد grade در آن از آفست 0x8e شروع میشود (جایی که بردار متغیرهای محلی آغاز میگردد)، درحالیکه wasm-objdump تابع را از 0x8b برچسبگذاری میکند (جایی که فیلد اندازه قرار دارد). فهرست خروجی radare2 از کلمات نمادین قدیمیتر مثل get_local، set_local و brtable 11 4 4 ... با چاپ تعداد برچسبها استفاده میکند. بایتهای فایل در هر سه ابزار یکسان هستند اما شیوهی نگارش کلمات تفاوت دارد؛ بنابراین هنگام مقایسه باید حتماً نام ابزاری را که خروجی با آن ثبت شده مشخص کنید. در بررسیهای این درس هیچ ماژول پردازندهی WebAssembly در پوشهی پردازندههای Ghidra پیدا نشد و آن خروجی بهطور کامل خوانده نشد. افزونهی شخص ثالث ghidra-wasm-plugin v2.4.0 نسخهی Ghidra 12.0 را هدف قرار داده است. خود مشخصات استاندارد نیز بهصورت یک سند زنده مدیریت میشود. صفحهی W3C برای wasm-core-2 یک پیشنویس توصیهنامهی نامزد (Candidate Recommendation Draft) به تاریخ 13 اکتبر 2026 است که چکیدهی آن نسخهی 3.0 را توصیف میکند و پیشنویس ویراستاران نیز دقیقاً همین تاریخ را دارد.
متادیتای ECMA-335 پایگاه دادهی خود برنامه است
یک اسمبلی .NET در واقع یک فایل PE است. فایل LedgerNet.dll با بایتهای MZ شروع میشود، سه بخش به نامهای .text، .rsrc و .reloc دارد، و نوع ماشین را 0x14c اعلام میکند چون کامپایلر کدی مستقل از پلتفرم تولید کرده است. دایرکتوری دادهی شمارهی 14 از هدر اختیاری یعنی هدر زمان اجرای CLR (CLR Runtime Header)، به آدرس نسبی مجازی (RVA) معادل 0x2008 اشاره میکند. بخش II.25.3.3 استاندارد ECMA-335 ساختار 72 بایتی این آدرس را مشخص میسازد:
48 00 00 00 Cb 72
02 00 05 00 MajorRuntimeVersion 2, MinorRuntimeVersion 5
78 21 00 00 MetaData RVA 0x2178
84 07 00 00 MetaData size 1924
01 00 00 00 Flags: COMIMAGE_FLAGS_ILONLY
05 00 00 06 EntryPointToken 0x06000005, MethodDef row 5, which is Main
ریشهی متادیتا در RVA معادل 0x2178 با امضای 42 53 4a 42 شروع میشود که معادل رشتهی BSJB است؛ به دنبال آن نسخهی 1.1 و رشتهی v4.0.30319 میآید. پس از آن هدر پنج استریم قرار دارد: ابتدا #~ در آفست 0x6c، سپس استریمهای #Strings، #US، #GUID و #Blob. استاندارد در بخش II.24.2 میگوید از هر نوع استریم حداکثر یک نمونه وجود دارد. استریم #~ با یک بایت HeapSizes آغاز میشود (در این فایل مقدار آن صفر است، بنابراین تمام ایندکسهای رشته، GUID و blob با عرض دو بایت خوانده میشوند) و سپس یک ماسک 64 بیتی Valid قرار دارد که نشان میدهد کدام جدولها در فایل حاضر هستند. در فایل ما 14 بیت فعال است:
| جدول | شناسه | ردیفها | اندازه ردیف | دادههای موجود در ردیفها |
|---|---|---|---|---|
| Module | 0x00 | 1 | 10 | نام ماژول و GUID آن |
| TypeRef | 0x01 | 21 | 6 | System.Object، System.Console و 19 مورد دیگر |
| TypeDef | 0x02 | 4 | 14 | <Module>، Ledger و دو ساختار کمکی کامپایلر |
| Field | 0x04 | 2 | 6 | Labels و مقداردهی اولیهی یک آرایهی استاتیک |
| MethodDef | 0x06 | 6 | 14 | پنج متد به همراه .cctor |
| Param | 0x08 | 5 | 6 | before، after، score، values، score |
| MemberRef | 0x0a | 16 | 6 | 12 سازندهی صفت (attribute) و 4 متدی که Main فراخوانی میکند |
| CustomAttribute | 0x0c | 12 | 6 | صفتهایی که کامپایلر و SDK اضافه کردهاند |
| StandAloneSig | 0x11 | 3 | 2 | سه امضای متغیرهای محلی |
| Assembly, AssemblyRef, ClassLayout, FieldRva, NestedClass | 1, 2, 1, 1, 1 | مشخصات هویتی، ارجاعات، چیدمان دادهها |
اندازهی ردیفها در هیچ جای فایل ذخیره نشده است. بخش II.22 نوع هر ستون را تعریف میکند و بخش II.24.2.6 قواعد تعیین اندازه را مشخص میسازد: ایندکس یک هیپ بر اساس بایت HeapSizes دو یا چهار بایت است؛ ایندکس یک جدول ساده تا زمانی که آن جدول کمتر از 65536 ردیف داشته باشد دو بایت عرض دارد؛ ایندکسهای کدگذاریشده (coded index) که میتوانند با b بیت تگ به n جدول اشاره کنند، تا زمانی که همهی آن جدولها کمتر از 2^(16-b) ردیف داشته باشند دو بایت فضا میگیرند. کلاس CliAssembly اندازهی این 14 ردیف را بر اساس همین قوانین و تعداد ردیفها محاسبه میکند. گزارش System.Reflection.Metadata در فیکسچرهای آزمایشی ما دقیقاً همین اندازهها و همین آفستهای جدول را نشان میدهند؛ تستی که نوشتهایم صحت این محاسبات را اثبات میکند.
بخش III.1.9 ساختار توکن را تعریف میکند: بایت بالا جدول و سه بایت پایین شمارهی ردیف (شروع از 1) است. توکن 0x06000002 ردیف دوم جدول MethodDef را مشخص میکند. این ردیف دارای RVA برابر 0x2060، نام Grade و یک امضای blob برابر با 00 01 08 08 است: قرارداد فراخوانی پیشفرض، یک پارامتر، بازگشت از نوع int32 و پارامتر از نوع int32. فیلد ParamList آن به ردیف 3 جدول Param اشاره میکند که نام آن score است. جدول Param دلیلی است که به خاطر آن ابزار ILSpy میتواند از یک بیلد Release بدون اطلاعات اضافه، امضای تابع را بهصورت Grade(int score) چاپ کند. در آدرس 0x2060 بدنهی متد با یک هدر چاق (fat header) آغاز میشود:
13 30 flags 0x13: fat, init locals; size 3 dwords
02 00 MaxStack 2
2d 00 00 00 CodeSize 45
01 00 00 11 LocalVarSigTok 0x11000001
ردیف 1 جدول StandAloneSig مقدار 07 01 08 را دارد: ساختار LOCAL_SIG، یک متغیر محلی و نوع int32. متغیر محلی فقط نوع دارد و نام ندارد. استاندارد ECMA-335 نام متغیرهای محلی را در قالب دادههای اشکالزدایی بخش Partition V ذخیره میکند و داتنت SDK آنها را در فایلهای PDB مینویسد. این فیکسچر با تنظیم DebugType برابر none ساخته شده است؛ بنابراین هیچ جدول و هیچ فایلی نام bucket را با خود حمل نمیکند. در نقطهی مقابل، تابع PercentChange از یک هدر ریز (tiny header) استفاده کرده است: بایت اول آن 3a معادل 0x0e << 2 | 2 است که نشاندهندهی بدنهای 14 بایتی بدون متغیر محلی و با استک پیشفرض 8 است.
45 بایت کد متد Grade با جدول آپکد بخش Partition III رمزگشایی میشوند. دستور switch آپکد 0x45 است که یک عدد u32 برای شمارش مقاصد و سپس همان تعداد آفستِ int32 نسبت به دستورِ بعد از جدول به دنبال آن میآید:
IL_0000: 02 ldarg.0
IL_0001: 1f 0a ldc.i4.s 10
IL_0003: 5b div
IL_0004: 0a stloc.0
IL_0005: 06 ldloc.0
IL_0006: 1c ldc.i4.6
IL_0007: 59 sub
IL_0008: 45 05 00 00 00 08 00 00 00 06 00 00 00 04 00 00 00 02 00 00 00 02 00 00 00
switch (IL_0029, IL_0027, IL_0025, IL_0023, IL_0023)
IL_0021: 2b 08 br.s IL_002b
IL_0023: 1a 2a ldc.i4.4, ret
کامپایلر پیش از رسیدن به switch عدد 6 را از متغیر کم کرده است، بنابراین حالت 6 نخستین مقصد پرش است. متد disassemble_cil() توکنها را با پیمایش جدولها نمایش میدهد: ldsfld Labels، call Grade، call System.Console::WriteLine و ldstr ' ' از هیپ #US. مرجع اصلی برای تمام این بخشها، برنامهی MetaDump است؛ یک برنامهی کوتاه سیشارپ در فیکسچرها که بر پایهی System.Reflection.Metadata (کتابخانهی درون ILSpy) ساخته شده است. این برنامه تعداد ردیفها، اندازه و آفست هر جدول، مقادیر RVA، امضاها، نام پارامترها و بایتهای IL هر متد و 226 فیلد کلاس System.Reflection.Emit.OpCodes را چاپ میکند. تست compare_with_metadump() تطابق کامل هر 246 بررسی را اثبات میکند. جدول زمان اجرای محیط اجرا 218 آپکد بههمراه هشت جایگاه موقت prefix دارد. جدول این درس 219 مدخل دارد، زیرا دستور no. از بخش III.2.2 را که در جدول Emit وجود نداشت در بر میگیرد.
ابزارهای این فرمت: نرمافزار ILSpy 11.0 (تحت مجوز MIT، منتشرشده در 2026-08-12) روی macOS از طریق نسخهی دسکتاپ Avalonia اجرا میشود. ابزار خط فرمانی ilspycmd نسخهی 11.1.0.9782 کدهای IL را با سوئیچ -il چاپ میکند و با -m 0x06000002 میتوان متد را بر اساس توکن انتخاب کرد. ابزار dnSpyEx 6.6.0 ادامهدهندهی راه dnSpy است که فقط برای ویندوز عرضه میشود. دیزاسمبلر کدهای IL یعنی ildasm هماکنون به جای داخل SDK در قالب بستههای NuGet منتشر میشود. نرمافزار radare2 6.2.4 این متادیتا را میخواند: دستور rabin2 -I مشخصات را arch cil و class PE32 اعلام میکند، و دستور is تابع را بهصورت Ledger.Grade(int:score):int لیست میکند که در آن نام پارامتر و هر دو نوع داده مستقیماً از جدولهای Param و Signature خوانده شدهاند. این بیلد از radare2 فاقد دیزاسمبلر کدهای CIL است. این فیکسچر با داتنت SDK 8.0.205 بیلد شده، چون نسخهی نصبشده روی سیستم سازنده بوده است. فریمورک .NET 10 از تاریخ 2025-11-11 نسخهی جاری است و دقیقاً همین ساختارهای استاندارد ECMA-335 را تولید میکند.
هر نگهدارنده چه چیزهایی را حفظ میکند؟
تابع grade را در تمام هر پنج نگهدارنده درون یک جدول بگذارید. تمام سطرهای این جدول با دستور python3 code/main.py keeps و از طریق اجرای خوانندهها روی فیکسچرها به دست آمدهاند:
| نگهدارنده | بایتهای بدنه | نام تابع | نام پارامترها | نوع پارامتر و نوع بازگشتی | نام متغیرهای محلی | شماره خطوط | کنترل جریان |
|---|---|---|---|---|---|---|---|
| آبجکت ELF با clang -O0 | 118 | grade در .symtab |
ندارد | ندارد | ندارد | ندارد، بدون بخشهای .debug_* |
5 پرش و 1 پرش غیرمستقیم از طریق جدول بازنشانی |
| CPython 3.14 pyc | 96 | co_qualname |
score |
ندارد | bucket |
co_linetable، خط اول 10 |
4 جفت مقایسه و POP_JUMP_IF_FALSE |
کلاس JVM با javac -g |
50 | جدول مقادیر ثابت | score در LocalVariableTable |
(I)I |
ندارد (بهجز خود پارامتر) | LineNumberTable، 6 ردیف |
1 دستور tableswitch |
| WebAssembly | 63 | بخش Export | فقط با وجود بخش name | (i32) -> i32 |
فقط با وجود بخش name | ندارد | 6 بلوک تودرتو، 1 br_table و 4 دستور br |
| اسمبلی .NET | 45 | MethodDef |
score در Param |
static int32(int32) |
ندارد، فقط انواع داده در StandAloneSig |
ندارد (بدون فایل PDB) | 1 دستور switch و 1 دستور br.s |
این جدول نتایج دیکامپایلرهایی را که خواهید دید توضیح میدهد. در درس 34 دیدیم که چگونه امضای int total(const int *, int) با امضای int total(long param_1, uint param_2) که Ghidra از روی کد بومی استنتاج کرده بود مقایسه شد؛ چرا که کدهای بومی هیچکدام از این ستونها را ذخیره نمیکنند. فایل pyc تمام نامها را نگه میدارد ولی هیچ نوع دادهای ندارد و کنترل جریان آن شامل پرشهایی است که ساختارشان در هر نسخه دگرگون میشود؛ در نتیجه دیکامپایلرهای پایتون با تغییر هر نسخه از کار میافتند اما بازیابی نامها در آنها رایگان تمام میشود. فایل کلاس انواع داده و استکهای اعتبارسنجیشده را نگه میدارد، با روشن بودن -g نامها را حفظ میکند و دستور switch را در قالب یک دستور بیان میکند. یک ماژول Wasm ساختار را بهطور ذاتی حفظ میکند؛ بنابراین ابزار wasm2wat تبدیل رفتوبرگشتی بدون نقصی دارد و تنها اطلاعات از دست رفته نامها هستند. اسمبلی .NET کاملترین توصیف ممکن را نگهداری میکند زیرا جدولهای متادیتا نقشی هنجاری (normative) دارند، اما با این حال برای نام متغیرهای محلی همچنان به فایل PDB وابسته است.
این جدول به شما نشان میدهد دیکامپایلر چه دادههایی برای کار در اختیار داشته است؛ کدام بخش از خروجی رونوشت محضِ یک واقعیت ذخیرهشده است و کدام بخش حاصل استنتاج ابزار به حساب میآید. وجود نام یک پارامتر در خروجی ILSpy یک کپی مستقیم است، اما نام یک متغیر محلی در همان خروجی (مگر با حضور PDB) یک استنتاج است. در درس 35 این تفاوت را تحت عنوان اعتبار (authority) هر ادعا ثبت خواهیم کرد.
پیادهسازی
اجرای دمو:
cd phases/13-tools-and-protocols/43-bytecode-and-managed-formats
python3 code/main.py
خروجی برنامه یک گزارش جامع در قالب JSON است. برای هر فایل pyc هر دو نوع هدر، اندازهی کل فایل و اندازهی کد grade، تعداد دستورات و نام آپکدها از خروجیهای ثبتشده و امکان خوانده شدن آن توسط مفسر فعلی گزارش میشود. برای هر بیلد کلاس، نسخه، اندازهها، تعداد جدولهای اشکالزدایی و میزان انطباق با javap ثبت میشود. برای فایلهای Wasm بخشها، انواع، توابع، محتویات بخش name و انطباق با wasm-objdump بررسی میشود. برای اسمبلی هدر CLI، استریمها، تعداد سطرهای جداول، متدها و انطباق با System.Reflection.Metadata نشان داده میشود. در انتها جدول مقایسهای نگهداری دادهها (keeps table) و فهرست مفسرهای شناساییشده در مسیر PATH میآید.
فایل code/main.py را با این ترتیب بخوانید:
- کلاس
Readerمکاننما (cursor) و عرض اعداد صحیح را مدیریت میکند. توابعuleb()وsleb()خوانندههای LEB128 موردنیاز برای Wasm و هیپ blob داتنت هستند. - توابع
read_pyc_header()،Unmarshallerو کلاسPycFileبرای خواندن فایلهای pyc پیادهسازی شدهاند. تابعparse_dis_text()متنهای ذخیرهشدهیdisرا میخواند تا برای مقایسهی بیننسخهای نیازی به چهار مفسر مختلف در زمان آزمون نباشد. - کلاس
ClassFileفایل کلاس را میخواند وdisassemble_jvm()کدهای آن را رمزگشایی میکند. توابعparse_javap_code()وcompare_with_javap()بررسی مرجع هستند. - کلاس
WasmModuleبخشهای ماژول را میخواند وdisassemble_wasm()عبارتها را همراه با عمق آنها رمزگشایی میکند. تابعcompare_with_wasm_objdump()بررسی مرجع است. - کلاس
CliAssemblyساختارهای PE، هدر CLI، ریشهی متادیتا، استریمها و جدولها را میپیماید. متغیرهایTABLE_SCHEMASوCODED_INDEXESدر واقع پیادهسازی دادهای بخشهای II.22 و II.24.2.6 هستند. متدdisassemble_cil()بدنهی متد را رمزگشایی کرده وcompare_with_metadump()آن را بررسی میکند. - کلاس
NativeObjectبخش کافی از هدر ELF را میخواند تا نمادهای توابع را لیست کند و خروجی درس 33 را به عنوان متن اصلی خود میگیرد. توابعwhat_each_keeps()وfigure_data()جدول مقایسهای را میسازند.
پنج دستور دیگر نیز وجود دارد: دستورات python3 code/main.py pyc FILE، class FILE، wasm FILE و dll FILE ساختار و فهرست دستورات تمام توابع فایل دادهشده را چاپ میکنند. دستور python3 code/main.py compare سورس ledger.py را با تمام مفسرهای موجود در مسیر PATH کامپایل کرده و مشخصات خروجی هر یک را چاپ میکند.
استفاده از ابزارها
این مراحل را به ترتیب برای بررسی هر نگهدارندهای که ساختهاید به کار ببرید:
- بایتهای اولیه را بخوانید و نوع قالب را نام ببرید. دو بایت نخست به همراه
0d 0aنشانهی pyc، بایتهایca fe ba beنشانهی فایل کلاس، مقدار00 61 73 6dنشانهی ماژول، و4d 5aبا دایرکتوری دادهی 14 نشانهی اسمبلی است. - هدر را بخوانید و شماره نسخهی تولیدکننده را یادداشت کنید. برای pyc این مقدار کلمهی جادویی است، برای فایل کلاس شماره نسخهی اصلی، و برای اسمبلی رشتههای نسخهی محیط اجرا و متادیتا.
- هر دیکامپایلری را که قصد استفاده از آن را دارید، پیش از اجرا از نظر سازگاری با آن نسخه بررسی کنید. سقف نسخه و جایی را که ابزار این سقف را مستند کرده ثبت کنید.
- خوانندهی خود و ابزار استانداردِ همان قالب را روی همان فایل اجرا کرده و نتایج را سطربهسطر مقایسه کنید. هرگونه ناهمخوانی نشاندهندهی باگ در خواننده یا تغییر در ابزار است و تا هرکدام فهمیده نشود، این فهرست شواهد (evidence) معتبری نخواهد بود.
- جدول نگهداری را برای تابعی که هدف شماست تکمیل کنید. مشخص کنید چه دادههایی حفظ و چه دادههایی گم شدهاند، و نام جدول، صفت یا بخشی را که منبع داده بوده ذکر کنید.
- تازه در این مرحله به سراغ خروجی دیکامپایلر بروید. مشخص کنید هر نام و نوع داده در آن، رونوشت محضِ یک واقعیت ذخیرهشده است یا یک استنتاج.
فایلهای متنی مرجع را همراه نگهدارندهها نگه دارید. با تغییر نسخهی مفسر، JDK، SDK یا wabt، فایلها را با اسکریپت build_fixtures.sh مجدداً بسازید و دوباره خروجیها را ثبت کنید، زیرا تغییر خروجی به معنی تغییر فیکسچر است.
کارگاه تعاملی
آزمایش الف: خواندن هدر و سپس شیء کد
از پوشهی code، مفسر پایتون را اجرا کنید:
cd phases/13-tools-and-protocols/43-bytecode-and-managed-formats/code
python3 -q
دستورات زیر را وارد کنید:
from main import *
pyc = load_pyc("ledger.cpython-311.pyc")
pyc.header.to_dict()
pyc.module.names
pyc.functions()["grade"].varnames
len(pyc.functions()["grade"].code)
پیش از اولین فراخوانی، کلمهی جادویی را یادداشت کنید. سپس فایل ledger.cpython-314.pyc را بارگذاری کرده و طول code این دو نسخه را مقایسه کنید. سپس دستور import marshal; marshal.loads(pyc.data[16:]) را برای فایل 3.11 در مفسر فعلی امتحان کنید و خطا را بخوانید.
آزمایش ب: زنجیرهی جدول مقادیر ثابت
cls = load_class("Ledger-g.class")
cls.constant(13)
cls.constant(14)
cls.describe_constant(13)
[row["text"] for row in jvm_listing(cls, "grade")]
پیش از فراخوانی describe_constant، ایندکس شمارهی #13 را تا رسیدن به #8 و #14 و سپس مدخلهای Utf8 بهصورت دستی دنبال کنید. سپس فایل Ledger-gnone.class را بارگذاری کرده و برای هر دو فایل متد cls.method("grade").attribute_names() را صدا بزنید. آرایهی Code یکسان است، اما صفات تفاوت دارند.
آزمایش ج: بررسی عمق و برچسبها
module = load_wasm("ledger.wasm")
[(row["depth"], row["text"]) for row in wasm_listing(module, "grade")]
module.function_name(2)
load_wasm("ledger-names.wasm").names["locals"][2]
ردیف مربوط به br_table را پیدا کرده و عمق آن را بخوانید. تعداد ردیفهای end بعد از آن را بشمارید و هر کدام را با یک شماره برچسب در جدول تطبیق دهید. سپس فایل ledger.wat را باز کنید و بلاک block $six را که برچسب 3 نام برده پیدا کنید.
آزمایش د: از توکن تا بایتها
asm = load_assembly("LedgerNet.dll")
asm.method("Grade")
body = asm.method_body(asm.method("Grade")["RVA"])
body.fat, body.max_stack, body.code_size, hex(body.local_signature_token)
asm.token_text(0x11000001)
[row["text"] for row in cil_listing(asm, "GradeLabel")]
آفست فیزیکی آدرس 0x2060 در فایل را با استفاده از هدر بخش .text بهصورت دستی محاسبه کنید و نتیجه را با خروجی asm.rva_to_offset(0x2060) بسنجید. سپس مقدار asm.row(0x08, 3) را بررسی کرده و موقعیت قرارگیری نام score را پیدا کنید.
تمرین عملی
یک زبان مبدأ پنجم به مقایسه اضافه کنید، یا یک واقعیت ششم را در جدول بسنجید.
نیازمندیها:
- تابعی شامل یک حلقه و یک متغیر محلی در زبانی که کامپایلرش را دارید بنویسید؛ مثلاً Kotlin به فایل کلاس، یا Rust به ماژول Wasm. فایل خروجی و فهرست ابزار مرجع آن را به همراه دستور دقیق در
build_fixtures.shcommit کنید. - خوانندهی مربوطه را طوری تنظیم کنید که در تمام دستورات با مرجع مطابقت داشته باشد. آپکدهای جدید را تنها در صورتی اضافه کنید که فیکسچر واقعاً به آنها نیاز دارد.
- تابع
what_each_keeps()را برای این نگهدارندهی جدید گسترش دهید، و یک واقعیت دیگر را برای همهی قالبها به جدول بیفزایید: آیا بدنهی تابع شامل جدول استثناها (exception table) یا ناحیهیtryهست یا خیر. - این نگهدارندهی جدید را به
figure_data()اضافه کرده و فایلsite/figures-re-43.jsرا مجدداً بسازید. - تستی بنویسید که اگر خوانندهای دادهای را بدون ذکر دقیق نام جدول، صفت یا بخشِ نگهدارندهی آن حفظشده گزارش کند، شکست بخورد.
شرط موفقیت: اجرای python3 code/main.py keeps سطر جدید را همراه با شواهد مستند برای هر سلول نشان دهد و تمامی مقایسهها با ابزارهای مرجع همچنان مطابقت داشته باشند.
خروجی این درس
این درس شامل فایل outputs/skill-bytecode-and-managed-formats.md است. از این فایل برای تبدیل یک فایل بایتکد به گزارشی از هدر، فهرست بررسیشدهی یک تابع، و جدول واقعیتهایی که مشخص میکند هر نام، نوع و ساختار حفظ شده یا از دست رفته است، استفاده کنید. این مسیر نیازمند ابزار رسمی قالب به عنوان مرجع، بررسی نسخه پیش از هر دیکامپایلر، و مشخص کردن محل اصلی دادههای از دست رفته است.
بررسی صحت کارکرد
دمو و مجموعهی تستهای قطعی را اجرا کنید:
cd phases/13-tools-and-protocols/43-bytecode-and-managed-formats
python3 code/main.py
python3 -m unittest discover -s code/tests -v
بررسی باید موارد زیر را اثبات کند:
- چهار هدر pyc دارای کلمات جادویی 3495، 3531، 3571 و 3627، فلگهای 3 و هش 8 بایتی هستند؛ و فایلهای برچسب زمانی دارای فلگ 0، mtime برابر 1791975565 و اندازه 497 هستند.
- تمام کلمات جادویی برای 3.11 و بالاتر در بازهای قرار دارند که قاعدهی
2900 + 50nبه انتشار آن داده است. - تبدیلکنندهی مستقیم (unmarshaller) همان نامها، مقادیر ثابت، بایتهای کد، نام متغیرها و جدول خطوط را مشابه
marshal.loads()روی فایلی که مفسر فعلی نوشته برمیگرداند. - هر چهار فایل نامهای ماژول، تاپل
('score', 'bucket')برایgrade، خط اول 10 وledger.pyرا به عنوان نام فایل حفظ کردهاند. - مقدار
grade.co_codeدر چهار نسخه به ترتیب 80، 62، 70 و 96 بایت است و خروجیهای ثبتشده شامل 31، 26، 26 و 35 دستور هستند. - خروجی نسخهی 3.11 شامل
POP_JUMP_FORWARD_IF_FALSEاست، 3.12 و 3.13 دارایRETURN_CONSTهستند، و 3.14 دستوراتLOAD_SMALL_INT،LOAD_FAST_BORROWوNOT_TAKENرا دارد. - خروجی
-C -Oپایتون 3.14 تعداد 13 مدخل کش در تابعgradeنشان میدهد (پنج مورد بعد ازBINARY_OP) و خروجی مفسر تطبیقیCOMPARE_OP_INTرا نشان میدهد. - هدر بدون
0d 0a، هدر ناقص، و بدنهای که شیء کد نیست خطایFormatErrorصادر میکنند. - لیست دستورات تنها برای فایل pyc تولیدشده با مفسر در حال اجرا وجود دارد و با خروجی ثبتشدهی
disمطابقت دارد. - فایلهای کلاس نسخهی اصلی 65 یا 69 دارند، اندازهشان 1581، 1832، 1404 و 1581 بایت است و تعداد ثابتهایشان 82، 94، 79 و 82 است.
- ثابت شمارهی
#13بهMethod Ledger.grade:(I)Iو#47بهString Fترجمه میشود. - دیزاسمبلر JVM در هر چهار بیلد، در تمام 108 دستور با
javap -c -pتطابق کامل دارد. - دستور
tableswitchدر pc معادل 4 دارای طول 36 بایت با کران پایین 6، کران بالا 10، پیشفرض 48 و مقاصد 46، 44، 42، 40 و 40 است. - سه سطح مختلف اشکالزدایی تعداد صفات را تغییر میدهند ولی 50 بایتِ کد متد
gradeرا یکسان میگذارند. - مقاصد پرش در دستورات
ifne،if_icmpgeوgotoهمگی نسبت به آپکد پرش سنجیده میشوند. - جادویی نامعتبر، بایتهای اضافی انتهای فایل یا آپکد تعریفنشده خطای
FormatErrorمیدهند. - هفت بخش ماژول Wasm آفستهای محتوایی دارند که
wasm-objdump -hچاپ میکند، و بیلد دارای نام، بخش سفارشیnameرا در 293 تا 425 اضافه میکند. - انواع، import تابع
env.log، پنج مورد export و بخش داده در 1024 دقیقاً مطابق با متنledger.watرمزگشایی میشوند. - دیزاسمبلر Wasm در هر دو بیلد در تمام 77 دستور با
wasm-objdump -dهمخوانی دارد. - تابع
gradeدارای 6 بلوک تودرتو، یکbr_tableبا برچسبهای4 4 4 4 4 4 3 2 1 0 0و پیشفرض 4 در عمق 6، و یک متغیر محلیi32است. - نام توابع در نبود بخش name از exportها میآید و نام متغیرهای محلی تنها همراه با آن بخش وجود دارد.
- مقدار LEB128 برای
e4 00برابر 100، برای7fبرابر 1- و برای80 08برابر 1024 است، و انواع بلوکها مشابهwasm-objdumpنمایش داده میشوند. - پیشوند اشتباه، نسخه 2 یا آپکد ناشناخته خطای
FormatErrorصادر میکنند. - اسمبلی از نوع PE32 با ماشین
0x14c، هدر CLI در RVA معادل0x2008، محیط اجرای 2.5، فلگILONLYو نقطهی ورود0x06000005است. - نسخهی متادیتا
v4.0.30319است، پنج استریم وجود دارند و 14 جدول تعداد و اندازهی ردیفهایی دارند که مرجع گزارش میکند. - خوانندهی .NET در تمام 246 بررسی با System.Reflection.Metadata مطابقت دارد.
- متد
Gradeدارای پارامترscore، امضایstatic int32(int32)، هدر چاق باMaxStackبرابر 2 و 45 بایت کد، و یک متغیر محلیint32بدون نام است. - مقاصد
switchدر CIL و عملوندهایbr.s،ldsfld،callوldstrاز روی جداول و هیپها ترجمه میشوند. - جدول آپکدهای CIL تمام آپکدهای محیط اجرا را پوشش داده و تنها
no.را اضافه کرده است. - جدول نگهداری نام پارامترها را در چهار نگهدارنده موجود و در کد بومی ناموجود گزارش میدهد، و انواع داده را در سه نگهدارنده ثبت میکند.
- تمام ردیفهای گزارش دمو برای ابزارهای مرجع انطباق n از n را نشان میدهند.
- شکل تعاملی دقیقاً همان دادههای تولیدشده توسط
figure_data()را در خود دارد.
حالتهای شکست
| حالت شکست | گزارش ابزار ضعیف | واقعیتی که باید با تحلیل اثبات شود |
|---|---|---|
| دیکامپایلر از نسخهی مفسر قدیمیتر است | خطای Unsupported opcode، خروجی ناقص، یا ادعای مبهمسازی کد |
کلمهی جادویی، نسخهی مربوطه، و سقف پشتیبانی مستندشدهی ابزار |
| خواندن pyc با مفسر نامناسب | خطای marshal.loads یا برگرداندن دادههای بیمعنی |
خوانندهی ما نامها، مقادیر ثابت و ابعاد را از فایلهای 3.9 تا 3.15 میخواند و دستورات از dis متناظر میآیند |
| اشتباه گرفتن آپکدهای تخصصیشده با محتوای فایل | دیدن کدهایی مثل COMPARE_OP_INT در فهرست دستورات |
فایل pyc فرم نامختص را نگه میدارد؛ تخصصیسازی در حافظه و بعد از گرم شدن رخ میدهد |
| فرض وجود نام متغیرهای محلی در بیلد عادی javac | نمایش اسامی ساختگی مثل var1 و var2 به عنوان نام سورس |
جدول LocalVariableTable فقط با -g حاضر است؛ 50 بایتِ کد در هر دو حالت کاملاً یکسان است |
| خطای محاسبهی مقصد پرش به اندازهی طول دستور | فهرست JVM که مقاصد پرش آن به وسط دستورات هدایت میشوند | آفستها باید نسبت به آپکد پرش محاسبه شده و با javap بررسی شوند |
| نادیده گرفتن پدینگ دستور switch | دستور tableswitch با مقصد پیشفرض اشتباه و حالتهای جابهجا شده |
صفر تا سه بایت پدینگ برای تراز 4 بایتی در pc؛ طول کل 36 بایت برای پنج شاخه |
| فرض بر حذف کامل نامها در ماژول Wasm | عبارت "no symbols" | بخش Export نام تمام توابع صادرشده را دارد؛ متغیرهای محلی به بخش name نیاز دارند و wasm-strip تنها این بخش را حذف میکند |
| تفاوت نگارش در سه ابزار مختلف | مغایرت get_local با local.get، یا تابعی در 0x8b در برابر 0x8e |
بایتها یکسان هستند؛ باید مشخص شود خروجی با چه ابزاری ثبت شده است |
| حدس زدن اندازهی ردیفهای جداول | خوانده شدن نادرست تمام ردیفهای بعد از جدول اول | اندازهی ردیفها از روی HeapSizes، تعداد سطرها و عرض ایندکسهای کدگذاریشده، برابر با مرجع |
| انتظار وجود نامهای محلی در اسمبلی Release | نمایش متغیرهایی مثل V_0 به عنوان نامهای اصلی سورس |
متغیرهای محلی در StandAloneSig فقط نوع دارند؛ نامها تنها درون PDB است که با DebugType برابر none نوشته نشده است |
| حدس زدن آپکد یا کد نوع ناشناخته توسط خواننده | یک کلمهی نمادین باورپذیر | صدور خطای صریح FormatError با آفست مربوطه، و توقف مقایسه با مرجع در همان نقطه |
| ثبت خروجی مرجع از روی بیلدی دیگر | پاس شدن آزمونها روی متنهای منسوخ | اسکریپت build_fixtures.sh نسخهی تمام ابزارها را ثبت کرده و فایلهای versions.txt و sha256.txt را مینویسد |
دستورالعمل عملیاتی
همیشه اول هدر را بخوانید و تولیدکنندهای را که نشان میدهد تعیین کنید. نگهدارنده را با خوانندهای بخوانید که ناشناختهها را نمیپذیرد، و خروجی آن را در همان فایل با ابزار استاندارد همان فرمت مقایسه کنید. سپس برای هر نام و نوع در خروجی دیکامپایلر، صراحتاً بگویید که آیا نگهدارنده آن را ذخیره کرده بود یا ابزار آن را استنتاج کرده است.
برای مطالعهی بیشتر
- PEP 552: Deterministic pycs
- CPython
Include/internal/pycore_magic_number.h - Python
marshalmodule documentation - CPython
Python/marshal.c - Python 3.14
dismodule documentation - PEP 659: Specializing Adaptive Interpreter
- PEP 626: Precise line numbers for debugging and other tools
- PEP 790: Python 3.15 Release Schedule
- uncompyle6 on PyPI, decompyle3 on PyPI, xdis on PyPI, pycdc on GitHub, PyLingual on GitHub
- The Java Virtual Machine Specification, Java SE 25, Chapter 4: The class File Format
- The Java Virtual Machine Specification, Java SE 25, Chapter 6: The Java Virtual Machine Instruction Set
- javac manual, Java SE 25 and javap manual
- CFR, Procyon, Vineflower, Fernflower in intellij-community, JADX
- Harrand, Soto-Valero, Monperrus, Baudry, "Java Decompiler Diversity and its Application to Meta-decompilation" (JSS 2020)
- ECMA-335, Common Language Infrastructure, 6th edition (June 2012)
- PE Format, Microsoft Learn
- System.Reflection.Metadata.Ecma335.TableIndex
- ILSpy on GitHub, ilspycmd on NuGet, dnSpyEx on GitHub
- WebAssembly Core Specification: Binary Format, Modules and Values
- WebAssembly Core Specification: Custom Sections (name section)
- WebAssembly tool-conventions: Debugging
- wabt 1.0.42 release notes, wasm-tools, Binaryen, ghidra-wasm-plugin
- radare2 on GitHub, version 6.2.4 built from source for this lesson
