فاز 13 · درس 43

مهندسی معکوس بایت‌کد پایتون، JVM، دات‌نت و WebAssembly

نوع
ساختنی
زمان
حدود 150 دقیقه
زبان‌ها
Python, Java, C#, WebAssembly text
پیش‌نیاز
فاز 13 · درس 32 (ساخت ساختار ELF از صفر) و درس 33 (دیزاسمبلی و جریان کنترل)
چهار جعبه شیشه‌ای شفاف حاوی اسناد و برچسب‌های اطلاعاتی گوناگون در کنار یک ذره‌بین مهندسی
فهرست مطالب
  1. اهداف یادگیری
  2. صورت مسئله
  3. هدر pyc از چهار کلمه تشکیل شده است
  4. ماژول Marshal شیء کد را فیلدبه‌فیلد می‌نویسد
  5. چهار مفسر و چهار بایت‌کد گوناگون برای تابع grade
  6. فایل کلاس ساختار درونی خود را در یک دیکشنری نگه می‌دارد
  7. رمزگشایی دستی دستور tableswitch
  8. ماژول Wasm آرایه‌ای از بخش‌ها است
  9. متادیتای ECMA-335 پایگاه داده‌ی خود برنامه است
  10. هر نگهدارنده چه چیزهایی را حفظ می‌کند؟
  11. پیاده‌سازی
  12. استفاده از ابزارها
  13. کارگاه تعاملی
  14. آزمایش الف: خواندن هدر و سپس شیء کد
  15. آزمایش ب: زنجیره‌ی جدول مقادیر ثابت
  16. آزمایش ج: بررسی عمق و برچسب‌ها
  17. آزمایش د: از توکن تا بایت‌ها
  18. تمرین عملی
  19. خروجی این درس
  20. بررسی صحت کارکرد
  21. حالت‌های شکست
  22. دستورالعمل عملیاتی
  23. برای مطالعه‌ی بیشتر

وقتی یک فایل باینری کامپایل‌شده از پایتون، جاوا، سی‌شارپ یا وب‌اسمبلی را به ابزارهای مهندسی معکوس یا دی‌کامپایلر می‌سپارید، مواجهه با خطاهایی مانند کدهای ناشناخته یا مبهم‌سازی گمراه‌کننده بسیار رایج است. بسیاری از توسعه‌دهندگان بدون آگاهی از ساختار درونی هدر و جدول‌های این کانتینرها، خروجی‌های حدسی دی‌کامپایلرها را واقعیت محض فرض می‌کنند؛ در حالی که هر فرمت بخشی از اطلاعات سورس‌کد را نگه می‌دارد و بخشی دیگر را دور می‌ریزد.

این درس به شما یاد می‌دهد که چگونه بایت‌کد را در چهار اکوسیستم اصلی شامل پایتون (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 را در هر پنج نگهدارنده کنار یکدیگر مقایسه خواهیم کرد.

یک تابع، پنج کانتینرتابع grade از ledger
The grade function as stored in one container, with the facts the container keepsx86-64 object keeps 1 of 6 facts about grade. Body 118 bytes, 33 instructions, 6 control-flow rows marked: 5 jumps, 1 indirect jump through a table.ELF relocatable object, x86-64, clang -O0نام تابعgradeنام پارامترهاهیچ‌کدامنوع پارامترهاهیچ‌کدامنوع بازگشتیهیچ‌کدامنام متغیرهای محلیهیچ‌کدامشماره خطوطهیچ‌کدامافستبایت‌هادستور0x3055pushq %rbp0x3148 89 e5movq %rsp, %rbp0x3489 7d f8movl %edi, -0x8(%rbp)0x3748 63 45 f8movslq -0x8(%rbp), %rax0x3b48 69 c0 67 66 66 +1 Bimulq $0x66666667, %rax, %rax0x4248 89 c1movq %rax, %rcx0x4548 c1 f9 22sarq $0x22, %rcx0x4989 camovl %ecx, %edx0x4b48 c1 e8 3fshrq $0x3f, %rax0x4f89 c1movl %eax, %ecx0x5189 d0movl %edx, %eax0x538d 44 08 faleal -0x6(%rax,%rcx), %eax0x5789 c1movl %eax, %ecx0x5948 89 4d f0movq %rcx, -0x10(%rbp)0x5d83 e8 04subl $0x4, %eax0x6077 38ja 0x9a <grade+0x6a>0x6248 8b 45 f0movq -0x10(%rbp), %rax0x6648 8d 0d 00 00 00 +1 Bleaq (%rip), %rcx0x6d48 63 04 81movslq (%rcx,%rax,4), %rax0x7148 01 c8addq %rcx, %rax0x74ff e0jmpq *%rax0x76c7 45 fc 04 00 00 +1 Bmovl $0x4, -0x4(%rbp)0x7deb 22jmp 0xa1 <grade+0x71>0x7fc7 45 fc 03 00 00 +1 Bmovl $0x3, -0x4(%rbp)0x86eb 19jmp 0xa1 <grade+0x71>0x88c7 45 fc 02 00 00 +1 Bmovl $0x2, -0x4(%rbp)0x8feb 10jmp 0xa1 <grade+0x71>0x91c7 45 fc 01 00 00 +1 Bmovl $0x1, -0x4(%rbp)0x98eb 07jmp 0xa1 <grade+0x71>0x9ac7 45 fc 00 00 00 +1 Bmovl $0x0, -0x4(%rbp)0xa18b 45 fcmovl -0x4(%rbp), %eax0xa45dpopq %rbp0xa5c3retq
مربع آبی توپر حقیقتی است که کانتینر ذخیره می‌کند و مربع توخالی حقیقتی است که از دست رفته است. سطرهای آبی دستورهای جریان کنترل هستند: پرش‌ها و یک جدول در کد محلی (native) و CIL، جفت‌های مقایسه-و-پرش در CPython، دستور tableswitch در JVM، و بلوک‌های تودرتو همراه با br_table در WebAssembly. سطرهای کهربایی نام متغیر را درون خودِ دستور حمل می‌کنند، که فقط در فهرست CPython دیده می‌شود.

هدر 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 را با این ترتیب بخوانید:

  1. کلاس Reader مکان‌نما (cursor) و عرض اعداد صحیح را مدیریت می‌کند. توابع uleb() و sleb() خواننده‌های LEB128 موردنیاز برای Wasm و هیپ blob دات‌نت هستند.
  2. توابع read_pyc_header()، Unmarshaller و کلاس PycFile برای خواندن فایل‌های pyc پیاده‌سازی شده‌اند. تابع parse_dis_text() متن‌های ذخیره‌شده‌ی dis را می‌خواند تا برای مقایسه‌ی بین‌نسخه‌ای نیازی به چهار مفسر مختلف در زمان آزمون نباشد.
  3. کلاس ClassFile فایل کلاس را می‌خواند و disassemble_jvm() کدهای آن را رمزگشایی می‌کند. توابع parse_javap_code() و compare_with_javap() بررسی مرجع هستند.
  4. کلاس WasmModule بخش‌های ماژول را می‌خواند و disassemble_wasm() عبارت‌ها را همراه با عمق آن‌ها رمزگشایی می‌کند. تابع compare_with_wasm_objdump() بررسی مرجع است.
  5. کلاس CliAssembly ساختارهای PE، هدر CLI، ریشه‌ی متادیتا، استریم‌ها و جدول‌ها را می‌پیماید. متغیرهای TABLE_SCHEMAS و CODED_INDEXES در واقع پیاده‌سازی داده‌ای بخش‌های II.22 و II.24.2.6 هستند. متد disassemble_cil() بدنه‌ی متد را رمزگشایی کرده و compare_with_metadump() آن را بررسی می‌کند.
  6. کلاس 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 کامپایل کرده و مشخصات خروجی هر یک را چاپ می‌کند.

استفاده از ابزارها

این مراحل را به ترتیب برای بررسی هر نگهدارنده‌ای که ساخته‌اید به کار ببرید:

  1. بایت‌های اولیه را بخوانید و نوع قالب را نام ببرید. دو بایت نخست به همراه 0d 0a نشانه‌ی pyc، بایت‌های ca fe ba be نشانه‌ی فایل کلاس، مقدار 00 61 73 6d نشانه‌ی ماژول، و 4d 5a با دایرکتوری داده‌ی 14 نشانه‌ی اسمبلی است.
  2. هدر را بخوانید و شماره نسخه‌ی تولیدکننده را یادداشت کنید. برای pyc این مقدار کلمه‌ی جادویی است، برای فایل کلاس شماره نسخه‌ی اصلی، و برای اسمبلی رشته‌های نسخه‌ی محیط اجرا و متادیتا.
  3. هر دی‌کامپایلری را که قصد استفاده از آن را دارید، پیش از اجرا از نظر سازگاری با آن نسخه بررسی کنید. سقف نسخه و جایی را که ابزار این سقف را مستند کرده ثبت کنید.
  4. خواننده‌ی خود و ابزار استانداردِ همان قالب را روی همان فایل اجرا کرده و نتایج را سطر‌به‌سطر مقایسه کنید. هرگونه ناهمخوانی نشان‌دهنده‌ی باگ در خواننده یا تغییر در ابزار است و تا هرکدام فهمیده نشود، این فهرست شواهد (evidence) معتبری نخواهد بود.
  5. جدول نگهداری را برای تابعی که هدف شماست تکمیل کنید. مشخص کنید چه داده‌هایی حفظ و چه داده‌هایی گم شده‌اند، و نام جدول، صفت یا بخشی را که منبع داده بوده ذکر کنید.
  6. تازه در این مرحله به سراغ خروجی دی‌کامپایلر بروید. مشخص کنید هر نام و نوع داده در آن، رونوشت محضِ یک واقعیت ذخیره‌شده است یا یک استنتاج.

فایل‌های متنی مرجع را همراه نگهدارنده‌ها نگه دارید. با تغییر نسخه‌ی مفسر، 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.sh commit کنید.
  • خواننده‌ی مربوطه را طوری تنظیم کنید که در تمام دستورات با مرجع مطابقت داشته باشد. آپ‌کدهای جدید را تنها در صورتی اضافه کنید که فیکسچر واقعاً به آن‌ها نیاز دارد.
  • تابع 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 را می‌نویسد

دستورالعمل عملیاتی

همیشه اول هدر را بخوانید و تولیدکننده‌ای را که نشان می‌دهد تعیین کنید. نگهدارنده را با خواننده‌ای بخوانید که ناشناخته‌ها را نمی‌پذیرد، و خروجی آن را در همان فایل با ابزار استاندارد همان فرمت مقایسه کنید. سپس برای هر نام و نوع در خروجی دی‌کامپایلر، صراحتاً بگویید که آیا نگهدارنده آن را ذخیره کرده بود یا ابزار آن را استنتاج کرده است.

برای مطالعه‌ی بیشتر

خودآزمایی

به سؤال‌ها جواب بدهید تا ببینید چقدر از درس در ذهنتان مانده است.

  1. پیش از درسیک دکامپایلر روی یک فایل pyc. که چهار بایت اول آن 2b 0e 0d 0a است، پیام «Unsupported opcode» را چاپ می‌کند. هدر چه چیزی را نشان می‌دهد؟
  2. حین درسچرا main.py هنگام اجرا روی نسخه 3.14 می‌تواند نام‌ها، ثابت‌ها و جدول خطوط یک فایل pyc نسخه 3.11 را بخواند، اما نمی‌تواند دستورهای آن را لیست کند؟
  3. حین درسدر Ledger-g.class بایت‌های آدرس pc 4 برابر با aa 00 00 00 00 00 00 2c 00 00 00 06 00 00 00 0a و سپس پنج مقدار 4 بایتی است. سه بایت صفر پس از aa چیستند؟
  4. حین درسدستور javac -g:none حجم فایل Ledger.class را از 1832 بایت (در حالت javac -g) به 1404 بایت کاهش می‌دهد. کدام بایت‌ها حذف شده‌اند؟
  5. حین درسفایل ledger.wasm فاقد بخش name است، با این حال wasm-objdump روی تابع عبارت <grade> را چاپ می‌کند. این نام از کجا آمده و چه نامی هنوز وجود ندارد؟
  6. پس از درسسطر MethodDef برای Grade دارای blob امضای 00 01 08 08 است و هدر بدنه، توکن امضای محلی 0x11000001 را با blob برابر 07 01 08 مشخص می‌کند. کدام حقیقت درباره Grade همچنان در این اسمبلی غایب است؟
  7. پس از درسدر کدام‌یک از این پنج کانتینر، دکامپایلر باید ساختار switch را به جای خواندن به عنوان یک دستور یا یک بلوک، از پرش‌ها بازسازی کند؟

سؤالات متداول

چرا دی‌کامپایلر روی فایل pyc خطای Unsupported opcode یا مبهم‌سازی می‌دهد؟

در اغلب موارد فایل مبهم‌سازی نشده است؛ بلکه از مفسر جدیدتری (مانند پایتون 3.14) کامپایل شده در حالی که جدول آپ‌کدهای دی‌کامپایلر قدیمی‌تر است. بررسی کلمه جادویی (magic number) هدر فایل pyc نسخه واقعی را مشخص می‌کند.

آیا نام متغیرهای محلی در فایل کلاس JVM ذخیره می‌شود؟

خیر، به صورت پیش‌فرض ذخیره نمی‌شود. نام متغیرهای محلی تنها زمانی در جدول LocalVariableTable نگهداری می‌شود که کامپایل با فلگ javac -g انجام شده باشد.

چه تفاوتی میان حفظ اطلاعات در کد Native و بایت‌کد WebAssembly وجود دارد؟

کد ماشین بومی ساختار کنترلی و انواع داده را کاملاً می‌اندازد، اما ماژول WebAssembly ساختار کنترلی (مانند بلوک‌ها و حلقه‌ها) و نوع متغیرها را بر اساس استاندارد حفظ می‌کند و تنها نام‌ها را حذف می‌نماید مگر اینکه بخش سفارشی name در آن درج شده باشد.

ما رو توی گوگل بیشتر ببین

با افزودن نکست ادمین به «منابع ترجیحی» گوگل، درس‌ها و مطالب ما را در بخش اخبار برتر جستجو بیشتر می‌بینید.