
على حبري
كتب بالفعل عن اعتراض دعوات النظام عن طريق
ptrace . كتب اليكسا عن هذا الموضوع أكثر تفصيلا ، والتي قررت أن تترجم.
من أين تبدأ
يحدث الاتصال بين البرنامج المصحح والمصحح باستخدام الإشارات. هذا يعقد إلى حد كبير الأشياء الصعبة بالفعل ؛ للمتعة ، يمكنك قراءة
قسم BUGS من man ptrace .
هناك طريقتان مختلفتان على الأقل لبدء التصحيح:
ptrace(PTRACE_TRACEME, 0, NULL, NULL) أصل العملية الحالية مصحح أخطاء لها. لا حاجة إلى مساعدة من الوالد ؛ ينصح man بلطف: "ربما لا ينبغي أن تقدم العملية هذا الطلب إذا كان الوالد لا يتوقع تتبعه." (في مكان آخر من الرجال ، هل رأيت عبارة "ربما لا ينبغي أن" ؟) إذا كانت العملية الحالية تحتوي بالفعل على مصحح أخطاء ، فستفشل المكالمة.ptrace(PTRACE_ATTACH, pid, NULL, NULL) سيجعل العملية الحالية مصحح أخطاء لـ pid . إذا كان لدى pid بالفعل مصحح أخطاء ، فستفشل المكالمة. SIGSTOP إرسال SIGSTOP إلى عملية تصحيح الأخطاء ، ولن تستمر في العمل حتى يزيلها المصحح.
هاتان الطريقتان مستقلتان تمامًا ؛ يمكنك استخدام واحد أو آخر ، ولكن لا يوجد أي فائدة في الجمع بينهما.
من المهم ملاحظة أن
PTRACE_ATTACH غير فوري: بعد
ptrace(PTRACE_ATTACH) ، عادةً ما يتم استدعاء
waitpid(2) للانتظار حتى "يعمل"
PTRACE_ATTACH .
يمكنك بدء العملية الفرعية تحت تصحيح الأخطاء باستخدام
PTRACE_TRACEME كما يلي:
static void tracee(int argc, char **argv) { if (ptrace(PTRACE_TRACEME, 0, NULL, NULL) < 0) die("child: ptrace(traceme) failed: %m"); if (raise(SIGSTOP)) die("child: raise(SIGSTOP) failed: %m"); execvp(argv[0], argv); die("tracee start failed: %m"); } static void tracer(pid_t pid) { int status = 0; if (waitpid(pid, &status, 0) < 0) die("waitpid failed: %m"); if (!WIFSTOPPED(status) || WSTOPSIG(status) != SIGSTOP) { kill(pid, SIGKILL); die("tracer: unexpected wait status: %x", status); } } void shim_ptrace(int argc, char **argv) { pid_t pid = fork(); if (pid < 0) die("couldn't fork: %m"); else if (pid == 0) tracee(argc, argv); else tracer(pid); die("should never be reached"); }
بدون استدعاء
raise(SIGSTOP) ، قد يتضح أن
execvp(3) سينفذ قبل أن تكون العملية الأصل جاهزة لهذا ؛ ثم لن تبدأ إجراءات مصحح الأخطاء (على سبيل المثال ، اعتراض استدعاءات النظام) من بداية العملية.
عند بدء تشغيل تصحيح الأخطاء ،
ptrace(PTRACE_SYSCALL, pid, NULL, NULL) كل
ptrace(PTRACE_SYSCALL, pid, NULL, NULL) "بإذابة" عملية تصحيح الأخطاء حتى يتم إدخال أول من استدعاء النظام ، ثم حتى يغادر استدعاء النظام.
المجمّع عن بعد
ptrace(PTRACE_SYSCALL) لا
ptrace(PTRACE_SYSCALL) أية معلومات إلى المصحح؛ إنه ببساطة يعد بأن العملية التي يتم تصحيحها ستتوقف مرتين عند كل مكالمة نظام. للحصول على معلومات حول ما يحدث في العملية التي تم تصحيحها - على سبيل المثال ، النظام الذي يطلق عليه توقف - تحتاج إلى الصعود إلى نسخة من سجلاتها المخزنة بواسطة النواة في
struct user بتنسيق يعتمد على البنية المحددة. (على سبيل المثال ، في x86_64 ، سيكون رقم الاتصال في حقل
regs.orig_rax ، المعلمة الأولى التي تم تمريرها ستكون في
regs.rdi ، وما إلى ذلك) تعليقات Alexa: "يبدو أنك تكتب رمز التجميع في C والذي يعمل مع سجلات المعالج البعيد."
بدلاً من البنية الموضحة في
sys/user.h ، قد يكون من الأنسب استخدام ثوابت الفهرس المعرفة في
sys/reg.h :
#include <sys/reg.h> /* . */ long ptrace_syscall(pid_t pid) { #ifdef __x86_64__ return ptrace(PTRACE_PEEKUSER, pid, sizeof(long)*ORIG_RAX); #else // ... #endif } /* . */ uintptr_t ptrace_argument(pid_t pid, int arg) { #ifdef __x86_64__ int reg = 0; switch (arg) { case 0: reg = RDI; break; case 1: reg = RSI; break; case 2: reg = RDX; break; case 3: reg = R10; break; case 4: reg = R8; break; case 5: reg = R9; break; } return ptrace(PTRACE_PEEKUSER, pid, sizeof(long) * reg, NULL); #else // ... #endif }
في هذه الحالة ، لا تختلف محطتان من عملية تصحيح الأخطاء - عند مدخل استدعاء النظام وعند الخروج منه - بأي طريقة من وجهة نظر مصحح الأخطاء ؛ بحيث يجب على مصحح الأخطاء نفسه أن يتذكر حالة كل عملية من عمليات تصحيح الأخطاء: إذا كان هناك العديد من العمليات ، فلا أحد يضمن أن زوجًا من الإشارات من عملية واحدة سيأتي على التوالي.
أحفاد
يضمن أحد خيارات
ptrace ، وهو
PTRACE_O_TRACECLONE ، أن يتم تصحيح
PTRACE_O_TRACECLONE جميع الأطفال الذين تم
PTRACE_O_TRACECLONE تلقائيًا عند
خروجهم من fork(2) . هناك نقطة خفية إضافية هنا وهي أن المتحدرين الذين تم أخذهم للتصحيح أصبحوا "أطفالًا
waitpid " من مصحح الأخطاء ، وسوف يستجيب
waitpid ليس فقط لإيقاف "الأطفال المباشرين" ، ولكن أيضًا لإيقاف تصحيح "الأطفال الزائفين". يحذر الرجل من هذا:
"إعداد إشارة WCONTINUED عند استدعاء waitpid (2) غير مستحسن: الحالة" المستمرة "عملية ، ويمكن أن يؤدي استهلاكها إلى إرباك الوالد الحقيقي للتتبع." - على سبيل المثال "الأطفال الزائفون" لديهم والدين يستطيعان انتظار توقفهما. بالنسبة إلى مبرمج مصحح الأخطاء ، هذا يعني أن
waitpid(-1) سينتظر ليس فقط إيقاف الأطفال المباشرين ، ولكن أيضًا لأي من عمليات تصحيح الأخطاء.
إشارات
(محتوى إضافي من أحد المترجمين: هذه المعلومات ليست في مقالة باللغة الإنجليزية)كما سبق ذكره في البداية ، يحدث التواصل بين البرنامج المصحح والمصحح باستخدام الإشارات. تستقبل العملية
SIGSTOP عند توصيل مصحح الأخطاء بها ، ثم
SIGTRAP كل مرة يحدث فيها شيء مثير للاهتمام في العملية التي يتم تصحيحها - على سبيل المثال ، مكالمة النظام أو تلقي إشارة خارجية. يتلقى المصحح ، بدوره ،
SIGCHLD كل مرة واحدة من عمليات التصحيح (وليس بالضرورة الطفل الفوري) "يتجمد" أو "يتجمد".
ptrace(PTRACE_SYSCALL) العملية الجاري تصحيحها عن طريق استدعاء
ptrace(PTRACE_SYSCALL) (قبل أول إشارة أو استدعاء النظام) أو
ptrace(PTRACE_CONT) (قبل الإشارة الأولى). عند
SIGSTOP/SIGCONT إشارات
SIGSTOP/SIGCONT أيضًا لأغراض غير متعلقة
ptrace يمكن أن تنشأ مشاكل مع
ptrace : إذا "قام مصحح الأخطاء"
SIGSTOP تجميد "العملية
SIGSTOP التي استقبلت
SIGSTOP ، فسوف تبدو من الخارج كما لو تم تجاهل الإشارة ؛ إذا لم "
SIGCONT مصحح الأخطاء" العملية الجاري تصحيحها ،
SIGCONT يتمكن
SIGCONT الخارجي من "
SIGCONT " منها.
الآن بالنسبة للجزء الممتع: يحظر Linux العمليات من
تصحيح الأخطاء لأنفسهم ، ولكنه لا يمنع إنشاء حلقات عندما يقوم أحد الوالدين والطفل بتصحيح أخطاء بعضهم البعض. في هذه الحالة ، عندما تتلقى إحدى العمليات أي إشارة خارجية ، "تتجمد" عبر
SIGTRAP - ثم
SIGCHLD إرسال
SIGCHLD إلى العملية الثانية ، وأيضًا "يتجمد" عبر
SIGTRAP . من المستحيل إخراج "
SIGCONT المشتركين" من حالة توقف تام عن طريق إرسال
SIGCONT من الخارج ؛ الطريقة الوحيدة هي قتل (
SIGKILL ) الطفل ، ثم الوالد الخروج من التصحيح و "تجميد". (إذا قتلت الوالد ، سيموت الطفل معه). إذا قام الطفل
PTRACE_O_EXITKILL الخيار
PTRACE_O_EXITKILL ، فسوف يموت الوالد الذي تم تصحيحه من قبله بوفاته.
أنت الآن تعرف كيفية تنفيذ زوج من العمليات التي ، عند تلقي أي إشارة ، تتجمد إلى الأبد وتموت معًا فقط. لماذا قد يكون هذا ضروريًا في الممارسة ، لن أشرح :-)