فاز 13 · درس 42

تحلیل پویای برنامه با دیباگرها، ردگیری و ارزیابی ادعاهای ایستا

نوع
ساختنی
زمان
حدود 130 دقیقه
زبان‌ها
Python, C, arm64 assembly, lldb command language
پیش‌نیاز
فاز 13 · درس 33 (دیس‌اسمبلی و جریان کنترل)، فاز 13 · درس 35 (شواهد، اطمینان و ناشناخته‌ها)
نمایی از یک ذره‌بین بزرگ بر فراز گره‌های گراف جریان برنامه و خطوط حافظه دودویی به همراه علائم توقف قرمز و شواهد ارزیابی
فهرست مطالب
  1. اهداف یادگیری
  2. صورت مسئله
  3. یک دیباگر: سه سرویس سیستم‌عامل و یک جدول
  4. نقاط توقف: جایگزینی یک بایت یا یک کلمه
  5. رمزگشایی دستی شاخه‌ی پرش
  6. نقاط دیده‌بانی: سخت‌افزاری و محدود
  7. گام‌زدن، هوک‌های توقف و حالت دسته‌ای
  8. تدوین برنامه بر اساس لیستینگ
  9. اجرای بیلد O0-: یازده توقف
  10. اجرای بیلد O2-: صفر توقف
  11. فایل‌های Core dump
  12. مقایسه‌ی روبه‌روی gdb و lldb
  13. ابزارهای ردگیری: درخواست‌های برنامه از کرنل
  14. ضبط و بازپخش
  15. یک اجرا چه چیزی را ثابت می‌کند؟
  16. دیباگ‌کردن صرفاً باینری‌های خودتان در macOS
  17. پیاده‌سازی از صفر
  18. استفاده از ابزارهای واقعی
  19. آزمایشگاه تعاملی
  20. آزمایشگاه الف: خواندن دستی یک توقف
  21. آزمایشگاه ب: انشعاب شرطی و مقصد آن
  22. آزمایشگاه ج: اجرایی که هرگز متوقف نشد
  23. آزمایشگاه د: دستکاری شواهد
  24. تمرین عملی
  25. خروجی این درس
  26. اعتبارسنجی
  27. حالت‌های شکست در محیط عملیاتی
  28. دستورالعمل اجرایی
  29. برای مطالعه‌ی بیشتر

تحلیل ایستای کد و خواندن متادیتاهای باینری مانند DWARF اطلاعات ارزشمندی درباره‌ی چیدمان داده‌ها و گراف جریان کنترل به ما می‌دهد، اما این اطلاعات فقط پیش‌بینی رفتار کد روی کاغذ هستند. زمانی که کامپایلر بهینه‌سازی‌هایی مانند درون‌خطی‌سازی (Inlining) یا ادغام محاسبات را در سطوحی مانند O2- اعمال می‌کند، رفتاری که در زمان اجرا شکل می‌گیرد می‌تواند فرسنگ‌ها با حدس‌های اولیه‌ی ما متفاوت باشد.

این درس به شما می‌آموزد که چگونه با طراحی یک بستر خودکار با دیباگر lldb، اجرای برنامه را زیر نظر بگیرید، نقاط توقف و دیده‌بانی (Watchpoint) را به‌درستی بنشانید و ادعاهای تحلیل ایستا را بر پایه‌ی مشاهدات واقعی در زمان اجرا به سه وضعیت تأییدشده (Confirmed)، ردشده (Refuted) یا دیده‌نشده (Not-observed) تفکیک کنید.

تمام مراحل این آموزش، از بررسی رجیسترها و ساختار فریم گرفته تا واکاوی فایل‌های Core dump، به‌صورت محلی و روی CPU سیستم شخصی قابل اجرا و تکرار هستند و به محیط‌های ابری یا پیش‌نیازهای پیچیده‌ی سخت‌افزاری وابسته نیستند.

برخورد با نقطه‌ی توقف (Breakpoint hit)، مشاهده‌ای تجربی درباره‌ی یک‌بار اجرای یک بیلد مشخص است. در این درس بستر آزمایشی (Harness) می‌سازیم که این مشاهدات را به احکام «تأییدشده» (Confirmed)، «ردشده» (Refuted) و «دیده‌نشده» (Not-observed) روی ادعاهای درس‌های ایستا تبدیل می‌کند.

اهداف یادگیری

  • توضیح این‌که یک دیباگر در لینوکس (ptrace) و macOS (پورت‌های وظیفه‌ی Mach و پورت‌های استثنا) به چه سرویس‌هایی از سیستم‌عامل نیاز دارد، و چرا شل ایزوله‌شده در ساندباکس (Sandbox) نمی‌تواند برنامه‌ی تحت دیباگ (Debuggee) را اجرا کند.
  • تنظیم نقاط توقف نرم‌افزاری با نام تابع و آدرس لیستینگ، تنظیم یک نقطه‌ی دیده‌بانی سخت‌افزاری (Hardware watchpoint)، و خواندن محدودیت‌هایی که پردازنده بر هر دو تحمیل می‌کند.
  • هدایت lldb در حالت دسته‌ای (Batch mode) از طریق Python با هوک توقف (Stop hook) برای ثبت رجیسترها، حافظه و متغیرهای فریم در هر توقف، و تجزیه‌ی گزارش متنی به یک خط زمانی (Timeline).
  • تطبیق ادعاهای ایستای درس‌های 32، 33، 34 و 41 با آن خط زمانی: رجیسترهای آرگومان، آفستِ فیلدهای استراکت، این‌که کدام یال از یک پرش شرطی طی شده و کدام فراخوانی‌ها رخ داده‌اند.
  • اجرای همان برنامه برای بیلد -O2، خواندن نقاط توقفی که تطبیق می‌یابند (Resolve) اما هرگز برخورد ثبت نمی‌کنند، و تفکیک آن‌چه این رخداد اثبات می‌کند از آنچه اثبات نمی‌کند.
  • ذخیره و بارگذاری فایل Core dump، و برشمردن بخش‌هایی که یک Core dump بر پایه‌ی استک نادیده می‌گیرد.
  • تطبیق مجموعه‌ی دستورهای gdb و lldb، و مشخص‌کردن سیستم‌عامل، نگارش و مجوزهای strace، ltrace، bpftrace، dtrace و rr.
  • بیان شرایط امضای کد در macOS که به lldb امکان می‌دهد باینری‌های بیلدشده توسط خودتان را دیباگ کند، همراه با دو شرطی که تحت آن‌ها از این کار امتناع می‌ورزد.

صورت مسئله

درس 41 ساختار struct account را از DWARF خواند و اعلام کرد فیلد balance در آفست 4 قرار دارد. درس 33 گراف جریان کنترل classify را ساخت و گفت اگر موجودی منفی باشد، اولین شاخه به بلوک return -1 می‌رود. درس 32 گفت دو آرگومان اول در x0 و x1 تحویل داده می‌شوند. تمام این جمله‌ها از بایت‌ها استخراج شدند؛ هیچ‌یک از آن‌ها در عمل مشاهده نشدند.

سپس یک ایجنت همان برنامه را می‌خواند و گزارش می‌دهد: «main توابع bump، classify و sum_balances را صدا می‌زند». جدول نمادها هر سه را فهرست کرده است، بنابراین ادعا مطمئن به‌نظر می‌رسد. شما برنامه را با -O2 کامپایل می‌کنید، روی هر تابع یک نقطه‌ی توقف می‌گذارید و آن را اجرا می‌کنید. پروسس چهار خط خروجی‌اش را چاپ می‌کند و خارج می‌شود. تمام نقاط توقف مقدار hit count = 0 را گزارش می‌دهند.

ایجنت درباره‌ی کد منبع اشتباه نکرده بود؛ اشتباه او درباره‌ی بیلدی بود که در موردش سؤال شد. کامپایلر هر سه فراخوانی را درون‌خطی (Inline) کرده و حاصل sum_balances(rows, 3) را با مقدار ثابت 315 جایگزین کرده بود. تنها راه دانستن این موضوع برای یک باینری مشخص روی این ماشین، اجرای آن زیر دیباگر و مشاهده‌ی مستقیم است.

این درس بستر آزمایشی برای این مشاهده را پیاده‌سازی می‌کند: کد account.c را کامپایل می‌کند، لیستینگ را می‌خواند، یک نشست lldb برنامه‌ریزی می‌کند، آن را می‌راند، و در نهایت به ادعاهای ایستا بر اساس مشاهداتِ حین توقف امتیاز می‌دهد. این امتیازدهی سه نتیجه دارد: confirmed یعنی توقف نشان داد ادعا برای این اجرا درست بوده است؛ refuted یعنی توقف نشان داد ادعا نادرست بوده است؛ not-observed یعنی هیچ توقفی نتوانست آن را بسنجد، و گزارش علت آن را ثبت می‌کند. سپس درس 35 هر حکم را به‌صورت یک رکورد observed با مرجعیت controlled-replay ذخیره می‌کند.

خط زمانی اجراaccount.c تحت lldb، 8 ادعای ایستا
Timeline of one lldb run with the static claim each stop confirms or refutesaccount-O0, run finished: 8 confirmed, 0 refuted, 0 not observed.اجرای account-O0راه‌اندازیbumpclassifysum_balancesexit 0012345678910خروجادعاتوقفحکم تا اینجاثبات‌های آرگومان✓✓✓✓✓تأییدشده (5/0)آفست‌های فیلد✓✓✓✓✓✓✓✓✓✓✓تأییدشده (11/0)branch-1✓✓✓تأییدشده (3/0)branch-2✓✓تأییدشده (2/0)فراخوانی‌ها✓تأییدشده (1/0)یک‌بار ذخیره✓تأییدشده (1/0)استراید✓تأییدشده (1/0)خروجی✓تأییدشده (1/0)✓ تأیید در اینجا ✗ رد در اینجا ? دیده نشده در اجرای کامل · آزمایش‌نشده در این توقفدایره: نقطه توقف ورود لوزی: جانشین branch مربع: watchpoint دایره توخالی: خروج
دایره‌ها نقاط توقف (breakpoint) ورود به تابع هستند، لوزی‌ها نقاط توقف روی جانشین branch و مربع همان watchpoint سخت‌افزاری روی a->balance است. علامت تیک نشان‌دهنده ادعای تأییدشده در آن توقف، ضربدر نشانه رد ادعا، و علامت سؤال ادعایی است که در طول اجرای کامل هرگز مشاهده نشده است. بیلد -O2 بدون هیچ توقفی تا خروج اجرا می‌شود، بنابراین احکام آن فقط از ستون خروج حاصل می‌شوند.

یک دیباگر: سه سرویس سیستم‌عامل و یک جدول

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

در لینوکس این سرویس ptrace(2) است. صفحه‌ی manual آن می‌گوید «ابزاری فراهم می‌کند که یک پروسس (ردگیر یا Tracer) با آن بتواند اجرای پروسس دیگری را مشاهده و کنترل کند». دستور PTRACE_TRACEME به پروسس فرزند اجازه می‌دهد اعلام کند والدش آن را ردگیری خواهد کرد، PTRACE_ATTACH پروسسی غیرمرتبط را در دست می‌گیرد، PTRACE_PEEKDATA وظیفه‌ی خواندن «یک کلمه در آدرس addr از حافظه‌ی پروسس ردگیری‌شده» را انجام می‌دهد، PTRACE_GETREGS رجیسترها را می‌خواند، و PTRACE_SINGLESTEP یک دستورالعمل را اجرا می‌کند. ماژول Yama عملیات attach را محدود می‌کند: مقدار پیش‌فرض 1 در /proc/sys/kernel/yama/ptrace_scope به‌معنای «پروسس مقصد باید از فرزندان فراخواننده باشد» است، 2 فقط به مدیر سیستم اجازه‌ی اتصال می‌دهد، و 3 اتصال را برای همه مسدود می‌کند.

در macOS رابط ptrace محدودتر است. مستندات Apple توابع PT_TRACE_ME، PT_ATTACHEXC، PT_STEP، PT_CONTINUE و PT_KILL را فهرست می‌کند و بیان می‌دارد با PT_ATTACHEXC، «سیگنال‌های فرزند به‌صورت استثناهای Mach به والد ارسال می‌شوند». حافظه و رجیسترها اصلاً از مسیر ptrace مدیریت نمی‌شوند. ابزار debugserver در lldb ابتدا تابع task_for_pid() را فراخوانی می‌کند تا یک پورت وظیفه‌ی Mach (یا Mach task port) برای پروسس بگیرد، سپس با task_set_exception_ports() ترتیبی می‌دهد که نقاط توقف به‌صورت پیام‌های استثنا دریافت شوند. دیمن سیستمی taskgated تصمیم می‌گیرد چه کسی مجاز به فراخوانی task_for_pid است.

هزینه‌ی این معماری را می‌توانید در متنی که این درس ذخیره می‌کند ببینید. شل این نشست زیر یک ساندباکس Seatbelt اجرا می‌شود. همان برنامه‌ای که خارج از ساندباکس یازده توقف ایجاد می‌کند، داخل آن با این خطا روبه‌رو می‌شود:

(lldb) run
error: process exited with status -1 (attach failed ((os/kern) invalid address))

پروسس ساخته شد اما امکان در اختیار گرفتن آن وجود نداشت. خطا مربوط به مجوزهای دیباگر است و هیچ ارتباطی با خود برنامه ندارد. بستر آزمایشی ما این مورد را به‌صورت شکست در اجرا (Launch failure) با صفر توقف و بدون هیچ حکمی ثبت می‌کند.

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

نقاط توقف: جایگزینی یک بایت یا یک کلمه

نقطه‌ی توقف نرم‌افزاری دستورالعملی است که پردازنده هنگام رسیدن به آن دچار تله (Trap) می‌شود؛ این دستور روی اولین بایت‌های دستورالعملی که می‌خواهید در آن متوقف شوید بازنویسی می‌شود. در معماری x86-64 این دستور INT3 با آپ‌کدِ تک‌بایتی CC است که راهنمای Intel آن را با عنوان «مختص فراخوانی روتین مدیریت استثنای دیباگ» توصیف می‌کند. در arm64 دستور چهاربایتی BRK #imm16 است که استثنای آن «استثنا را با مقدار 0x3C برای فیلد EC در ESR_ELx ثبت می‌کند». راهنمای Arm می‌افزاید آدرس بازگشت ترجیحی خودِ دستور BRK است؛ به همین دلیل هنگام توقف، آدرس دستورالعمل جایگزین‌شده گزارش می‌شود نه آدرس دستور بعدی.

دیباگر lldb نقاط توقف را بر اساس نام، بر اساس فایل و خط، یا بر اساس آدرس تنظیم می‌کند. در حالتِ مبتنی بر نام، از پرولوگ (Prologue) تابع صرف‌نظر می‌کند:

(lldb) breakpoint set -n bump
Breakpoint 1: where = account-O0`bump + 12 at account.c:13:19, address = 0x00000001000004bc

آدرس bump + 12 چهارمین دستورالعمل است. سه دستور پیش از آن sub sp, sp, #0x10، str x0, [sp, #0x8] و str w1, [sp, #0x4] هستند؛ یعنی آماده‌سازی فریم استک که جدول خطوط DWARF آن را به خط اول تابع نسبت می‌دهد. در لحظه‌ی توقف، x0 و x1 همچنان حاوی آرگومان‌ها هستند، زیرا پرولوگ صرفاً آن‌ها را کپی کرده است.

در حالتِ مبتنی بر آدرس، lldb باید بداند این آدرس به کدام ماژول تعلق دارد. نوشتن خام دستور breakpoint set -a 0x100000504 پیش از ساخت پروسس، آدرسی از زمان بارگذاری (Load address) بدون بخش معادل است و در نخستین تست در کل فرایند اجرا با وضعیت unresolved و hit count = 0 باقی ماند. ساختاری که بستر آزمایشی استفاده می‌کند ماژول را مشخص می‌سازد:

(lldb) breakpoint set -a 0x100000504 -s account-O0
Breakpoint 4: address = 0x0000000100000504

با فلگ -s، این عدد یک آدرس فایلی داخل account-O0 به حساب می‌آید، lldb جابه‌جایی بارگذاری (Load slide) را اضافه می‌کند و پس از اجرا وضعیت به resolved, hit count = 2 تغییر می‌یابد. لیستینگ درس 33 آدرس‌های فایل را ارائه می‌دهد، بنابراین همین شیوه لیستینگ ایستا را به توقف پویا متصل می‌کند.

رمزگشایی دستی شاخه‌ی پرش

نخستین پرش شرطی در classify در حالت -O0 کلمه‌ی 36f800a8 در آدرس 0x1000004f0 است که llvm-objdump آن را به‌صورت tbz w8, #0x1f, 0x100000504 نمایش می‌دهد. این را بر اساس کدگذاری Arm A64 برای TBZ رمزگشایی (Decode) می‌کنیم:

  1. بیت 31 مقدار b5 یعنی بیت بالایی شماره‌ی بیت است و مقدار آن 0 است.
  2. بیت‌های 30 تا 25 مقدار 011011 یعنی گروه test-and-branch هستند. بیت 24 برابر 0 است که TBZ را انتخاب می‌کند نه TBNZ.
  3. بیت‌های 23 تا 19 مقدار b40 هستند، یعنی پنج بیت پایینی شماره‌ی بیت: 11111 معادل 31. با توجه به b5 = 0، بیت مورد بررسی 31 است که بیت علامت یک مقدار 32 بیتی محسوب می‌شود.
  4. بیت‌های 18 تا 5 معادل imm14 هستند: 00000000000101 معادل 5. آفست پرش برابر است با imm14 * 4 یعنی 20 (0x14)، بنابراین مقصد 0x1000004f0 + 0x14 = 0x100000504 است.
  5. بیت‌های 4 تا 0 برابر Rt هستند: 01000 معادل 8، بنابراین رجیستر w8 است.

رجیستر w8 یک دستور پیش‌تر توسط ldr w8, [x8, #0x4] بارگذاری شده بود که متناظر با a->balance در آفست 4 است. TBZ زمانی می‌پرد که بیت صفر باشد. پرش زمانی رخ می‌دهد که بیت علامت پاک (صفر) باشد، یعنی balance >= 0. کد منبع به‌صورت if (a->balance < 0) return -1; نوشته شده بود، بنابراین کامپایلر شرط را معکوس کرده است: یالِ پرش از بلوک return -1 رد می‌شود، و مسیر مستقیم (Fall-through) در 0x1000004f4 وارد آن می‌شود. ادعای ایستایی که بستر آزمایشی با خود دارد، یک ادعای یال همراه با گزاره است: اگر a->balance >= 0 باشد پرش انجام می‌شود و در غیر این صورت مسیر مستقیم طی می‌شود. تحلیل سطح‌بالاترِ شاخه‌ی دوم نیز به همین ترتیب است: subs w8, w8, #0xc8 عبارت balance - 200 را محاسبه می‌کند و b.le 0x100000524 زمانی پرش می‌کند که حاصل حداکثر صفر باشد، یعنی balance <= 200.

نقاط دیده‌بانی: سخت‌افزاری و محدود

یک نقطه‌ی دیده‌بانی (Watchpoint) پروسس را هنگام دسترسی به یک آدرس حافظه متوقف می‌کند. این قابلیت نمی‌تواند با دستورالعمل جایگزین‌شده کار کند، چون هر دستوری در هر نقطه‌ای از برنامه ممکن است به آن حافظه دسترسی پیدا کند. به همین دلیل پردازنده‌ها تعداد کمی مقایسه‌کننده‌ی آدرس دارند. در x86-64 این مقایسه‌کننده‌ها رجیسترهای دیباگ هستند: DR0 تا DR3 حاوی «آدرس‌های حداکثر 4 نقطه‌ی توقف» هستند و DR7 هرکدام را فعال کرده و شرایط آن (اجرا، نوشتن، یا خواندن/نوشتن) و طول آن (1، 2، 4 یا 8 بایت) را مشخص می‌کند. در arm64 زوج‌های DBGWVR<n>_EL1 قرار دارند که «آدرس مجازی داده را برای تطبیق نقطه‌ی دیده‌بانی نگه می‌دارند»، و DBGWCR<n>_EL1 برای کنترل‌ها تعبیه شده‌اند؛ راهنمای Arm می‌گوید «هر پیاده‌سازی می‌تواند بین 2 تا 64 نقطه‌ی دیده‌بانی داشته باشد».

دیباگر lldb تعداد اختصاص‌یافته را گزارش می‌کند:

(lldb) watchpoint set variable a->balance
Watchpoint created: Watchpoint 1: addr = 0x16fdfe00c size = 4 state = enabled type = m
(lldb) watchpoint list
Number of supported hardware watchpoints: 4

چهار عدد روی این ماشین. آدرس تحت‌نظر 0x16fdfe00c برابر است با x0 + 4: یعنی اشاره‌گر استراکت در توقف bump به‌علاوه‌ی آفست DWARF برای balance. مقدار size = 4 نشان‌دهنده‌ی متغیر int است. مقدار type = m یعنی تغییر (Modify). وقتی اسلات‌ها تمام شوند، lldb از تنظیم نقطه‌ی دیده‌بانی بعدی خودداری می‌کند، و از آنجا که یک ناحیه‌ی ترازنشده یا ناحیه‌ای با اندازه‌ی غیر توانی از 2 ممکن است دو اسلات را اشغال کند، تعداد نقاط دیده‌بانی قابل‌تنظیم می‌تواند کمتر از چهار باشد.

نقاط دیده‌بانی داده بعد از انجام دسترسی گزارش می‌شوند. راهنمای Intel می‌گوید «دستوری که در حافظه می‌نویسد، داده‌ی قبلی را پیش از ایجاد استثنای دیباگ ناشی از نقطه‌ی توقف داده، بازنویسی می‌کند»، و lldb مقدار قبلی را در کش خود نگه می‌دارد تا هر دو را چاپ کند:

Watchpoint 1 hit:
old value: 250
new value: 255
* thread #1, queue = 'com.apple.main-thread', stop reason = watchpoint 1
    frame #0: 0x00000001000004d0 account-O0`bump(a=0x000000016fdfe008, by=5) at account.c:14:12

توقف در 0x1000004d0 رخ می‌دهد، یعنی دستورالعملِ بعد از str w8, [x9, #0x4] در 0x1000004cc. یک نکته‌ی ظریف را باید قبل از تجزیه‌ی خروجی بدانید: درست پس از watchpoint set، نرم‌افزار lldb پیام Watchpoint 1 hit: را با مقدار قبلیِ خالی و new value: 250 چاپ می‌کند. این همان خواندن اولیه است و بستر آزمایشی ما برخوردی را که مقدار قبلی نداشته باشد نادیده می‌گیرد.

گام‌زدن، هوک‌های توقف و حالت دسته‌ای

قواعد دستوری lldb به‌صورت <noun> <verb> [-options] [arguments] است. افعال گام‌زدن (Stepping) عبارت‌اند از thread step-in (s)، thread step-over (n)، thread step-inst (si)، thread step-inst-over (ni) و thread step-out (finish). دستور thread until 12 تا یک خط مشخص پیش می‌رود. دستور thread return <expr> زودتر از موعد همراه با یک مقدار برمی‌گردد. بستر آزمایشی ما گام‌به‌گام پیش نمی‌رود، بلکه در آدرس‌های مشخص‌شده متوقف می‌شود و خواندن اطلاعات را به یک هوک توقف (Stop hook) می‌سپارد:

target stop-hook add -o "register read x0 x1 pc" -o "memory read -s4 -fx -c24 $x0" -o "frame variable -P 1"

گزینه‌ی -o می‌تواند تکرار شود، و این دستورات به ترتیب در هر توقف اجرا می‌شوند. دستور register read دو رجیستر آرگومان و شمارنده‌ی برنامه (PC) را برمی‌گرداند. دستور memory read -s4 -fx -c24 $x0 تعداد 24 کلمه‌ی چهاربایتی را در قالب هگزادسیمال از آدرس موجود در x0 چاپ می‌کند که معادل 96 بایت و سه سطر از struct account است. دستور frame variable -P 1 آرگومان‌ها و متغیرهای محلی را چاپ می‌کند و اشاره‌گرها را تا یک سطح بسط می‌دهد، تا a پنج فیلدش را نشان دهد.

حالت دسته‌ای با lldb -b اجرا می‌شود. راهنمای برنامه می‌گوید --batch دستورات -s، -S، -o و -O را اجرا کرده و خارج می‌شود، و اگر دستوری «به‌دلیل سیگنال یا کرش متوقف شد، دیباگر به خط فرمان تعاملی بازمی‌گردد». بستر آزمایشی این دستورات را در فایلی نوشته و با -s تحویل می‌دهد. یک تنظیم در این میان حیاتی است: settings set interpreter.stop-command-source-on-error false. بدون این گزینه، اولین continue بعد از خروج پروسس کل فایل را می‌بندد و breakpoint list هرگز اجرا نمی‌شود. با فعال بودن آن، lldb پیام error: Process must be launched. را چاپ می‌کند و ادامه می‌دهد.

دو ویژگی در ساختار خروجی نحوه‌ی کار پارسر را مشخص می‌کند: نخست، خروجی هوک هنگام اجرای خودِ هوک چاپ می‌شود و بلوک Process N stopped همراه با خط frame #0 بعد از آن می‌آید. بنابراین پارسر خطوط رجیستر، حافظه و متغیر را در یک قطعه‌ی معلق نگه می‌دارد و آن قطعه را به اولین خط frame #0 بعدی پیوند می‌زند. قطعه‌ای که شمارنده‌ی برنامه‌ی آن در dyld در _dyld_start باشد، توقف ورودی قبل از اجرای برنامه است و هیچ frame #0ای به آن تعلق نمی‌گیرد. دوم، یک نقطه‌ی توقف با گزینه --auto-continue اصلاً توقفش را گزارش نمی‌دهد و هوک توقف برای آن اجرا نمی‌شود. نگارش اولیه‌ی بستر ما از auto-continue استفاده می‌کرد و هیچ رکوردی ثبت نکرد. بنابراین، ساختار فعلی از نقاط توقف ساده و تعداد مشخصی دستور continue استفاده می‌کند.

تدوین برنامه بر اساس لیستینگ

متد Plan.commands() فایل دستورات را از خروجی متنی llvm-objdump -d مربوط به باینری هدف تولید می‌کند. برای account-O0:

breakpoint set -n bump
breakpoint set -n classify
breakpoint set -n sum_balances
breakpoint set -a 0x100000504 -s account-O0
breakpoint set -a 0x1000004f4 -s account-O0
breakpoint set -a 0x100000524 -s account-O0
breakpoint set -a 0x100000514 -s account-O0
target stop-hook add -o "register read x0 x1 pc" -o "memory read -s4 -fx -c24 $x0" -o "frame variable -P 1"
run
watchpoint set variable a->balance
watchpoint list
continue
...
breakpoint list
watchpoint list -v

سه نقطه‌ی توقفِ مبتنی بر نام همان نقاط ورود توابع هستند. چهار نقطه‌ی توقفِ مبتنی بر آدرس، مقاصد و مسیرهای مستقیم هر پرش شرطی هستند که Listing.conditional_branches() در تابع classify پیدا می‌کند: یعنی tbz در 0x1000004f0 و b.le در 0x100000510. نقطه‌ی دیده‌بانی در اولین توقف که برنامه انتظار دارد bump باشد تنظیم می‌شود. به دنبال آن شانزده دستور continue می‌آیند؛ رقمی بیش از نیازِ هر اجرا که موارد اضافه‌ی آن پس از خروج پروسس بدون خطر با شکست مواجه می‌شوند. این سقف تعداد دستور، برآمده از شناخت قبلی کد نیست، بلکه فقط یک سقف بالاست.

ادعاها توسط claims_for() از همان ورودی‌ها ساخته می‌شوند:

شناسه نوع ادعا منبع
arg-registers argument-register در هر ورود، اولین آرگومان‌ها در x0 و x1 تحویل می‌شوند درس 32 و AAPCS64
field-offsets field-offset ساختار struct account معادل 32 بایت است: kind در 0، balance در 4، code در 8، rate در 16 و label در 24 درس 41 و دامپ DWARF
branch-1 branch-taken دستور tbz در 0x1000004f0 اگر a->balance >= 0 باشد به 0x100000504 می‌رود، در غیر این صورت به 0x1000004f4 یال‌های درس 33 و تحلیل سطح‌بالای درس 34
branch-2 branch-taken دستور b.le در 0x100000510 اگر a->balance <= 200 باشد به 0x100000524 می‌رود، در غیر این صورت به 0x100000514 یال‌های درس 33 و تحلیل سطح‌بالای درس 34
calls call-edge تابع main یک بار bump، سه بار classify و یک بار sum_balances را صدا می‌زند متن سورس و nm
store-once store تابع bump یک بار در a->balance می‌نویسد و مقدار جدید برابر مقدار قبلی به‌علاوه‌ی by است شبه‌کد درس 34
stride struct-stride مقدار list[i].balance در آدرس list + 32*i + 4 قرار دارد درس 41 و قاعده‌ی داده‌های مرکب در psABI
output behaviour برنامه عبارت‌های class 0 1 -1 و sum 315 را چاپ می‌کند محاسبات کد منبع

آفست‌های فیلدها از تابع struct_layout() به‌دست می‌آیند؛ تابعی که DW_TAG_structure_type با مقدار DW_AT_byte_size (0x20) و پنج مدخل DW_TAG_member را به همراه DW_AT_data_member_location از دامپ dSYM می‌خواند. این برنامه هرگز عدد 4 را دستی وارد نمی‌کند، بلکه آن را می‌خواند.

اجرای بیلد O0-: یازده توقف

اولین توقف، نقطه‌ی توقف bump است. هوک این اطلاعات را قبل از بلوک توقف چاپ کرد:

      x0 = 0x000000016fdfe008
      x1 = 0x0000000000000005
      pc = 0x00000001000004bc  account-O0`bump + 12 at account.c:13:19
0x16fdfe008: 0x00000073 0x000000fa 0x00000009 0x00000000
0x16fdfe018: 0x00000000 0x3ff40000 0x00000719 0x00000001
0x16fdfe028: 0x0000006c 0xffffffd8 0x00000003 0x00000000
0x16fdfe038: 0x00000000 0x40000000 0x00000721 0x00000001
(account *) a = 0x000000016fdfe008 {
  kind = 's'
  balance = 250
  code = 9
  rate = 1.25
  label = 0x0000000100000719 "savings"
}
(int) by = 5

هشت کلمه‌ی سطر اول را با آفست‌های DWARF مقایسه کنید: کلمه‌ی 0 در آفست 0 مقدار 0x73 است که کاراکتر 's' (بایت kind) را نشان می‌دهد و سه بایت پدینگ (حاشیه‌گذاری) به دنبال آن آمده است. کلمه‌ی 1 در آفست 4 مقدار 0xfa یعنی 250 (balance) است. کلمه‌ی 2 در آفست 8 مقدار 0x9 (code) است که در نیمه‌ی پایینی یک short بوده و دو بایت پدینگ بالای آن قرار دارد. کلمه‌ی 3 در آفست 12 پدینگ پیش از داده‌ی double با تراز 8 بایتی است. کلمات 4 و 5 در آفست 16 مقدار 0x3ff4000000000000 هستند که کدگذاری استاندارد IEEE-754 برای عدد 1.25 است. کلمات 6 و 7 در آفست 24 مقدار 0x100000719 هستند، یعنی اشاره‌گر label به بخش __TEXT. هشت کلمه‌ی بعدی با 0x6c یعنی 'l' و 0xffffffd8 یعنی -40 آغاز می‌شوند: سطر سوم یعنی loan، در فاصله‌ی 32 بایت بعد. همه‌ی اعداد با خروجی frame variable مطابقت دارند؛ بنابراین _check_layout() وضعیت ادعای field-offsets را در این توقف به‌صورت confirmed ثبت می‌کند، و این کار را در هر یازده توقفی که x0 حاوی اشاره‌گر استراکت است تکرار می‌نماید.

مقدار x0 برابر a و x1 برابر by است، پس arg-registers در این ورودی تأیید می‌شود. بستر آزمایشی فقط آرگومان‌های تعریف‌شده را مقایسه می‌کند. در توقف‌های classify مقدار x1 همچنان 0x5 خوانده می‌شود که بازمانده‌ای از bump است؛ چون classify فقط یک آرگومان دارد، چیزی با آن مقایسه نمی‌شود. رجیستری که آرگومانی در آن نگهداری نمی‌شود صرفاً نویز است و ادعای درس 32 نیز حرفی درباره‌ی آن نزده است.

توقف دوم همان نقطه‌ی دیده‌بانی است؛ مقدار قدیمی 250، مقدار جدید 255 و by = 5. در کلِ اجرا یک‌بار برخورد صورت گرفته، پس store-once تأیید می‌شود. پس از آن سه بار اجرای classify رخ می‌دهد که هر کدام یک توقف ورود و یک یا دو توقف در یال‌ها دارند:

توقف آدرس علت توقف چه چیزی را می‌سنجد
2 0x1000004e8 breakpoint 2.1 ورود، a->balance = 100، x0 = a
3 0x100000504 breakpoint 4.1 پرش tbz انجام شد، 100 >= 0، ادعای branch-1 تأیید شد
4 0x100000524 breakpoint 6.1 پرش b.le انجام شد، 100 <= 200، ادعای branch-2 تأیید شد
5 0x1000004e8 breakpoint 2.1 ورود، a->balance = 255
6 0x100000504 breakpoint 4.1 پرش tbz انجام شد، 255 >= 0
7 0x100000514 breakpoint 7.1 مسیر مستقیم b.le طی شد، 255 > 200
8 0x1000004e8 breakpoint 2.1 ورود، a->balance = -40
9 0x1000004f4 breakpoint 5.1 مسیر مستقیم tbz طی شد، -40 < 0
10 0x100000544 breakpoint 3.1 ورود به sum_balances، x1 = 3

تابع _check_branches() توقف‌ها را بر اساس فراخوانی‌ها گروه‌بندی می‌کند، موجودی را در نقطه‌ی ورود می‌خواند، و برای هر توقفِ روی یال، مقصد بعدی پیش‌بینی‌شده با گزاره‌ی تحلیل‌شده را محاسبه می‌کند. پنج توقف روی یال‌ها و پنج تطابق کامل: branch-1 سه بار و branch-2 دو بار تأیید می‌شوند. توقف 5 موجودی را 255 نشان می‌دهد؛ مقداری که نقطه‌ی دیده‌بانی، نوشتن آن را ثبت کرده بود. همه‌ی شواهد (Evidence) در یک برنامه‌ی واحد به هم پیوند می‌خورند.

در توقف 10 هوک 24 کلمه را از list خواند. تابع _check_stride() کلمات 1، 9 و 17 یعنی آفست‌های 4، 36 و 68 را برمی‌دارد: مقادیر 100، 255 و -40. سپس هر یک را با موجودی دیده‌شده در classify در همان آدرس مقایسه می‌کند، مجموع آن‌ها را 315 به‌دست می‌آورد، و این جمع را با خط خروجی sum 315 برنامه تطبیق می‌دهد. در ادامه، پروسس با وضعیت 0 خارج می‌شود و breakpoint list دفعات برخورد 1، 3 و 1 را برای نام‌ها و 2، 1، 1 و 1 را برای چهار یال نشان می‌دهد. هر هشت ادعا با وضعیت confirmed و با پشتوانه‌ی 25 رکورد خاتمه می‌یابند؛ 23 رکورد مربوط به توقف‌ها و دو رکورد مربوط به پایان اجرا.

اجرای بیلد O2-: صفر توقف

همان کد منبع در حالت -O2 به برنامه‌ای کاملاً متفاوت تبدیل می‌شود. خروجی llvm-objdump -d برای main شامل چهار دستور bl است که همگی printf را صدا می‌زنند. به‌جای فراخوانی‌ها، دستورات mov w8, #-0x1، mov w8, #0x1، یک صفر از طریق xzr، و mov w8, #0x13b (معادل 315) قرار گرفته‌اند. کامپایلر تمام نتایج را در زمان کامپایل محاسبه کرده است. دستور nm همچنان _bump، _classify و _sum_balances را فهرست می‌کند، زیرا توابعی عمومی (External) هستند و نسخه‌های خارج از خط آن‌ها باید برای فراخوانی توسط آبجکت‌های دیگر باقی بمانند. فایل dSYM برای هر چهار تابع مدخل DW_TAG_subprogram دارد و هیچ مدخل DW_TAG_inlined_subroutineای ندارد، بنابراین حتی اطلاعات دیباگ نیز جزئیات درون‌خطی شدن را ثبت نکرده‌اند.

برنامه‌ی تست برای این بیلد شامل سه نقطه‌ی توقف نام‌گذاری‌شده است و هیچ نقطه‌ی توقفی روی یال‌ها ندارد. تابع classify در بیلد -O2 شش دستورالعمل بیشتر نیست: ldr، cmp، cset، cmp، csinv، ret و هیچ پرش شرطی در آن دیده نمی‌شود. ادعاهای مربوط به یال‌ها در لیستینگ -O0 در این باینری هیچ آدرسی ندارند و بستر آزمایشی آن‌ها را منتقل نمی‌کند. هر ادعا درباره‌ی یک یال، به بیلدی تعلق دارد که آن یال در لیستینگش موجود باشد.

روند اجرا:

(lldb) breakpoint set -n bump
Breakpoint 1: where = account-O2`bump at account.c:13:16, address = 0x0000000100000460
(lldb) run
sizeof 32 align 8
offsets 0 4 8 16 24
class 0 1 -1
sum 315
Process 48444 exited with status = 0 (0x00000000)
(lldb) watchpoint set variable a->balance
error: Command requires a process which is currently stopped.
(lldb) breakpoint list
1: name = 'bump', locations = 1, resolved = 1, hit count = 0

هر نقطه‌ی توقف آدرس‌دهی شد (Resolved). آدرس وجود دارد، نماد وجود دارد، و lldb دستور BRK را در آنجا نوشته است. با این حال تعداد برخوردها صفر است، زیرا واکشی دستورالعمل (Instruction fetch) هیچ‌گاه به آن آدرس نرسید. احکام نهایی:

ادعا حکم رکورد
calls refuted پس از یک اجرای کامل با کد خروج 0، توقف‌های ورودی: bump 0، classify 0، sum_balances 0
output confirmed چهار خط خروجی و کد خروج 0
arg-registers not-observed هیچ توقفی آن را نسنجید، تمام دفعات برخورد 0 بودند
field-offsets not-observed هیچ توقفی آن را نسنجید
store-once not-observed توقفی در bump رخ نداد، بنابراین نقطه‌ی دیده‌بانی هرگز تنظیم نشد
stride not-observed توقفی در sum_balances رخ نداد

دقت کنید که رد این ادعا دقیقاً چه معنایی دارد: ادعا این بود که main این سه تابع را در این برنامه صدا می‌زند. برای این بیلد و این اجرای به‌خصوص، صدا نمی‌زند؛ و بستر آزمایشی بررسی کرد که برنامه تا انتها اجرا شده باشد، چرا که صفر بودن برخوردها قبل از خروج برنامه فقط نشان می‌داد که اجرا هنوز به فراخوانی نرسیده است. این ادعا نمی‌گفت که توابع کدهای مرده (Dead code) هستند و اجرا هم چنین چیزی را نشان نمی‌دهد؛ این توابع اکسپورت شده‌اند و ماژول دیگری می‌تواند آن‌ها را فراخوانی کند. چهار ادعای دیگر نه تأیید شده‌اند و نه رد. استراکت در این بیلد نیز فیلد balance را در آفست 4 دارد و DWARF در -O2 بر آن صحه می‌گذارد، اما هیچ توقفی در این اجرا حافظه را نخواند تا آن را اثبات کند. برچسب not-observed این مرز را حفظ می‌کند، و درس 35 این مجهول را باز نگه می‌دارد تا زمانی که در اجرایی واقعاً مشاهده شود.

فایل‌های Core dump

فایل Core فایلی است که حافظه و رجیسترهای پروسس را روی دیسک ذخیره می‌کند تا دیباگر بتواند وضعیت پروسس متوقف‌شده را پس از بسته‌شدن آن بررسی کند. در مستندات core(5) متعلق به Apple آمده است: «تعداد کمی از سیگنال‌ها که موجب پایان غیرعادی پروسس می‌شوند، باعث می‌شوند رکوردی از وضعیت پروسس در دیسک نوشته شود»، در مسیر /cores/core.pid، «مشروط بر آن‌که پروسس پایان‌یافته مجوز نوشتن در آن پوشه را داشته باشد و آن پوشه از قبل موجود باشد». در این سیستم، sysctl مقادیر kern.coredump: 1 و kern.corefile: /cores/core.%P را گزارش می‌کند، پوشه‌ی /cores با سطح دسترسی drwxrwxr-t root admin وجود دارد، و خروجی ulimit -c در شل برابر 0 است. تا زمانی که سقف مجاز را با ulimit -c unlimited، یا در سطح سیستم با launchctl limit core unlimited بالا نبرید، هیچ Core dumpای توسط کرنل نوشته نمی‌شود.

دیباگر lldb می‌تواند بدون بروز کرش، به درخواست کاربر یک فایل Core بنویسد. در نقطه‌ی توقف bump:

(lldb) process save-core --style stack account-O0-stack.core
Saving 49152 bytes of data for memory region at 0x16fdf4000

Modified-memory or stack-memory only corefile created.  This corefile may 
not show library/framework/app binaries on a different system, or when 
those binaries have been updated/modified. Copies are not included
in this corefile.  Use --style full to include all process memory.

حجم فایل 53,248 بایت بود. بارگذاری آن نیازمند حضور فایل باینری در کنار آن است:

(lldb) target create "./account-O0" --core "account-O0-stack.core"
(lldb) bt
* thread #1, stop reason = ESR_EC_BRK_AARCH64 (fault address: 0x0)
  * frame #0: 0x00000001000004bc account-O0`bump(a=0x000000016fdfe008, by=5) at account.c:13:19
    frame #1: 0x0000000100000640 account-O0`main at account.c:40:5
    frame #2: 0x000000019a949d54 dyld`start + 7184
(lldb) frame variable *a
(account) *a = (kind = 's', balance = 250, code = 9, rate = 1.25, label = "")
(lldb) frame variable a->label
(const char *) a->label = 0x0000000100000719 ""

رد پشته (Backtrace) و فیلدهای عددی دست‌نخورده باقی مانده‌اند، چرا که روی استک ذخیره‌شده توسط Core قرار دارند. فیلد label با وجود اشاره‌گر معتبرش، به‌صورت رشته‌ای خالی چاپ می‌شود. اشاره‌گر 0x100000719 صحیح است؛ بایت‌هایی که به آن‌ها اشاره می‌کند همان رشته‌ی لفظی "savings" در سگمنت __TEXT فایل اجرایی هستند، و یک Core dump بر پایه‌ی استک این سگمنت را ذخیره نمی‌کند. اشاره‌گر مشاهده شده، اما رشته در دسترس نیست و lldb هم آن را بازسازی نمی‌کند. گزینه‌ی --style full تمام حافظه‌ی پروسس را ذخیره می‌کند که برای این برنامه‌ی 34 کیلوبایتی روی سیستم آزمایشی حجمی معادل 2.1 گیگابایت داشت، چون پروسس کش مشترک (Shared cache) را نگاشت می‌کند. علت توقف برابر با ESR_EC_BRK_AARCH64 درج شده است: فایل Core استثنای BRK حاصل از نقطه‌ی توقف را ثبت کرده است.

در لینوکس، کرنل فایل Core را طبق الگوی /proc/sys/kernel/core_pattern می‌نویسد؛ قالبی شامل %p برای شناسه‌ی پروسس (PID)، %e برای نام دستور، %s برای سیگنال و %t برای زمان؛ و علامت | در ابتدای الگو، داده‌ها را به یک برنامه‌ی مجزا هدایت می‌کند. توزیع‌های مبتنی بر systemd از این قابلیت برای سرویس systemd-coredump استفاده می‌کنند، و دستورات coredumpctl list، info، dump و debug داده‌های ذخیره‌شده را بازیابی می‌نمایند. هر دو ابزار gdb prog core و lldb -c core prog خروجی را باز می‌کنند.

مقایسه‌ی روبه‌روی gdb و lldb

این دو دیباگر یک کار مشخص را با واژگانی متفاوت انجام می‌دهند. نگاشت COMMAND_MAP در بستر آزمایشی ما 35 سطر دارد که با اجرای python3 code/main.py commands چاپ می‌شود. دستوراتی که در این درس استفاده شده‌اند:

کار gdb lldb نام مستعار در lldb
توقف در یک تابع break bump breakpoint set -n bump b bump
توقف در آدرسی در یک ماژول مشخص break *0x1000004f0 breakpoint set -a 0x1000004f0 -s account-O0
اجرای دستور در هر توقف define hook-stop ... end target stop-hook add -o '...'
دیدن نقاط توقف و تعداد برخوردها info breakpoints breakpoint list br l
دیده‌بانی تغییرات یک متغیر watch a->balance watchpoint set variable a->balance wa s v a->balance
فهرست نقاط دیده‌بانی info watchpoints watchpoint list -v wa l -v
خواندن یک رجیستر p $x0 register read x0
خواندن حافظه به‌صورت کلمه‌به‌کلمه x/8xw 0x16fdfe008 memory read -s4 -fx -c8 0x16fdfe008 x -s4 -fx -c8 0x16fdfe008
نمایش آرگومان‌ها info args frame variable --no-locals
اجرای یک دستورالعمل (تک‌گام) stepi thread step-inst si
اجرا تا زمان بازگشت تابع finish thread step-out finish
ذخیره‌ی فایل Core gcore core process save-core --style stack core
باز کردن فایل Core gdb account-O0 core lldb -c core account-O0

دستور p در lldb نام مستعاری برای dwim-print است که متغیر ساده را از مسیر frame variable چاپ می‌کند و برای موارد دیگر به expression متوسل می‌شود. دستور v همان frame variable است. در اسکریپت‌های تست همیشه از frame variable استفاده کنید: این دستور موقعیت‌ها را از DWARF می‌خواند و هرگز کدی را داخل برنامه‌ی تحت دیباگ اجرا نمی‌کند.

نگارش‌ها و مجوزها در تاریخ 2026-10-14: نرم‌افزار lldb از نسخه‌گذاری LLVM تبعیت می‌کند و انتشار فعلی در llvm.org نسخه‌ی LLVM 23.1.3 بتاریخ 2026-10-06 تحت مجوز Apache License 2.0 همراه با استثنائات LLVM است. پکیج منیجر Homebrew آن را در فرمول مجزای lldb عرضه می‌کند. ابزارهای Command Line Tools روی این سیستم نسخه‌ی lldb-1703.0.236.21 (شماره‌گذاری اختصاصی Apple) با پایتون داخلی 3.9.6 را ارائه می‌دهند که از قبل مجوزهای لازم برای دیباگ را دارد. نگارش GDB 18.1 در 2026-09-25 تحت مجوز GPL-3.0-or-later منتشر شد. یادداشت‌های انتشار آن تغییرات ویندوز، .gdb_index، ثبات‌های FPMR در AArch64 و gdb.Corefile در پایتون را ذکر کرده و نامی از Darwin نبرده است. تا نسخه‌ی 18.1 نرم‌افزار GDB هیچ پشتیبانی پیش‌فرضی از پروسس‌های Apple silicon ندارد: یک سری تکه (Patch) با عنوان «پشتیبانی اولیه از AArch64-Darwin» شامل 15 تکه در اکتبر 2025 ثبت شد و در فوریه 2026 یکی از توسعه‌دهندگان مسیر الحاق آن را از طریق واگذاری حق نشر و تکه‌های BFD مشخص کرد. فرمول gdb در Homebrew دارای پکیج‌های پیش‌ساخته‌ی arm64 است اما با تارگت ساختگی --target=x86_64-apple-darwin20 کامپایل می‌شود و پول‌ریکوئستِ سازنده‌ی آن قید کرده است: «دیباگ‌کردن باینری‌های aarch64-darwin از طریق GDB امکان‌پذیر نیست». هر دیباگر دیگری غیر از دیباگر Apple در macOS به یک گواهی خودامضا (Self-signed) و مجوزِ com.apple.security.cs.debugger نیاز دارد، در غیر این صورت فراخوانی task_for_pid با خطای «(os/kern) failure (0x5)» متوقف می‌شود. جایگاه gdb در این دوره لینوکس است؛ جایی که دبیانِ پایدار gdb 16.3 را برای arm64 ارائه می‌دهد و ابزار rr نیز در آن در دسترس است.

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

دیباگر وضعیت برنامه را در لحظه‌ی توقف نشان می‌دهد. یک ابزار ردگیری (Tracer) اثرات برنامه بر سیستم را بدون متوقف‌کردن طولانی‌مدت آن ثبت می‌کند. این ابزارها وابسته به سیستم‌عامل هستند.

ابزار سیستم‌عامل نگارش مجوز چه چیزی را ثبت می‌کند
strace لینوکس 7.2 بتاریخ 2026-08-18 LGPL-2.1-or-later فراخوانی‌های سیستمی همراه با آرگومان‌ها و مقادیر بازگشتی
ltrace لینوکس 0.7.3 آخرین بسته‌ی رسمی، اسنپ‌شات‌های گیت در دبیان GPL-2.0 فراخوانی‌های کتابخانه‌های پویا
bpftrace لینوکس v0.27.0 بتاریخ 2026-09-10 Apache-2.0 پروب‌های کرنل و کاربر از طریق eBPF
dtrace و dtruss macOS Sun D 1.19 در macOS 26.2 CDDL برگرفته از OpenSolaris سان پروب‌های زبان D؛ دستور dtruss فراخوانی‌های سیستمی را چاپ می‌کند

دستور dtruss در راهنمای خود متذکر می‌شود «از آنجا که این ابزار از DTrace استفاده می‌کند، فقط کاربران با دسترسی root می‌توانند آن را اجرا کنند». راهنمای dtrace(1) در Apple اضافه می‌کند «با تنظیمات پیش‌فرض مکانیزم SIP (محافظت از یکپارچگی سیستم)، برنامه‌های D مجاز به دسترسی به آدرس‌های حافظه‌ی کرنل یا محتوای آن نیستند» و اقدامات مخرب نیز تحت SIP مسدود می‌شوند. دستور sudo dtruss ./account-O0 در این درس اجرا نشد، زیرا بستر آزمایشی بدون دسترسی روت اجرا می‌شود و این درس آن را بررسی‌نشده ثبت می‌کند. یک ابزار ردگیری به سؤالی متفاوت از بستر ما پاسخ می‌دهد: «آیا این اجرا برای هر خط printf یک فراخوانی write صادر کرد؟» سؤالی برای ابزار ردگیری است، درحالی‌که «آیا رجیستر x1 در bump مقدار 5 داشت؟» سؤالی برای دیباگر است. هر دو پرسش مختصِ همان یک اجرا هستند.

ضبط و بازپخش

ابزار rr یک بار اجرا را ثبت می‌کند و آن را بدون تغییر به‌طور قطعی تحت gdb با امکان اجرای معکوس (Reverse execution) بازپخش می‌نماید: یک نقطه‌ی دیده‌بانی روی متغیر مورد نظر می‌گذارید و با reverse-cont به سمت عقب و به نقطه‌ی نوشتن آن مقدار برمی‌گردید. پروژه‌ی سازنده آن را چنین توصیف می‌کند: «یک‌بار خطا را ضبط می‌کنید، سپس هر چند بار که بخواهید فایل ضبط‌شده را به‌طور قطعی دیباگ می‌کنید». آخرین نسخه‌ی آن 5.9.0 در تاریخ 2025-02-13 تحت مجوز MIT همراه با یک بخش BSD-2-Clause منتشر شده است. این ابزار فقط در لینوکس کار می‌کند. به شمارنده‌های عملکردی سخت‌افزار (Performance counters) برای شمارش شاخه‌ها نیاز دارد؛ بنابراین روی Intel Nehalem به بعد، برخی هسته‌های AMD Zen و «ریزمعماری‌های خاصی از AArch64 (مانند ARM Neoverse N1 یا سری M در Apple Silicon)» تحت لینوکس اجرا می‌شود. داخل ماشین مجازی، شمارنده‌ها باید به‌صورت مجازی‌سازی‌شده ارائه شوند، و ویکی پروژه می‌گوید «هیچ ابزار مجازی‌سازی دیگری در مک این کار را نمی‌کند»، در نتیجه Docker و Colima در این سیستم قادر به اجرای آن نیستند. سرویس Pernosco یک ابزار تجاری بر پایه‌ی ضبط‌های rr است.

نرم‌افزار gdb مکانیزم ضبط نرم‌افزاری اختصاصی خود را دارد. دستور record full رویدادها را به‌صورت «لاگی از اجرای پروسس» با سقف پیش‌فرض 200,000 دستورالعمل ثبت می‌کند، و record btrace از رهگیری شاخه‌های پردازنده‌های Intel بهره می‌برد و «داده‌ها را ثبت نمی‌کند». هیچ‌کدام در lldb در دسترس نیستند. روی این سیستم macOS، متنِ ثبت‌شده توسط بستر آزمایشی همان رکورد ضبط‌شده به حساب می‌آید: اجرای برنامه‌ای یکسان روی باینری یکسان، همان توقف‌ها را با همان ترتیب ایجاد می‌کند و تست‌های فیکسچر آن ترتیب را بررسی می‌کنند.

یک اجرا چه چیزی را ثابت می‌کند؟

یک اجرا، صرفاً یک ورودی را روی یک بیلد مشخص اثبات می‌کند. استاندارد C رفتار تعریف‌نشده (Undefined behavior) را این‌گونه تشریح می‌کند: «رفتاری در زمان استفاده از ساختار برنامه‌ای غیرقابل‌حمل یا معیوب، یا داده‌های نادرست، که این سند هیچ الزامی برای آن تحمیل نمی‌کند»؛ بنابراین برنامه‌ای که متغیرِ علامت‌دارِ int در آن سرریز (Overflow) شود، کاملاً مجاز است که در -O0 و -O2 خروجی‌های متفاوتی چاپ کند. در درس 36 این موضوع را در خانه‌های سرریز percent_change دیدیم. ابزار UBSan این وضعیت را به یک توقف تبدیل می‌کند: فلگ‌های -fsanitize=signed-integer-overflow -fsanitize-trap=undefined موجب می‌شوند سرریز «یک دستورالعملِ تله (Trap) را اجرا کند» که در lldb به‌صورت یک توقف با دلیلی مشخص ثبت می‌شود.

کد بهینه‌سازی‌شده جای متغیرها را نیز جابه‌جا می‌کند. راهنمای GDB می‌گوید «بهینه‌ساز کد شما را دگرگون کرده است؛ دیباگر آن‌چه را که واقعاً در سطح ماشین وجود دارد نشان می‌دهد» و متغیری که بر اثر بهینه‌سازی حذف شده باشد چیزی است که «GDB هرگز آن را نمی‌بیند». دیباگر GDB این متغیر را به‌صورت <optimized out> نمایش می‌دهد و در کدهای منبع خود آن را از <unavailable> متمایز می‌کند: گزینه‌ی optimized out یعنی متغیر «اصلاً در برنامه وجود ندارد»، اما unavailable یعنی متغیر «در سطح ماشین در برنامه هست، اما GDB راهی برای دسترسی به آن ندارد». دیباگر lldb زمانی عبارت <variable not available> را چاپ می‌کند که مقدار شمارنده‌ی برنامه (PC) خارج از تمام بازه‌های موجود در فهرست مکان‌های DWARF آن متغیر باشد. نقطه‌ی دیده‌بانی روی یک متغیر محلی که در رجیستر ذخیره شده باشد هرگز فعال نمی‌شود، زیرا رجیستر حافظه نیست. اجرای حالت -O2 در بالا نمونه‌ی حاد این موضوع را نشان می‌دهد: کل تابع ادغام و حذف شد، و سکوتِ نقطه‌ی دیده‌بانی‌ای که هرگز تنظیم نشده بود، هیچ چیزی را اثبات نمی‌کند.

به همین دلیل بستر آزمایشی سه نوع وضعیت گزارش می‌دهد و هرگز وضعیت چهارمی در کار نیست. توقفی که ادعا را بسنجد و با آن مطابقت داشته باشد، با وضعیت confirmed ثبت می‌شود. توقفی که آن را بسنجد و تطبیق نداشته باشد، refuted می‌شود. ادعایی که هیچ توقفی امکان سنجش آن را نداشته باشد، به همراه دلیلِ آن به‌صورت not-observed ثبت می‌گردد و تعداد برخوردهای نقاط توقف بخشی از آن دلیل است. دفتر کل درس 35 دو مورد اول را به‌عنوان رکوردهای observed با مرجعیت controlled-replay می‌پذیرد و مورد سوم را به‌عنوان مجهولی باز باقی می‌گذارد.

دیباگ‌کردن صرفاً باینری‌های خودتان در macOS

در پلتفرم Apple silicon، لینکر به‌طور خودکار کار امضای کد را انجام می‌دهد. راهنمای ld(1) اپل درباره‌ی گزینه‌ی -adhoc_codesign می‌گوید: «حالت پیش‌فرض برای باینری‌های Apple Silicon استفاده از امضای موقت (Ad-hoc) است». دستور codesign -dvvv account-O0 روی فایل فیکسچر این خروجی را چاپ می‌کند:

Format=Mach-O thin (arm64)
CodeDirectory v=20400 size=387 flags=0x20002(adhoc,linker-signed) hashes=9+0 location=embedded
Signature=adhoc
TeamIdentifier=not set

مقدار 0x20002 حاصل‌جمع CS_ADHOC (0x2) و CS_LINKER_SIGNED (0x20000) به معنای «امضاشده به‌طور خودکار توسط لینکر» در هدرهای امضای کد Apple است. دستور codesign -d --entitlements - پس از نام فایل هیچ چیزی چاپ نمی‌کند: باینری فاقد مجوزهای خاص (Entitlements) است. دیباگر lldb آن را اجرا کرد، در تمام نقاط توقف متوقف شد، یک نقطه‌ی دیده‌بانی سخت‌افزاری گذاشت و فایل Core را ایجاد نمود؛ هم‌زمان DevToolsSecurity -status اعلام می‌کرد: «حالت توسعه‌دهنده در حال حاضر غیرفعال است» و مکانیزم SIP نیز فعال بود.

دو مجوز دسترسی، کنترل دیباگ‌کردن پروسس‌ها را در دو جهت مختلف بر عهده دارند: مجوز com.apple.security.cs.debugger متعلق به خود دیباگر است: «مقداری بولی که نشان می‌دهد آیا برنامه دیباگر است و می‌تواند به پروسس‌های دیگر متصل شده یا پورت‌های وظیفه را دریافت کند». برنامه‌ی debugserver در Apple فرم خصوصی آن را دارد و خروجی codesign -d --entitlements - روی نسخه‌ی موجود در ابزارهای خط فرمان مجوزهای com.apple.private.cs.debugger، com.apple.private.set-exception-port، com.apple.private.thread-set-state و com.apple.private.logging.diagnostic را نشان می‌دهد. در سمت مقابل، مجوز com.apple.security.get-task-allow مخصوص پروسسِ مقصد است. محیط Xcode آن را به بیلدهای دیباگ تزریق می‌کند، هنگام خروجی نهایی حذفش می‌کند، و اعتبارسنجی Apple (Notarization) برنامه‌ای را که هنوز حاوی آن باشد رد می‌کند. مهندسان Apple قاعده‌ی امتناع را چنین اعلام کرده‌اند: پورت وظیفه زمانی مسدود می‌شود که پروسس مقصد دارای قابلیت محیط اجرای محافظت‌شده (Hardened Runtime) بوده و فاقد مجوز get-task-allow باشد. بیلدِ عادی کامپایلر clang در حالت Hardened نیست و هیچ‌یک از این شرایط را برنمی‌انگیزد. اگر زمانی فیکسچری را با codesign --options runtime امضا کردید، قبل از شروع دیباگ باید دوباره آن را با دستور codesign -s - -f --entitlements get-task-allow.plist امضا کنید.

سه نکته‌ی پایانی این بخش: نخست، ممکن است در اولین بارِ لاگین پنجره‌ی «Developer Tools Access needs to take control of another process for debugging to continue» را ببینید؛ دستور sudo DevToolsSecurity -enable این نیاز به گذرواژه را برای اعضای گروه‌های admin یا _developer از بین می‌برد. دوم، برای دیباگ کدهای شخصی‌تان نیازی به غیرفعال‌کردن SIP ندارید. Apple تأکید می‌کند «برای اجرای برنامه‌ها و دیباگ آن‌ها از طریق Xcode نیازی به غیرفعال‌کردن SIP نیست»، و تنها مؤلفه‌ی SIP که با دیباگ تلاقی دارد موردی است که دستور csrutil enable --without debug آن را باز می‌کند؛ آن هم فقط برای بررسی پروسس‌های دیگران. سوم، ساندباکس Seatbelt در این نشست فراخوانی task_for_pid را به‌کلی مسدود می‌کند، و فیکسچر sandbox-O0.lldb.txt نتیجه‌ی آن را نشان می‌دهد. ناتوانی در اجرای برنامه یک واقعیت محیطی است و سیستم ما نیز آن را به همین صورت گزارش می‌دهد.

تمامی مباحث این درس مربوط به برنامه‌هایی است که خودتان کامپایل کرده‌اید. همین دستورات تنها زمانی می‌توانند به پروسس برنامه‌ی دیگری متصل شوند که مالک آن صراحتاً اجازه داده باشد، و هیچ بخشی از این درس به آموزش تغییر این سازوکار نمی‌پردازد.

پیاده‌سازی از صفر

اجرای دموی برنامه:

cd phases/13-tools-and-protocols/42-dynamic-analysis-of-your-own-program
python3 code/main.py

خروجی برنامه‌ی دمو، یک گزارش واحد JSON از رونوشت‌های متنی ذخیره‌شده است. برای هر بیلد، این گزارش شامل نقاط توقف، وضعیت آدرس‌دهی و تعداد برخوردها، تعداد نقاط دیده‌بانی سخت‌افزاری، تک‌تک توقف‌ها همراه با رجیسترها و آرگومان‌ها، وضعیت خروج، خروجی‌های متنی برنامه، خطاهای چاپ‌شده توسط lldb، و ادعاها با احکامشان و رکوردهای شواهدِ پشتیبان آن‌ها است. علاوه بر این، رفت و برگشت ذخیره و بازیابی فایل Core، نتایج API پایتون، خطای ناشی از ساندباکس و ابزارهای خارجی غایب روی ماشین نیز ضمیمه شده‌اند.

قالب خروجی مورد انتظار:

{
  "program": "account.c",
  "variants": {
    "O0": {
      "binary": "account-O0",
      "breakpoints": {"1": {"spec": "name=bump", "address": "0x1000004bc", "resolved": true, "hits": 1}, "...": "..."},
      "hardwareWatchpoints": 4,
      "watchpoints": {"1": {"address": "0x16fdfe00c", "size": 4, "type": "m", "hits": [[250, 255]], "hitCount": 1}},
      "stops": [{"index": 0, "pc": "0x1000004bc", "function": "bump", "kind": "entry", "registers": {"x0": "0x16fdfe008", "x1": "0x5"}, "...": "..."}],
      "exitStatus": 0,
      "claims": [{"id": "arg-registers", "verdict": "confirmed", "confirmed": 5, "refuted": 0, "not-observed": 0, "...": "..."}],
      "evidence": [{"claim": "arg-registers", "verdict": "confirmed", "stop": 0, "observation": "bump: x0 = 0x16fdfe008 and a = 0x000000016fdfe008, x1 = 0x5 and by = 5"}]
    },
    "O2": {"stops": [], "claims": [{"id": "calls", "verdict": "refuted", "...": "..."}], "...": "..."}
  },
  "core": {"savedAt": {"function": "bump", "pc": "0x1000004bc"}, "labelString": "", "...": "..."},
  "types": {"python": "3.9.6", "size": 32, "...": "..."},
  "sandbox": {"stops": 0, "errors": ["process exited with status -1 (attach failed ((os/kern) invalid address))"]},
  "absentTools": ["bpftrace", "gdb", "llvm-objdump", "ltrace", "rr", "strace"]
}

کد فایل code/main.py را به این ترتیب مطالعه کنید:

  1. کلاس Listing متن خروجی llvm-objdump -d را به توابع و دستورالعمل‌ها تفکیک می‌کند و متد Function.conditional_branches() پرش‌ها را همراه با مقاصد و مسیرهای مستقیم‌شان مشخص می‌نماید.
  2. تابع struct_layout() ساختار استراکت و اعضای آن را از متن llvm-dwarfdump استخراج می‌کند.
  3. تابع lift_condition() یک پرش شرطی را به یک گزاره‌ی منطقی ارتقا می‌دهد (کاری که در درس 34 برای دستورات tbz، cbz و ساختارهای مقایسه و پرش انجام دادیم).
  4. کلاس Plan.commands() بر اساس لیستینگْ فایل دستورات lldb را تولید می‌کند، و Plan.lldb_command() فراخوانی حالت دسته‌ای را می‌سازد.
  5. تابع parse_transcript() خروجی را به ترتیب بررسی کرده و رکوردهای Stop شامل رجیسترها، حافظه، متغیرها و مقادیر نقطه‌ی دیده‌بانی، به همراه جدول نقاط توقف و وضعیت خروج را می‌سازد.
  6. تابع claims_for() هشت ادعا را مشخص می‌کند و evaluate() با توابع کمکی _check_* در هر توقف آن‌ها را می‌سنجد.
  7. تابع rollup() شواهد و رکوردها را تجمیع کرده و به ازای هر ادعا یک حکم نهایی صادر می‌کند که در آن وضعیت «ردشده» بر «تأییدشده» اولویت دارد.
  8. توابع capture() و capture_extras() فیکسچرها را می‌سازند، و تابع live() زمانی که clang و lldb در دسترس باشند، کل چرخه را روی یک بیلد تازه از نو اجرا می‌کند.

چهار دستور جانبی دیگر نیز در نظر گرفته شده است: دستور python3 code/main.py plan O0 فایل دستورات یک بیلد را چاپ می‌کند. دستور python3 code/main.py commands جدول تطبیق gdb و lldb را نمایش می‌دهد. دستور python3 code/main.py tools مشخص می‌کند کدام ابزارهای خارجی در متغیر PATH سیستم قرار دارند. دستور python3 code/main.py live O0 کد account.c را در یک پوشه‌ی موقت کامپایل می‌کند، بر اساس لیستینگ جدید نقشه را طرح‌ریزی می‌کند، lldb را اجرا می‌نماید و ادعاها را با داده‌های همان اجرا می‌سنجد. داخل ساندباکس، این دستور ناتوانی lldb در اجرای پروسس را همراه با متن خطا اعلام کرده و با کد 1 خارج می‌شود.

استفاده از ابزارهای واقعی

این بستر آزمایشی را روی برنامه‌ی خودتان به این ترتیب پیاده کنید:

  1. برنامه را با فلگ -g و همان سطح بهینه‌سازی که قرار است عرضه کنید بیلد کنید. فایل باینری، پوشه‌ی dSYM یا متادیتاهای DWARF، و متن خروجی llvm-objdump -d را در کنار هم نگه دارید.
  2. پیش از اجرا ادعاها را مکتوب کنید. هر ادعا باید نام یک تابع، یک رجیستر، آدرس یا آفست، یک مقدار مورد انتظار یا یک یال مشخص، و همچنین درس یا مستندی که ادعا از آن برگرفته شده است را قید کند. ادعایی که بعد از مشاهده‌ی توقف نوشته شود صرفاً توصیف همان توقف است و هیچ اعتبارسنجی‌ای انجام نمی‌دهد.
  3. نقشه‌ی اجرا را از روی لیستینگ بسازید و آن را بررسی کنید. تک‌تک آدرس‌های آن باید با خطی در لیستینگ همخوانی داشته باشند. اگر نقطه‌ی توقفی فاقد چنین خطی است، آن را حذف کنید.
  4. برنامه را یک بار در حالت دسته‌ای اجرا کنید و رونوشت متنی خروجی را با خط فرمان و نگارش دیباگر در هدر آن نگهداری نمایید. این گزارش همان شواهد شماست. احکام نهایی از آن استخراج می‌شوند و هر کاربری می‌تواند مجدداً به آن‌ها برسد.
  5. به ادعاها امتیاز دهید. قبل از خواندن موارد تأییدشده، تمام رکوردهای not-observed و دلایل آن‌ها را مطالعه کنید. این دلایل نشان می‌دهند کدام ادعاها به یک ورودی دیگر، بیلد متفاوت یا نقطه‌ی توقف جدید نیاز دارند.
  6. بیلد بهینه‌شده را با همان ادعاها اجرا کنید. منتظر نقاط توقف آدرس‌دهی‌شده با صفر برخورد در جاهایی که کامپایلر اینلاین کرده است باشید، و این ادعاها را در وضعیت باز و بلاتکلیف نگه دارید.
  7. رکوردهای تأییدشده و ردشده را با سطح اعتبار controlled-replay به‌ازای هر توقف، به دفتر کل درس 35 انتقال دهید. موارد دیده‌نشده را به‌عنوان مجهولات با ثبت تعداد دفعات برخورد در نقاط توقف رها کنید.

آزمایشگاه تعاملی

آزمایشگاه الف: خواندن دستی یک توقف

از پوشه‌ی code، مفسر Python را باز کنید:

cd phases/13-tools-and-protocols/42-dynamic-analysis-of-your-own-program/code
python3 -q

دستورات زیر را اجرا کنید:

from main import *
t = load_transcript("session-O0.lldb.txt")
s = t.stops[0]
hex(s.pc), s.function, s.reason, s.registers
[hex(w) for w in s.memory[:8]]
s.struct_pointer().fields

پیش از اجرای خط دوم، روی کاغذ بنویسید که انتظار دارید کدام کلمه مقدار 250 و کدام دو کلمه مقدار 1.25 را نگه دارند. سپس عبارت struct.unpack("<d", struct.pack("<II", s.memory[4], s.memory[5])) را اجرا کرده و مقایسه کنید. سپس به مقادیر t.stops[1].watch_hit و t.stops[1].pc دقت کنید و آن آدرس را در account-O0.objdump.txt پیدا کنید: متوجه خواهید شد این آدرس درست دستور بعد از ذخیره‌سازی در حافظه است.

آزمایشگاه ب: انشعاب شرطی و مقصد آن

plan = make_plan("O0")
plan.branches()
classify = plan.listing.function("classify")
[lift_condition(classify, b, "a->balance")[0] for b in plan.branches()]
[(hex(s.pc), s.struct_pointer().field_int("balance")) for s in t.stops_in("classify")]

هر توقف روی یال را با توقف در نقطه‌ی ورود آن تطبیق دهید و قبل از مشاهده‌ی شواهد، مشخص کنید گزاره‌ی تحلیلی کدام مقصد را پیش‌بینی می‌کند. سپس عبارت [e.to_dict() for e in evaluate(plan, t, struct_layout(open("fixtures/account-O0.dwarfdump.txt").read(), "account")) if e.claim.startswith("branch")] را اجرا نموده و مقایسه نمایید.

آزمایشگاه ج: اجرایی که هرگز متوقف نشد

o2 = load_transcript("session-O2.lldb.txt")
len(o2.stops), o2.exit_status, o2.hits()
o2.errors[:3]
make_plan("O2").branches()
rollup(evaluate(make_plan("O2"), o2, struct_layout(open("fixtures/account-O2.dwarfdump.txt").read(), "account")))

هرکدام از وضعیت‌های not-observed را با کلمات خودتان توضیح دهید، سپس فایل account-O2.objdump.txt را باز کرده و چهار مقدار ثابتی را که در تابع main جایگزین فراخوانی‌ها شده‌اند بیابید. در ادامه تعداد تگ‌های DW_TAG_inlined_subroutine را در فایل account-O2.dwarfdump.txt با تابع count_tags() بشمارید.

آزمایشگاه د: دستکاری شواهد

text = open("fixtures/session-O0.lldb.txt").read().replace("x1 = 0x0000000000000005", "x1 = 0x0000000000000006", 1)
broken = parse_transcript(text)
[e.to_dict() for e in evaluate(plan, broken, struct_layout(open("fixtures/account-O0.dwarfdump.txt").read(), "account")) if e.claim == "arg-registers"][:2]

تغییر مقدار یک رجیستر باعث می‌شود نخستین رکورد arg-registers به حالت refuted درآید و به همراه آن وضعیت کل این ادعا نیز رد شود، درحالی‌که چهار مدخل دیگر همچنان confirmed می‌مانند. سپس خط old value: 250 را قبل از نقطه‌ی دیده‌بانی پاک کنید و ببینید چگونه store-once به حالت not-observed افت می‌کند. ابزار ارزیابی که نتوان آن را وادار به اعلام خطا کرد، اصلاً ارزیابی واقعی انجام نمی‌دهد.

اگر lldb در شل شما اجازه‌ی اجرای پروسس را دارد، در پایان دستورات python3 main.py live O0 و python3 main.py live O2 را اجرا کنید. آدرس‌های پشته با فیکسچرها تفاوت خواهند داشت، اما احکام نهایی باید دقیقاً یکسان باشند.

تمرین عملی

بستر آزمایشی را با افزودن یک ادعا درباره‌ی حلقه‌ی موجود در sum_balances گسترش دهید.

نیازمندی‌ها:

  • یال بازگشت حلقه (Loop back edge) در sum_balances را در لیستینگ بیلد -O0 با تابع conditional_branches() پیدا کنید و یک نقطه‌ی توقف در ابتدای حلقه (Loop header) قرار دهید.
  • ادعایی اضافه کنید مبنی بر این‌که هدر حلقه برای مقدار count = 3، دقیقاً count + 1 بار اجرا می‌شود؛ که معادل یال بازگشت درس 33 به‌علاوه‌ی شرط خروج است.
  • هوک توقف یا نقشه‌ی اجرا را به‌گونه‌ای گسترش دهید که در هر توقف در هدر، متغیر شمارنده‌ی حلقه از داخل اسلات استک آن خوانده شود، و متد _check_loop() را بنویسید که توالی مقادیر شمارنده را ثبت کند.
  • فایل متنی رونوشت را با دستور python3 main.py capture O0 روی سیستمی که lldb در آن دسترسی اجرا دارد دوباره ضبط کنید و مجموعه‌ی تست‌ها را مجدداً برانید. تست مربوط به نقشه تا زمانی که فایل دستورات ثبت‌شده (commit-شده) با نقشه‌ی جدید مطابقت نداشته باشد مردود خواهد شد، که این دقیقاً هدف تست است.
  • تستی بنویسید که یکی از مقادیر شمارنده را در رونوشت دستکاری کند و تأیید کند که ادعا به وضعیت refuted تغییر می‌یابد.

موفقیت زمانی است که گزارش نهایی، توقف‌های هدر حلقه را به ترتیب به همراه مقادیر شمارنده نشان دهد، و در اجرای بیلد -O2، این ادعای جدید با دلیلی مبنی بر برداری‌سازی (Vectorized) یا حذف محاسباتی حلقه، در وضعیت not-observed باقی بماند.

خروجی این درس

این درس شامل فایل outputs/skill-dynamic-analysis-of-your-own-program.md است. از این فایل استفاده کنید تا برنامه‌ای که خودتان ساخته‌اید، لیستینگ آن و مجموعه‌ای از ادعاهای ایستا را به یک رونوشت متنی ذخیره‌شده از دیباگر و یک حکم نهایی برای هر ادعا تبدیل نمایید؛ همراه با ثبت دقیق توقفی که آن حکم را ایجاد کرده و ذکر دلیل برای هر ادعایی که مشاهده نشده است. این خروجی برای Claude، Cursor، Codex، OpenClaw، Hermes یا هر ایجنتی که فایل‌های SKILL.md را می‌خواند قابل استفاده است.

اعتبارسنجی

اجرای دمو و مجموعه‌ی تست‌های قطعی:

cd phases/13-tools-and-protocols/42-dynamic-analysis-of-your-own-program
python3 code/main.py
python3 -m unittest discover -s code/tests -v

مجموعه تست‌ها بدون نیاز به اجرای مستقیم lldb کار می‌کند و رونوشت‌های ذخیره‌شده در code/fixtures/ را می‌خواند. اعتبارسنجی باید موارد زیر را اثبات کند:

  • پارسر لیستینگ، هر چهار تابع را با بازه‌های مربوطه و دو پرش شرطی classify را در بیلد -O0 با مشخص‌کردن مقصد و مسیر مستقیم پیدا کند.
  • تحلیل سطح‌بالای tbz w8, #0x1f به‌صورت a->balance >= 0 و تحلیل subs به همراه b.le به‌صورت a->balance <= 200 باشد.
  • تابع main در بیلد -O0 توابع bump، سه بار classify و sum_balances را صدا کند و در بیلد -O2 فقط printf را فراخوانی نماید.
  • در معماری x86-64 کدهای نمادین پرش شرطی j<cc> شرطی لحاظ شوند و jmp و callq غیرشرطی باشند.
  • متادیتاهای DWARF یک استراکت 32 بایتی با اعضایی در آفست‌های 0، 4، 8، 16 و 24 ارائه دهند.
  • ساختار DWARF در بیلد -O2 شامل چهار زیربرنامه و بدون هیچ مدخل درون‌خطی باشد، و دستور nm همچنان این سه تابع را فهرست کند.
  • نقشه‌ی طراحی‌شده برای هر دو بیلد با فایل دستورات ذخیره‌شده یکسان باشد، و هدر رونوشت، دستور اجرای lldb را ثبت کرده باشد.
  • رونوشت -O0 دارای یازده توقف در توابع مورد انتظار، کد خروج 0 و چهار خط خروجی باشد.
  • خروجی هوک به توقفِ پس از آن متصل شده باشد؛ شامل x1 = 5، بایت‌های حافظه که از x0 شروع می‌شوند و رشته‌ی عنوان.
  • جدول نقاط توقف وضعیت آدرس‌دهی و تعداد برخوردهای 1، 3، 1، 2، 1، 1 و 1 را نشان دهد.
  • نقطه‌ی دیده‌بانی به طول 4 بایت از نوع modify در x0 + 4 تنظیم شده باشد، یک بار با مقدار قدیمی 250 و مقدار جدید 255 فعال شود، و 4 اسلات سخت‌افزاری گزارش شود.
  • رونوشت بیلد -O2 صفر توقف، سه نقطه‌ی توقف آدرس‌دهی‌شده با صفر برخورد، بدون نقطه‌ی دیده‌بانی، و یک خطا به ازای هر continue اضافه داشته باشد.
  • اجرای درون ساندباکس قبل از هر توقفی با خطای اتصال (Attach) متوقف شود.
  • چرخه‌ی خواندن و نوشتن فایل Core رد پشته و مقدار balance = 250 را حفظ کرده و محتوای رشته‌ی پشت label را از دست بدهد.
  • رابط پایتون پنج فیلد، اندازه‌ی 32 و محدوده‌ی توابع را بدون نیاز به اجرای پروسس بخواند.
  • رونوشت ساختگی دستی در x86-64 با رجیسترهای rdi، rsi و rip به‌درستی پارس شود.
  • تمام ادعاها در -O0 با تعداد رکوردهای 5، 11، 3، 2، 1، 1، 1 و 1 تأیید شوند.
  • شواهد بررسی پرش شامل مقدار متغیر، شرط منطقی و یال طی‌شده باشند.
  • شواهد آفستِ فیلدها کلمات حافظه را با چیدمان ساختار در DWARF تطبیق دهند.
  • مدارک مربوط به عملیات ذخیره‌سازی و فاصله‌ی گام، مقادیر قدیم و جدید و موجودی‌های سه سطر را گزارش کنند.
  • اجرای بیلد -O2 فراخوانی‌ها را رد کرده و بقیه‌ی ادعاها را در وضعیت دیده‌نشده باقی بگذارد.
  • دستکاری مقدار رجیستر موجب رد ادعا شود، حذف برخورد نقطه‌ی دیده‌بانی وضعیت را به دیده‌نشده تغییر دهد، و گزینه‌ی ردشده بر خروجی نهایی اولویت یابد.
  • جدول دستورات برای هر سطر، معادل‌های gdb و lldb را داشته باشد و جدول سخت‌افزار چهار رجیستر دیباگ x86 را نام ببرد.
  • برنامه‌ی دمو ابزارهای ناموجود را فهرست کند نه این‌که متوقف شود، و اجرای زنده ابزار غایب را با نام گزارش دهد.
  • داده‌های نمودارها از هر دو نشست استخراج شده و ماژول وب‌سایت همان داده‌ها را در بر داشته باشد.

حالت‌های شکست در محیط عملیاتی

نقص و شکست گزارش ضعیف چه می‌گوید بستر آزمایشی چه چیزی را باید اثبات کند
نقطه‌ی توقفِ حل‌شده با صفر برخورد «تابع اجرا می‌شود» یا «تابع کد مرده است» اجرای کامل برنامه همراه با کد خروج، تعداد برخوردها، و صدور حکم refuted صرفاً برای یال فراخوانی
خواندن رجیستر در توقف اشتباه «مقدار x1 داخل classify برابر 5 است» مقایسه‌ی صرفاً آرگومان‌های رسمی در لحظه‌ی ورود به تابع
خواندن استراکت بدون داشتن آفست‌ها ستونی از کلمات هگزادسیمال نامفهوم نگاشت تک‌تک کلمات به مقادیر DW_AT_data_member_location و مقایسه‌ی آن‌ها با frame variable
امتیازدهی به پرش صرفاً از روی نام نمادین «دستور tbz یعنی پرش در صورت منفی بودن» گزاره‌ی منطقی استخراج‌شده از کدبندی دستور و تطبیق آن با توقف در آدرس بعدی
تنظیم نقطه‌ی دیده‌بانی روی متغیر محلی در رجیستر «هیچ عملیات نوشتنی رخ نداد» تنظیم نقطه‌ی دیده‌بانی روی آدرس حافظه و ثبت برخورد، و در غیر این صورت ثبت رکورد not-observed
نقاط توقف با گزینه‌ی Auto-continue یک لاگ متنی تمیز بدون هیچ توقفی استفاده از نقاط توقف معمولی، یک هوک توقف و تعداد محدودی دستور continue
نقطه‌ی توقف آدرس‌محور بدون مشخص‌کردن ماژول ثبت وضعیت unresolved, hit count = 0 استفاده از فلگ -s MODULE تا آدرس لیستینگ به آدرس لود نگاشت شود
توقف کل اسکریپت با اولین خطای دستور چاپ‌نشدن breakpoint list در پایان مقداردهی interpreter.stop-command-source-on-error false
تلقی فایل Core استک‌محور به‌عنوان کل حافظه گزارش label = "" به‌عنوان مقدار واقعی برنامه نوع فایل Core، آدرس اشاره‌گر و سگمنتی که اشاره‌گر به آن ارجاع دارد
اجرای شل داخل ساندباکس «دیباگر lldb خراب است» ثبت attach failed ((os/kern) invalid address) به‌عنوان محدودیتی در محیط اجرا
باینری با حالت Hardened و بدون get-task-allow «مک اجازه‌ی دیباگ نمی‌دهد» دو شرط موجود در قوانین Apple و امضای مجدد باینری ساخته‌شده توسط خودتان
دور ریختن رونوشت متنی پس از تحلیل احکام صادرشده بدون هیچ منبع و مدرک فایل گزارش دیباگ، خط فرمان فراخوانی و نگارش دقیق دیباگر

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

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

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

خودآزمایی

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

  1. پیش از درسشما در یک بیلد از برنامه خود، روی bump، classify و sum_balances نقطه توقف قرار می‌دهید، برنامه را تا انتها اجرا می‌کنید و هر نقطه توقف مقدار hit count = 0 را گزارش می‌دهد. این اجرا چه چیزی را ثابت می‌کند؟
  2. حین درسدر نقطه توقف bump، دیباگر lldb مقادیر x0 = 0x16fdfe008 و x1 = 0x5 را چاپ می‌کند و دستور memory read -s4 -fx -c8 $x0 مقادیر 0x00000073 0x000000fa 0x00000009 0x00000000 0x00000000 0x3ff40000 0x00000719 0x00000001 را چاپ می‌کند. کلمه دوم کدام ادعای ایستا را تأیید می‌کند؟
  3. حین درسدر تابع classify در سطح -O0، اولین branch شرطی tbz w8, #0x1f, 0x100000504 با مسیر مستقیم (fall-through) در 0x1000004f4 است. برنامه در 0x1000004f4 برای حسابی با موجودی -40 متوقف می‌شود. آیا ادعای یال در درس 33 تأیید می‌شود یا رد؟
  4. حین درسدستور watchpoint set variable a->balance مشخصات size = 4, type = m را گزارش می‌دهد، و watchpoint list عبارت Number of supported hardware watchpoints: 4 را چاپ می‌کند. چرا lldb اصلاً چنین عددی را گزارش می‌دهد؟
  5. پس از درسشما یک core dump با دستور process save-core --style stack در نقطه توقف bump ذخیره کرده‌اید. بارگذاری آن با lldb -c مقدار balance = 250 اما label = "" را با وجود دست‌نخورده بودن اشاره‌گر 0x100000719 نشان می‌دهد. چه اتفاقی افتاده است؟
  6. پس از درسدر اپل سیلیکون، چرا دستور clang -g -o account account.c فایلی تولید می‌کند که lldb می‌تواند بدون نیاز به امضای اضافی آن را دیباگ کند؟
  7. پس از درسکدام گزاره درباره ابزار ضبط و بازپخش (record-and-replay) با rr برای یک یادگیرنده در مک مجهز به اپل سیلیکون درست است؟

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

چرا یک نقطه‌ی توقف در بیلد بهینه‌شده با سطح O2- با وجود تطبیق آدرس، هرگز متوقف نمی‌شود؟

کامپایلر در سطوح بهینه‌سازی بالا ممکن است توابع را درون‌خطی (Inline) کرده یا محاسبات را در زمان کامپایل انجام دهد؛ در نتیجه آدرس تابع همچنان در جدول نمادها هست اما اجرای برنامه اصلاً به آن نقطه نمی‌رسد.

چرا رشته‌های اشاره‌شده در ساختارها ممکن است در یک فایل Core dump معمولی قابل خواندن نباشند؟

فایل‌های Core پیش‌فرض بر پایه‌ی استک (Stack-style) تنها حافظه‌ی پشته را ذخیره می‌کنند و سگمنت داده یا کد برنامه (مانند TEXT__) که رشته‌های ثابت در آن قرار دارند را در بر نمی‌گیرند.

تفاوت وضعیت «ردشده» (Refuted) با «دیده‌نشده» (Not-observed) در ارزیابی ادعاها چیست؟

وضعیت ردشده زمانی است که توقف در برنامه رخ داده اما مقادیر رجیستر یا متغیرها با ادعا همخوانی نداشته‌اند؛ دیده‌نشده یعنی شرایط اجرا یا بهینه‌سازی به گونه‌ای بوده که هیچ توقفی برای سنجش ادعا امکان‌پذیر نشده است.

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

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