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

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

Основная особенность резидентной программы — событийный характер ее выполнения: вместо предсказуемого регулярного выполнения алгоритма имеют место асинхронные, происходящие в непредсказуемые моменты вре­мени вызовы обработчиков прерываний. При этом их активизация может происходить на фоне любых других программ, находящихся в этот момент в заранее не известном состоянии. Состояние отдельных обработчиков самого резидента также может быть различным. Эти проблемы в целом характерны для параллельно выполняющихся и взаимодействующих программ, а обработ­чики прерываний и использующие их резидентные программы представляют собой элементы параллелизма в изначально однозадачной среде.

Кроме того, взаимодействие с резидентной программой является нетривиальной задачей ввиду отсутствия явно поддерживаемых MS-DOS интерфейсов для такого взаимодействия.

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

Реентерабельность (и пригодность для рекурсий) для обычных подпро­грамм обеспечивается в большинстве языков использованием «автомати­чес­ких» переменных (класс хранения auto в терминологии C/C++): они выделя­ются в стеке заново для каждой «копии» вызова подпрограммы. Однако пол­ностью исключить использование глобальных переменных не всегда возможно (как минимум, регистры процессора являются своего рода «переменными», глобальными для всех программ), а доступное пространство в стеке для резидентных программ всегда ограничено, так как по умолчанию они вынуждены использовать стек той программы, на фоне которой был вызван обработчик. Для резидента можно зарезервировать собственный стек и пере­ключаться на него при каждом вызове обработчика, но тогда сама эта область вместе с обслуживающими ее счетчиками окажется разделяемой между «копиями» вызовов, а динамическое выделение и управление несколькими стеками будет иметь очень сложную реализацию, особенно учитывая общие ограничения на доступную память.

В результате на практике обычно удается сделать реентрабельными лишь относительно простые обработчики. Для более сложных же приходится ограничиваться «частичную» реентерабельностью — повторный вызов возмо­жен и безопасен, но функции при этом не выполняются или выполняются частично. Типична реализация такого подхода с помощью флага активности (фактически простейшего семафора):

is_active DB 0;переменная-семафор

Int_Handler PROC FAR

cmp cs:is_active, 0;проверка семафора

jz work

iret;выход – обработчик уже активен

work:

inc cs:is_active;закрыть семафор

…;функции обработчика

inc cs:is_active;открыть семафор

Int_Handler ENDP

Важно обеспечить атомарность (непрерывность) проверки значения семафора и его изменение, иначе повторное остается вероятность повторного вызова между этими инструкциями. В данном случае непрерывность обеспе­чивается предварительным запретом прерываний перед передачей управления обработчику, но на это можно рассчитывать не всегда. Система команд x86 содержит инструкцию xchg — элементарный, гарантированно непрерываемый обмен двух значений, но воспользоваться им не всегда удобно. Заметим, что проблему представляют также и «вложенные» запреты и разрешения прерыва­ний флагом IF, когда приходится корректно восстанавливать его значение на каждом «уровне».

Также возможно решить проблему повторного вхождения организацией «отложенных» вызовов. Для этого функционал делится между двумя обработ­чиками. Первый («инициирующий») реентерабелен, он лишь проверяет условия выполнения основных функций, и если это в данный момент невозможно, формирует соответствующий запрос и ставит его в очередь. Второй обработчик («исполнительный») активизируется периодически, например, по таймеру, и при наличии запроса начинает его выполнять; если не завершена обработка пред­ыдущего запроса, он пропускает очередной цикла до следующей активизации. В простейшем случае «очередь запросов» может представлять собой флаг или счетчик, указывающий на необходимость выполнения обработчика.

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

Аналогичные проблемы свойственны не только пользовательским про­грам­мам: реентерабельность стандартных «системных» обработчиков в общем случае также не гарантируется. Практически прерывания BIOS реентерабельны или имеют собственную блокировку критических участков, поэтому их вызов предположительно безопасен в любое время (документация этого не гаран­тирует). Прерывания DOS заведомо нереентерабельны.

Наиболее часто требуются обращения к прерыванию DOS int 21h. Так как полностью отказаться от него не всегда возможно, необходимо выбирать моменты для безопасного вызова. Имеются следующие основные возможности.

Контроль флагов InDOS (нахождение в обработчике функций DOS) и CriticalError (нахождение в обработчике критической ошибки). Адрес байта InDOS возвращается функцией int 21h AH=34h (необходимо вызывать заранее, в секции инициализации так как нереентерабельность распространяется и на нее), CriticalError расположен в предыдущем байте памяти либо может быть получен недокументированным вызовом int 21h AX=6D06h. Начальные значения фла­гов — нулевые. Обработчики функций DOS инкрементируют InDOS при входе и декрементрируют при выходе. Обработчик критической ошибки устанав­ливает CriticalError и сбрасывает InDOS.

Обработка прерывания «холостого хода» int 28h позволяет определить период, когда DOS находится в состоянии ожидания ввода-вывода. Внутри обработчика int 28h можно безопасно обращаться к функциям DOS с номерами выше 0Ch независимо от состояния флагов.

В обоих случаях удобной будет описанная выше схема «отложенного вызова»: «исполнительный» обработчик устанавливается на прерывание холостого хода или проверяет возможность безопасного вызова DOS по флагам.

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

Кроме того, обработчики многих функций DOS предполагают, что вызывающая их в данный момент программа является также и текущий с точки зрения системы, однако в случае вызова из резидента это правило нарушается. Так как для идентификатором программы служит адрес её PSP, необходимо, дождавшись возможности безопасного обращения к DOS, в первую очередь сохранить текущий PSP (функция AH=62h) и зарегистрировать в качестве текущего свой (функция AH=50h), а по окончании работы — восстановить сохраненный.

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

– прямое обращение к переменным и процедурам резидентной программы — необходимо знание ее внутренней структуры, то есть требуется или документирование, или наличие отдельной программы, обладающей этим «знанием» (например, транзитный запуск той же программы, из которой устанавливается резидент, с соответствующими ключами);

– захват и использование в качестве программного интерфейса определенных прерываний и/или их функций — универсально и эффективно, но может быть недостаточно гибко и создавать конфликты с другими программами;

– поддержка резидентом «горячих» клавиш — только интерактивное управление, но не интерфейс между программами.

В свою очередь, обнаружение резидента также может быть решено несколькими способами:

– поиск сигнатуры (характерного достаточно длинного значения), содер­жащейся в определенном месте кода программы, путём сканирования памяти;

– обнаружение характерных для этого резидента изменений программной среды, в первую очередь прерываний и/или их функций;

– выделение специальных «диагностических» прерываний и/или их функций.

Наиболее эффективным, но достаточно сложным и трудоёмким решением для взаимодействия с резидентами является унифицированный интерфейса, основанный на использовании специально выделенного для него мультиплексного (или мультиплексорного) прерывания int 2Fh.

Основная идея состоит в наличии у резидента обработчика int 2Fh. Все эти обработчики каскадируются. В ходе установки каждая новая программа получает свободный идентификатор, который запоминается для сравнения. В дальнейшем этот идентификатор передается при обращениях к функциям int 2Fh: опознав его, программа выполняет функцию, иначе передает управление дальше по цепочке обработчиков. Кроме того, программа должна содержать по фиксированному адресу унифицированный блок параметров, которые описывают её и могут быть использованы для управления извне.

Отдельной, достаточно часто встречающейся задачей является деинсталляция резидентных программ. Она распадается на две подзадачи: деактивация (отключение) обработчиков и удаление резидентной части из памяти. MS-DOS не предоставляет готового решения для обоих из них.

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

old_handler_off DW?

old_handler_seg DW?

is_enabled DB?

Int_Handler PROC FAR

cmp cs:is_enabled, 0

jnz work

iret

work:

…;функции обработчика

Int_Handler ENDP

Для отключения обработчика достаточно обнулить флаг разрешения, для включения — записать туда ненулевое значение.

Более сложный, но и более интересный способ — внутренняя «таблица переходов» (на примере единственного обработчика):

act_handler_off DW?

act_handler_seg DW?

old_handler_off DW?

old_handler_seg DW?

; общая часть обработчика – переход по таблице

Int_Handler PROC FAR

jmp dword ptr cs:act_handler_off

Int_Handler ENDP

; рабочая часть обработчика

Handler_Work PROC FAR

call dword ptr cs:old_handler_off

Handler_Work ENDP

Для включения и выключения обработчика в ячейки act_handler записывается точка входа в «рабочую» часть Handler_Work или сохраненный адрес старого обработчика. Этот подход хорош тем, что «таблицу переходов» и устанавли­ваемые в таблицу векторов обработчики могут быть компактно сгруппированы в начале программы, тогда при выгрузке резидента а памяти остаются только они, а «рабочие» функции могут быть отброшены.

Выгрузка резидента из памяти включает в себя освобождение занимаемого им блока памяти и всех выделенных ему ресурсов, включая открытые файлы. Хороший способ — завершение резидента стандартной функцией int 21h AH=4Ch. Однако DOS всегда предполагает завершение текущей программы, поэтому предварительно надо временно переключить адрес текущего PSP на PSP резидента. Альтернатива — выполнять освобож­дение «вручную».

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

Описанные способы подразумевают, что резидентная программа сама обеспечивает свое отключение и выгрузку. Если все это требуется сделать из внешней программы, не имеющей сведений о структуре резидента, то задача существенно усложняется, и возможным становится, как правило, только достаточно грубое «общее» решение: периодические «моментальные снимки» среды, включающие таблицу векторов и карту распределения памяти. На основании этой информации выполняется откат системы к одному из предыдущих состояний. Может использоваться также и постоянный монито­ринг функций распределения памяти и управления векторами прерываний.

Контрольные вопросы

1) Каскадные обработчики прерываний.

2) Проблемы идентификации обработчика прерывания.

3) Мультиплексное прерывание.

4) Проблемы при удалении обработчика прерывания.

5) Проблема реентерабельности обработчиков прерываний.

6) Повторное вхождение в прерывания DOS.


Понравилась статья? Добавь ее в закладку (CTRL+D) и не забудь поделиться с друзьями:  



double arrow
Сейчас читают про: