Системное программное обеспечение - Учебное пособие (Терехин А.Н.)

4.8      трассировка процессов.

Обзор форм межпроцессного взаимодействия в UNIX был бы не полон, если бы мы не рассмотрели простейшую форму взаимодействия, используемую для отладки — трассировку процессов. Принципиальное отличие трассировки от остальных видов межпроцессного взаимодействия в том, что она реализует модель «главный-подчиненный»: один процесс получает возможность управлять ходом выполнения, а также данными и кодом другого.

В UNIX трассировка возможна только между родственными процессами: процесс-родитель может вести трассировку только непосредственно порожденных им потомков, при этом трассировка начинается только после того, как процесс-потомок дает разрешение на это.

Далее схема взаимодействия процессов путем трассировки такова: выполнение отлаживаемого процесса-потомка приостанавливается всякий раз при получении им какого-либо сигнала, а также при выполнении вызова exec(). Если в это время  отлаживающий процесс осуществляет системный вызов wait(), этот вызов немедленно возвращает управление. В то время, как трассируемый процесс находится в приостановленном состоянии, процесс-отладчик имеет возможность анализировать и изменять данные в адресном пространстве отлаживаемого процесса и в пользовательской составляющей его контекста. Далее, процесс-отладчик возобновляет выполнение трассируемого процесса до следующей приостановки (либо, при пошаговом выполнении, для выполнения одной инструкции).

Основной системный вызов, используемый при трассировке,– это ptrace(), прототип которого выглядит следующим образом:

#include <sys/ptrace.h>

int ptrace(int cmd, pid, addr, data);

где cmd – код выполняемой команды, pid – идентификатор процесса-потомка, addr – некоторый адрес в адресном пространстве процесса-потомка, data – слово информации.

Чтобы оценить уровень предоставляемых возможностей, рассмотрим основные коды - cmd операций этой функции.

cmd = PTRACE_TRACEME — ptrace() с таким кодом операции  сыновний процесс вызывает в самом начале своей работы, позволяя тем самым трассировать себя. Все остальные обращения к вызову ptrace() осуществляет процесс-отладчик.

cmd = PTRACE_PEEKDATA — чтение слова из адресного пространства отлаживаемого процесса по адресу addr, ptrace()  возвращает значение этого слова.

cmd = PTRACE_PEEKUSER — чтение слова из контекста процесса. Речь идет о доступе к пользовательской составляющей контекста данного процесса, сгруппированной в некоторую структуру, описанную в заголовочном файле <sys/user.h>. В этом случае параметр addr указывает смещение относительно начала этой структуры. В этой структуре размещена такая информация, как регистры, текущее состояние процесса, счетчик адреса и так далее. ptrace()  возвращает значение считанного слова.

cmd = PTRACE_POKEDATA — запись данных, размещенных в параметре data, по адресу addr в адресном пространстве процесса-потомка.

cmd  = PTRACE_POKEUSER — запись слова  из data в контекст трассируемого процесса со смещением addr. Таким образом можно, например, изменить счетчик адреса трассируемого процесса, и при последующем возобновлении трассируемого процесса его выполнение начнется с инструкции, находящейся по заданному адресу.

cmd  = PTRACE_GETREGS, PTRACE_GETFREGS — чтение регистров общего назначения (в т.ч. с плавающей точкой) трассируемого процесса и запись их значения по адресу data.

cmd  = PTRACE_SETREGS, PTRACE_SETFREGS — запись в регистры общего назначения (в т.ч. с плавающей точкой) трассируемого процесса данных, расположенных по адресу data в трассирующем процессе.

cmd = PTRACE_CONT — возобновление выполнения трассируемого процесса. Отлаживаемый процесс будет выполняться до тех пор, пока не получит какой-либо сигнал, либо пока не завершится.

cmd = PTRACE_SYSCALL, PTRACE_SINGLESTEP — эта команда, аналогично PTRACE_CONT, возобновляет выполнение трассируемой программы, но при этом произойдет ее остановка после того, как выполнится одна инструкция. Таким образом, используя PTRACE_SINGLESTEP, можно организовать пошаговую отладку. С помощью команды PTRACE_SYSCALL возобновляется выполнение трассируемой программы вплоть до ближайшего входа или выхода из системного вызова. Идея использования PTRACE_SYSCALL в том, чтобы иметь возможность контролировать значения аргументов, переданных в системный вызов трассируемым процессом, и возвращаемое значение, переданное ему из системного вызова.

cmd = PTRACE_KILL — завершение выполнения трассируемого процесса.

 Общая схема использования механизма трассировки.

Рассмотрим некоторый модельный пример, демонстрирующий общую схему построения отладочной программы (см. также Рис. 17):

...

if ((pid = fork()) == 0)

{

ptrace(PTRACE_TRACEME, 0, 0, 0);

/* сыновний процесс разрешает трассировать себя */

exec(“трассируемый процесс”, 0);

/* замещается телом процесса, который необходимо трассировать */

}

else

{

/* это процесс, управляющий трассировкой */

wait((int ) 0);

/* процесс приостанавливается до тех пор, пока от трассируемого процесса не придет сообщение о том, что он приостановился */

 

for(;;)

{

ptrace(PTRACE_SINGLESTEP, pid, 0, 0);

/* возобновляем выполнение трассируемой программы */

wait((int ) 0);

/* процесс приостанавливается до тех пор, пока от трассируемого процесса не придет сообщение о том, что он приостановился */

ptrace(cmd, pid, addr, data);

/* теперь выполняются любые действия над трассируемым процессом */

}

}

Рис. 17 Общая схема трассировки процессов

Предназначение процесса-потомка — разрешить трассировку себя. После вызова ptrace(PTRACE_TRACEME, 0, 0, 0) ядро устанавливает для этого процесса бит трассировки. Сразу же после этого можно заместить код процесса-потомка кодом программы, которую необходимо отладить. Отметим, что при выполнении системного вызова exec(), если для данного процесса ранее был установлен бит трассировки, ядро перед передачей управления в новую программу посылает процессу сигнал SIGTRAP. При получении данного сигнала трассируемый процесс приостанавливается, и ядро передает управление процессу-отладчику, выводя его из ожидания в вызове wait().

Процесс-родитель вызывает wait() и переходит в состояние ожидания до того момента, пока потомок не перейдет в состояние трассировки. Проснувшись, управляющий процесс, выполняя функцию ptrace(cmd, pid, addr, data) с различными кодами операций, может производить любое действие с трассируемой программой, в частности, читать и записывать данные в адресном пространстве трассируемого процесса, а также разрешать дальнейшее выполнение  трассируемого процесса  или производить его пошаговое выполнение. Схема пошаговой отладки показана в примере выше и на рисунке: на каждом шаге процесс-отладчик разрешает выполнение очередной инструкции отлаживаемого процесса и затем вызывает wait() и погружается в состояние ожидания, а ядро возобновляет выполнение трассируемого потомка, исполняет трассируемую команду и вновь передает управление отладчику, выводя его из ожидания .

 Трассировка процессов.

/* Процесс-сын: */

int main(int argc, char **argv)

{       

/* деление на ноль – здесь процессу будет послан сигнал SIGFPE – floating point exception */

return argc/0;

}

 

Процесс-родитель:

#include <stdio.h>

#include <sys/types.h>

#include <unistd.h>

#include <signal.h>

#include <sys/ptrace.h>

#include <sys/user.h>

#include <sys/wait.h>

int main(int argc, char *argv[])

{

pid_t pid;

int status;

struct user_regs_struct REG;

if  ((pid = fork()) == 0) {

/*находимся в процессе-потомке, разрешаем трассировку */

ptrace(PTRACE_TRACEME, 0, 0, 0);  

execl(“son”, ”son”, 0);    /* замещаем тело процесса */

/* здесь процесс-потомок будет остановлен с сигналом SIG_TRAP, ожидая команды продолжения выполнения от управляющего процесса*/

}

/* в процессе-родителе */

          while (1) {

/* ждем, когда отлаживаемый процесс приостановится */

wait(&status);

/*читаем содержимое регистров отлаживаемого процесса */

ptrace(PTRACE_GETREGS, pid, &REG, &REG);

/* выводим статус отлаживаемого процесса, номер сигнала, который его остановил и значения прочитанных регистров  */

printf("signal = \%d, status = \%#x, EIP=\%#x ESP=\%#x ", WSTOPSIG(status), status, REG.eip, REG.esp);

if (WSTOPSIG(status) != SIGTRAP) {

          if (!WIFEXITED(status)) {

/* завершаем выполнение трассируемого процесса */

                   ptrace (PTRACE_KILL, pid, 0, 0);

          }

          break;

}

/* разрешаем выполнение трассируемому процессу */                

ptrace (PTRACE_CONT, pid, 0, 0);

}

}