
على حبري
كتب بالفعل عن اعتراض دعوات النظام عن طريق
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
، فسوف يموت الوالد الذي تم تصحيحه من قبله بوفاته.
أنت الآن تعرف كيفية تنفيذ زوج من العمليات التي ، عند تلقي أي إشارة ، تتجمد إلى الأبد وتموت معًا فقط. لماذا قد يكون هذا ضروريًا في الممارسة ، لن أشرح :-)