Написание резидентных программ для MS DOS
В статье описаны основные шаги, используемые при написании резидентных программ на ассемблере для операционной системы MS DOS. В конце статьи подробно разобран пример резидентной программы.
Резидентная программа для MS DOS представляет собой фрагмент кода, постоянно находящийся в оперативной памяти компьютера и вызываемый при возникновении определённых условий. Далее будет показано как написать резидентную программу на ассемблере, постоянно находящуюся в памяти и вызываемую при возникновении в системе прерываний. Сначала рассмотрим определения и основные типы прерываний для процессоров x86.
Прерывание для процессоров x86 представляет собой некоторое событие в системе, нуждающееся в определённой обработке. При возникновении прерывания, за исключением одного случая, выполнение текущей программы прерывается и происходит обработка прерывания. После обработки прерывания продолжается выполнение прерванной программы.
Для процессоров x86 существуют следующие виды прерываний: аппаратные, программные и внутренние прерывания процессора. Аппаратные прерывания, в свою очередь, разделяются на маскируемые и немаскируемые. Маскируемые аппаратные прерывания при определённых условиях могут быть проигнорированны процессором, а немаскируемые прерывания обрабатываются всегда.
Аппаратное прерывание можно определить как запрос от некоторого периферийного устройства (клавиатура, последовательный порт, дисковод и т. д.) на обработку данных этого устройства, управление им или возникновение исключительной ситуации для этого устройства. При возникновении такого запроса выполнение текущей программы прерывается (если это прерывание не замаскировано) и вызывается процедура обработчика прерывания. Обработчик прерывания выполняет необходимые действия для получения данных от периферийного устройства или для управления им и возвращает управление в прерванную программу.
Программные прерывания представляют собой вызов каких-либо функций или сервисов операционной системы и прикладных программ с использованием команды INT XX, где XX - номер прерывания от 0 до 255. Внутренние прерывания процессора возникают при выполнении программой каких-либо операций, вызывающих фатальные ошибки (например, деление на 0, переполнение при делении, выход за границы сегмента и т. д.), а также при использовании режима отладки.
В любом случае, при возникновении прерывания какого-либо типа вызывается обработчик этого прерывания, который представляет собой специальным образом оформленную процедуру. Для аппаратных прерываний обработчик прерывания должен помимо работы с устройством, вызвавшим прерывание, выполнить некоторые операции по управлению аппаратурой механизма прерываний процессора x86.
Рассмотрим процесс написания процедуры обработчика прерывания на ассемблере, вызываемого при возникновении программного прерывания. Общая структура и синтаксис для обработчика программного прерывания:
NAME proc ; 1. сохранение модифицируемых регистров . . . ; 2. инициализациясегментных регистров . . . ; 3. выполнение необходимых действий . . . ; 4. восстановление используемых регистров . . . IRET NAME ENDP |
Идентификатор NAME определяет имя процедуры обработчика, которое может быть любой последовательностью разрешённых в ассемблере символов, но не должно быть служебным или зарезервированным словом.
В секции 1 выполняется сохранение всех регистров, изменяемых в процедуре обработчика. Это необходимо для того, чтобы после возвращения управления в прерванную программу, она получила регистры в том же виде, какими они были до вызова программного прерывания. Если прерывание должно возвращать в вызвавшую его программу некоторые результаты в регистрах, то сохранять значение этих регистров не требуется.
В секции 2 выполняется инициализация сегментных регистров DS, ES или SS для обращения процедуры обработчика прерывания к своим внутренним данным, стеку или некоторому дополнительному сегменту. Значения инициализируемых регистров должны быть сохранены в секции 1.
В секции 3, собственно, выполняется основной код процедуры обработчика прерывания, выполняются необходимые действия и заносятся значения в регистры, если прерывание должно возвращать в вызвавшую его программу некоторые результаты в регистрах.
В секции 4 происходит восстановление значений для изменённых процедурой обработчика прерывания регистров, кроме тех регистров, в которых вызвавшей прерывание программе возвращаются результаты.
Команда IRET выполняет возврат из процедуры обработчика прерывания в вызвавшую его программу.
Рассмотрим подробнее какие действия выполняют команды INT и IRET.
Так как при выполнении команды INT XX должен быть вызван некоторый обработчик прерывания с номером XX необходимо по номеру узнать адрес обработчика в памяти (сегмент и смещение). Для этого служит специальная таблица векторов прерываний, располагающаяся по адресу 0000:0000 в оперативной памяти компьютера. Эта таблица содержит 256 четырёхбайтовых значений, определяющих адреса обработчиков прерываний в памяти. Первые 15 четырёхбайтовых значений в таблице зарезервированны для аппаратных прерываний (маскируемых и немаскируемых) и для внутренних прерываний процессора. Остальные значения в таблице определяют адреса обработчиков программных прерываний. Среди этих значений есть и такие, которые предназначены для пользовательских обработчиков программных прерываний. Первые два байта для каждой ячейки в таблице определяют смещение обработчика соответствующего программного прерывания. Следующие два байта определяют сегмент обработчика прерывания. При вызове команды INT XX выполняются следующие действия:
В стеке сохраняются в следующей последовательности: регистр флагов, сегментный регистр CS, регистр указателя команд IP. Сбрасываются флаги IF и TF в регистре флагов.
Вычисляется смещение относительно начала таблицы векторов прерываний: смещение=XX * 4, где XX - номер прерывания.
В сегментный регистр CS по вычисленному смещению из таблицы векторов прерываний заносится значение сегмента обработчика прерывания, а в регистр IP - смещение обработчика прерывания.
Происходит передача управления на обработчик программного прерывания. При этом все регистры кроме CS, IP и регистра флагов сохраняют своё значение таким, каким оно было до вызова команды INT XX.
Таким образом, при входе в обработчик программного прерывания, в стеке находятся значения регистров CS, IP и регистра флагов. Эти значения находились в данных регистрах до вызова команды INT XX. В вершине стека располагается значение регистра IP.
При вызове команды IRET выполняются следующие действия:
Из стека восстанавливается значение регистра IP.
Из стека восстанавливается значение регистра CS.
Из стека восстанавливается значение регистра флагов.
Происходит передача управления в прерванную программу, на команду, находящуюся непосредственно за командой программного прерывания INT XX.
После выполнения команды IRET структура стека становится такой же, какой она была до вызова команды INT XX.
Таковы основные моменты, используемые при написании обработчиков программных прерываний. Рассмотрим теперь структуру и работу обработчиков аппаратных прерываний.
В отличие от обработчиков программных прерываний, обработчики аппаратных прерываний вызываются не командой INT, а самим процессором. Выше было сказано, что при написании обработчиков аппаратных прерываний они должны выполнять ещё и некоторые действия по управлению аппаратурой механизма прерываний процессора x86. В простейшем случае, структура такого обработчика выглядит следующим образом:
NAME PROC ; 1. сохранение модифицируемых регистров . . . ; 2. инициализациясегментных регистров . . . ; 3. выполнение необходимых действий . . . ; 4. восстановление используемых регистров . . . MOV AL, 20h OUT 20h, AL IRET NAME endp |
Команда OUT 20h, AL выполняет действия по управлению аппаратурой механизма прерываний процессоров x86. Конкретно, она посылает сигнал EOI (End Of Interrupt - конец прерывания) в контроллер прерываний, сообщая ему таким образом, что обработка аппаратного прерывания завершена.
При возникновении аппаратного прерывания от некоторого периферийного устройства контроллер прерываний выполняет проверку, не замаскировано ли это прерывание. Если оно не замаскировано, то контроллер выполняет сравнение приоритетов этого прерывания с другим, если несколько прерываний поступили в контроллер одновременно. Если прерывание замаскировано или заблокировано, то оно игнорируется контроллером. После выбора прерывания с более высоким приоритетом (логика назначения приоритетов прерываниям может быть запрограммирована пользователем) контроллер посылает сигнал INTR (Interrupt Request - запрос прерывания) в процессор. Если в процессоре в регистре флагов сброшен флаг прерывания IF, то сигнал INTR игнорируется. Если флаг IF установлен, то процессор отвечает контроллеру сигналом INTA (Interrupt Acknoledge) на что контроллер, в свою очередь, посылает процессору номер вектора прерывания для выбранного прерывания и блокирует все прерывания этого и более низкого приоритета. Процессор по полученному номеру вектора прерывания отыскивает в таблице векторов прерываний адрес соответствующего обработчика аппаратного прерывания и вызывает его.
Команда OUT 20h, AL, вызываемая в конце обработчика аппаратного прерывания, разблокирует контроллер прерываний, разрешая ему работу с ранее заблокированными прерываниями.
Если требуется написать обработчик аппаратного прерывания, который должен только выполнять определённые действия при возникновении аппаратного прерывания (например, выдавать звуковой сигнал при нажатии на любую клавишу), всю работу по управлению соответствующей аппаратурой можно возложить на системный обработчик этого аппаратного прерывания. В таком случае, структура обработчика будет следующей:
SYS_HANDLER DD ? . . . NAME PROC PUSHF CALL CS:SYS_HANDLER ; 1. сохранение модифицируемых регистров . . . ; 2. инициализациясегментных регистров . . . ; 3. выполнение необходимых действий . . . ; 4. восстановление используемых регистров . . . IRET NAME endp |
Команда CALL CS:OLD_HANDLER вызывает системный обработчик нужного аппаратного прерывания, который выполняет все необходимые действия по управлению аппаратурой и контроллером прерываний. OLD_HANDLER определяет ячейку памяти размером в двойное слово (4 байта) для хранения адреса системного обработчика прерывания. Команда PUSHF создаёт в стеке структуру для команды IRET, вызываемой в системном обработчике. Подобный подход можно использовать и для программных прерываний, когда помимо тех действий, которые выполняет системный обработчик программного прерывания (например, INT 10h - прерывание BIOS) нужно выполнить какие-либо дополнительные действия. Также можно определить структуру обработчика программного или аппаратного прерывания, когда системный обработчик вызывается в конце процедуры нашего обработчика:
SYS_HANDLER DD ? . . . NAME PROC ; 1. сохранение модифицируемых регистров . . . ; 2. инициализациясегментных регистров . . . ; 3. выполнение необходимых действий . . . ; 4. восстановление используемых регистров . . . JMP SYS_HANDLER NAME endp |
В этом случае команда JMP SYS_HANDLER выполняет дальний переход на системный обработчик прерывания, поэтому в конце нашего обработчика не нужно вызывать команду IRET - она будет вызвана в системном обработчике.
После того, как определено, каким образом оформить процедуру обработчика аппаратного или программного прерывания, рассмотрим действия, необходимые для того, чтобы эта процедура обработчика вызывалась при возникновении прерывания.
Как уже было сказано выше, в оперативной памяти компьютера по адресу 0000:0000 располагается таблица векторов прерываний, элементы которой определяют адреса обработчиков прерываний в памяти. Для обработчика программного или аппаратного прерывания без вызова системного обработчика нужно лишь записать в соответствующий элемент таблицы векторов прерываний значение сегмента и смещения этого обработчика. Рассмотрим необходимые операции для записи сегмента и смещения в таблицу для обработчика программного или аппаратного прерывания с номером N:
MOV AX, 0000H ; запись в ES значения MOV ES, AX ; сегмента 0000h MOV DI, N ; запись в DI номера обработчика MOV CL, 2 ; умножение DI SHL DI, CL ; на 4 MOV AX, OFFSET HANDLER ; запись в AX смещения обработчика STOSW ; сохранение смещения в таблице MOV AX, SEGMENT HANDLER ; запись в AX сегмента обработчика STOSW ; сохранение сегмента в таблице |
После выполнения этих действий и при выполнении команды INT N будет вызван обработчик, адрес которого был установлен в таблице векторов прерываний.
Рассмотрим теперь необходимые операции для установки сегмента и смещения в таблице для обработчика программного или аппаратного прерывания, в котором будет вызван системный обработчик этого прерывания. Для этого перед записью в таблицу новых значений сегмента и смещения нужно сначала сохранить значения сегмента и смещения системного обработчика:
SYS_HANDLER DD ? ; определение ячейки памяти для хранения ; адреса системного обработчика . . . MOV AX, 0000H ; запись в ES значения MOV ES, AX ; сегмента 0000h MOV DI, N ; запись в DI номера обработчика MOV CL, 2 ; умножение DI SHL DI, CL ; на 4 MOV WORD PTR SYS_HANDLER, ES:[DI] ; сохранение смещения системного обработчика MOV AX, OFFSET HANDLER ; запись в AX смещения нового обработчика STOSW ; сохранение смещения в таблице MOV WORD PTR SYS_HANDLER+2, ES:[DI] ; сохранение сегмента системного обработчика MOV AX, SEGMENT HANDLER ; запись в AX сегмента нового обработчика STOSW ; сохранение сегмента в таблице |
При установке значений сегмента и смещения обработчика аппаратного прерывания нужно до этого сбросить флаг IF (команда CLI), а после установки новых значений установить флаг IF (команда STI). Это необходимо для того, чтобы в процессе установки значений сегмента и смещения не возникло аппаратное прерывание.
Приведённые выше фрагменты кода можно упростить, используя функции прерывания DOS INT 21h. Функция DOS 35h позволяет получить адрес обработчика прерывания. При этом в регистр AH записывается номер функции (35h), в регистр AL записывается номер прерывания. После вызова прерывания INT 21h в регистре ES возвращается значение сегмента обработчика указанного прерывания. В регистре BX взвращается значение смещения обработчика указанного прерывания:
SYS_HANDLER DD ? . . . MOV AH, 35H MOV AL, N INT 21H MOV WORD PTR SYS_HANDLER, BX MOV WORD PTR SYS_HANDLER+2, ES |
Функция DOS 25h позволяет установить адрес обработчика прерывания. В регистр AH записывается номер функции (25h), в регистр AL записывается номер прерывания. В регистр DS записывается значение сегмента обработчика прерывания, адрес которого нужно установить в таблице векторов прерываний. В регистр DX записывается значение смещения обработчика прерывания:
MY_HANDLER PROC . . . MY_HANDLER_ENDP . . . MOV AH, 25H MOV AL, N MOV DS, SEGMENT MY_HANDLER MOV DX, OFFSET MY_HANDLER INT 21H |
При написании обработчика аппаратного прерывания нужно учитывать то, что он должен быть завершен до возникновения очередного аппаратного прерывания для этого обработчика. Если это условие не выполнено, то, в лучшем случае, возникшее прерывание будет потеряно. В худшем случае невыполнение этого условия может повлечь за собой потерю данных или зависание компьютера. Например, при написании аппаратного обработчика прерываний от таймера, код обработчика должен выполнятся по времени менее 1/18 с., так как прерывания от таймера по умолчанию генерируются 18.2 раз в секунду. То же можно сказать и об обработчике аппаратных прерываний от клавиатуры - код обработчика должен выполняться не дольше задержки повторения символов.
Также при написании любого обработчика прерывания нужно инициализировать сегментный регистр DS, если обработчик обращается к каким-либо внутренним ячейкам памяти. Вместо этого можно использовать обращение к ячейкам памяти с использованием префикса замены сегмента (например CS:[BX]), но это увеличивает размер соответствующей команды. Использование префикса замены сегмента эффективно в том случае, когда количество обращений к внутренним ячейкам памяти обработчика невелико (2 - 3).
Рассмотрим теперь средства для завершения резидентной программы и сохранении части её кода в памяти для последующего использования при возникновении прерываний.
Прерывание DOS INT 27h. Прерывание предназначено для завершения программы и сохранения её резидентной в памяти. Для этого в регистр DX нужно занести количество байт для сохраняемой части программы плюс один байт. Начало сохраняемой части программы совпадает со смещением 0000h относительно кодового сегмента, поэтому для COM программ нужно учитывать размер префикса программного сегмента (PSP - Program Segment Prefix) - 256 байт.
Прерывание DOS INT 21h, функция 31h. Функция 31h прерывания DOS INT 21h выполняет те же действия, что и прерывание DOS INT 27h, но в регистр DX заносится размер сохраняемой части программы в параграфах (блоки памяти длиной 16 байт). Также имеется возможность определить код возврата при завершении резидентной программы (заносится в регистр AL).
Ниже приведёна общая структура резидентной программы:
;
данные для резидентной части
программы... HANDLER PROC ... HANDLER
ENDP ... ; основная часть программы (нерезидентная) ... ;
установка (и получение) адреса
обработчика |
Следующий фрагмент кода даёт пример определения основной структуры резидентной программы на ассемблере (программа типа COM):
CODE SEGMENT ; определение кодового сегмента ASSUME CS:CODE, DS:CODE ; CS и DS указывают на сегмент кода ORG 100H ; размер PSP для COM файла BEGIN: JMP START ; переход на нерезидентную часть программы SYS_HANDLER DD ? ; определение ячейки памяти для хранения ; адреса системного обработчика A DW 0 ; определение внутренних ячеек памяти для B DB 1 ; резидентной части программы ... KB_HANDLER PROC ; процедура обработчика прерываний ; от клавиатуры PUSHF ; создание в стеке структуры для IRET CALL CS:SYS_HANDLER ; вызов системного обработчика ... IRET ; возврат из обработчика KB_HANDLER ENDP KB_END: ; метка для определения размера резидентной ; части программы C DB 2 ; ячейки памяти для нерезидентной части D DW 3 ; программы ... START: ; начало нерезидентной части программы ... MOV AH, 35H ; получение адреса системного обработчика MOV AL, 09H ; прерываний от клавиатуры INT 21H MOV WORD PTR SYS_HANDLER, BX MOV WORD PTR SYS_HANDLER+2, ES MOV AH, 25H ; установка адреса нового обработчика MOB AL, 09H ; прерываний от клавиатуры MOV DX, OFFSET KB_HANDLER INT 21H MOV DX, (KB_END + 10FH) / 16 ; вычисление размера резидентной части INT 31H ; завершение резидентной программы с ; сохранением части её кода в памяти CODE ENDS ; конец сегмента кода END BEGIN ; конец программы |
Рассмотрим пример резидентной программы "часы" на ассемблере (CLOCK2.ZIP - 827 байт). Программа перехватывает обработчик прерываний от таймера и с возникновением очередного прерывания выводит в левом верхнем углу экрана текущее время. Ниже представлен исходный текст программы с комментариями.
code segment ; определение кодового сегмента assume cs:code,ds:code ; CS и DS указывают на сегмент кода org 100h ; размер PSP для COM программы start: jmp load ; переход на нерезидентную часть old dd 0 ; адрес старого обработчика buf db ' 00:00:00 ',0 ; шаблон для вывода текущего времени decode proc ; процедура заполнения шаблона mov ah, al ; преобразование двоично-десятичного and al, 15 ; числа в регистре AL shr ah, 4 ; в пару ASCII символов add al, '0' add ah, '0' mov buf[bx + 1], ah ; запись ASCII символов mov buf[bx + 2], al ; в шаблон add bx, 3 ret ; возврат из процедуры decode endp ; конец процедуры clock proc ; процедура обработчика прерываний от таймера pushf ; создание в стеке структуры для IRET call cs:old ; вызов старого обработчика прерываний push ds ; сохранение модифицируемых регистров push es push ax push bx push cx push dx push di push cs pop ds mov ah, 2 ; функция BIOS для получения текущего времени int 1Ah ; прерывание BIOS xor bx, bx ; настройка BX на начало шаблона mov al, ch ; в AL - часы call decode ; вызов процедуры заполнения шаблона - часы mov al, cl ; в AL - минуты call decode ; вызов процедуры заполнения шаблона - минуты mov al, dh ; в AL - секунды call decode ; вызов процедуры заполнения шаблона - секунды mov ax, 0B800h ; настройка AX на сегмент видеопамяти mov es, ax ; запись в ES значения сегмента видеопамяти xor di, di ; настройка DI на начало сегмента видеопамяти xor bx, bx ; настройка BX на начало шаблона mov ah, 1Bh ; атрибут выводимых символов @@1: mov al, buf[bx] ; цикл для записи символов шаблона в видеопамять stosw ; запись очередного символа и атрибута inc bx ; инкремент указателя на символ шаблона cmp buf[bx], 0 ; пока не конец шаблона, jnz @@1 ; продолжать запись символов @@5: pop di ; восстановление модифицируемых регистров pop dx pop cx pop bx pop ax pop es pop ds iret ; возврат из обработчика clock endp ; конец процедуры обработчика end_clock: ; метка для определения размера резидентной ; части программы load: mov ax, 351Ch ; получение адреса старого обработчика int 21h ; прерываний от таймера mov word ptr old, bx ; сохранение смещения обработчика mov word ptr old + 2, es ; сохранение сегмента обработчика mov ax, 251Ch ; установка адреса нашего обработчика mov dx, offset clock ; указание смещения нашего обработчика int 21h ; вызов DOS mov ax, 3100h ; функция DOS завершения резидентной программы mov dx, (end_clock - start + 10Fh) / 16 ; определение размера резидентной ; части программы в параграфах int 21h ; вызов DOS code ends ; конец кодового сегмента end start ; конец программы
Рассмотрим работу программы.
Идентификатор old определяет ячейку памяти размером 4 байта, которая хранит адрес старого обработчика прерываний от таймера. Эта ячейка будет нужна, когда будет вызываться старый обработчик прерываний от таймера. Идентификатор buf определяет шаблон для формирования строки, содержащей значение текущего времени в формате часы:минуты:секунды. Последний байт в шаблоне - нулевой - нужен для определения длины шаблона.
Процедура decode преобразует двоично-десятичное число в регистре AL в два ASCII символа, соответствующих значению часов, минут или секунд (это зависит от конкретного значения, записанного в регистре AL). Процедура прибавляет к значению разряда числа (младший разряд находится в первых четырёх битах регистра AL, старший - в старших четырёх битах) ASCII код символа '0', тем самым формируя ASCII код для цифры младшего или старшего разряда. Далее этот ASCII код записывается в текущую позицию шаблона. После записи в шаблон значение указателя на текущий символ шаблона увеличивается на 3 (две цифры для часов, минут или секунд, плюс символ ':').
Процедура clock является обработчиком прерываний от таймера. Дело в том, что номер аппаратного обработчика прерываний от таймера - 08h. Но в системном обработчике этого аппаратного прерывания есть вызов INT 1Ch. Прерывание 1Ch определяет пользовательский обработчик прерываний от таймера, который вызывается системным. Таким образом, ячейка old хранит адрес старого пользовательского обработчика прерываний от таймера. В начале процедуры clock командой PUSHF в стеке подготавливается структура для команды IRET и затем вызывается старый пользовательский обработчик прерываний от таймера. Далее в процедуре clock сохраняются в стеке все модифицируемые регистры (в том числе и сегментные). После этого инициализируется сегментный регистр DS для последующего обращения к ячейкам памяти резидентной части программы. Следующим шагом является получение значения текущего времени из BIOS при помощи прерывания BIOS 1Ah. После вызова команды INT 1Ah в регистре CH находится значение часов, в регистре CL - значение минут и в регистре DH - значение секунд. Каждое значение представлено в двоично-десятичном формате - младший разряд числа находится в младших четырёх битах регистра, а старший разряд числа - в старших четырёх битах. После получения значения текущего времени регистр BX настраивается на начало шаблона для записи в шаблон значений часов, минут и секунд. Далее три раза вызывается процедура decode для записи в шаблон соответственно часов, минут и секунд. После записи в шаблон необходимой информации происходит вывод символов шаблона в левый верхний угол экрана. Для этого регистр ES настраивается на сегмент видеопамяти, а регистр DI настраивается на начало сегмента видеопамяти. Далее в цикле происходит вывод в видеопамять символов шаблона и атрибутов. После этого восстанавливаются значения всех модифицируемых процедурой регистров и командой IRET происходит возврат из обработчика.
В основной (нерезидентной) части программы при помощи функции DOS 35h происходит получение адреса старого пользовательского обработчика прерываний от таймера (прерывание 1Сh). Значения сегмента и смещения старого обработчика записываются в ячейку old. Далее устанавливается адрес нашего обработчика прерываний от таймера. Для этого в регистр DX записывается смещение нашего обработчика (смещение процедуры clock) и вызывается функция DOS 25h (регистр DS уже содержит значение сегмента нашего обработчика). После этого вычисляется размер резидентной части программы в параграфах. Для этого сначала вычисляется размер резидентной части в байтах, не считая префикса программного сегмента. Затем к полученному значению прибавляется размер префикса программного сегмента - 256 и число 0Fh. Прибавление числа 0Fh необходимо, чтобы округление при делении на 16 осуществлялось в большую сторону. После вычисления размера резидентной части в параграфах происходит вызов функции DOS 31h, которая завершает программу и сохраняет часть её кода в памяти. Резидентная программа запущена.
Copyright © 2000 by HackMaster