Трассировка процессов с помощью Ptrace -- Часть 1.

Автор: Sandeep S
Перевод: Андрей Киселев

Системный вызов ptrace является основой основ для программ-отладчиков, таких как gdb, но все же принципы работы с этим системным вызовом недостаточно хорошо освещены в документации, если не считать, что самой лучшей документацией являются исходные тексты ядра! Я постараюсь продемонстрировать основные приемы при работе с ptrace и некоторые его функциональные возможности, доступные в инструментальных средствах подобных gdb.

1. Введение

ptrace() -- это системный вызов, который дает возможность одному процессу управлять исполнением другого. Он так же позволяет изменять содержимое памяти трассируемого процесса. Трассируемый процесс ведет себя как обычно до тех пор пока не получит сигнал. Когда это происходит, процесс переходит в состояние останова, а процесс-трассировщик информируется об этом вызовом wait(). После этого процесс-трассировщик, через вызовы ptrace, определяет реакцию трассируемого процесса. Исключение составляет сигнал SIGKILL, который само собой разумеется, уничтожает процесс.

Кроме того, можно задать переход трассируемого процесса в состояние останова по определенному событию, которое возникло в ходе его исполнения. Это происходит только в том случае, если процесс-трассировщик установил какие-либо флаги событий в контексте трассируемого процесса. Трассировщик может даже завершить трассируемый процесс, установив при этом код его завершения. После выполнения каких-либо действий трассировщик может завершить отлаживаемый процесс или продолжить его исполнение.

Обратите внимание: Ptrace() очень сильно зависит от аппаратной архитектуры. Приложения, использующие ptrace, очень тяжело переносятся на другие аппаратные платформы.

2. Подробнее

Объявление ptrace() выглядит следующим образом.


        #include <sys/ptrace.h>
        long  int ptrace(enum __ptrace_request request, pid_t pid,
                void * addr, void * data)


Вызову передаются четыре аргумента, где request -- определяет что необходимо сделать. pid -- это ID трассируемого процесса. addr -- это смещение в пользовательском пространстве трассируемого процесса, откуда будет прочитано слово данных и возвращено в качестве результата работы вызова.

Родительский процесс может породить дочерний процесс и выполнять его трассировку посредством вызова ptrace с аргументом request, имеющим значение PTRACE_TRACEME. Процесс-трассировщик может выполнять трассировку уже существующего процесса, используя значение PTRACE_ATTACH для аргумента request. Значения, которые может принимать аргумент request, обсуждаются ниже.

2.Как работает ptrace().

Всякий раз, когда вызывается ptrace, в первую очередь выполняется блокировка ядра. А непосредственно перед возвратом блокировка снимается. Рассмотрим работу ptrace() для различных значений аргумента request.

PTRACE_TRACEME:

Это значение используется при трассировке дочернего процесса. Как уже говорилось выше, любой сигнал (за исключением SIGKILL), как поступивший от системы, так и поступивший через вызов exec, от самого процесса, вынуждает процесс перейти в состояние останова, что позволяет "родителю" определять дальнейший ход развития событий. Единственное, что делает ptrace() в этом случае -- проверяет, установлен ли флаг PT_PTRACED для текущего процесса. Если флаг не установлен, то проверяются права доступа и флаг устанавливается. Все остальные аргументы игнорируются.

PTRACE_ATTACH:

Это значение используется в том случае, если необходимо выполнить трассировку существующего процесса. Единственное замечание: ни один процесс не сможет получить контроль над процессом init или над самим собой. Трассировка этих процессов является недопустимой. Выполнив вызов ptrace, с этим значением аргумента request, процесс становится "родителем" для процесса с ID равным pid. Однако, вызов getpid(), выполняемый "дочерним" процессом, по-прежнему будет возвращать PID реального родителя.

После обычной проверки прав доступа, проверяется -- не производится ли попытка получить контроль над процессом init или над самим собой, не установлен ли флаг PT_PTRACED. Если проблем не возникло, то устанавливается флаг PT_PTRACED. Затем исправляются ссылки трассируемого процесса, например, он удаляется из очереди задач, поле ссылки на родительский процесс изменяется (подлинный родитель остается тем же самым). Процесс снова помещается в очередь и ему передается сигнал SIGSTOP. Аргументы addr и data игнорируются.

PTRACE_DETACH:

Прекращает трассировку процесса. В этот момент принимается решение о прекращении или продолжении работы трассируемого процесса. Отменяются все изменения, произведенные по PTRACE_ATTACH/PTRACE_TRACEME. Через аргумент data устанавливается код завершения. В поле связи, у трассируемого процесса, восстанавливается ссылка на настоящего родителя. Сбрасывается бит пошаговой отладки. И наконец, трассируемый процесс "пробуждается". Аргумент addr игнорируется.

PTRACE_PEEKTEXT, PTRACE_PEEKDATA, PTRACE_PEEKUSER:

При значениях аргумента request PTRACE_PEEKTEXT и PTRACE_PEEKDATA, родительскому процессу возвращается слово, находящееся по адресу addr в адресном пространстве трассируемого (дочернего) процесса. Оба эти значения request приводят к одинаковым результатам. В случае PTRACE_PEEKUSER - читается слово из структуры типа user (см. sys/user.h), размещенной в системном адресном пространстве и соответствующей трассируемому процессу. Аргумент addr задает смещение от начала структуры. Прочитанное слово возвращается через аргумент data. В случае успеха возвращается 0. Исходное значение аргумента data игнорируется.

PTRACE_POKETEXT, PTRACE_POKEDATA, PTRACE_POKEUSER:

При значениях request, PTRACE_POKETEXT и PTRACE_POKEDATA, производится запись значения аргумента data по адресу addr в пространстве трассируемого процесса. Оба эти значения приводят к одинаковым результатам.

В случае PTRACE_POKEUSER, производится запись в структуру типа user, соответствующей трассируемому процессу. Следует быть очень осторожным при работе с этим параметром, поскольку в данном случае мы вторгаемся в область ядра. После выполнения большого количества проверок, ptrace выполняет запись в указанную позицию структуры, при этом доступными для записи оказываются далеко не все элементы структуры. Аргумент addr в данном случае определяет смещение относительно начала структуры.

PTRACE_SYSCALL, PTRACE_CONT:

Обе эти команды активируют трассируемый процесс. В случае PTRACE_SYSCALL дочернему процессу предписывается остановиться на следующем системном вызове. PTRACE_CONT -- просто возобновляет работу трассируемого процесса. И в том и в другом случае, если аргумент data не равен нулю или SIGSTOP, ptrace() передает его процессу как сигнал, который необходимо обработать. При этом ptrace() сбрасывает бит пошаговой трассировки и устанавливает/сбрасывает бит трассировки системных вызовов. Аргумент addr игнорируется.

PTRACE_SINGLESTEP:

Имеет тот же смысл, что и PTRACE_SYSCALL, за исключением того, что трассируемый процесс останавливается после исполнения каждой инструкции. Устанавливает бит пошаговой трассировки. Как и выше, аргумент data содержит код завершения для трассируемого процесса. Аргумент addr игнорируется.

PTRACE_KILL:

Используется для завершения трассируемого процесса. Завершение производится следующим образом. Ptrace() проверяет -- "жив" ли трассируемый процесс, затем устанавливает код завершения дочернего процесса в значение sigkill, сбрасывает бит пошаговой трассировки и активирует дочерний процесс, который в соответствии с кодом завершения прекращает свою работу.

2.2 Аппаратно-зависимые значения для аргумента request

Описанные выше значения аргумента request являются аппаратно-независимыми. Значения, описываемые ниже, позволяют читать/изменять регистры процессора дочернего процесса, а потому очень тесно связаны с аппаратной реализацией системы. Набор доступных регистров включает в себя регистры общего назначения и регистры FPU (арифметического сопроцессора).

PTRACE_GETREGS, PTRACE_GETFPREGS, PTRACE_GETFPXREGS:

При этих значениях request, после обычной проверки прав доступа, производится копирование значений регистров общего назначения, регистров с плавающей точкой, дополнительных регистров с плавающей точкой дочернего процесса в переменную родительского процесса data. Копирование выполняется с помощью функций getreg() и __put_user(), аргумент addr игнорируется.

PTRACE_SETREGS, PTRACE_SETFPREGS, PTRACE_SETFPXREGS:

При этих значениях аргумента request выполняется запись в регистры процессора трассируемого процесса. В данном случае доступ к отдельным регистрам ограничивается. Значения регистров берутся из аргумента data. Аргумент addr игнорируется.

2.3 Возвращаемые значения системного вызова ptrace()

В случае успеха ptrace() возвращает ноль. В случае возникновения ошибки -- возвращается значение -1, а код ошибки -- в переменной errno. Поскольку при выполнении операций PEEKDATA/PEEKTEXT, даже в случае успеха может быть возвращено значение -1, то лучше выполнять проверку на наличие ошибки по переменной errno. Коды ошибок могут быть следующими

EPERM : Отсутствие прав доступа.

ESRCH : Требуемый процесс не найден или уже трассируется.

EIO : Недопустимый код запроса (request) или задан недопустимый адрес памяти для чтения/записи.

EFAULT : Была сделана попытка записи информации в область памяти, но скорее всего эта память не существует или недоступна.

К сожалению, зачастую ошибки EIO и EFAULT порождаются практически идентичными ситуациями, из-за чего очень сложно интерпретировать разницу между ними.

3. Небольшой пример.

Если вы нашли описание аргументов чересчур сухим, не отчаивайтесь. Далее я попробую представить ряд маленьких программ, которые проиллюстрируют все вышесказанное.

Вот первый пример. Здесь родительский процесс подсчитывает число инструкций, выполненных тестовой программой, которая запускается как дочерний процесс.

Тестовая программа выводит содержимое текущего каталога и подсчитывает количество затраченных машинных инструкций.



#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <syscall.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <errno.h>


int main(void)
{
        long long counter = 0;  /*  Счетчик машинных инструкций     */
        int wait_val;           /*  значение, возвращаемое потомком */
        int pid;                /*  pid потомка                     */

        puts("Минутку терпения");

        switch (pid = fork()) {
        case -1:
                perror("fork");
                break;
        case 0: /*  запуск дочернего процесса        */
                ptrace(PTRACE_TRACEME, 0, 0, 0);
                /* 
                 *  необходимо, чтобы передать
                 *  управление дочернему процессу
                 */ 
                execl("/bin/ls", "ls", NULL);
                /*
                 *  выполнить программу и заставить 
                 *  потомка остановиться и передать сигнал
                 *  родителю, теперь родитель 
                 *  сможет перейти в PTRACE_SINGLESTEP   
                 */ 
                break;
                /*  завершение дочернего процесса  */
        default:/*  запуск родительского процесса  */
                wait(&wait_val); 
                /*   
                 *   родитель ожидает, пока потомок не остановится 
                 *   на следующей инструкции (execl()) 
                 */
                while (wait_val == 1407 ) {
                        counter++;
                        if (ptrace(PTRACE_SINGLESTEP, pid, 0, 0) != 0)
                                perror("ptrace");
                        /* 
                         *   переход в пошаговый режим
                         *   и активация потомка
                         */
                        wait(&wait_val);
                        /*   ожидание выполнения следующей инструкции  */
                }
                /*
                 * цикл продолжается до тех пор, пока
                 * потомок не завершит работу; wait_val != 1407
                 * младший байт = 0177L и старший = 05 (SIGTRAP)
                 */
        }
        printf("Количество машинных инструкций : %lld\n", counter);
        return 0;
}


Скопируйте текст программы в текстовый редактор, сохраните ее в файл file.c и дайте команды на выполнение:

cc file.c

./a.out

В результате работы программы, на экран будут выведены содержимое текущего каталога и количество затраченных машинных инструкций. Теперь попробуйте перейти в другой каталог и запустить программу оттуда. Сравните полученные результаты. (Обратите внимание, если у вас медленная машина, то вывод может занять довольно продолжительное время). (На P4 1.7 ГГц на это ушло около 7 секунд. Прим.ред.)

4. Заключение

Ptrace() -- это средство отладки программ. Он может использоваться и для трассировки системных вызовов. Родительский процесс может начать трассировку, вызвав сначала функцию fork(2), для запуска дочернего процесса, а затем дочерний процесс может выполнить PTRACE_TRACEME, за которым (как правило) следует выполнение exec(3) (в примере выше -- это программа "ls"). Затем, после выполнения каждой инструкции, родитель может просматривать значения регистров потомка, данные в памяти и влиять на протекание процесса исполнения. В следующей части статьи я приведу пример программы, которая использует различные особенности ptrace(). До скорой встречи!

Sandeep S

Я -- студент последнего курса Правительственного Технического Колледжа в городе Thrissur, штат Kerala, Индия. В круг моих интересов входят FreeBSD, сетевые технологии и Теоретическая Информатика.
Copyright (C) 2002, Sandeep S.
Copying license http://www.linuxgazette.com/copying.html
Published in Issue 81 of Linux Gazette, August 2002

Вернуться на главную страницу