logo search
Операционные системы

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

Отцовский процесс формирует дочерний процесс и ожидает его завершения посредством обращения к системному вызову wait(). Дочерний процесс подтверждает право родителя его трассировать (обращаясь к системному вызову ptrace() с кодом cmd = PTRACE_TRACEME и нулевыми оставшимися аргументами). После чего он меняет свое тело на тело процесса, которое необходимо отлаживать (посредством обращения к одному из системных вызовов exec()). После смены тела данный процесс приостановится на точке входа и пошлет сигнал SIGTRAP родительскому процессу. Именно с этого момента начинается отладка: отлаживаемый процесс загружен, он готов к отладке и находится в начальной точке процесса. Дальше родительский трассирующий процесс может делать все те действия, которые ему необходимы по отладке: запустить процесс с точки останова, читать содержимое различных переменных, устанавливать контрольные точки и т.п.

Отладчики бывают двух типов: адресно-кодовыми и символьными. Адресно-кодовые отладчики оперируют адресами тела отлаживаемого процесса, в то время как символьные отладчики позволяют оперировать объектами языка, т.е. переменными языка и операторами языка.

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

Итак, трассируемый процесс исполняется, и управление, наконец, передается на машинное слово по адресу A. Происходит деление на ноль. Соответственно, происходит прерывание, система передает сигнал. И отладчик через системный вызов wait() получает код возврата и «понимает», что в дочернем процессе случилось деление на ноль. Отладчик посредством системного вызова ptrace() читает адрес остановки в контексте дочернего процесса. Далее анализируется причина остановки. Если причина остановки явилось деление на ноль, то возможны две ситуации: это действительно деление на ноль, как ошибка, либо деление на ноль, как контрольная точка. Для идентификации этой ситуации отладчик обращается к таблице контрольных точек и ищет там адрес останова подчиненного процесса. Если в данной таблице указанный адрес встретился, то это означает, что отлаживаемый процесс пришел на контрольную точку (иначе деление на ноль отрабатывается как ошибка).

Находясь в контрольной точке, отладчик может производить различные манипуляции с трассируемым процессом (читать данные, устанавливать новые контрольные точки и т.п.). Далее встает вопрос, как корректно продолжить подчиненный процесс. Для этого производится следующие действия. По адресу A записывается оригинальное машинное слово. После этого системным вызовом ptrace() включаем шаговый режим. И выполняем одну команду (которую только что записали по адресу A). Из-за включенного режима пошаговой отладки подчиненный процесс снова остановится. Затем отладчик выключает режим пошаговой отладки и запускает процесс с текущей точки.

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

Предположим, необходимо просмотреть содержимое некоторой переменной v. Для этого трассируемый процесс должен быть остановлен. По адресу останова можно определить, в какой точке программы произошел останов. На основании информации об этой точке программы можно, обратившись к содержимому базы данных, определить то пространство имен, доступных из этой точки. Если интересующая нас переменная v оказалась доступна, то продолжается работа: происходит обращение к базе данных и определяется тип данной переменной. Если тип переменной v — статическая переменная, то в соответствующей записи будет адрес, по которому размещена данная переменная (этот адрес станет известным на этапе редактирования связей). И с помощью ptrace() отладчик берет содержимое по этому адресу. Также из базы данных берется информация о типе переменной (char, float, int и т.п.), и на основе этой информации пользователю соответствующим образом отображается значение указанной переменной v.

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

Если переменная v — регистровая переменная, то происходит обращение к базе данных, считывается номер регистра, затем идет обращение к сегменту кода и считывается содержимое нужного регистра.

Для записи значений в переменные происходит та же последовательность действий.

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