بسیاری از برنامهنویسان باینریهای کامپایلشده را جعبههای سیاهی از بایتهای غیرقابلفهم میدانند و برای درک محتوای آنها به ابزارهای سنگینی مانند Ghidra یا متغیرهای وابستهی محیطی متکی میشوند. در محیطهای توسعه و سرورها، راهاندازی این ابزارهای پیچیده نیازمند ملزومات محیطی متعدد است، در حالی که درک دقیق مدل دادهای پشت باینریها همهچیز را شفاف میکند.
یک فایل باینری کامپایلشده در حقیقت مجموعهای ساختیافته از جدولهای متصل به هم است. با نوشتن یک خوانشگر مستقل با کتابخانهی استاندارد پایتون، نهتنها از ابزارهای خارجی بینیاز میشوید، بلکه متوجه خواهید شد کد ماشین چطور در کنار جدولهای نمادها و اطلاعات بازآدرسدهی سازماندهی میشود.
برنامهی کامپایلشده مجموعهای از جدولها است که به یکدیگر ارجاع میدهند. ابزار خوانشگری که خودتان نوشتهاید خروجی مشخصی تولید میکند که میتوانید آن را بایتبهبایت بررسی کنید.
اهداف یادگیری
- بیان قاعدهی مجوز دسترسی برای تمام اهداف این مسیر آموزشی و محدودیتهای چهارگانهی بند 17 U.S.C. 1201(f).
- خواندن هدر ELF، جدول هدر بخشها و هر دو جدول رشتهها با
struct.unpack. - رمزگشایی (Decode) درایههای
.symtabو رکوردهای.relaبر اساس نامگذاریهای استاندارد gABI و x86-64 psABI. - بازگرداندن بایتهای دقیق یک تابع مشخص و نگاشت تکتک آفستهای فایل به ساختار مالک آن.
- خواندن هدرهای برنامه و نگاشت یک آدرس مجازی به آفست فایل از طریق
PT_LOAD. - خواندن فرمانهای بارگذاری (Load commands) در یک آبجکتفایل Mach-O و انطباق مفهومی هرکدام با ساختارهای متناظر در ELF.
پیش از شروع: مجوزها و قوانین حقوقی
تمام اهداف بررسیشده در درسهای 32 تا 36 برنامههایی هستند که خودتان نوشته و کامپایل کردهاید. سورسکد در فایل code/fixtures/ledger.c قرار دارد و شامل چهار تابع کوتاه به زبان C است. شما مالک سورسکد، دستور کامپایلر و فایلهای خروجی کامپایلشده (Object files) حاصل از آن هستید. در اینجا هیچ مجوزی برای خواندن، هیچ قفل نرمافزاری برای رد کردن، و هیچ کد متعلق به شخص ثالثی وجود ندارد. این مسیر آموزشی هرگز به موضوعاتی مثل محافظتهای نرمافزاری، بررسی لایسنس یا برنامههایی که متعلق به شما نیست نمیپردازد.
این قاعده از آنجا ناشی میشود که قانون، فرایند دیساسمبلی (Disassembly) را نوعی نسخهبرداری در نظر میگیرد. در پروندهی Sega Enterprises Ltd. v. Accolade, Inc., 977 F.2d 1510 (9th Cir. 1992)، دادگاه حکم داد نسخههای میانی ایجادشده در جریان دیساسمبلی، طبق قانون کپیرایت مصداق نسخهبرداری هستند. دادگاه در ادامه اعلام کرد: «چنانچه دیساسمبلی تنها راه دسترسی به ایدهها و عناصر عملکردی موجود در یک برنامهی رایانهای دارای حق نشر باشد و دلیل موجهی برای این دسترسی وجود داشته باشد، دیساسمبلی از نظر حقوقی مصداق استفادهی منصفانه (Fair use) از اثر محافظتشده است.»
پروندهی دوم Sony Computer Entertainment, Inc. v. Connectix Corp., 203 F.3d 596 (9th Cir. 2000) است. شرکت Connectix هنگام ساخت یک شبیهساز (Emulator)، از بایوس پلیاستیشن کپیبرداری کرد و دادگاه این نسخهبرداریها را مصداق استفادهی منصفانه دانست. در هر دو پرونده، کپیبرداری با هدف تحقیق و مطالعه مورد حمایت قرار گرفت؛ اما هیچیک از این احکام شامل استفاده از کدهای کپیشده در محصول نهایی نمیشدند.
دو مادهی قانونی این مرزها را تعیین میکنند. بند 17 U.S.C. 1201(f) به شخصی که «حق استفاده از یک نسخهی برنامهی رایانهای را بهصورت قانونی به دست آورده است» اجازه میدهد تدابیر کنترل دسترسی را دور بزند، اما مشروط به اینکه «صرفاً با هدف شناسایی و تحلیل آن دسته از عناصر برنامه باشد که برای دستیابی به سازگاری عملیاتی (Interoperability) یک برنامهی مستقل با سایر برنامهها ضروری هستند». چهار محدودیت متوالی اعمال میشوند: نسخهی قانونی، هدف سازگاری عملیاتی، کدهایی که پیشتر «بهسادگی در دسترس» نبودهاند، و تحلیلی که در سایر جنبهها ناقض حقوق نباشد. بخش 1201(f) صرفاً استثنایی بر قاعدهی ممنوعیت دور زدن تدابیر فنی است؛ مسئلهی نسخهبرداری همچنان تابع قواعد استفادهی منصفانه باقی میماند.
در اتحادیهی اروپا، بند 5(3) از دستورالعمل Directive 2009/24/EC ناظر بر مطالعهی نرمافزار است. فردی که حق استفاده از یک نسخه را دارد میتواند «عملکرد برنامه را برای تعیین ایدهها و اصولی که زیربنای هر عنصر از برنامه است، مشاهده، بررسی یا آزمایش کند». این حق هنگام بارگذاری، نمایش، اجرا، انتقال یا ذخیرهسازی برنامه اعمال میشود.
مادهی 6 دیکامپایل (Decompilation) را بدون اجازهی دارندهی حق نشر مجاز میداند، مشروط بر اینکه «برای به دست آوردن اطلاعات ضروری جهت دستیابی به سازگاری عملیاتی یک برنامهی مستقل، غیرقابل چشمپوشی باشد». شروط آن با بند 1201(f) همخوانی دارد: اقدامات توسط دارندهی لایسنس انجام شود، اطلاعات قبلاً بهسادگی در دسترس نبوده باشد، و اقدامات صرفاً به بخشهای مورد نیاز محدود بماند. مادهی 6(2) استفاده از نتیجه را برای اهداف دیگر یا برای برنامهای که در بیان بهصورت اساسی مشابه باشد ممنوع میکند. طبق مادهی 8، هر شرط قراردادی که مغایر با مادهی 6 یا بند 5(3) باشد، باطل و بلااثر است.
| منبع | آنچه مجاز است | شروط |
|---|---|---|
| بند 17 U.S.C. 1201(f) | دور زدن کنترل دسترسی برای تحلیل برنامه | نسخهی قانونی، هدف سازگاری عملیاتی، عدم دسترسی ساده به اطلاعات، عدم نقض قوانین دیگر |
| دستورالعمل 2009/24/EC بند 5(3) | مشاهده، بررسی و آزمایش یک برنامهی در حال اجرا | داشتن حق استفاده از نسخه، اقدام در چارچوب فرایند عادی بارگذاری و اجرا |
| دستورالعمل 2009/24/EC مادهی 6 | دیکامپایل کردن | دارندهی لایسنس، هدف سازگاری عملیاتی، عدم دسترسی ساده به اطلاعات، محدود بودن به بخشهای مورد نیاز |
| Sega v. Accolade | دیساسمبلی به عنوان استفادهی منصفانه | تنها راه دسترسی به ایدهها و عناصر عملکردی، وجود دلیل موجه |
| Sony v. Connectix | نسخهبرداریهای میانی حین مهندسی معکوس | غیرناقض بودن محصول نهایی |
این نکات مستقیماً از متن قوانین و آرای قضایی استخراج شدهاند و مشاورهی حقوقی به شمار نمیروند. اگر در آینده هدفی متعلق به شما نبود، حتماً پیش از باز کردن فایل، مجوز کتبی بگیرید و آن را در کنار شواهد (Evidence) خود نگه دارید. بخش 2.1 از راهنمای Reverse Engineering 101 ارجاعات کامل و پروندههای دادگاه دادگستری اروپا (CJEU) را در اختیارتان میگذارد.
صورت مسئله
فایل ledger.c را کامپایل کردهاید و دستور llvm-objdump -t را روی فایل آبجکت اجرا کردهاید. خروجی، تابع percent_change را با مقدار 0000000000000000 و اندازهی 0000000000000010 لیست میکند. کدام 16 بایت از این فایل 1,768 بایتی متعلق به این تابع است؟ این فهرست همچنین رکورد R_X86_64_PC32 .rodata+0x10 را در آفست 0x36 نشان میدهد و چهار بایت موجود در آن نقطه از فایل صفر هستند. این بایتها در نهایت به چه چیزی تبدیل میشوند و چرا وقتی جدول از 0x14 شروع میشود، مقدار افزوده (Addend) برابر با 0x10 است؟
ابزار rea به پرسشهای مربوط به چیدمان حافظه از طریق inspect_binary_layout پاسخ میدهد؛ ابزاری که تنها روی Linux x64 با نسخهی pwntools 4.15.0 از طریق متغیر محیطی REA_PWNTOOLS_PYTHON در دسترس است. این ابزار روی سیستم Mac وجود ندارد. دستور address_to_file_offset نیز نیازمند یک نشست اجرایی در Ghidra و یک کپی کامل از بایتهای فایل اصلی با هش SHA-256 یکسان است. ابزار inspect_artifact نیز صرفاً محتوای نگهدارنده (Container) را بر اساس مسیر و هش محتوا فهرست میکند و هرگز جداول داخلی یک آبجکتفایل را باز نمیکند. بنابراین باید خوانشگر را خودتان در حدود 950 خط کد پایتون با struct.unpack و بدون هیچ کتابخانهی جانبی بنویسید. تستها تکتک جدولها را با متون خروجی llvm-objdump که قبلاً در قالب فایلهای آزمایشی (Fixtures) ذخیره شدهاند، مقایسه میکنند.
ساختار فایل: جدولهایی که به جدولهای دیگر اشاره دارند
راهنمای استاندارد System V gABI در فصل 4، شکل 4-1، دو نما از یک فایل را ترسیم میکند. «نمای پیوند» (Linking view) شامل یک هدر ELF، یک جدول هدر برنامهی اختیاری، بخشها (Sections) و یک جدول هدر بخشهای الزامی است. «نمای اجرا» (Execution view) شامل یک هدر ELF، جدول هدر برنامهی الزامی، سگمنتها (Segments) و یک جدول هدر بخشهای اختیاری است. gABI جملهای کلیدی اضافه میکند که بر کل ساختار خوانشگر ما حاکم است: «تنها هدر ELF جایگاه ثابتی در فایل دارد». هر ساختار دیگری با آفستی پیدا میشود که در ساختار دیگری ذخیره شده است.
همچنین gABI انواع اسکالر را مشخص کرده و بیان میکند: «ELF از فیلدهای بیتی (Bit-fields) استفاده نمیکند». اندازهی فیلدهای Elf64_Addr و Elf64_Off برابر با 8 بایت، Elf64_Half برابر با 2 بایت، Elf64_Word برابر با 4 بایت و Elf64_Xword برابر با 8 بایت است. استاندارد x86-64 psABI در بخش 4.1.1 مواردی شامل ELFCLASS64، ترتیب بایت ELFDATA2LSB، مقدار EM_X86_64 (62) برای e_machine و مقدار 0 برای e_flags را الزامی میداند. ترکیب اندیان کوچک (Little-endian) با طولهای ثابت به این معناست که میتوان با استفاده از struct.unpack و پیشوند <، هر رکورد را با یک فراخوانی خواند.
64 بایت اول فایل ledger-x86_64-O1.o به این صورت است:
00000000: 7f45 4c46 0201 0100 0000 0000 0000 0000 .ELF............
00000010: 0100 3e00 0100 0000 0000 0000 0000 0000 ..>.............
00000020: 0000 0000 0000 0000 2804 0000 0000 0000 ........(.......
00000030: 0000 0000 4000 0000 0000 4000 0b00 0100 ....@.....@.....
این مقادیر را با صفحهی "ELF Header" در gABI تطبیق دهید. فیلد e_ident شامل 16 بایت است: 7f 45 4c 46 (بایتهای جادویی)، کلاس 2 (64 بیتی)، دادهی 1 (Endianness کوچک)، نسخهی 1، شناسهی OS ABI برابر با 0 (System V) و پدینگ (حاشیهگذاری).
سپس فیلد e_type با مقدار 0x0001 نشاندهندهی ET_REL (آبجکتفایل بازآدرسپذیر) است. مقدار e_machine یعنی 0x003e برابر با 62 یا EM_X86_64 است. مقدار e_entry صفر است زیرا یک آبجکتفایل نقطهی ورود ندارد. مقدار e_phoff صفر است زیرا آبجکتفایل فاقد هدرهای برنامه است. مقدار e_shoff برابر با 0x428، آفست جدول هدر بخشها در فایل است. ردیف آخر مقادیر e_ehsize برابر با 0x40، e_phentsize برابر با 0، e_phnum برابر با 0، e_shentsize برابر با 0x40، e_shnum برابر با 0x0b (11 هدر) و e_shstrndx برابر با 0x0001 را در خود جای داده است.
برنامهی خوانشگر این ردیف را در قالب یک فراخوانی پیاده میکند:
fields = struct.unpack_from("<HHIQQQIHHHHHH", data, 16)
رشتهی فرمت دقیقاً همان نمودار gABI است که از چپ به راست خوانده میشود: دو نیمکلمه (Halfword)، یک کلمه (Word)، سه مقدار 8 بایتی، یک کلمه و شش نیمکلمه. خوانشگر فایلی را که بایتهای جادویی، کلاس یا ترتیب بایت متفاوتی داشته باشد نمیپذیرد، و مقادیر e_shentsize یا e_phentsize غیر از 64 و 56 را نیز رد میکند. هرگونه اندازهی نادرست باعث خطا در تمام آفستهای بعدی میشود؛ بنابراین این بررسی در ابتدای کد قرار میگیرد.
هدرهای بخشها و دو جدول رشتهها
هر هدر بخش، ساختاری از نوع Elf64_Shdr به طول 64 بایت است (شکل 4-8 در gABI): فیلدهای sh_name، sh_type، sh_flags، sh_addr، sh_offset، sh_size، sh_link، sh_info، sh_addralign و sh_entsize. رشتهی فرمت آن <IIQQQQIIQQ است. فیلد sh_name آفستی درون بخشی است که ایندکس آن در e_shstrndx مشخص شده و این نام با اولین بایت صفر پایان مییابد. در این آبجکتفایل، e_shstrndx برابر با 1 است و clang آن بخش را .strtab نامگذاری کرده؛ در نتیجه یک جدول واحد هم نام بخشها و هم نام نمادها (Symbols) را پوشش میدهد.
خوانشگر، 11 هدر این آبجکت کامپایلشده با بهینهسازی -O1 را چاپ میکند:
Idx Name Type Flags Offset Size Addr Link Info Align
0 NULL 0x0 0x0 0x0 0 0 0
1 .strtab STRTAB 0x368 0xbe 0x0 0 0 1
2 .text PROGBITS AX 0x40 0x9f 0x0 0 0 16
3 .rela.text RELA I 0x2a8 0x48 0x0 10 2 8
4 .rodata PROGBITS A 0xe0 0x50 0x0 0 0 8
5 .rela.rodata RELA I 0x2f0 0x78 0x0 10 4 8
6 .rodata.str1.1 PROGBITS AMS 0x130 0xa 0x0 0 0 1
7 .comment PROGBITS MS 0x13a 0x2f 0x0 0 0 1
8 .note.GNU-stack PROGBITS 0x169 0x0 0x0 0 0 1
9 .llvm_addrsig LLVM_ADDRSIG E 0x368 0x0 0x0 10 0 1
10 .symtab SYMTAB 0x170 0x138 0x0 1 9 8
ایندکس 0 هدر تهی (Null header) رزروشده است. بخش .text شامل 159 بایت کد در آفست 0x40 فایل (دقیقاً پس از هدر) است که پرچمهای A (تخصیصیافته در حافظه) و X (قابل اجرا) دارد. بخش .rodata شامل 80 بایت دادهی فقطخواندنی است. بخش .rodata.str1.1 شامل پنج رشتهی برچسب F\0D\0C\0B\0A\0 با طول 10 بایت است و پرچمهای M (قابل merge) و S (رشتهها) را دارد.
بخش .comment شامل امضای کامپایلر به طول 47 بایت یعنی \0Apple clang version 17.0.0 (clang-1700.6.3.2)\0 است. بخش .llvm_addrsig دارای نوع 0x6fff4c03 است که در محدودهی اختصاصی سیستمعامل یعنی بین SHT_LOOS و SHT_HIOS قرار میگیرد که توسط gABI رزرو شده و فایل ELF.h متعلق به LLVM آن را نامگذاری کرده است. پرچم E در آن نشاندهندهی SHF_EXCLUDE است؛ یعنی پیونددهنده (Linker) آن را کنار میگذارد.
ستونهای sh_link و sh_info همان اشارهگرهای میان جداول هستند. بخش .rela.text دارای sh_info برابر با 2 و sh_link برابر با 10 است؛ به این معنی که بخش 2 را تکه (Patch) میزند و نمادهای خود را از بخش 10 میگیرد. بخش .symtab دارای sh_link برابر با 1 (جدول رشتهها برای نام نمادها) و sh_info برابر با 9 (ایندکس اولین نماد غیرمحلی) است. مقدار sh_addr در تمامی سطرها صفر است؛ چرا که آبجکتفایل هنوز آدرسی در حافظه ندارد و پیونددهنده این آدرسها را تخصیص میدهد.
دو قاعدهی gABI به خوانشگر اجازه میدهند نقشهی کاملی از فایل ترسیم کند. بخشها «پیوسته در حافظه هستند و با یکدیگر همپوشانی ندارند» و فایل «ممکن است حاوی فضای غیرفعال باشد». خوانشگر تمام ساختارها را بر اساس آفست مرتب میکند و فاصلههای خالی را پر میکند:
0x0 +64 header ELF header
0x40 +159 section .text
0xdf +1 gap inactive space
0xe0 +80 section .rodata
0x130 +10 section .rodata.str1.1
0x13a +47 section .comment
0x169 +7 gap inactive space
0x170 +312 section .symtab
0x2a8 +72 section .rela.text
0x2f0 +120 section .rela.rodata
0x368 +190 section .strtab
0x426 +2 gap inactive space
0x428 +704 table section header table
این سه فاصلهی خالی در مجموع 10 بایت هستند و همگی ناشی از تراز حافظه (Alignment) هستند. بخش .rodata دارای sh_addralign برابر با 8 است؛ بنابراین بهجای 0xdf از آفست 0xe0 آغاز میشود. بخش .symtab ترازی برابر با 8 دارد، بنابراین 7 بایت بدون استفاده پس از .comment باقی میماند. جدول هدر بخشها نیز روی مرز 8 بایتی تراز شده است.
بخشهایی از نوع SHT_NOBITS هیچ بایتی از فایل را اشغال نمیکنند؛ در نتیجه نقشهی حافظه آنها را در نظر نمیگیرد. هیچ عنصری در این آبجکتفایل چنین نوعی ندارد. بخش .bss در یک برنامهی پیوندخورده از این نوع است و اگر خوانشگری در آنجا به مقدار sh_size اتکا کند، دادههایی فراتر از انتهای فایل را خواهد خواند.
نمادها: پیوند، نوع، بخش، مقدار، اندازه
هر رکورد Elf64_Sym برابر با 24 بایت است (شکل 4-18 در gABI): شامل فیلدهای st_name، st_info، st_other، st_shndx، st_value، st_size با فرمت <IBBHQQ. فیلد st_info دو بخش را در خود فشرده است: نوع پیوند (Binding) در 4 بیت بالا و نوع نماد در 4 بیت پایین. راهنمای gABI پیوندها را با نامهای STB_LOCAL (0)، STB_GLOBAL (1)، STB_WEAK (2) و انواع نماد را با نامهای STT_NOTYPE (0)، STT_OBJECT (1)، STT_FUNC (2)، STT_SECTION (3)، STT_FILE (4) مشخص میکند. 2 بیت پایینی st_other نمایانگر سطح دید (Visibility) است که در اینجا در تمام موارد برابر با STV_DEFAULT (0) است.
Idx Value Size Bind Type Section Name
0 0x0 0 LOCAL NOTYPE *UND*
1 0x0 0 LOCAL FILE *ABS* ledger.c
2 0x0 20 LOCAL OBJECT .rodata labels.rel
3 0x0 0 LOCAL SECTION .rodata .rodata
4 0x2 2 LOCAL OBJECT .rodata.str1.1 .L.str.1
5 0x4 2 LOCAL OBJECT .rodata.str1.1 .L.str.2
6 0x6 2 LOCAL OBJECT .rodata.str1.1 .L.str.3
7 0x8 2 LOCAL OBJECT .rodata.str1.1 .L.str.4
8 0x0 0 LOCAL SECTION .rodata.str1.1 .rodata.str1.1
9 0x0 16 GLOBAL FUNC .text percent_change
10 0x10 46 GLOBAL FUNC .text grade
11 0x40 31 GLOBAL FUNC .text total
12 0x60 63 GLOBAL FUNC .text grade_label
ایندکس 0 نماد تعریفنشدهی رزروشده است. فیلد st_shndx بخشی را تعیین میکند که نماد را تعریف کرده است و سه مقدار رزروشده دارد: SHN_UNDEF با مقدار 0، SHN_ABS با مقدار 0xfff1 برای نام فایل، و SHN_COMMON با مقدار 0xfff2. نمادهای مربوط به بخشها نام اختصاصی ندارند؛ بنابراین llvm-objdump نام همان بخش را برای آنها چاپ میکند و خوانشگر ما نیز همین کار را انجام میدهد. قاعدهی gABI مبنی بر اینکه «تمام نمادهای دارای پیوند STB_LOCAL قبل از نمادهای ضعیف (Weak) و سراسری (Global) میآیند» در 9 سطر اول دیده میشود، و بخش .symtab دقیقاً به همین علت مقدار 9 را در sh_info ثبت کرده است.
در یک فایل بازآدرسپذیر، st_value نمایانگر آفست درون همان بخش مربوط به نماد است، زیرا مقدار sh_addr صفر است. در نتیجه، آفست فایل برای یک تابع از رابطهی sh_offset + st_value - sh_addr به دست میآید و طول آن همان st_size است:
section = elf.sections[symbol.st_shndx]
file_offset = section.sh_offset + (symbol.st_value - section.sh_addr)
code = elf.data[file_offset : file_offset + symbol.st_size]
برای تابع percent_change این مقدار برابر با 0x40 + 0 است و آن 16 بایت عبارتند از: 85 ff 74 09 29 fe 6b c6 64 99 f7 ff c3 31 c0 c3. اندازهها به اندازهی نقطهی شروع اهمیت دارند. تابع grade در آفست 0x3e از بخش .text به پایان میرسد. دو بایت بعدی یعنی 66 90 در واقع دستور nop هستند که کامپایلر اضافه کرده است تا تابع بعدی یعنی total در مرز 16 بایتی یعنی آفست 0x40 آغاز شود. این دو بایت متعلق به هیچ نمادی نیستند. درس 33 از st_size به عنوان شرط توقف دیساسمبلر استفاده میکند و درس 36 پیش از مقایسهی نسخهی بازسازیشده، این بایتها را ماسک میکند.
بازآدرسدهیها: معنای بایتهای صفر
هر رکورد Elf64_Rela دارای 24 بایت است (شکل 4-23 در gABI): شامل r_offset، r_info و مقدار علامتدار r_addend با فرمت <QQq. استاندارد gABI فیلد r_info را با دو ماکرو تفکیک میکند: ماکروی ELF64_R_SYM(i) معادل i >> 32 و ماکروی ELF64_R_TYPE(i) معادل i & 0xffffffff. در یک فایل بازآدرسپذیر، r_offset آفستی درون بخشی است که مقدار sh_info در این جدول به آن اشاره دارد. نام انواع و فرمولها برگرفته از جدول 4.9 در x86-64 psABI است. در این جدول، S مقدار نماد، A مقدار افزوده (Addend)، P مکان اعمال پچ و L درایهی PLT است. نوع R_X86_64_64 برابر با S + A، نوع R_X86_64_PC32 برابر با S + A - P و نوع R_X86_64_PLT32 برابر با L + A - P محاسبه میشود.
فایل حاصل از بهینهسازی -O1 هشت رکورد دارد:
.rela.text 0x36 R_X86_64_PC32 .rodata+0x10
.rela.text 0x84 R_X86_64_PC32 .rodata+0x24
.rela.text 0x93 R_X86_64_PC32 .rodata-0x4
.rela.rodata 0x0 R_X86_64_PC32 .rodata.str1.1
.rela.rodata 0x4 R_X86_64_PC32 .L.str.1+0x4
.rela.rodata 0x8 R_X86_64_PC32 .L.str.2+0x8
.rela.rodata 0xc R_X86_64_PC32 .L.str.3+0xc
.rela.rodata 0x10 R_X86_64_PC32 .L.str.4+0x10
رکورد اول را بررسی کنیم. آفست 0x36 درون تابع grade قرار دارد که از 0x10 تا 0x3e کشیده شده است. دستورالعمل موجود در آفست 0x33 مقدار 48 8d 0d 00 00 00 00 است که یک دستور lea با آدرسدهی نسبی به ثبات RIP محسوب میشود، و جابهجایی (Displacement) چهار بایتی آن در آفست 0x36 مینشیند. پردازنده این جابهجایی را به آدرس دستورالعمل بعدی، یعنی 0x3a، اضافه میکند.
کامپایلر میخواهد نتیجه به .rodata + 0x14 اشاره کند؛ نقطهای که جدولی با پنج درایهی 32 بیتی مقادیر 1, 2, 3, 4, 4 شروع میشود. بنابراین مقدار ذخیرهشده باید (.rodata + 0x14) - 0x3a باشد. فرمول S + A - P به ازای P = 0x36 حاصل .rodata + A - 0x36 را به دست میدهد. با مساوی قرار دادن این دو رابطه، مقدار A = 0x14 - 4 = 0x10 به دست میآید. مقدار افزوده حامل آفست هدف منهای آن چهار بایتی است که بین محل جابهجایی و انتهای دستور فاصله انداختهاند.
این جدول همان روشی است که کامپایلر clang برای پیادهسازی دستور switch در تابع grade تحت فلگ -O1 به کار برده است. کد، مقدار score / 10 را از طریق ضرب در 0x66666667 و دو شیفت محاسبه میکند، عدد 6 را از آن تفریق میکند، با 4 مقایسه مینماید و در نهایت به این جدول ارجاع میدهد. فایل ELF با فلگ -O1 هیچ نمادی را برای این جدول نگه نمیدارد؛ مرجع مورد استفاده، نماد بخش .rodata به همراه یک مقدار افزوده است، و تنها نسخهی کامپایلشدهی arm64 از همین سورس است که آن را l_switch.table.grade نامگذاری میکند. رکورد دوم در آفست 0x84 از تابع grade_label به جدول دومی از مقادیر 8 بایتی در .rodata + 0x28 اشاره میکند. رکورد سوم در 0x93 به .rodata + 0x0 یعنی شروع labels.rel اشاره دارد.
پنج رکورد بخش .rela.rodata محتوای labels.rel را پر میکنند. هر درایه در آفست 4i قرار گرفته و مقدار افزودهی 4i را دریافت میکند؛ در نتیجه S + A - P به آدرس رشته منهای پایهی جدول خلاصه میشود. تابع grade_label یک درایه را با دستور movslq میخواند، پایهی جدول را دوباره به آن اضافه میکند و به اشارهگر میرسد. اولین رکورد به نماد بخش .rodata.str1.1 با مقدار افزودهی 0 اشاره دارد زیرا رشتهی F اولین بایت آن بخش است. سایر رکوردها به .L.str.1 تا .L.str.4 اشاره میکنند که برچسبهای محلی کامپایلر برای رشتههای D، C، B و A هستند.
یک سورسکد واحد در سه سطح مختلف بهینهسازی، جدولهای کاملاً متفاوتی میسازد. خوانشگر این تفاوت را بدون نیاز به دیساسمبلر نشان میدهد:
| ساخت | محل استقرار labels |
نحوهی انتخاب در grade |
نحوهی دسترسی grade_label به grade |
|---|---|---|---|
| -O0 | بخش .data، پنج اشارهگر 8 بایتی، پنج رکورد R_X86_64_64 |
جدول پرش (Jump table) در .rodata: پنج رکورد R_X86_64_PC32 با اشاره به داخل .text |
یک فراخوانی با رکورد R_X86_64_PLT32 grade-0x4 |
| -O1 | بخش .rodata بهصورت labels.rel، پنج درایهی نسبی 32 بیتی |
جدول مقادیر در .rodata+0x14، یک رکورد R_X86_64_PC32 |
بهصورت درونخطی (Inlined)، بدون فراخوانی |
| -O2 | مشابه -O1 | مشابه -O1 | بهصورت درونخطی، اندازهی total به 140 بایت میرسد |
در سطح بهینهسازی -O0، ساختار switch یک جدول پرش واقعی است که پنج درایهی آن به داخل بخش .text در آفستهای +0x91، +0x8c، +0x87، +0x82 و +0x86 ارجاع میدهند. فراخوانی تابع grade نیز به همان دلیلی که برای lea گفته شد، مقدار افزودهی -0x4 دارد. در بهینهسازی -O1، بهینهساز متوجه شد که همهی شاخهها مقداری ثابت برمیگردانند؛ بنابراین پرشها را با جدولی از نتایج جایگزین کرد. درس 33 هر دو ساختار را رمزگشایی (Decode) میکند.
هدرهای برنامه: نمای اجرا برای بارگذار سیستم
یک آبجکتفایل بازآدرسپذیر هدر برنامه ندارد؛ بنابراین مقدار e_phnum برابر با 0 است. یک فایل اجرایی یا یک فایل اشتراکی (Shared object) دارای این هدرها است و بارگذار سیستمعامل (Loader) هیچ بخش دیگری از فایل را نمیخواند. ساختار Elf64_Phdr برابر با 56 بایت است (شکل 5-1 در gABI): شامل فیلدهای p_type، p_flags، p_offset، p_vaddr، p_paddr، p_filesz، p_memsz، p_align با فرمت <IIQQQQQQ. استاندارد gABI سه قاعده را برای درایههای PT_LOAD مشخص میکند: آنها بر اساس ترتیب صعودی p_vaddr چیده میشوند؛ اگر p_memsz بزرگتر از p_filesz باشد فضای باقیمانده با صفر پر میشود؛ و فیلد p_vaddr به پیمانهی p_align با p_offset همنهشت است.
تمام اهداف این مسیر برنامههایی هستند که خودتان ساختهاید و هیچ لینکری روی این سیستمعامل Mac خروجی اجرایی لینوکس از ledger.o تولید نمیکند. بنابراین فایل آزمایشی tiny-x86_64 توسط اسکریپت code/fixtures/build_tiny_elf.py ساخته میشود که 736 بایت را با متد struct.pack مینویسد. این فایل شامل هدر ELF، دو هدر برنامه، تابع _start به طول 36 بایت، بخش .rodata به طول 3 بایت، یک جدول نمادها و شش هدر بخش است. تابع _start رشتهی ok\n را در توصیفگر فایل 1 مینویسد و با وضعیت خروج 42 پایان مییابد. این فایل روی لینوکس x86-64 اجرا میشود و در اینجا به عنوان نمونهای برای ارزیابی مسیر پردازش هدر برنامه استفاده شده است.
Program headers:
PT_LOAD r-x offset=0x0 vaddr=0x400000 filesz=0x2e0 memsz=0x2e0 align=0x1000
PT_GNU_STACK rw- offset=0x0 vaddr=0x0 filesz=0x0 memsz=0x0 align=0x10
entry 0x4000b0 is file offset 0xb0 through PT_LOAD
تنها سگمنت PT_LOAD کل این فایل 736 بایتی را روی آدرس 0x400000 نگاشت میکند. خوانشگر با پیدا کردن سگمنت PT_LOAD دربرگیرندهی آدرس و محاسبهی p_offset + (address - p_vaddr)، آدرس حافظه را به آفست فایل تبدیل میکند. برای نقطهی ورود برنامه، این محاسبه بهصورت 0 + (0x4000b0 - 0x400000) = 0xb0 انجام میشود که همان ابتدای بخش .text است.
این همان محاسباتی است که ابزار address_to_file_offset در rea انجام میدهد. برای یک هدف در Ghidra، این ابزار مقادیر {address, file_offset} را برمیگرداند. ارائهدهندهی Hopper مقادیر provider_file_offset و image_base_file_offset را نیز گزارش میدهد. این ابزار نیازمند یک نسخهی دستنخورده از فایل اصلی است و هش SHA-256 آن باید کاملاً با فایل هدف مطابقت داشته باشد. در مستندات ابزار inspect_binary_layout از rea، نکتهی مهمی ذکر شده است: «آدرس پیوندخورده لزوماً آدرس محیط اجرا (Runtime) نیست». یک فایل اجرایی مستقل از موقعیت (PIE)، در آدرس پایهای بارگذاری میشود که کرنل (Kernel) انتخاب میکند؛ در نتیجه آفست فایل تنها مختصات پایدار است.
هدرهای برنامه علت اجرای موفق یک فایل باینری فاقد نماد (Stripped) را نیز توضیح میدهند. یک لینکر میتواند جدول هدر بخشها را بهطور کامل حذف کند که در این صورت e_shoff صفر میشود. ابزاری که صرفاً روی بخشها پیمایش میکند، دیگر چیزی نخواهد دید؛ اما بارگذار سیستم اصلاً نیازی به این بخشها ندارد.
همین مفاهیم در قالب ساختار Mach-O
شرکت اپل مشخصات مکتوبی در قالب متن برای ساختار Mach-O منتشر نمیکند؛ هدرهای موجود در سورسکد هستهی XNU نقش مرجع استاندارد را دارند: فایلهای EXTERNAL_HEADERS/mach-o/loader.h، nlist.h و reloc.h. فایل آزمایشی ledger-arm64-O1.o همان کد ledger.c است که با دستور clang -target arm64-apple-macos -O1 -fno-asynchronous-unwind-tables -c بیلد شده است.
ساختار mach_header_64 برابر با 32 بایت است: magic، cputype، cpusubtype، filetype، ncmds، sizeofcmds، flags، reserved. بایتهای جادویی برابر با 0xfeedfacf یعنی MH_MAGIC_64 است. مقدار cputype برابر با 0x0100000c یعنی CPU_TYPE_ARM64، فیلد filetype برابر با 1 یا همان MH_OBJECT است. فیلد ncmds برابر با 5 و مقدار sizeofcmds برابر با 616 است: فرمانهای بارگذاری دقیقاً 616 بایت بعد از هدر را به خود اختصاص دادهاند و هرکدام با ساختار {cmd, cmdsize} آغاز میشوند. پرچم 0x2000 نمایانگر MH_SUBSECTIONS_VIA_SYMBOLS است.
Load commands:
0 LC_SEGMENT_64 cmdsize=472 at 0x20
1 LC_BUILD_VERSION cmdsize=24 at 0x1f8
2 LC_LINKER_OPTIMIZATION_HINT cmdsize=16 at 0x210
3 LC_SYMTAB cmdsize=24 at 0x220
4 LC_DYSYMTAB cmdsize=80 at 0x238
فرمان LC_SEGMENT_64 (با کد 0x19) معادل Mach-O برای هر دو جدول ساختار ELF است. ساختار segment_command_64 شامل این فیلدها است: segname، vmaddr، vmsize، fileoff، filesize، maxprot، initprot، nsects و flags. سپس در ادامهی همین دستور، به تعداد nsects رکورد از نوع section_64 (هر کدام 80 بایت) قرار میگیرند. توضیحات فایل loader.h برای ساختار MH_OBJECT قید میکند که بخشها «در یک سگمنت بینام و بدون پدینگ (حاشیهگذاری) سگمنت» قرار دارند. فایل آزمایشی ما دقیقاً همین را نشان میدهد: یک سگمنت بدون نام، مقدار fileoff برابر با 648، مقدار filesize برابر با 456 و پنج بخش.
Idx Section Addr Size Offset Reloff Nreloc Type
1 __TEXT,__text 0x0 0xd0 648 1104 6 S_REGULAR
2 __DATA,__const 0xd0 0x28 856 1152 5 S_REGULAR
3 __TEXT,__cstring 0xf8 0xa 896 0 0 S_CSTRING_LITERALS
4 __TEXT,__const 0x108 0x40 912 0 0 S_REGULAR
5 __LD,__compact_unwind 0x148 0x80 976 1192 4 S_REGULAR
هر بخش نام سگمنت والد خود را در فیلد segname میآورد؛ بنابراین دو بخش با نام __const کاملاً از هم متمایزند، زیرا یکی __DATA,__const و دیگری __TEXT,__const است. رکوردهای بازآدرسدهی از طریق فیلدهای reloff و nreloc به ازای هر بخش نگهداری میشوند، در حالی که در فرمت ELF یک بخش مجزای .rela.X به ازای هر هدف ایجاد میشود.
هر رکورد relocation_info شامل 8 بایت است. ابتدا یک مقدار علامتدار r_address میآید. به دنبال آن یک فیلد 32 بیتی فشرده قرار دارد که بهصورت r_symbolnum (24 بیت)، r_pcrel (1 بیت)، r_length (2 بیت)، r_extern (1 بیت) و r_type (4 بیت) تقسیم میشود. شش رکورد موجود در __text در واقع سه جفت از رکوردهای ARM64_RELOC_PAGE21 و ARM64_RELOC_PAGEOFF12 هستند، یعنی به ازای هر ارجاع به جدول یک جفت. از آنجا که ترکیب دو دستور adrp و add در معماری arm64 همان کاری را انجام میدهد که یک دستور lea در معماری x86-64 انجام میداد، هر ارجاع به دو رکورد نیاز دارد. پنج رکورد موجود در __DATA,__const از نوع ARM64_RELOC_UNSIGNED با طول r_length برابر با 3 هستند؛ بنابراین هر کدام یک اشارهگر مطلق 8 بایتی را پچ میکنند.
در سطح بهینهسازی -O1، خروجی arm64 آرایهی labels را به عنوان اشارهگرهای ساده نگه داشته، در حالی که بیلد x86-64 یک جدول نسبی برای آن ساخته است.
فرمان LC_SYMTAB (با کد 0x2) شامل مقادیر symoff برابر با 1240، nsyms برابر با 17، stroff برابر با 1512 و strsize برابر با 176 است؛ حاصل جمع 1512 و 176 برابر با 1688 یعنی دقیقاً اندازهی فایل میشود. هر ساختار nlist_64 برابر با 16 بایت است: n_strx، n_type، n_sect، n_desc و n_value. فیلد n_type یک بیتفیلد است. برابری N_TYPE (ماسک 0x0e) با مقدار N_SECT (0xe) نشان میدهد نماد درون بخشی تعریف شده که شمارهی ترتیبی آن بر پایهی یک، برابر با n_sect است. بیت N_EXT (0x01) نماد خارجی را مشخص میکند.
ساختار nlist_64 فیلدی برای اندازه ندارد. خوانشگر برای به دست آوردن طول 24 بایتی تابع _percent_change، آدرس نماد بعدی در همان بخش را که تابع _grade در آدرس 0x18 است مبنا قرار میدهد. نمادهای زبان C در اینجا یک خط زیر (_) اضافه در ابتدا دارند؛ بنابراین نام تابع _percent_change است.
فرمان LC_DYSYMTAB (با کد 0xb) همان قاعدهی اولویت نمادهای محلی را ثبت میکند که در sh_info از هدر ELF دیدیم: مقادیر ilocalsym برابر با 0، nlocalsym برابر با 13، iextdefsym برابر با 13 و nextdefsym برابر با 4. فرمان LC_BUILD_VERSION (با کد 0x32) مقادیر platform برابر با 1 (macOS)، نسخهی حداقل minos برابر با 26.0 و sdk برابر با 26.2 را بهصورت اعداد صحیح فشردهی XXXX.YY.ZZ ثبت میکند. به دلیل وجود همین فرمان، این فایل آزمایشی arm64 بهصورت بایتهای مستقیم در مخزن commit شده است؛ زیرا کامپایل مجدد روی سیستمی دیگر با SDK متفاوت، فایل متفاوتی تولید میکند.
متد compare_formats در خوانشگر ما، دو فایل را در کنار یکدیگر مقایسه میکند:
| مفهوم | ساختار ELF در ledger-x86_64-O1.o |
ساختار Mach-O در ledger-arm64-O1.o |
|---|---|---|
| شناسهی فایل | فیلد e_ident با بایتهای 7f 45 4c 46، کلاس 64، دادهی LSB |
فیلد magic با مقدار 0xfeedfacf |
| ماشین | فیلد e_machine برابر با 62 یعنی EM_X86_64 |
فیلد cputype برابر با 0x100000c یعنی CPU_TYPE_ARM64 |
| نوع فایل | فیلد e_type برابر با 1 یعنی ET_REL |
فیلد filetype برابر با 1 یعنی MH_OBJECT |
| جایگاه جدولها | مقدار e_shoff برابر با 0x428، 11 هدر بخش |
5 فرمان بارگذاری در 616 بایت پس از هدر |
| کد اجرایی | بخش .text در آفست 0x40، اندازه 0x9f، پرچمهای AX |
بخش __TEXT,__text در آفست 648، اندازه 0xd0، درون سگمنت بینام |
| نام نمادها | بخش .symtab (بخش 10)، نامها در .strtab از طریق sh_link برابر با 1 |
فرمان LC_SYMTAB با symoff برابر 1240، تعداد nsyms برابر 17، نامها در stroff برابر 1512 |
| اندازهی نماد | فیلد st_size: تابع percent_change برابر با 16 بایت |
بدون فیلد: تفریق آدرس نماد بعدی از فعلی، 24 بایت |
| بازآدرسدهیها | بخش .rela.text، سه رکورد، R_X86_64_PC32 |
بخش __text با reloff برابر 1104، شش رکورد از نوع PAGE21 و PAGEOFF12 |
| بایتهای تابع | تابع percent_change: 16 بایت در آفست 0x40 |
تابع _percent_change: 24 بایت در آفست 0x288 |
ابزار rea فایلهای Mach-O را با inspect_macho بررسی میکند؛ دستوری که دستورات otool -h -l، nm -gjU، dyld_info -imports -exports و dwarfdump --uuid را از طریق ابزار xcrun فراخوانی میکند. خوانشگر شما به هیچکدام از اینها نیازی ندارد و دقیقاً همان مقادیر symoff و stroff را استخراج میکند که دستور otool -l نمایش میدهد. ابزار open_binary در rea فرمتهای ELF، MZ، PE و نسخههای Mach-O چندمعماری (Universal) یا تکمعماری (Thin) را از روی بایتهای جادویی تشخیص میدهد. همچنین فیلد e_machine در آفست 18 را مستقیماً میخواند. این دقیقاً همان 20 بایت اولی است که در اینجا پیاده کردهاید. در مورد فایلی که هیچ هدری ندارد، باید مشخصات را بهصورت format: raw-binary همراه با معماری و آدرس ورود تعیین کنید. ارائهدهندهی Ghidra فایلهای چندمعماری را رد میکند و میخواهد بخش مربوط به یک معماری جدا شود؛ خوانشگر شما نیز با دیدن بایتهای 0xcafebabe همین رفتار را در پیش میگیرد.
پیادهسازی از صفر
اجرای خوانشگر که با کتابخانهی استاندارد پایتون نوشته شده است:
cd phases/13-tools-and-protocols/32-program-anatomy-elf-from-scratch
python3 code/main.py
این برنامه ابتدا فایل ledger-x86_64-O1.o را میخواند و هدر، بخشها، نمادها، بازآدرسدهیها و نقشهی آفستهای فایل آن را چاپ میکند. سپس بایتهای چهار تابع را نمایش داده، فایل ledger-arm64-O1.o را پردازش کرده و جدول مقایسهای را خروجی میدهد. در نهایت نیز هدرهای برنامهی فایل tiny-x86_64 را چاپ میکند. سطرهای مورد انتظار به این شکل است:
percent_change: 16 bytes at file offset 0x40: 85 ff 74 09 29 fe 6b c6 64 99 f7 ff c3 31 c0 c3
grade: 46 bytes at file offset 0x50: 48 63 c7 48 69 c0 67 66 66 66 ...
entry 0x4000b0 is file offset 0xb0 through PT_LOAD
برای خواندن یک فایل مشخص میتوانید مسیر آن را ارسال کنید: python3 code/main.py code/fixtures/ledger-x86_64-O0.o. خوانشگر بر اساس چهار بایت اول، ساختار ELF یا Mach-O را تشخیص میدهد.
کد فایل code/main.py را به این ترتیب مطالعه کنید:
- تابع
ElfFile._read_header()بایتهای جادویی، کلاس، ترتیب بایت و اندازهی درایهها را بررسی کرده و سپس هدر را باز میکند. - توابع
ElfFile._read_sections()وread_cstring()جدول هدر بخشها را خوانده و نامها را بر اساسe_shstrndxپیدا میکنند. - تابع
ElfFile._read_symbols()فیلدst_infoرا تفکیک کرده، نامها را با ارجاع بهsh_linkبرمیگرداند و ایندکسهای رزروشدهی بخشها را به فرمتی خوانا تبدیل میکند. - تابع
ElfFile._read_relocations()تمام بخشهایSHT_RELAرا پیمایش کرده، فیلدr_infoرا میشکند و نام انواع را بر اساس جدول 4.9 استخراج میکند. - متدهای
ElfFile.function_span()وfunction_bytes()یک نماد را به بازهای از فایل و بایتهای واقعی آن تبدیل میکنند. - تابع
ElfFile.file_offset_map()تمام ساختارها را بر اساس آفست مرتب کرده و فضاهای خالی را مشخص میسازد. - توابع
ElfFile._read_program_headers()وaddress_to_file_offset()به پردازش فایلهای اجرایی میپردازند. - کلاس
MachOFileبخشهای هدر، فرمانهای بارگذاری، ساختارLC_SEGMENT_64به همراه بخشهای آن،LC_SYMTAB،LC_BUILD_VERSIONو بازآدرسدهیهای هر بخش را بازخوانی میکند. - تابع
compare_formats()مفاهیم مشترک را در هر دو فایل همراه با مقادیر استخراجشده در کنار هم قرار میدهد.
استفاده از ابزارهای واقعی
برای استفاده از این خوانشگر در درسهای بعدی، پس از اضافه کردن پوشهی code این درس به sys.path، آن را با دستور from main import ElfFile ایمپورت کنید. در درس 33 توابع function_bytes و relocations_for(".text") از این کلاس فراخوانی میشوند. درس 35 خروجی آن را به عنوان شواهد (Evidence) ثبت میکند و درس 36 از آن برای استخراج آفستهای بازآدرسدهی جهت ماسک کردن بایتها در مقایسه استفاده میکند.
کد C اختصاصی خود را با همان دستور clang -target x86_64-unknown-linux-gnu -c به یک فایل آبجکت کامپایل کنید و این خوانشگر را روی آن اجرا نمایید. نام بازآدرسدهیها را صرفاً زمانی اضافه کنید که در جدول 4.9 استاندارد x86-64 psABI آمده باشند. نوع یک بخش را تنها در صورتی ثبت کنید که در gABI یا اسناد ابزار کامپایلر شما آمده باشد. هر زمان که خوانشگر با مقداری ناشناخته روبرو شود، بهجای حدس زدن، مقدار عددی را در مبنای 16 چاپ میکند.
کارگاه تعاملی
بخش اول: بررسی دستی هدر فایل
درون پوشهی code، محیط پایتون را باز کنید:
cd phases/13-tools-and-protocols/32-program-anatomy-elf-from-scratch/code
python3 -q
این دستورات را اجرا کنید:
import struct
from main import ElfFile, FIXTURES
data = (FIXTURES / "ledger-x86_64-O1.o").read_bytes()
data[:4], data[4], data[5]
struct.unpack_from("<HHIQQQIHHHHHH", data, 16)
elf = ElfFile(data)
elf.header.e_shoff, elf.header.e_shnum, elf.sections[elf.header.e_shstrndx].name
چندتایی خروجی نشان میدهد که e_type برابر با 1، e_machine برابر با 62، و e_shoff برابر با 1064 یعنی 0x428 است. سطر آخر مشخص میکند که بخش 1 یعنی .strtab نام بخشها را در خود دارد.
بخش دوم: جدول مقادیر پشت ساختار switch
rodata = elf.section_bytes(elf.section(".rodata"))
[int.from_bytes(rodata[i : i + 4], "little") for i in range(0x14, 0x28, 4)]
[int.from_bytes(rodata[i : i + 8], "little") for i in range(0x28, 0x50, 8)]
[r for r in elf.relocations_for(".text") if r.symbol == ".rodata"]
هر دو جدول مقادیر 1, 2, 3, 4, 4 را دارند. این مقادیر را با فایل ledger.c مقایسه کنید: عبارت grade(65) مقدار 65 / 10 - 6 = 0 را محاسبه کرده و درایهی ایندکس 0 یعنی عدد 1 را میخواند که همان مقدار شاخهی case 6 است. دستور grade(100) درایهی ایندکس 4 یعنی عدد 4 را بازمیگرداند که خروجی مشترک شاخههای case 10 و case 9 است.
بخش سوم: مقایسهی سه بیلد مختلف از یک سورسکد
for level in ("O0", "O1", "O2"):
obj = ElfFile.from_path(FIXTURES / f"ledger-x86_64-{level}.o")
print(level, [s.name for s in obj.sections if s.name in (".data", ".rodata")])
print(level, sorted({r.type_name for r in obj.relocations}))
print(level, {s.name: s.st_size for s in obj.symbols if s.type == "FUNC"})
آبجکت حاصل از کامپایل با فلگ -O0 شامل بخش .data و سه نوع بازآدرسدهی است. خروجیهای کامپایل -O1 و -O2 فاقد بخش .data هستند و فقط یک نوع بازآدرسدهی دارند. اندازهی تابع total در فلگ -O0 برابر با 66 بایت، در -O1 برابر با 31 بایت و در -O2 برابر با 140 بایت است.
بخش چهارم: بررسی همان تابع در فرمت Mach-O
from main import MachOFile
macho = MachOFile.from_path(FIXTURES / "ledger-arm64-O1.o")
[(c.name, c.cmdsize) for c in macho.load_commands]
macho.function_span("_percent_change")
macho.function_bytes("_percent_change")[:4].hex()
طول این بازه 24 بایت و در آفست 648 فایل است. چهار بایت اول a0000034 است که نمایش اندیان کوچک از دستورالعمل 0x340000a0 به شمار میرود؛ دستوری که خروجی llvm-objdump -d آن را بهصورت cbz w0, 0x14 نمایش میدهد. این همان شرط before == 0 است؛ آزمونی که بیلد x86-64 آن را با بایتهای 85 ff 74 09 کدگذاری کرده بود.
تمرین عملی
قابلیتهای خوانشگر را برای پشتیبانی از یک برنامهی پیوندخورده توسعه دهید. روی یک سیستم لینوکس x86-64، فایلهای clang -O1 -shared -fPIC code/fixtures/ledger.c -o libledger.so و clang -O1 code/fixtures/ledger.c -o ledger_main را به همراه یک تابع main دلخواه کامپایل کنید و خوانشگر را روی هر دو اجرا نمایید.
نیازمندیها:
- خواندن بخش
.dynsymبا ساختارElf64_Sym؛ چرا که مقدارSHT_DYNSYMبرابر با 11 بوده و چیدمان رکورد بدون تغییر باقی میماند. - خواندن بخش
.dynamicدر قالب رکوردهایElf64_Dynمتشکل از فیلدهایd_tagوd_val، و نامگذاری فیلدهایDT_NEEDED،DT_STRTAB،DT_SYMTAB،DT_RELAوDT_JMPRELبر اساس صفحهی "Dynamic Section" در gABI. - فهرست کردن نمادهای تعریفنشده با مقدار
st_shndxبرابر باSHN_UNDEFبه عنوان توابع ورودی (Imports)، و رکوردهایR_X86_64_JUMP_SLOTدر بخش.rela.pltبه عنوان نقاط فراخوانی آنها. - نگاشت هر سگمنت
PT_LOADبه بخشهای درون آن، با استناد به قاعدهی gABI مبنی بر اینکه «هر سگمنت فایل آبجکت شامل یک یا چند بخش است». - اثبات این نکته از طریق
address_to_file_offsetکه بایتهای هر تابع تعریفشده در فایل اشتراکی، دقیقاً با بایتهای خروجیfunction_bytesمطابقت دارد. - ذخیرهی متن خروجی دستورات
llvm-objdump -h -t -r -pوllvm-objdump -Tبرای هر دو فایل درون مسیرcode/fixtures/و ارزیابی خروجی خوانشگر با آنها در قالب تستهای خودکار.
معیار موفقیت شما زمانی است که فهرست توابع ورودی، نقشهی سگمنتها و بازهی تمام توابع کاملاً با خروجیهای ذخیرهشدهی ابزار مطابقت داشته باشند، بدون آنکه حین اجرای تستها هیچ ابزار جانبی فراخوانی شود.
تمرین ارزیابیشده
پنج پرسش دربارهی فایلهای آزمایشی این درس آماده شده است. پاسخها را با استفاده از کدی که نوشتهاید استخراج کنید و در فایل code/lab_answers.json بنویسید. مقادیر عددی را بهصورت عدد یا رشتههای هگزادسیمال مثل "0x40"، و لیستها را بهصورت آرایههای متناظر در JSON ثبت کنید.
t1: مقدار e_shoff برای فایل ledger-x86_64-O1.o، بهصورت عدد.t2: مقدار st_size برای تابع percent_change در فایل ledger-x86_64-O1.o.t3: آفست بخش .text در فایل ledger-x86_64-O1.o، بهصورت هگزادسیمال.t4: نام انواع بازآدرسدهی در فایل ledger-x86_64-O0.o، بهصورت مرتبشده و در قالب یک لیست.t5: پنج مقدار عددی صحیح در آفست .rodata+0x14 برای فایل ledger-x86_64-O1.o، در قالب یک لیست.
پاسخهای خود را بررسی کنید:
cd phases/13-tools-and-protocols/32-program-anatomy-elf-from-scratch
python3 code/lab.py
اسکریپت ارزیابی، مقدار مورد انتظار را با اجرای code/main.py روی فایلهای موجود در مخزن محاسبه میکند. این اسکریپت برای هر بخش یکی از وضعیتهای PASS، FAIL یا TODO و در نهایت یک امتیاز کلی را چاپ میکند، و تنها در صورتی با کد 0 خارج میشود که هر پنج مورد تأیید شوند. با دستور python3 code/lab.py my-answers.json میتوانید فایل دیگری را بررسی کنید. فایل تست code/tests/test_lab.py نشان میدهد که اسکریپت ارزیابی، پاسخهای درست را تأیید و پاسخهای اشتباه را رد میکند.
مشاهدهی نحوهی اجرا
محیط تعاملی Inside a Decompiler این درس را در قالب دو ایستگاه اول خود اجرا میکند. ابزار چرخان، هدر فایلی را میخواند که تابع percent_change را در بر دارد و بخش پایینی با ورود به بخش .text، نمادها را استخراج میکند. نمایش نمادها را خاموش کنید تا متوجه شوید جدول نمادها چه جزئیاتی را با خود حمل میکرده است.
خروجی این درس
این درس فایل مهارتی outputs/skill-program-anatomy-elf-from-scratch.md را تولید میکند. این مهارت در سیستمهایی مانند Claude، Cursor، Codex، OpenClaw، Hermes یا هر ایجنت دیگری که از استاندارد SKILL.md پشتیبانی میکند قابل استفاده است. این مهارت فایل آبجکت یا فایل اجرایی ساختهشده توسط کاربر را تحویل گرفته و گزارش کاملی از ساختار و چیدمان بایتها ارائه میدهد؛ گزارشی که بازهی تکتک ساختارها، نمادها و رکوردهای بازآدرسدهی را مشخص کرده و صحت آنها را با ابزارهای استاندارد مقایسه میکند.
اعتبارسنجی
اجرای دمو و مجموعهی تستهای معین:
cd phases/13-tools-and-protocols/32-program-anatomy-elf-from-scratch
python3 code/main.py
python3 -m unittest discover -s code/tests -v
این آزمونها موارد زیر را تأیید میکنند:
- مطابقت دقیق ایندکس، نام، اندازه، آدرس و نوع تکتک بخشها با خروجی ذخیرهشدهی
llvm-objdump -hبرای هر سه آبجکتفایل x86-64 و فایل اجرایی کوچک. - مطابقت دقیق مقدار، نوع پیوند، جنس نماد، بخش، اندازه و نام نمادها با خروجی متنی
llvm-objdump -t. - تطابق کامل آفست، نام نوع، نماد و مقدار افزودهی رکوردهای بازآدرسدهی با خروجی
llvm-objdump -r، از جمله رکوردهایR_X86_64_PLT32وR_X86_64_64در فلگ بهینهسازی -O0. - انطباق هر دو هدر برنامهی
tiny-x86_64با خروجیllvm-objdump -pو خالی بودن این جدول در آبجکتفایلها. - صحت اندازهی تابع
percent_change(16 بایت) در آفست0x40مطابق با بایتهای مورد انتظار و قرارگیری تمام توابع در محدودهی.text. - پوشش کامل تمامی بایتهای فایلهای آزمایشی توسط نقشهی آفست، همراه با مشخص شدن جایگاه سه فضای خالی تراز حافظه در فایل کامپایلشده با -O1.
- نگاشت درست نقطهی ورود فایل
tiny-x86_64به آفست0xb0از طریقPT_LOADو عدم پذیرش نگاشت آدرس در آبجکتفایل بازآدرسپذیر. - انطباق هدر Mach-O، تمام فرمانهای بارگذاری، سگمنت، پنج بخش موجود،
LC_SYMTABوLC_BUILD_VERSIONبا خروجی ثبتشدهی--private-headers. - مطابقت نمادها و 15 بازآدرسدهی فایل Mach-O با خروجی
llvm-objdumpو اندازهی 24 بایتی تابع_percent_changeدر آفست 648. - مدیریت خطای
FormatErrorدر مواجهه با بایتهای جادویی اشتباه، کلاس 32 بیتی، فایل Big-endian، جدول ناقص، باینری چندمعماری و فایل سورسکد C. - تولید بایتبهبایت فایل
tiny-x86_64مطابق با نسخهی ذخیرهشده توسط اسکریپتbuild_tiny_elf.py. - میزبانی بایتهای دقیق فایل
ledger-x86_64-O1.oدر ماژول مربوطه یعنیsite/figures-re-32.js.
حالتهای شکست در محیط عملیاتی
| حالت شکست | گزارش حاصل از ارزیابی ناقص | آنچه خوانشگر باید اثبات کند |
|---|---|---|
استفاده از st_value به عنوان آفست مستقیم فایل |
«تابع percent_change در آفست 0 قرار دارد» |
آفست واقعی sh_offset + st_value - sh_addr است که اینجا 0x40 میشود |
| شمارش بایتهای پدینگ به عنوان کد اجرایی | «طول تابع grade برابر با 48 بایت است» |
فیلد st_size برابر با 46 است و بایتهای 66 90 بعد از آن متعلق به هیچ نمادی نیست |
| خواندن نام بخشها از جدول اشتباه | تولید نامهای نامفهوم یا بروز Exception | فیلد e_shstrndx جدول صحیح را تعیین میکند که اینجا بخش 1 است |
اتکا به sh_size برای بخشهای SHT_NOBITS |
تلاش برای خواندن فراتر از انتهای فایل | بخش .bss بایتی در فایل ندارد و باید در نقشه نادیده گرفته شود |
| خواندن بایتهای صفر به عنوان عملوند مستقیم دستور | «دستور lea 0(%rip) است» |
رکورد بازآدرسدهی در .rela.text در آفست 0x36 به .rodata+0x10 ارجاع میدهد |
| تلقی مقدار افزوده به عنوان آفست هدف | «شروع جدول از آفست 0x10 است» |
رابطهی S + A - P با انتهای دستورالعمل، آدرس 0x14 را نتیجه میدهد |
| پیمایش بخشها روی یک باینری Strip شده | «فایل هیچ کدی ندارد» | سگمنتهای PT_LOAD تمام آدرسهای مورد نیاز بارگذار را پوشش میدهند |
| یکی فرض کردن آدرس لینکشده با آدرس زمان اجرا | محاسبهی آفست اشتباه با تغییر آدرس پایه | آفست فایل حاصل از PT_LOAD تنها مختصات پایدار است |
جستجوی بخشهای Mach-O صرفاً بر اساس sectname |
ادغام نادرست دو بخش __const با یکدیگر |
شناسهی یکتا ترکیب segname,sectname است |
انتظار وجود فیلد اندازه در رکورد nlist_64 |
بروز Exception یا خواندن صفر بایت | شروع نماد بعدی در همان بخش مشخصکنندهی پایان تابع است |
| خواندن فایل چندمعماری مشابه یک فایل Mach-O معمولی | «بایتهای جادویی 0xcafebabe ناشناخته هستند» |
فایل باید رد شده و ارائهی بخش تکمعماری درخواست شود |
| کامپایل مجدد آبجکت arm64 روی یک سیستم Mac دیگر | «فایل آزمایشی تغییر کرده است» | فیلد LC_BUILD_VERSION نگارش SDK را نگه میدارد؛ بنابراین بایتهای ثبتشده در مخزن ملاک هستند |
قواعد عملیاتی
هدری را بخوانید که به اعتبار آن اطمینان دارید، آفستهای درجشده در آن را دنبال کنید و تکتک فیلدها را بر مبنای اسناد مرجع استانداردی که آن را تعریف کرده نامگذاری نمایید. خروجی ابزارهای استانداردی را که کد خود را با آنها مقایسه کردهاید در قالب فایلهای آزمایشی در مخزن ثبت کنید تا امکان ارزیابی مجدد بدون نیاز به آن ابزارها فراهم باشد. وقتی مقداری در مشخصات استاندارد نام مشخصی ندارد، مقدار عددی آن را گزارش دهید. این ابزار خوانشگر که عملکرد آن بایتبهبایت قابل سنجش است، پایهای خواهد بود که تمام درسهای بعدی بر روی آن بنا میشوند.
برای مطالعهی بیشتر
- System V gABI، فصل 4: Object Files و صفحات ELF Header، Sections، Symbol Table و Relocation
- System V gABI، فصل 5: Program Header و Dynamic Linking
- System V ABI, AMD64 Architecture Processor Supplement، جدول 4.9 Relocation Types
- هدرهای هستهی Apple XNU: فایلهای
loader.h،nlist.hوreloc.h - قوانین 17 U.S.C. 1201 و دستورالعمل Directive 2009/24/EC
- احکام دادگاه: پروندههای Sega v. Accolade, 977 F.2d 1510 و Sony v. Connectix, 203 F.3d 596
- مستندات ارائهدهندههای rea در نسخهی 6.5.0: فایلهای
docs/binary-diagnostics.mdوdocs/ghidra-provider.md
