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

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

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

Кроме того, взаимодействие с резидентной программой является нетри­виальной задачей ввиду отсутствия явно поддерживаемых 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.

Задание

Запрограммировать клавиатурный шпион. Данная программа должна накапливать у себя в памяти вводимые пользователем символы и с заданной периодичностью (например 20 с.) сохра­нять их в файл spy.txt. Перехва­ты­ваемые прерывания – клавиатура и таймер.

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

 

Лабораторная работа №6
Приложения Windows с использованием Win 32 API

Цели работы:

1) изучить особенности приложений Windows и их структуру;

2) изучить функции Win 32 API и Window Messages для создания, выпол­нения, завершения приложений;

3) научиться создавать оконные приложения;

4) ознакомиться со средой программирования.