На Хабре уже писали про перехват системных вызовов с помощью
Общение между отлаживаемой программой и отладчиком происходит при помощи сигналов. Это существенно усложняет и без того непростые вещи; ради развлечения можете прочесть раздел BUGS в
Есть как минимум два разных способа начать отладку:
Эти два метода полностью независимы; можно пользоваться либо одним, либо другим, но нет никакого смысла их сочетать. Важно отметить, что
Запустить дочерний процесс под отладкой при помощи
Без вызова
Когда отладка начата, то каждый вызов
Вместо структуры, описанной в
При этом две остановки отлаживаемого процесса — на входе в системный вызов и на выходе из него — никак не различаются с точки зрения отладчика; так что отладчик должен сам помнить, в каком состоянии находится каждый из отлаживаемых процессов: если их несколько, то никто не гарантирует, что пара сигналов от одного процесса придёт подряд.
Одна из опций
(Бонус-контент от переводчика: этой информации нет в англоязычной статье)
Как уже было сказано в самом начале, общение между отлаживаемой программой и отладчиком происходит при помощи сигналов. Процесс получает
«Разморозка» отлаживаемого процесса осуществляется вызовом
Теперь самое интересное: Linux запрещает процессам отлаживать самих себя, но не препятствует созданию циклов, когда родитель и ребёнок отлаживают друг друга. В этом случае, когда один из процессов получает любой внешний сигнал, то он «замерзает» по
Теперь вы знаете, как реализовать пару процессов, которые при получении любого сигнала оба зависают вечным сном, и умирают только вместе. Зачем это может быть нужно на практике, я пояснять не буду :-)
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
, то с его смертью умрёт и отлаживаемый им родитель.Теперь вы знаете, как реализовать пару процессов, которые при получении любого сигнала оба зависают вечным сном, и умирают только вместе. Зачем это может быть нужно на практике, я пояснять не буду :-)
Prototik
ser-mk
Я так понимаю что бы защитить от отладки свою программу?