Сегодня мы поговорим о стеке (stack). Что же это такое? Если просто —
это выделенная область памяти для хранения произвольных данных. Мы
помещаем туда на какое-то время значения, содержащиеся в регистрах,
например, если необходимо туда загрузить какое-то другое значение, а
потом можем его вернуть назад, причём не обязательно в этот регистр.
Если мы откроем программу AFDPro, в самом верху (чуть правее середины)
мы увидим надпись "Stack", а под ним — его первые значения (верхушку).
По мере того, как мы кладём туда какие-нибудь данные, стек растёт —
последнее записанное значение оказывается сверху, а остальные смещаются
вниз.
Это то, что мы видим в отладчике. На самом деле надо
запомнить, что стек растёт снизу вверх, т.е. его начало находится по
адресу 0FFFFh (и хранится в регистре SP, а точнее— SS:SP, когда наша
программа будет состоять не из одного сегмента), а самый конец — в
00000h.
Помещает данные в стек команда push, а возвращает — pop. Например:
... mov ax,9 ;Заносим в ax число 9 push ax ;Заносим 9 в стек pop ax ;Вынимаем 9 из стека в ax Как
видим, оперировать мы можем только последним значением в стеке. Однако всё усложняется тем, что
стек используем не только мы, но и сам ассемблер. Например, при
использовании оператора call туда заносится адрес выхода из
подпрограммы. Возьмём программу из урока 5:
.286 CSEG segment assume cs:CSEG, ds:CSEG, es:CSEG, ss:CSEG org 100h begin: ;Всё написанное выше пока опускаем.
jmp exit ;"Прыгаем" на метку exit, не выполняя операторы ниже.
NameProg proc ;Начало нашей подпрограммы. mov ah,9 ;Загружаем в регистр ah число 9 (указываем функцию). mov dx,offset helloworld ;Указываем, что за фразу мы будем выводить. int 21h
;Выводим фразу. ret NameProg endp ;Конец подпрограммы.
exit: ;Метка на шаге 2.
call NameProg ;Вызываем подпрограмму вывода, которая выводит фразу.
int 20h ;Выходим в DOS.
helloworld db 'Hello, world!$' ;Определяем переменную helloworld, доступную побайтно, с фразой ;"Hello, world!". В одинарных кавычках, после знака "!" ставим ;знак "$".
;Завершение программы. CSEG ends end begin
Запустим отладчик afdpro test.com
и посмотрим, как меняется стек. При выполнении оператора call отладчик
"прыгает" на адрес mov ah,9, при этом в стек заносится адрес выхода из
подпрограммы (используется при выполнении ret). Попробуем
воспользоваться этим (жирным шрифтом выделены добавленные строки):
.286 CSEG segment assume cs:CSEG, ds:CSEG, es:CSEG, ss:CSEG org 100h begin: ;Всё написанное выше пока опускаем.
jmp exit ;"Прыгаем" на метку exit, не выполняя операторы ниже.
NameProg proc ;Начало нашей подпрограммы. pop ax ;Вынимаем из стека значение для ret. mov ax,offset metka ;Подмениваем адрес возврата на адрес строки
mov ah,9 push ax ;Заносим в стек. metka: ;Метка-адрес для занесения в ax (куда прыгать). Или не ;использовать метку, а вместо строки mov ax,offset metka ;можно было написать mov ax,0107h
mov ah,9 ;Загружаем в регистр ah число 9 (указываем функцию). mov dx,offset helloworld ;Указываем, что за фразу мы будем выводить. int 21h ;Выводим фразу. ret NameProg endp ;Конец подпрограммы.
exit:
;Метка на шаге 2.
call NameProg ;Вызываем подпрограмму вывода, которая выводит фразу.
int 20h ;Выходим в DOS.
helloworld db 'Hello, world!$' ;Определяем переменную helloworld, доступную побайтно, с фразой ;"Hello, world!". В одинарных кавычках, после знака "!" ставим ;знак "$".
;Завершение программы. CSEG ends end begin Ассемблируем программу: ml test.asm
/AT. Запускаем. Вместо одинарного вывода фразы "Hello, world!" фраза выводится два раза! Почему? Понаблюдаем в отладчике: afdpro test.com.
Мы видим, как подменивается адрес выхода из подпрограммы, и вместо
перехода с ret на int 20 программа снова "прыгает" на mov ah,9 - фраза
выводится второй раз. Последнее значение стека команда ret обнуляет,
поэтому при следующем заходе в ret программа никуда не "прыгает" и
корректно оканчивает свою работу.
Данный
пример достаточно простой. Давайте его усложнив, поменяв адрес стека.
Возьмём программу из того же урока (выводящую строчку "Hello, world!" 5
раз):
.286 CSEG segment assume cs:CSEG, ds:CSEG, es:CSEG, ss:CSEG org 100h begin: ;Всё написанное выше пока опускаем. mov cx,5 ;Устанавливаем значение счётчика в 5.
metka: mov ah,9 ;Загружаем в регистр ah число 9 (указываем функцию). mov dx,offset helloworld ;Указываем, что за фразу мы будем выводить. int 21h ;Выводим фразу. loop metka ;Переходим на метку metka и уменьшаем cx на 1.
int 20h
;Выходим в DOS.
helloworld db 'Hello, world!$' ;Определяем переменную helloworld, доступную побайтно, с фразой ;"Hello, world!". В одинарных кавычках, после знака "!" ставим ;знак "$".
;Завершение программы. CSEG ends end begin И преобразуем к такому виду:
.286 CSEG segment assume cs:CSEG, ds:CSEG, es:CSEG, ss:CSEG org 100h begin: ;Всё написанное
выше пока опускаем.
mov sp,offset metka ;Изменяем начало стека на адрес нашей метки metka mov ax,0009h ;Заносим значение 00009h в ax push ax ;Помещаем цифру "9" в стек - но стек находится по адресу ;metka, т.е. функции mov ah,9 (сама метка места не занимает), ;таким образом, значение в стеке mov ah,9 смещается, ;а перед ним записывается число 9 - вместо цифры "5" ;в инструкции mov cx,5
mov cx,5 ;Устанавливаем значение счётчика в 5. ;Так было до тех пор, пока мы "на лету" не поменяли ;код программы при помощи стека.
metka: mov ah,9 ;Загружаем в регистр ah число 9 (указываем функцию). mov dx,offset helloworld ;Указываем, что за фразу мы будем выводить. int 21h ;Выводим фразу. loop metka ;Переходим на метку metka и уменьшаем cx на 1.
int 20h ;Выходим в DOS.
helloworld db
'Hello, world!$' ;Определяем переменную helloworld, доступную побайтно, с фразой ;"Hello, world!". В одинарных кавычках, после знака "!" ставим ;знак "$".
;Завершение программы. CSEG ends end begin
Вроде
ничего не изменилось, весь код, кроме трёх строк (выделенных жирным
шрифтом), сохранён. Но первая программа выведет строчку "Hello, world!" 5
раз, а вторая - 9. Потому что "на лету" мы подменили в самом коде
программы цифру "5" на "9". Вот она, сила ассемблера! Такого не могут себе позволить языки
высокого уровня.