Приключения с ptrace(2)

в 18:49, , рубрики: api, ptrace, отладка, Разработка под Linux, системное программирование, хитрости

Приключения с ptrace(2) - 1 На Хабре уже писали про перехват системных вызовов с помощью 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)
{
    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, то с его смертью умрёт и отлаживаемый им родитель.

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

Автор: tyomitch

Источник

* - обязательные к заполнению поля