Хабрахабр

[Перевод] Приключения с ptrace(2)

На Хабре уже писали про перехват системных вызовов с помощью ptrace; Алекса написал про это намного более развёрнутый пост, который я решил перевести.

С чего начать

Общение между отлаживаемой программой и отладчиком происходит при помощи сигналов. Это существенно усложняет и без того непростые вещи; ради развлечения можете прочесть раздел BUGS в man ptrace.

Есть как минимум два разных способа начать отладку:

  1. 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»?) Если у текущего процесса уже был отладчик, то вызов не удастся.
  2. 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)
/* Если требуются дополнительные опции для 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 каждый раз, когда один из отлаживаемых процессов (не обязательно непосредственный ребёнок) «замерзает» или «размерзает».

Когда сигналы SIGSTOP/SIGCONT используются ещё и для целей, не связанных с отладкой, то с ptrace могут возникнуть проблемы: если отладчик «разморозит» отлаживаемый процесс, получивший SIGSTOP, то извне это будет выглядеть, как будто сигнал был проигнорирован; если же отладчик не станет «размораживать» отлаживаемый процесс, то и внешний SIGCONT не сможет его «разморозить». «Разморозка» отлаживаемого процесса осуществляется вызовом ptrace(PTRACE_SYSCALL) (до первого сигнала либо системного вызова) либо ptrace(PTRACE_CONT) (до первого сигнала).

В этом случае, когда один из процессов получает любой внешний сигнал, то он «замерзает» по SIGTRAP — тогда второму процессу шлётся SIGCHLD, и тот тоже «замерзает» по SIGTRAP. Теперь самое интересное: Linux запрещает процессам отлаживать самих себя, но не препятствует созданию циклов, когда родитель и ребёнок отлаживают друг друга. (Если убивать родителя, то ребёнок умрёт вместе с ним.) Если же ребёнок включит опцию PTRACE_O_EXITKILL, то с его смертью умрёт и отлаживаемый им родитель. Вытащить таких «со-отладчиков» из дедлока невозможно посылкой SIGCONT извне; единственный способ — убить (SIGKILL) ребёнка, тогда родитель выйдет из-под отладки и «размёрзнет».

Зачем это может быть нужно на практике, я пояснять не буду 🙂 Теперь вы знаете, как реализовать пару процессов, которые при получении любого сигнала оба зависают вечным сном, и умирают только вместе.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть