Файл: О.А.Калашников. Ассемблер Это Просто. Учимся программировать.pdf
Добавлен: 16.02.2019
Просмотров: 29209
Скачиваний: 1689
Глава 6. Работа со стеком
57
Допустим, нам нужно временно сохранить некое число. Например, перед вызовом
процедуры, прерывания или циклом. Вот как это будет выглядеть (листинг 6.2).
Листинг 6.2. Временное сохранение числа в стеке
...
(01) mov ax,345h
(02) push ax
(03) mov ah,10h
(04) int 16h
(05) pop ax
...
Здесь мы загружаем в
ax
число 345h (01), сохраняем его (02), ждем нажатия
клавиши (03), (04). Функция
10h
прерывания
16h
возвращает в
ax
код нажатой кла-
виши, т. е. затирает в
ax
число, которое находилось до вызова прерывания. Как
только пользователь нажал любую клавишу, выполняется строка (05), которая дос-
танет из стека число 345h и запишет его в регистр
ax
. В итоге
ax
будет содержать
число 345h, что, как говорится, и требовалось доказать. Однако стоит заметить та-
кой момент. Допустим, мы помещаем в стек содержимое следующих регистров:
ax
,
bx
,
cx
:
...
push ax
push bx
push cx
...
Обратите внимание, что восстанавливать их из стека нужно в обратном порядке:
...
pop cx
pop bx
pop ax
...
Если вы поменяете местами регистры при восстановлении, то ничего страшного
не произойдет, только содержать они будут другие числа. Например:
...
mov ax,1234h
mov bx,5678h
push ax
push bx
pop ax
pop bx
...
Часть II. Усложняем задачи
58
Рис. 6.4. Результат неправильного использования стека
В итоге
ax
будет равен 5678h, а
bx
— 1234h (рис. 6.4).
Особую осторожность при работе со стеком следует соблюдать в подпрограм-
мах (листинг 6.3).
Листинг 6.3. Использование стека в подпрограммах
...
call Our_proc
int 20h
...
Our_proc proc
mov ax,15
push ax
mov ah,9
mov dx,offset Str
int 21h
ret
Our_proc endp
...
Обратите внимание, что мы "забыли" восстановить из стека
ax
в нашей про-
цедуре
Our_proc
. В таком случае компьютер, дойдя до оператора
ret
, вытащит из
стека не адрес возврата, а число 15 и передаст управление на инструкцию, которая
находится по смещению 15. Что находится по адресу 15 и дальше — не известно.
Машина, скорее всего, "зависнет" (см. рис. 6.5 и 6.6).
Глава 6. Работа со стеком
59
Рис. 6.5. Нарушаем работу стека в подпрограмме
Рис. 6.6. Результат нарушения работы стека в подпрограмме
Следить за стеком (как уже упоминалось) позволяет пара регистров
ss:sp
(сег-
мент стека : смещение). Программист может менять как сегмент, так и смещение,
но при этом следует иметь в виду, что перед сменой регистров
ss
и
sp
необходимо
запретить вызов всех аппаратных прерываний, а после изменения — разрешить.
Это позволяют сделать операторы
cli
и
sti
(табл. 6.3 и 6.4).
Таблица 6.3. Оператор
cli
Команда
Перевод
Назначение
Процессор
cli
Clear interrupt
— запретить пре-
рывания
Запретить прерывания
8086
Часть II. Усложняем задачи
60
Таблица 6.4. Оператор
sti
Команда
Перевод
Назначение
Процессор
sti
Restore interrupt
— восстановить
Разрешить прерывания
8086
Сразу отметим, что запрет вызова аппаратных прерываний замертво "вешает"
компьютер в глазах пользователя! Восстановление же — приводит его к нормаль-
ной работе. После команды
cli
всегда должна идти
sti
. Следует избегать запрета
вызова аппаратных прерываний на длительное время.
Для чего нужно запрещать прерывания? Скажем об этом в двух словах, т. к. ме-
ханизму работы прерываний необходимо посвятить целый раздел. Если что-то не
понятно — просто опустите, особо не заостряйте на этом внимания.
После создания программы и ее запуска на выполнение работает не только она
одна. Простейший пример — таймер, который автоматически вызывается пример-
но 18,2 раз в секунду для обновления часов таймера. Компьютер всегда что-то де-
лает! Даже тогда, когда, например, ждет от пользователя нажатия клавиши.
Что происходит, когда вызывается прерывание, например, от таймера? Пример-
но то же, что и при вызове процедуры. Компьютер запоминает в стеке адрес теку-
щей команды, а также все регистры, которые изменят прерывание, и переходит на
адрес прерывания, по которому находится процедура обработки этого прерывания
(например, процедура обработки таймера, которая обновит показания часов/
минут/секунд). Затем, когда процедура отработает, компьютер восстановит из сте-
ка адрес возврата и все сохраненные регистры, и наша программа продолжит вы-
полнение, даже "не подозревая" о том, что кто-то ее прерывал в процессе работы.
Все это происходит очень быстро и для пользователя совсем незаметно. Рассмот-
рим случай, когда мы меняем стековые регистры, при этом предварительно не за-
прещаем вызов аппаратных прерываний:
...
(01) mov ax,100h
(02) mov ss,ax
(03) mov sp,200h
...
Допустим, вызов прерывания таймера пришелся на тот момент, когда наша про-
грамма выполнила команду в строке (02). В этом случае
ss
равен 100h, а в
sp
еще
не занесено число 200h. Получается так, что сегмент стека именно тот, который нам
нужен, а смещение остается прежним, пусть это будет, допустим,
sp=0FFFEh
. В итоге,
ss=100h
, а
sp=0FFFEh
. Процессор и сохранит по этому адресу данные. Какой при этом
код программы или какие данные будут затерты — не известно. Мы ведь хотели сде-
лать
ss=100h
, а
sp=200h
! Хорошо, если две команды успели выполниться перед вызо-
вом прерывания. Хорошо также, если по адресу
0100h:0FFFEh
ничего нет (память
свободна). А если есть? Тогда компьютер, скорее всего, опять же "зависнет".
Отсюда вытекают три важных правила при работе со стековыми регистрами:
прежде чем менять регистры
ss:sp
, необходимо запретить вызов всех аппарат-
ных прерываний командой
cli
, а затем, после смены, разрешить их командой
Глава 6. Работа со стеком
61
sti
. Однако в современных компьютерах (Pentium) запрещать прерывания не-
обязательно, т. к. это автоматически делает процессор. Но если вы запустите эту
программу на более старой модели, то очень высока вероятность того, что она
просто "зависнет";
ss:sp
нужно устанавливать на свободную область памяти. При этом следует
убедиться, что код не утратил работоспособности;
не следует забывать о таком понятии, как переполнение стека. Мы знаем, что
после загрузки COM-программы в память,
ss
равен сегменту, куда загрузилась
программа, а
sp = 0FFFEh
. Код программы начинается с адреса
100h
(
org 100h
).
Вершина стека — конец сегмента. Если наша программа занимает, скажем,
2000h байт, то можем установить
sp
в 2200h. В этом случае мы отводим 100h
(именно сто) байт для стека (т. к. программа загружается в память по адресу
100h
(
org 100h
), то к 2000h нужно прибавить 100h). Стек, как вы помните, рас-
тет снизу вверх. Если мы переполним стек (например, поместим более 100h
данных), то затрется часть нашей программы снизу. Об этом следует помнить!
В листинге 6.4 приведен полный пример корректного изменения регистров стека.
Листинг 6.4. Корректное изменение регистров стека
...
cli
mov ax,0B900h
mov ss,ax
mov sp,100h
sti
...
Вы, вероятно, спросите: "А зачем вообще менять регистры
ss:sp
?"
Дело в том, что это иногда нужно делать в резидентных программах либо перед
освобождением памяти, изначально отведенной под стек, для загрузки программы
или оверлея (вспомогательных процедур, расположенных в отдельном файле),
а также во многих других случаях, которые мы будем позже рассматривать.
6.2. Программа для практики
6.2.1. Оператор nop
Прежде чем перейти к программе, рассмотрим новый оператор (табл. 6.5).
Таблица 6.5. Оператор
nop
Команда
Перевод
Назначение
Процессор
Nop
No operand
— нет операнда
Ничего не делает 8086