Глава 12. TSR-программы (резидентные).

- Я извиняюсь, - заговорил он по­дозрительно, - вы кто такой бу­дете? Вы - лицо официальное?

- Эх, Никанор Иванович! - заду­шевно воскликнул неизвестный. Что такое официальное лицо или

неофициальное? Все зависит от того, с какой точки зрения смот­реть на предмет, все это, Ника­нор Иванович, условно и зыбко.

М.А. Булгаков Мастер и Маргарита.

В данной главе будет рассмотрено явление, которое в операционной системе MS DOS играетроль многозадачности. С истинной многозадачностью мы столкнемся, когда будем рассматривать программирование в операционной системе Windows. Многоза­дачность же, рассматриваемая в этой главе, может быть названа «нелегальной».

Термин TSR происходит от английской фразы Terminate State Resident, т.е. "завер­шить и остаться резидентным". Резидентная программа, находясь в памяти, выполня­ет свои функции через перехваченные прерывания. Связь с резидентной программой также осуществляется посредством прерываний. Часто такие программы играют роль драйверов - программ поддержки, каких-либо внешних устройств. Наиболее часто употребляемые резидентныедрайверы обеспечивают русифицированную работу эк­рана и клавиатуры или поддерживают работу мыши. Популярны резидентные пере­водчики и различные резидентные детекторы, позволяющие на ранней стадии обна­руживать появление вируса.

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

держится значительная часть как чисто системной, так и прикладной программной поддержки в операционной системе MS DOS.

2. При написании любой программы нужно придерживаться определенных пра­вил, а ошибка в программе приводит к полной и частичной ее неработоспособ­ности.

3. Многие проблемы, рассмотренные в настоящей главе, носят общий характер для любых многозадачных сред.

Нижемы попытаемся изложить основные положения, которые необходимо знать при написании резидентных программ.

I. Как программа может остаться резидентной.

Существует два легальных документированных способа оставить программу ре­зидентной в памяти. Это прерывание 27Н, которое используется для СОМ-программ, и функция DOS 31Н, которая может использоваться как СОМ так и ЕХЕ-программа-ми. Начнем по порядку.

Прерывание 27Н.

Вход: DX - адрес первого байта за резидентной частью программы; CS-на начало PSP.

Отсчет ведется от начала PSP. Если NORES - метка начала нерезидентной части программы, то фрагмент, оставляющий программу в памяти, будет следующим:

LEA DX,NO_RES

INT 27H

Как видим, все легко и просто. Прерывание 27Н стандартным образом возвращает управление DOS с восстановлением векторов 22Н, 23Н, 24Н.

Функция ЗШ. АН-31Н,

AL - код выхода,

DX-число параграфов памяти, оставляемыхрезидентно. Отчет ведется от начала PSP.

Данная функция хороша тем, что размер оставляемого блока для нее не важен. Поэтому ею могут пользоваться ЕХЕ-программы. Конечно, в этом случае нужно пра­вильно рассчитать длину оставляемого блока.

Рассмотрим пример (Рис. 12.1). Советую обратить внимание на следующие мо­менты. В данном примере тип выравнивания сегментов (см. главу 13) - PARA, т.е. сегменты должны начинаться на границе параграфа. Поэтому мы добавляем кдлине каждого сегмента 16. Конечно, может оказаться, что мы зарезервируем несколько боль­ше байт, чем нужно (если длина сегмента окажется кратной но зато не потеряем ни одного байта. Второе, мы считаем длину каждого сегмента отдельно, т.к. сумма длин (в байтах) может оказаться больше 64 К. Наконец, мы используем здесь оператор ассемблера SIZE, который определяет длину указанного операнда в байтах. Хотя, ес­тественно, можно было бы обойтись и без него.

Имейте в виду, что, когда запускается программа, ей от родительского процесса передается окружение - область памяти, содержащей строки, помещаемые туда ко­мандами SET, PATH, PROMPT. Длина окружения может достигать 32 К. Если Ваша программа останется в памяти резидентной, то вместе с ней в памяти останется и окружение. мы остановимся    этой проблеме.

; сегмент данных DSEG SEGMENT

BOFER ЕВ 2000 ШР(?)

Pl DD ? DSEG ENDS

;сегмент

SSEG SEGMENT STACK ST     DW 100 DUP(?) SSEG ENDS /сегмент кода CODE SEGMENT

ASSUME CS:CODE, DS:DATA, SS:SSEG

BEGIN:

;все, что ниже в памяти не останется NO_RES:

M3V АХ, SIZE BUFER+SIZE Р1+16 ; сегмент данных

MOV ВХ, SIZE ST+16 ; сегмент стека

M3V ОХ,Ю_КЕЗ-ВЕС1Ы+1б ; часть сегмента кода

MDV CL,4

SHR AX,CL    ; делим на 16

SHR ВХ,СЪ

SHR DX,CL

ADD АХ,ВХ

ADD DX,AX

ADD DX,10H    /учтем PSP - ЮН параграфов

MOV AX, 3100H

. INT 21H

CODE ENDS

ЕЮ BEGIN

Рис. 12.1. Пример использования функции 31H для ЕХЕ-программы.

Итак, мы познакомились с легальными способами того, как программа может быть оставлена памяти. Если Вы наберетесь терпения, то в главе Пузнаете, как это делают компьютерные вирусы. На Рис. 12.10 представлена резидентная программа, остающа­яся в памяти по одному из вирусных способов.

II. Перехват прерываний.

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

В том случае, если после вызова прерывания Ваша программа должна еще что-то делать (см. ниже) нужно быть крайне осторожным. Может оказаться, что данное пре­рывание будет возвращать в регистрах некоторую информацию, которая не должна потеряться. Особенно это касается прерываний 21Н, 13Н, 25Н, 26Н, 16Н. В случае регистра флагов этот вопрос может быть решен с использованием RET 2 вместо IRET (ниже будут приведены несколько фрагментов). Что касается других регистров, то здесь надо пользоваться простыми и надежными правилами:

1) Перед вызовом прерывания все необходимые регистры должны содержать то же, что они содержали при входе в Вашу процедуру.

2) Перед выходом из Вашей процедуры, эти регистры должны содержать то же, что они содержали при возврате в Вашу процедуру из прерывания.

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

В случае аппаратных прерываний, таких, как 8Н, 9Н, правило значительно упро­щается, т.к. эти прерывания не меняют содержимое регистров. В этом случае в начале процедуры следует сохранить содержимое всех используемых регистров, а пе­ред выходом восстановить это содержимое (PUSH АХ/РОРАХи т.д.).

В заключение этого раздела сошлюсь на Рис. 12.2 и Рис. 12.3, которые иллюстри­руют сказанное выше.

; процедура обработки прерывания INT_N PROC FAR

; сохраняем нужные регистры,   в том числе и регистр флагов PUSHF PUSH AX

/выполняем необходимую работу

/восстанавливаем регистры

ГОР АХ POPF

/вызываем прерывание

CALL DWORD PTR CS:OLD_INT_N

/сохраняем нужные регистры PUSHF

PUSH AX

/выполняем необходимую работу

/•восстанавливаем регистры

гор АХ горе RETF 2 INT_N ENDP

Рис. 12.2. Схема 1 обработки прерывания резидентной программой.

/процедура обработки прерывания INT_N PROC EAR

/сохраняем нужные регистры,   в том числе и регистр флагов PUSHF PUSH АХ

/выполняем необходимую работу '

регистры

POP АХ ГОРЕ

прерывание JMP DWORD PTR CS:OLD_INT_N INT_N ENDP

Рис. 12.3. Схема 2 обработки прерывания резидентной программой.

III. Как обнаружить себя в памяти.

Если в Вашей программе не реализован механизм обнаружения собственной ко­пии в памяти, то при повторном запуске она снова останется в памяти и т.д. Согласи­тесь, что это не совсем удобно, а главное непрофессионально. Существует несколько способов обнаружения копии в памяти. Рассмотрим все по порядку:

1. Этот способ я назвал бы так: "внешняя привязка к перехваченному вектору пре­рывания". Суть этого метода заключается в том, что в теле программы помещается некоторый признак - обычно это слово. Далее делается предположение, что, посколь­ку резидентной программой был перехвачен определенный вектор, то он должен быть направлен в тело программы. Таким образом, мы получаем адрес входа в процедуру прерывания и по этому адресу ищем признак присутствия. На Рис. 12.4 представлен фрагмент TSR-программы с признаком присутствия. На Рис. 12.5 представлен фраг­мент той же программы, определяющий наличие своей копии в памяти. Для опреде­ленности приведен пример с перехватом прерывания 8. Подчеркну, что оба фрагмента относятся к одной и той же программе.

PRIZN DW АВ12Н ЮТ 8   PROC FAR

INT_8 ENDP

Рис. 12.4. Фрагмент TSR-программьс признаком присутствия.

M3V АХ,3508Н

INT 21H

СМ? ТОЮ PTR ES: [ВХ-2] ,PRIZN ,-проверябм на присутствие

Рис. 12.5. Фрагмент TSR-программы, определяющей свое присутствие в памяти.

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

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

В DOS существует прерывание которое, по сути, предназначено именно

для целей взаимодействия резиденттгых программ друг с другом, в том числе и для обнаружения себя в памяти. Это прерывание используют утилиты DOS: PRINT, ASSIGN, SHARE. Функции же 80H-FFH предоставлены пользователю (функции, ес­тественно, передаются через АН). Здесь, правда, существует одна не слишком при­ятная проблема. Могут совпасть номера функций, используемых разными програм­мами (и такие случаи уже были). Выйти из этого положения можно следующим спо­собом: Ваша программа должна реагировать не просто на посланный код, а, скажем, на последовательность кодов. Тем самым можно свести почти до нуля вероятность совпадений.

Заключая рассказ о данном методе, предлагаю (Рис. 12.6 -12.7) фрагменты про­граммы, иллюстрирующие это метод. Для определенности взято прерывание 16Н.

INT16 PRCC

СМР АН,20Н JNZ CONT

MDV АХ, 789АН ; код присутствия M3V ВХ, CS ; сегментный адрес

IRET

CONT:

JMP ШЭРХ) PTR CS:0LD16 INT16 ENDP

Рис. 12.6. Фрагмент, передающий код возврата.

M3V АН,20Н ПЧГ 16Н CMP АХ, 789АН

J2 YES ; программа в памяти

Рис. 12.7.Фрагмент, определяющий наличие программы в памяти.

3. Данный метод наиболее сложен, но и при правильной его реализации, это са­мый надежный подход. В конце главы будет приведена программа, в которой этот ме­тод будет реализован.

В главе 2 сообщалось о блоках памяти, которыми оперирует DOS. Резидентной программе также отводится свой блок. Метод обнаружения резидентной программы в памяти заключается в том, чтобы сканировать все занятые блоки на предмет обнару­жения программы. Сканировать можно все блоки подряд, но можно и сделать это бо­лее аккуратно. Оказывается, в заголовке блока содержится параграф владельца. В боль­шинстве случаев это PSP программы. Причем PSP может находиться в другом блоке памяти. Вот здесь и надо провести проверку. В комментарии к примеру на Рис. 12.8 сказано об этом более подробно. Начало PSP можно обнаружить по команде INT20H, стоящей вначале.

IV. Активизация резидентной программы/Проблемы неинтерабельности.

Щелкни кобылу в нос - она мах­нет хвостом.

Козьма Прутков.

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

Более четко (несколько сузив) эту проблему можно сформулировать следующим образом: можно ли во время выполнения какой-либо функции DOS снова запустить функцию DOS? Такая ситуация можетвозникнуть как раз втом случае, eamTSR-npo-грамма активизировалась во время работы функции DOS. Если TSR-программане использует функции DOS, то проблемы нет (есть другие - о них будет сказано позже). MS DOS — неинтерабельная операционная система, т.е. не позволяет запускать себя из себя же. В основном это связано с тем, что DOS устанавливает свой стек, и если войти в DOS вторично, то вторично будет установлен тот же стек и будет использо­ваться та же область данных - последствия этого очевидны.

Если Ваша резидентная программа предполагает использование функций DOS, то передтем, как это сделать, необходимо проверить внутренний флаг DOS. Это можно сделать при помощи недокументированной функции DOS 34Н31 Это функция возвра­щает в ES:[BX] указатель на байт, который равен 1,если выполняется какая-либо фун­кция DOS, и 0 в противном случае. Проверяя этот флаг, Ваша программа должна дож­даться, когда он установится в 0 и только тогда выполнить необходимую операцию или выдать меню (если затем предполагается выполнение "опасных" операций). Ис­пользование данного метода не всегда эффективно, т.к. некоторые программы ожида­ние нажатия клавиши осуществляют посредством функций DOS, т.е. флаг в течение этого ожидания будет равен

Другой подход предполагает перехват и отслеживание прерывания 21Н. При этом, перед тем как выполнять какую-либо функцию символьного ввода, следует проверять буфер клавиатуры и вызывать эту функцию только при появлении символа в буфере клавиатуры. На Рис. 12.8 представлен фрагмент программы, демонстрирующий ска­занное. Обращаю Ваше внимание, что отслеживается функция ОАН буферизирован-ного ввода. Проверка буфера осуществляется при помощи прерывания 16Н.' Задержка делается для получения безопасного "окна", в котором может быть вызвана резидент­ная программа. Заметьте, что выход из прерывания осуществляется по RETF2. При

активизирована.

Сама эта функция безопасна для выполнения.

Иногда для проверки наличия в буфере символа используют функцию ОВН.

INT21 PROC far STI

■ cmp АН,0АН jz A1Z

JMP   short A2ZA1Z: push CX push ax

a3z:

MOV   CX,OFFFFH

a4z:

MDV AH,1

loop a4z int 16h jz a3z POP ax pop CX

a2z:

MOV CS:FLAG_21,1

pushf

call ГЖШ PTR cs: [OFF_21] MOV CS:FLAG_21,0

RETF 2

INT21 ENDP

Рис. 12.8. Отслеживание прерывания 21H.

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

В качестве еще одной точки входа можно использовать прерывание Извест­но, что DOS вызывает это прерывание перед вызовом функций символьного ввода-вывода. Причем, когда вызывается это прерывание, система работает в безопасном режиме и можно активизировать резидентную программу. Как использовать это пре­рывание — см. Рис. 12.9 и комментарий к нему.

Для повышения безопасности Вашей программы следует, кроме 21-го прерыва­ния, отслеживать еще 13-е, 25-е и 26-е прерывания. Эти прерывания также небезопасны и могут вызываться некоторыми программами, минуя функции DOS (см. Рис. 12.9

и комментарий к нему).

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

V. Использование памяти.

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

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

2. Использовать внешнюю память. Вместо того чтобы писать данные в память, они записываются на диск. Но как быть в том случае, если обмен данных должен про­исходить быстро? Выберете область памяти, которая Вам удобна (только несистем­ную) и которая была свободна при запуске программы. При активизации TSR-про-грамма должна сбросить эту область памяти на диск. Теперь она может использовать память по своему усмотрению. При выходе из активного состояния область памяти должна быть восстановлена.

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

3. Использовать дополнительную и расширенную память. В главе 22 будет под­робно рассказано об этих видах памяти. Разумеется, и здесь возникает проблема со­вместного использования памяти несколькими программами. При запуске резидент­ная программа может зарезервировать часть дополнительной или расширенной памя­ти, а затем при активизации использовать ее.

VI. Конфликтные ситуации.

Рассмотрим некоторые конфликтные ситуации, которые могут возникнуть при работе с резидентными программами.

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

2. Будьте аккуратны со стеком. Если Ваша программа использует операции со сте­ком (к таковым относится и вызов процедуры), то лучше использовать свой стек, т.к. памяти может не хватить. Активизация Вашей программы может произойти в момент работы другой, при этом стек последней может быть в данный момент на исходе. Лич­но я для стека использую Р$Р, что удобно и вполне достаточно (см. Рис. 12.9). Впро­чем, Р$Р можно использовать и более рационально. Об этом будет сказано ниже.

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

3. Активизация резидентной программы может произойти во время работы дру­гой. Некоторые программы (к таким относятся многие программы фирмы Микрософт) перехватывая прерывание клавиатуры, реагируют на нажатие клавиш непосредствен­но через это прерывание. Если активизация резидентной программы предполагает работу с меню, то следует раз перенаправить вектор прерывания клавиатуры на Вашу процедуру обработки. Если этого не сделать, то Ваше меню не будет нормально работать или же произойдет "зависание". При выходе из активного состояния вектор, естественно, следует восстановить.

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

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

Этого часто не делают даже довольно уважаемые программы. мы, которая работала на момент активизации (см. главу 8). В этом случае может воз­никнуть маловероятная ситуация, когда описателей просто не хватит. Решитьданную проблему можно следующим образом: получить адрес PSPпрерванной задачи и со­хранить его (функция DOS 62Н), Установить адрес своего PSP (функция DOS 50Н недокументирована). Перед выходом из программы следует восстановить адрес ста­рого PSP (функция 50Н).

6. Также маловероятна (но возможна) следующая ситуация. Некоторые програм­мы после выполнения функции DOS вызывают функцию 59Н для получения расши­ренного кода ошибки. Резидентная программа может активизироваться перед вызо­вом этой функции. Если она также использует какие-то функции DOS, то код расши­ренной ошибки может измениться. Чтобы этого не случилось, следует предваритель­но получить код при помощи функции 59Н. Перед выходомже из активного состояния код расширенной ошибки должен быть восстановлен (функция 5DH, подфункция 5 легализована в DOS 5.0).   ... - .

7. Проблема неинтерабельности MS DOS. О том, как решить данную проблему, мы ужеговорили. Можно использовать и другой подход. Еслибымы знали, где расположена область системных стеков и ее размер, то могли бы сохранить эту область перед вызовом функций DOS, а затем при выходе из резидентной программы восстановить эту область. Данные об этой области можно получить при помощи функции 5DH (подфункция 6).

Вызов:

M3V AX,5D06H INT 21H

Выход:

DS:SI - начало области

СХ - размер области

VII. Пример резидентной программы.

Здесь приводится пример резидентной программы, которая, возможно, даже ока­жется для Вас полезной. Эта программа заносит копию экрана в файл SCR.EEN.TXTb текущей директории33. При вторичном запуске программа удаляет свою резидентную копию из памяти. Рассмотрим теперь основные моменты, на которые, на мой взгляд, следует обратить внимание.

К Обнаружение резидентной копии в памяти осуществляется посредством про­смотра занятых блоков памяти (процедура SCAN). Предполагается, что PSP начинает­ся сразу после заголовка блока. Конечно, блок может быть занят и окружением, но просмотр лишних блоков не мешает программе выполнять свои функции. Поиск пер­вого МСВ осуществляется через функцию 52Н прерывания 21Н.

2. Перед тем как остаться резидентной, программа освобождает блок памяти, за­нятой окружением (процедура FREEJENV).

То, что в [7] приводится пример программы, выполняющей похожие функции, - чистое со­впадение. С этой статьей я познакомился уже после написания программы в данной главе. Впрочем, легко увидеть, что программы совершенно разные.

3. При восстановлении векторов их старые значения находятся по смещению от начала PSP (процедура RE_VECT). Замечу, ЧТО эти смещения фактически совпадают со значением соответствующих меток. Используя числовые смещения, я хотел под­черкнуть лишний раз, что старые значения векторов берутся из резидентного модуля.

4. Обратите внимание на процедуры обработки прерываний. Особенно это касает­ся прерываний 25Н и 26Н. Они оставляют в стеке лишнее слово. Этим объясняется появление в этих процедурах команды POP SI. Не страшно, что будет испорчено со­держимое SI, ведь в руководстве сказано что, прерывания 25Н/26Н портят содержи­мое всех регистров, кроме DS, ES, SS, SP.

5. В программе практически не используется стек процесса, из которого активизи­руется программа. Программа устанавливает свой Стек на конец PSP (старшие адреса).

6. Копирование экрана производится В два приема. Вначале делается мгновенный снимок в буфер, находящийся внутри программы. Сброс же буфера В файл происхо­дит в безопасный момент либо из прерывания по времени, прерывания.

CODSEG SEGMENT ASSUME CS:CODSEG ORG 100H

BEGIN:

JMP BEG

PRIZN    Ш   OF034H ; смещение 103Н от начала PSP ; старые векторы прерывания

/Смещение от начала PSP

OLD09O

Ш

? ,

+105Н

OLD09S

Ш

?

+107Н

OLD08O

Ш

?

+109Н

OLD08S

Ш

?

+10ВН

0LD210

ш

?

+10DH

0LD21S

да

?

+10FH

0LD130

да

?

;+ШН

OLD13S

да

?

+113Н

0LD250

да

?

+115Н

OLD25S

да

? ,

+117Н

0LD260

да

?

+119Н

0LD26S

ОТ

?

+11ВН

0LD280

Ш

?

•+11DH

0LD28S

да

? ,

+11FH

OLD240

да

?

 

OLD24S

ОТ

?

 

OLD230

да

?

 

0LD23S

да

?

 

Р1

ЕВ   0 ;

запрос был

Р2

DB   0    ; буфер готов

РЗ        DB   О    /признак обработки ; признаки работы прерьшаний Р21      DB  0    / 21Н Р13      DB  0    / 13Н Р25      ЕВ   0    / 25Н Р26      DB   0    ; 26Н /буфер для хранения копии экрана BUFER    DB   2050 ШР(?) /для временного хранения регистров _АХ    ВД ?

ES Ш ? _SS       Ш ?

_sp ш    ? ;

/имя файла для записи копии экрана FILE      DB "SCREEN.TXT", О

/область резидентных процедур прерывания клавиатуры INT09 PROC FAR

CMP CS:P1,1   /запрос был

JZ YES

MOV CS:_AX,AX MOV CS:_ES,ES IN    AL,60H

CMP   AL,15    /не TAB ли?

JZ TAB

MOV AX,CS:_AX

JMP    ЯОТ YES

TAB:

MOV AX,0

MOV ES,AX

test byte PTR ES: [417H],4 JZ NO_CTRL

MOV запрос на выполнение

m_CTKL:

MOV AX,CS:_AX

MOV ES,CS:_ES

YES:

jmp Ш0Ю ptr CS: [OLD09O] INT09 ENDP /прерывание тайьера

FAR

/выполняем стандартную операцию PUSHF

CALL DWORD PTR CS: [OLD08O] CMP CS:P3,1

JNZ NO_WORK

JMP WORK NO_WORK:

MOV CS:P3,1 /устанавливаем свой стек,

MOV   CS: AX,AX

направленный на PSP

MOV MOV MOV MOV MOV

MOV

/сохраняем регистры

CS:_SS,SS CS:_SP,SP AX,CS SS,AX SP,100H AX,CS:_AX в

PUSH

AX

PUSH

BX

PUSH

ex

PUSH

DX

PUSH

SI

PUSH

DI

PUSH

ES

PUSH

DS

CMP

CS:P1,1

JNZ

N01

CMP

CS:P2,1

JZ

N02

CALL

VIDBUF

MOV

CS:P2,1

N02:

 

/проверка флагов

CMP

CS:P13,1

JZ

N01

CMP

CS:P25,1

JZ

N01

CMP

CS:P26,1

JZ

N01

CMP

CS:P21,1

JZ

Ю1

CALL

BUFJDISK

MOV

CS:P1,0

MOV

CS:P2,0

новом стеке

/был ли запрос /не готов ли буфер /копируем экран в буфер

/сбрасываем буфер на диск

N01:

/восстанавливаем регистры ■ POP   DS . ЮР ES

ЮР DI ЮР SI ЮР DX ЮР СХ ЮР ВХ ЮР АХ ; восстанавливаем старый стек MOV SS,CS:_SS MOV SP,CS:_SP MOV CS:P3,0

WORK:

IRET INT08 ENDP /прерывание 21 INT21 PROC EAR

STI

MOV CS:P21,1 PUSHF

CALL DWORD PTR CS: [0LD210]

MOV CS:P21,0

RETF 2 INT21 ENDP /прерывание 13 INT13 PROC EAR

MOV CS:P13,1

PUSHF

CALL DWORD PTR CS:[0LD130]

MOV CS:P13,0

RETF 2 INT13 ENDP ■ /прерывание 25 INT25 PROC FAR

MOV CS:P25,1

PUSHF

CALL DWORD PTR CS: [0LD250]

POP SI /убираем лишнее слою

MOV CS:P25,0

/при выкоде из процедуры одно лишнее слово остается в стеке

RETF

INT25 ENDP /прерывание 26 INT26 PROC FAR

MOV CS:P26,1

PUSHF

CALL DWORD PTR CS: [OLD260]

ЮР SI / убираем лишнее слово

MOV CS:P26,0

;при выходе из процедуры, одно лишнее слово остается в стеке

RETF INT26 ENDP

/•прерывание 28 - еще одна точка входа в процедуру BUF_DJ.SK INT28 PROC FAR PUSHF

CALL СТОЮ PTR CS: [OLD280] CMP CS:P2,1 JNZ NET MOV CS:P3,1

CALL

ЮV CS:P1,0

mov CS:P2,0 mov CS:P3,0

NET: ■

IRET

ENDP

/обработчик критических ошибок PROC FAR MOV AL, 3

IRET

ENDP

/обработчик прерывания 23Н

INT23 PROC FAR '

IRET

ENDP

/Другие процедуры

/ИЗ видеопамяти в буфер

PROC

/копируем экран в буфер

CLI

LEA DI,CS:BUFER XOR   SI, SI MOV AX,0B800H MOV ES,AX MOV CX,2000 XOR BL,BL

LO02:

MOV AL,ES:[SIJ MOV CS:(DI],AL

INC DI

ADD SI,2

INC BL ;в конце строки 13,10

CMP BL, 80

JMZ N03

XOR BL,BL    ■ ' ■

MDV BYTE PTR CS: [DI], 13

INC DI

mov byte ptr CS:[DI], 10

INC DI

N03:

LOOP L002 STI

RET

VID_BUF ENDP

;из буфера на доек BUFJDISK PROC

PUSH AX PUSH BX

push ex '

PUSH DX PUSH ES PUSH DS PUSH CS

ЮР DS

/устанавливаем обработчик критических ошибок MDV АХ,3524Н

INT 21H

MDV CS:OLD24S,ES MDV CS:OLD240,BX MDV AX,2524H DEA DX,CS:INT24

INT 21H

MOV   AX,3523H

INT 21H

MDV CS:OLD23S,ES MDV CS:OLD230,BX MDV AX,2523H LEA DX,CS:INT23

INT 21H ; открываем файл (file) mdv   АН, 3CH LEA DX,CS:FILE XOR CX,CX

ЮТ 21H

JC EX

MOV ВХ,АХ ; загшсываем в файл содержимое экрана

LEA DX,CS:BUFER

MOV АН,40Н

MOV CX,2050

ЮТ 21Н .•закрываем файл

MOV   АН, ЗЕН­ИТ 21H

ЕХ:

/восстанавливаем обработчик критических ошибок MOV АХ,2524Н MOV DX,CS:OLD240 MOV DS,CS,:OLD24S INT 21H MOV AX,2523H MOV DX,CS:OLD230 MOV DS,CS:OLD23S INT 21H TOP DS ЮР ES TOP DX POP CX ЮР BX ЮР AX RET

BUF_DISK ENDP RES_END:

;не резидентная часть /сканирование памяти

/если программа найдена, то в АХ - 1, иначе АХ - О /ES - сегмент,  где находится программа SCAN PROC

PUSH BX

PUSH DS

PUSH DX PUSH SI

/в SI сегмент тработакщэй программы  (не резидентной)

MOV SI,CS /находим первый блок

MOV АН,52Н

ЮТ 21Н

MOV АХ,1

LOO:

CMP

JZ CONT5

PUSH ES

POP BX

INC BX

;не данный ли это сегмент?

CMP SI,BX

JZ C0NT5 MOV DS,BX ли резидента в MOV BX,CS:PRIZN CMP ТОЮ PTRDS: [103H] ,BX JZ _END C0NT5: ~

последний ли это блок ?

CMP BYLE PTR ES: [0], 'M'

JZ CONT4

XOR AX,AX

JMP SHORT _END

CONT4:

адрес заголовка блока

MOV DX,ES:[3]

MOV BX,ES

ADD BX,DX

INC BX

MOV ES,BX JMP  SHORT LOO

_END:

MOV BX,ES

INC BX

MOV ES,BX

гор si

ГОР DX

LOP DS

ГОР BX RET

SCAN ENDP

векторы SEa?_VECT PROC

PUSH AX Глава12. TSR-программы (резидентные)

PUSH DX , .   • ;

PUSH ES ; . , : л

;сохраним старые векторы

MOV

АХ,3509Н

INT

21Н

MOV

CS:OLD09S,ES

MOV

CS:OLD09O,BX

MOV

AX,3508H

ЮТ

21Н

MOV

CS:OLD08S,ES

MOV

CS:OLD08O,BX

MOV

AX,3521H

ЮТ

21H

MOV

CS:OLD21S,ES

MOV

CS:OLD2lO,BX

MOV

AX,3513H

ЮТ

21Н

MOV

CS:0LD13S,ES

MOV

CS:0LD130,BX

MOV

AX,3525H

ЮТ

21H

MOV

CS:0LD25S,ES

MOV

CS:OLD250,BX

MOV

AX,3526H

ЮТ

MOV

CS:0LD26S,ES

MOV

CS:0LD260,BX

MOV

AX,3528H

ЮТ

21H

MOV

CS:0LD28S,ES

MOV

CS:OLD280,BX

/установим векторы

MOV AX,2509H

LEA DX, ЮТ09

ЮТ 21H

MOV AX,2508H

LEA DX,INT08

ЮТ 2Ш

MOV AX,2521H

LEA DX,INT21

ЮТ 2Ш

MOV AX,2513H

LEA DX, ЮТ13

ЮТ 21H

MOV AX,2525H

LEA DX,INT25

INT 21H

MOV AX,2526H

LEA DX,INT26

INT 21H

MOV AX,2528H

LEA

INT 21H

1ОР ES

ГОР DX LOP BX ГОР   АХ ■ RET

SET_VECT ENDP /восстановить векторы BE VECT PROC

PUSH

AX

 

 

PUSH

DX

 

 

PUSH

DS

 

 

MOV

AX,2509H

 

 

MOV

PTR

ES:

I105H]

MOV

DS,W0RD PTR

ES:

[107HJ

INT

21H

 

 

MOV

AX,2508H

 

 

MOV

PTR

ES:

[109H]

MOV

DS,WORD PTR

ES:

[10BH]

INT

21H

 

 

MOV

AX,2521H

 

 

MOV

PTR

ES:

[10DH]

MOV

DS,TORD PTR

ES:

[10FH]

INT

21H

 

 

MOV

AX,2513H

 

 

MOV

PTR

ES:

[НІН]

MOV

DS,WORD PTR

ES:

[113HJ

INT

21H

 

 

MOV

AX,2525H

 

 

MOV

PTR

ES:

[115H]

MOV

DS,WORD PTR

ES:

[117H]

INT

21H

 

 

MOV

AX,2526H

 

 

MOV

PTR

ES:[119H]

MOV

DS,WDRD PTR

ES:

[11BH]

INT 21H

MOV   АХ,2528Н

MOV   DX,WORD PTR ES: [11DH]

MOV  DS,WORD PTR ES:[11FH]

INT 21H

ЮР DS

ЮР DX

ЮР AX

RET

RE_VECT ENDP ;ОСВОбОДИТЬ память ERASE PROC

MOV AH,49H

INT 21H

RET

ERASE ENDP

;ОСВОбОДИТЬ окружение FREE_ENV PROC

PUSH AX PUSH ES

MOV ES,DS: [2CH]

CALL ERASE POP ES ЮР AX

RET

FREE_ENV ENDP

; ОСНОВНая программа

BEG:

CALL SCAN

CMP AX, 0 JZ NO_PR

CALL

CALL ERASE

LEA DX,TEXT2 MOV AH,9

INT 21H

RET

NO_PR:

CALL

CALL FREE_ENV LEA DX,TEXT1 MDV AH, 9

INT 21H

LEA DX,RES_END

INT 27H

DB установлена в

DB удалена из

CODSEG   ENDS ' '

END BEGIN

Рис.  12.9. Законченный пример резидентной программы. Копирование экрана по CTRL+TAB.

VIII. Еще одна резидентная программа.

Данная резидентная программа сточки зрения ее применения не представляет особо­го интереса. Она перехватывает прерывание 17Н и перенаправляет весь вывод на принтер Причем работает теми программами, которые использу-

ют для вывода непосредственно прерывание 17Н. Те программы, которые делают это че­рез DOS'c-вские функции, работать с ней не будут вследствие неинтерабельности DOS. Я нествил задачей преодолеть эту проблему. Задача была другой. Я хотел лишь показать, как в принципе можно освобождать PSP программы, оставляя в памяти только код.

SEGMENT

ASSUME CS:CODE

ORG 100Н "'

BEGIN:

JMP UST DES   DW ?

PATH DB 'C:\FILE.TXT',0

BUF  DB ?

PR    DB   0 ' .

OLD_VEC   DW ?

CW ? INT17 PROC

подачи сигнала о наличии программы в памяти CMP АН, 78H

PAR

MOVAH,80H

MOV DX,CS

ADD DX, 16 ; (!) Вы поняли?

LEA BP,CS:PR

MOVSI,CS:OIX)_VEC MOV DI,&:OIJD_VEC+2 IRET

PAR:

push DS

push bx push ex

push DX . :

CMP CS:PR,0

PUSH АХ JNZ DDD

; создать при первом обращении MOV АН,ЗСН MDV СХ,О

PUSH CS ЮР DS

. LEA DX,CS:PATH INT 21Н MOV  CS:PR,1 MOV CS:DES,AX

DDD:

; здесь обычная обработка POP AX CMP AH, 0

JNZ KQN /нет посылаемого символа

/запись

MOV АН,40Н MOV СХД MOV CS:BUF,AL MOV BX,CS:DES LEA DX,CS:BUF PUSH CS POP DS INT 21H JC OKI CMP AX,CX JZ KON

OKI:

/открыть, если файл был закрыт . MOV AL,2 MOV AH, 3DH LEA DX,DS:PATH

INT 21H

/указатель на конец

MOV CS:DES,AX

MOV BX,AX

MOV AH,42H

MOV CX, 0

MOV DX, 0

MOV AL, 2

INT 21H

/запись

MOV АН,40H

MOV CX, 1

LEA DX,CS:BUF

INT 2Ш

KON:

MDV POP DX

TOP CX ГОР BX

TOP DS

IRET INT17 ENDP UST:

резидентная часть MDVAH, 78H LNT 17H CMP АН, 80H JNZ DAL

из памяти

CLOSE:

MDV ES,DX

PUSH ES

XOR AX,AX MDV ES,AX CLI

MDVES: [17H*4],SI

MDVES: [17HM+2], DI

ГОР ES

MOV AH,49H

INT 21H

LEA DX,TEXT2

MOV AH, 9

INT 21H

;выход

MDV АН, 4CH

INT 21H

DAL:

MOV PR,0

окружение MOV ES,DS:[2CH] MDV AH, 4 9H

INT 21H

вектор 17Н XOR AX, AX MDV ES,AX MDVAX,ES: [17H*4] MDV CS: OLD_VEC, AX MDVAX,ES: [17H*4+2]

mdv MDV CLI

mdv mdv

; найти сегментный адрес MDV ВХДб'.

MDV AX, OFFSET _END XOR DX,DX

BX INC AX

MDV BX,AX

PUSH CS

ГОР ES

/уменьшить выделенного пространства

MDV

INT 21H

место для PSP MDV ВХДб MDV AH,48Н

INT 21H

MDV адрес нового PSP

/пересылка PSP . MDV SI,0

MDV mdv CLD

■ REP MDVSB / пересьшка /указать новый PSP MDV AH,50H

MDV BX,ES

INT 21H

блок нового PSP MDV AH,49H

INT 21H

/разобраться с блокаж памяти /первый блок

MOV

DEC BX

MDV

MDV WORD PTR DS: [1],0      /блок свободный

mdv

PUSH AX

MOV TORD PTR DS: [3] ,15 /это блок всего 240 байт

• • теперь второй, блок ADD ВХ, 16 MOVDS,BX INC ВХ

MOV BYTE PTR DS:[0], 'M' /заголовок блока

MOV WORD PTR DS:[1],BX /сегментный адрес блока

MOV AX, OFFSET CS:UST

SUB AX,256

MOV CX, 16

XOR   DX, DX

DIV CX

INC AX

MOV DS: [3] ,AX /размер блока

MOV DX, DS ADD DX,AX

INC DX

MOV DS,DX ЮР DX SUB DX, 16

SUB DX,AX

SUB DX, 1 /теперь третий блок

MOV BYTE PTR DS:[0b'M'

MOV  WORD PTR DS: ULO   /блок свободен MOV  DS:[3],DX /размер блока

/выдать сообщение

PUSH CS

ГОР DS

LEA DX,TEXT1

MOV

INT 21H

/выйти обычным образом MOV АН,4СН

INT 21Н

ТЕХТ1 DB  'Программаустановлена в память.',13,10,'$'

DB удалена из

_END:

CODE ENDS

END BEGIN

Рис. 12.10. Пример TSR-программыосвобождающей свой PSP.

Поясню подробнее, как работает данная программа.

Как и программа имеет резидентную и нерезидентную части. Рези-

дентная часть заканчивается меткой UST.

2. Резидентная часть фактически является процедурой обработки прерывания 17Н. Вся информация, идущая через это прерывание, выводится в файл

C:\FILE.TXT. В регистре АН возвращается байт готовности принтера. Пере­менная PR содержит признак того, открыт файл FILE.TXT или нет. Нюанс, однако, заключается том, что мы выходим из программы, которая ранее выводила данные на принтер, то файлы автоматически закрываются. Поэтому мной предусмотрена простая проверка того, открыт в действительности файл или нет. Если нет, то файл снова открывается и указатель переносится в конец.

3. Через прерывание 17Н программа также узнает, загружена ее резидентная часть в память или нет. При этом через регистры возвращается необходимая инфор­мация для выгрузки программы из памяти.

4. Данная программа занимает в памяти всего байта. Такая компактность достигается удалением из памяти PSP. Это наиболее сложная часть програм­мы. Изложу пошаговый алгоритм.

а) "Обрезаем" программу по метке END, т.е. освобождаем память за про­граммой.

б) Выделяем блок памяти в 256 байтдля нового PSP.

в) Копируем PSP в этот блок.

г) Указываем на новый PSP (функция 50Н).

д) Теперь освобождаем этот блок.

е) Далее наиболее тонкая работа. Приходится работать на уровне блоков МСВ.

Участок памяти PSP делается свободным. Затем устанавливается блок, в

котором будет находиться резидентная часть программы. Наконец объяв­ляется свободной оставшаяся часть программы.

ж) Выход осуществляется обычным способом - через функцию 4СН. При этом, естественно, блоки, выделенные не через функции DOS, не освобождаются.

5. Создание нового PSP, вообще говоря, не обязательно. Но такая технология понадобится, если Вы захотите хранить в PSP данные (см. также главу 11, Рис. 11.9) или остаться резидентным по другой схеме: сдвигом кода программы в область PSP.

В виде послесловия к данной главе.

Часто применение TSR-программ может быть весьма неожиданным. Лет 10 назад автору попалась программа, не желающая нормально работать с EGA-монитором. Суть проблемы заключалась втом, что портился русский шрифт. Причем на VGA-адаптере

шло нормально. Не имея времени детально разбираться том, почему так проис­ходит, я написал небольшую резидентную программу, которая по нажатию определен­ной клавиши инициализировала экран через INT 10H (т.е. происходила перезагрузка

шрифтов), а затем удаляла себя из памяти. Проблема была решена за ЗО минут.

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