На Хабре уже писали про перехват системных вызовов с помощью ptrace; Алекса написал про это намного более развёрнутый пост, который я решил перевести.С чего начать
Общение между отлаживаемой программой и отладчиком происходит при помощи сигналов. Это существенно усложняет и без того непростые вещи; ради развлечения можете прочесть раздел BUGS в
man ptrace.Есть как минимум два разных способа начать отладку:
ptrace(PTRACE_TRACEME, 0, NULL, NULL)сделает родителя текущего процесса отладчиком для него. Никакого содействия от родителя при этом не требуется;manненавязчиво советует: «A process probably shouldn't make this request if its parent isn't expecting to trace it.» (Где-нибудь ещё в манах вы видели фразу «probably shouldn't»?) Если у текущего процесса уже был отладчик, то вызов не удастся.ptrace(PTRACE_ATTACH, pid, NULL, NULL)сделает текущий процесс отладчиком дляpid. Если уpidуже был отладчик, то вызов не удастся. Отлаживаемому процессу шлётся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) не возвращает отладчику никакой информации; он просто обещает, что отлаживаемый процесс дважды остановится при каждом системном вызове. Чтобы получать информацию о том, что происходит с отлаживаемым процессом — например, в каком именно системном вызове он остановился — нужно лезть в копию его регистров, сохранённую ядром в struct user в формате, зависящем от конкретной архитектуры. (Например, на x86_64 номер вызова будет в поле regs.orig_rax, первый переданный параметр — в regs.rdi, и т.д.) Алекса комментирует: «ощущение, как будто пишешь на Си ассемблерный код, работающий с регистрами удалённого процессора». Вместо структуры, описанной в
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, обеспечивает, что все дети отлаживаемого процесса будут автоматически браться под отладку в момент выхода из fork(2). Дополнительный тонкий момент здесь в том, что потомки, взятые под отладку, становятся «псевдо-детьми» отладчика, и waitpid будет реагировать не только на остановку «непосредственных детей», но и на остановку отлаживаемых «псевдо-детей». Man предупреждает по этому поводу: «Setting the WCONTINUED flag when calling waitpid(2) is not recommended: the “continued” state is per-process and consuming it can confuse the real parent of the tracee.» — т.е. у «псевдо-детей» получается по два родителя, которые могут ждать их остановки. Для программиста отладчика это означает, что waitpid(-1) будет ждать остановки не только непосредственных детей, а любого из отлаживаемых процессов.Сигналы
(Бонус-контент от переводчика: этой информации нет в англоязычной статье)
Как уже было сказано в самом начале, общение между отлаживаемой программой и отладчиком происходит при помощи сигналов. Процесс получает
SIGSTOP при подключении к нему отладчика, и затем SIGTRAP каждый раз, когда в отлаживаемом процессе происходит что-то «интересное» — например, системный вызов или получение внешнего сигнала. Отладчик, в свою очередь, получает SIGCHLD каждый раз, когда один из отлаживаемых процессов (не обязательно непосредственный ребёнок) «замерзает» или «размерзает».«Разморозка» отлаживаемого процесса осуществляется вызовом
ptrace(PTRACE_SYSCALL) (до первого сигнала либо системного вызова) либо ptrace(PTRACE_CONT) (до первого сигнала). Когда сигналы SIGSTOP/SIGCONT используются ещё и для целей, не связанных с отладкой, то с ptrace могут возникнуть проблемы: если отладчик «разморозит» отлаживаемый процесс, получивший SIGSTOP, то извне это будет выглядеть, как будто сигнал был проигнорирован; если же отладчик не станет «размораживать» отлаживаемый процесс, то и внешний SIGCONT не сможет его «разморозить».Теперь самое интересное: Linux запрещает процессам отлаживать самих себя, но не препятствует созданию циклов, когда родитель и ребёнок отлаживают друг друга. В этом случае, когда один из процессов получает любой внешний сигнал, то он «замерзает» по
SIGTRAP — тогда второму процессу шлётся SIGCHLD, и тот тоже «замерзает» по SIGTRAP. Вытащить таких «со-отладчиков» из дедлока невозможно посылкой SIGCONT извне; единственный способ — убить (SIGKILL) ребёнка, тогда родитель выйдет из-под отладки и «размёрзнет». (Если убивать родителя, то ребёнок умрёт вместе с ним.) Если же ребёнок включит опцию PTRACE_O_EXITKILL, то с его смертью умрёт и отлаживаемый им родитель.Теперь вы знаете, как реализовать пару процессов, которые при получении любого сигнала оба зависают вечным сном, и умирают только вместе. Зачем это может быть нужно на практике, я пояснять не буду :-)
