Смекни!
smekni.com

Управление процессами (стр. 6 из 7)

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

Снятие контрольной точки делается также просто: восстанавливается содержимое по соответствующему адресу и из таблицы выбрасывается соответствующая строка. Можно сделать контрольную точку так, чтобы она работала, к примеру, только 10 раз. Для этого надо добавить в таблицу еще счетчик, из которого при каждом приходе в контрольную точку будет вычитаться единица, и как только он обнулится, контрольная точка будет автоматически снята.

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

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

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

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

Давайте попробуем написать маленький пример. Мы будем писать программу в нотации операционной системы Free BSD.Для других операционных систем надо уточнить функцию ptrace в мануалах.

Отлаживаемый процесс

int main()/* эта программа находится в процессе-сыне SON */

{

int i;

return i/0;

}

Процесс - отладчик

#iinclude <stdio.h>

#iinclude <unistd.h>

#iinclude <signal.h>

#iinclude <sys/types.h>

#iinclude <sys/ptrace.h>

#iinclude <sys/wait.h>

#iinclude <machine/reg.h>

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

{

pid_f pid;

int status;

struct reg REG;

switch (pid=fork()){/* формируем процесс, в pid - код ответа */

case -1: perror("Ошибка fork");exit(25);/* Обработка ошибки */

case 0: ptrace( PT_TRACE_ME, 0, 0, 0); execl("SON","SON",0);

/* В сыне: разрешаем отладку и загружаем процесс SON. При этом произойдет приостановка сына перед выполнением первойкоманды нового тела процесса (а точнее в нем возникнет событие, связанное с сигналом SIG_TRAP) */

default: break;/* В отце: выход из switch*/

}

for(;;) {

wait(&status);/*ждем возникновения события в сыне (сигнала SIG_TRAP)*/

ptrace(PT_GETREGS,pid,(caddr_t)&REG,0);/* Читаем регистры, например, чтобы их распечатать */

printf("EIP=%0.8x&bsol; + ESP=%0.8x&bsol;n",REG.r_eip, REG.r_esp); /*печатаем регистры EIP и ESP */

if(WIFSTOPPED(status)||WIFSIGNALED(status)){ /*проверяем с помощью макросов условия функции wait, и если все нормально, продолжаем разбирать причину остановки программы*/

printf("Сигналы: ");

switch(WSTOPSIG(status)){ /*анализируем код сигнала, по которому произошла остановка */

case SIGINT: printf("INT &bsol;n"); break;/*выводим причину остановки*/

case SIGTRAP: . . . . . . break;

.

.

.

default: printf("%d",WSTOPSIG(status));

}

if (WSTOPSIG(status)!=SIGTRAP) exit(1); /*Если процесс остановился не по SIGTRAP тогда выходим*/

if (WIFEXITED(status)){ /*проверяем случай, если процесс завершился нормально*/

printf("Процесс завершился, код завершения = %d &bsol;n", WEXITSTATUS(status));

exit(0);

}

ptrace(PT_CONTINUE, pid, (caddr_t) 1, 0); /*Если был SIGTRAP, продолжаем процесс*/

} /* End for(;;) */

exit(0);

}

Мы обработали ситуацию остановки и ситуацию нормального завершения процесса. При первой итерации цикла, мы остановимся и дождемся первого сигнала SIGTRAP. По этому сигналу мы выведем нужную нам информацию. Затем, проверим, не закончился ли наш процесс нормально, и т.к. он не может закончиться нормально (он выполняет деление на ноль), то мы обратимся к функции ptrace, которая продолжит процесс с прерванного места. Мы снова попадем на wait. Здесь мы дождемся события, связанного с делением на ноль, и обработаем это событие. В итоге, на стандартный вывод попадут две порции данных: первая - вывод точки прерывания, вершины стека и сигнала для начальной ситуации, и второе - мы получим блок той же информации и код сигнала FPI (Float Point Interrupt)на делении на ноль.

Рекомендуется разобрать этот пример и адаптировать для ваших машин на практикуме.

Лекция №16

Нелокальные переходы

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

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

Для решения такой задачи, мы должны уметь обрабатывать сигналы, возникающие в процессе. Это можно сделать с помощью функции signal и ее возможностей. Кроме того, нам бы хотелось возвращаться в некоторые помеченные в программе точки, не через последовательность входов и выходов из функции, а с помощью безусловного перехода (какбы goto N), потому что механизм обработки сигналов не позволит корректно работать с такой задачей. Можно в принципе написать функцию обработки сигнала, которая при возникновении одного из сигналов делает повторный вызов всей программы. Но это не совсем корректно, т.к. при вызове функции-обработчика сигнала фиксируется состояние стека, и в общем случае система ожидает выхода из функции обработчика через return (при этом система возвращает нас в прерванное место и освобождает стек). Если не будет произведен выход через return, в стеке будет накапливаться ненужная информация, и все это приведет к деградации системы.

Для решения таких задач имеются т.н. нелокальные переходы. Достигаются они с использованием двух функций, которые продекларированы в setjmp.h и имеют следующий интерфейс:

int setjmp(jmp_buf env);

int longjmp(jmp_buf env, int val);

Функция setjmp фиксирует точку обращения к этой функции, т.е. в структуре данных, связанной с переменной env сохраняется текущее состояние процесса (все атрибуты и в т.ч. состояние стека) в точке обращения к setjmp. При обращении к этой функции здесь, setjmp возвращает нулевое значение.

При обращении к функции longjmp(env, val)происходит передача управления на точку, атрибуты которой зафиксированы в структуре env, т.е. в то место, где было обращение к setjmp(env).При этом, после этого перехода setjmp вернет значение val.

Рассмотрим маленький пример.

# include <setjmp.h>

jmp_buf save;

void main(void)

{ int ret;

switch(ret=setjmp(save)){/* при первой проверке ret будет равно нулю */

case 0: printf("....."); /* этот printf выведет некоторую строку */

a(); /* вызов функции a() */

printf(".....");/* этот printf не сработаетникогда */

default: break;

}

}

void a(void)

{ longjmp(save, 1);/* длинный переход на setjmp, ret будет равно 1 */

}

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