Файл: О.А.Калашников. Ассемблер Это Просто. Учимся программировать.pdf
Добавлен: 16.02.2019
Просмотров: 29229
Скачиваний: 1689
Глава 15. Обработка аппаратных прерываний
149
будет находиться в памяти и контролировать прерывание
21h
еще до выхода! Та-
ким образом, наш резидент активизируется сразу после того, как мы установили на
него вектор прерывания (а мы это сделали, используя функцию
25h
прерывания
21h
) (листинг 15.2).
Листинг 15.2. Сохранение и установка вектора прерывания
...
;Установка обработчика 21h
mov ax, 3521h
int 21h ;получим и сохраним адрес (вектор) прерывания 21h
;вначале младшее слово (смещение)
mov word ptr Int_21h_vect,bx
;затем старшее (сегмент)
mov word ptr Int_21h_vect+2,es
mov ax, 2521h
mov dx,offset Int_21h_proc
;Завершаем, оставив резидентную часть в памяти
int 21h
...
Еще одним подтверждением того, что резидент активизируется до выполнения
команды
int 27h
, является тот факт, что даже в отладчике мы увидим результат
его работы. Символы начнут заменяться уже после установки процедуры
Int_21h_proc
на прерывание
21h
(рис. 15.1).
Рис. 15.1. Активизация резидента в процессе отладки
Часть III. Файловая оболочка, вирус, резидент
150
Для того чтобы резидентная часть программы продолжала корректно работать
после выхода, нам нужно оставить ее резидентной в памяти. Если мы после ус-
тановки прерываний выйдем в DOS, используя
int 20h
, то компьютер просто
"зависнет"! Память, где находятся наши обработчики (
Int_05h_proc
,
Int_21h_proc
,
Int_1Ch_proc
), освободится, и на ее место загрузятся совсем другие программы.
После чего адрес прерывания
21h
будет указывать на место в памяти, где должна
находиться процедура обработки этого прерывания (
Int_21h_proc
). Но процеду-
ры обработки прерывания в данном месте памяти больше не существует, и нико-
му неизвестно, что там теперь находится вместо нее. Поэтому компьютер "завис-
нет" или перестанет работать должным образом, если какая-либо иная программа
или же сам процессор попытается вызвать одно из перечисленных выше пре-
рываний.
15.1.1. Сохранение предыдущего
вектора прерывания
Для чего следует сохранять старый вектор
21h
?
DOS, при смене вектора прерывания, нигде не запоминает его прежний адрес.
Если программист его самостоятельно не сохранит, то наша процедура, получив
управление в первый раз, отработает и не будет "знать", что делать дальше, кроме
как просто вернуться из прерывания и передать дальнейшее управление той про-
грамме, которая вызывала это прерывание.
Например, обработчик перехватывает прерывание
21h
и следит за тем, вызыва-
ется ли функция
3Dh
(открытие файла) некоторой программой. Если так, то рези-
дент должен сохранить в своей области имя открываемого файла, а затем передать
управление на адрес старого обработчика
21h
, который и откроет файл. В случае,
если вызывается другая функция, отличная от
3Dh
, то следует также немедленно
передать управление предыдущему обработчику, который и выполнит необходи-
мые действия. Адрес прежнего обработчика может указывать как на ядро DOS, так
и на другой резидент, перехвативший прерывание
21h
и загрузившийся ранее на-
шего. Получается своеобразная цепочка, каждое звено которой (резидентные про-
граммы) должно в обязательном порядке сохранять адрес прежнего обработчика
прерывания
21h
, кроме, разумеется, самого ядра DOS, являющегося начальным
звеном и выполняющим необходимые действия.
Для сохранения предыдущего адреса того или иного прерывания необходимо
завести отдельные 32-разрядные переменные, способные хранить два слова (сег-
мент и смещение):
Int_21h_vect dd ?
или
Int_1Ch_vect dd ?
и т. п.
Здесь
dd
означает "define double word" — определить переменную с двойным
словом.
Глава 15. Обработка аппаратных прерываний
151
15.1.2. Способы передачи управления
на прежний адрес прерывания
Существуют 2 стандартных способа передачи управления на прежний адрес
(вектор) прерывания.
Первый способ
jmp dword ptr cs:[Int_21h_vect]
dword ptr
означает, что надо "прыгнуть", используя не только смещение, но
и сегмент (сегмент:смещение), которые находятся в переменной
Int_21h_vect
.
cs:
указывает на то, что переменная находится в текущем сегменте. А
cs:
все-
гда указывает на текущий сегмент, т. е. на тот, в котором выполняется код про-
граммы в настоящий момент. Если мы опустим
cs:
, то процессор по умолчанию
будет работать так:
jmp dword ptr ds:[Int_21h_vect]
А это неверно, т. к.
ds
будет содержать отличное от
cs
значение (см. в отладчике
файл-приложение).
Переход в другой сегмент происходит в тот момент, когда мы, например, вызы-
ваем некоторое прерывание (как в нашем примере resid15.asm). При вызове преры-
вания из сегментных регистров меняется только
cs
, а прочие регистры остаются
прежними, сохраняют те значения, которые были перед вызовом прерывания (под-
робнее об этом скажем далее).
Квадратные скобки (
[ ]
) указывают на то, что нужно "прыгнуть" на тот адрес,
который находится в переменной
Int_21h_vect
, хотя в данном случае квадратные
скобки можно было и опустить.
Команды вида
jmp dword ptr ...
называются "дальним
jmp
с переходом в дру-
гой сегмент".
Второй способ
call dword ptr cs:[Int_21h_vect]
Здесь рассуждаем, как и в первом случае. Но есть некоторые отличия.
Вы привыкли к тому, что
call
используется для вызова процедур внутри нашего
сегмента. Такие процедуры называют ближними. В этом случае вызывается даль-
няя процедура, расположенная в другом сегменте. Принципы вызова процедур сле-
дующие:
вызов ближней (
near
) процедуры. В данном случае в стек автоматически поме-
щается смещение следующей за командой
call
инструкции. После того как
ближняя процедура отработала, из стека извлекается сохраненный адрес того
места в памяти, куда необходимо вернуться, чтобы продолжить выполнение
программы. Ближняя процедура должна находиться в том же сегменте, в ко-
тором находится команда
call
, ее вызывающая. Пример вызова ближней
процедуры:
call Near_proc
Часть III. Файловая оболочка, вирус, резидент
152
вызов дальней (
far
) процедуры. В этом случае процессор автоматически поме-
щает в стек не только смещение, но и сегмент для возврата. Компьютер, дойдя
в процедуре до инструкции возврата, извлечет из стека сохраненные сегмент
и смещение и передаст управление по этому адресу. Дальняя процедура может
находиться в любом другом сегменте, отличном от того, откуда она вызывается.
Пример вызова процедуры, расположенной в другом сегменте:
call dword ptr cs:[Far_proc]
В данном случае сегмент и смещение вызываемой процедуры находятся
в переменной
Far_proc
, которая расположена в текущем сегменте, т. е. в том,
в котором выполняется в настоящий момент указанная выше команда
call
.
15.2. Инструкции ret и retf
15.2.1. Оператор ret
Пример работы оператора
ret
приведен в листинге 15.3.
Листинг 15.3. Вызов ближней процедуры
;Предположим, что стек пустой (ss=1234h, sp=0FFFFh).
...
(1) [1234:0100h] mov ax,0A0Bh
(2) [1234:0103h] call Our_proc ;Вызываем процедуру Our_proc
(3) [1234:0105h] mov dx,123h
...
(4) [1234:0200h] Our_proc proc
(5) [1234:0200h] mov dx,offset Message
...
;Выходим из процедуры (переходим на адрес 1234:0105h)
(6) [1234:0250h] ret
(7) [1234:0250h] Our_proc endp
...
Давайте предположим, что строка (1) расположилась по адресу
1234:0100h
,
а процедура
Our_proc
находится по адресу
1234:0200h
(см. адреса в скобках). Ко-
гда программа дойдет до строки (2), сегмент
cs
будет содержать адрес текущего
сегмента (
1234h
), а смещение
ip
— адрес следующей команды (
0105h
).
Глава 15. Обработка аппаратных прерываний
153
В момент выполнения строки (2) в стек заносится текущее состояние регистра
ip
(и только!), т. е. число
0105h
. Затем процессор переходит на адрес
0200h
(на
метку нашей процедуры). Как видите, директива
Our_proc proc
не занимает памя-
ти: она нужна только ассемблер-программам.
Начинает работать процедура
Our_proc
, при этом адрес возврата (в нашем при-
мере —
0105h
) находится в стеке. Мы уже обращали ваше внимание, что в про-
цедурах следует очень тщательно следить за стеком, т. к. если мы в подпрограмме
занесем в стек какое-нибудь число и впоследствии, перед выходом из процедуры,
его не извлечем оттуда, то процессор, дойдя до инструкции
ret
, вытащит из стека
не адрес возврата, а то число, которое находится на вершине стека.
Итак, процедура отработала. Процессор дошел до команды
ret
. Команда
ret
достает последнее число, находящееся на вершине стека в данный момент, и пе-
рейдет по этому адресу. Если мы со стеком ничего не намудрили (ничего в нем не
оставили и ничего "лишнего" не вытащили), то
ret
достанет именно число
0105h
,
т. е. то, что сохранила инструкция
call
, и перейдет по этому адресу.
В
А Ж Н О
!
При вызове ближней процедуры в стеке сохраняется только смещение следующей за
процедурой инструкции (в примере из листинга 15.3 — только 0105h).
15.2.2. Оператор retf
При вызове дальней процедуры в стеке сохраняется не только смещение, но
и сегмент для возврата. Например, мы точно знаем, что по адресу
3456:0400h
на-
ходится процедура вывода строки на экран. Проблема в том, как определить сег-
мент этой процедуры, отличный от того, в котором находится наша программа (не
1234h
, а
3456h
). Для этого занесем в некую переменную (пусть это будет
New_proc
)
два слова —
3456h
и
0400h
— адрес процедуры
New_proc
, включая сегмент и сме-
щение. Затем передаем управление этой процедуре (листинг 15.4).
Листинг 15.4. Вызов дальней подпрограммы
...
;Заносим сперва смещение
mov word ptr [New_proc],0400h
;Затем сегмент
mov word ptr [New_proc+2],3456h
;Вызываем процедуру
call dword ptr [New_proc]
...
New_proc dd ? ;Переменная для хранения адреса дальней процедуры (2 байта)
...
В листинге 15.4 мы вначале заносим в переменную
New_proc
смещение, а затем
сегмент процедуры (вспоминаем о том, что в компьютере данные хранятся в об-