مغامرة مع ptrace (2)

على حبري كتب بالفعل عن اعتراض دعوات النظام عن طريق ptrace . كتب اليكسا عن هذا الموضوع أكثر تفصيلا ، والتي قررت أن تترجم.


من أين تبدأ


يحدث الاتصال بين البرنامج المصحح والمصحح باستخدام الإشارات. هذا يعقد إلى حد كبير الأشياء الصعبة بالفعل ؛ للمتعة ، يمكنك قراءة قسم BUGS من man ptrace .

هناك طريقتان مختلفتان على الأقل لبدء التصحيح:

  1. ptrace(PTRACE_TRACEME, 0, NULL, NULL) أصل العملية الحالية مصحح أخطاء لها. لا حاجة إلى مساعدة من الوالد ؛ ينصح man بلطف: "ربما لا ينبغي أن تقدم العملية هذا الطلب إذا كان الوالد لا يتوقع تتبعه." (في مكان آخر من الرجال ، هل رأيت عبارة "ربما لا ينبغي أن" ؟) إذا كانت العملية الحالية تحتوي بالفعل على مصحح أخطاء ، فستفشل المكالمة.
  2. 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); } /*      ptrace,    . */ /* *  ,      *  ,      . *    --  API  ptrace! */ /*       PTRACE_SYSCALL. */ } /* (argc, argv) --    ,    . */ 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 ، فسوف يموت الوالد الذي تم تصحيحه من قبله بوفاته.

أنت الآن تعرف كيفية تنفيذ زوج من العمليات التي ، عند تلقي أي إشارة ، تتجمد إلى الأبد وتموت معًا فقط. لماذا قد يكون هذا ضروريًا في الممارسة ، لن أشرح :-)

Source: https://habr.com/ru/post/ar439882/


All Articles