Для начала рассмотрим две очень похожие модели памяти: модель tiny, которая используется в com-программах операционной системы MSDOS, и модель памяти flat, которая используется Windows.
TINY - это самая маленькая модель памяти используемая в программировании, она использует лишь один сегмент, и не может иметь размер более чем 64 килобайта. В этом сегменте распологаются и код программы, и данные, и стек. Не смотря на небольшой размер, ассемблер позволяет писать в этом формате довольно серьезные программы, если речь не идет, конечно, о 3д играх.
FLAT - так называемая "плоская модель памяти", имеет размер до 4 Гигобайт памяти, она так же организована как единый сегмент для кодов, данных и стека. Это основная модель памяти которая используется в защищенном режиме. Следует учитывать, что в защищеном режиме компьютер использует виртуальную адресацию, это когда компьютер обращается к несуществующим адресам памяти, в этом случае размер виртуального адресного пространства может превышать размер физической памяти и достигать 64 Тбайт.
Обе модели используют одни и те же регистры, только в случае flat используются их расширенные версии, например, в tiny адресация выполняется загрузкой в сегментный регистр адреса начала сегмента, а в регистр-указатель грузится смещение от начала сегмента, то есть выглядит это как-то так - ss:sp. В модели flat в те же регистры грузится селектор смещения и смещение внутри сегмента, то есть так ss:esp.
Так как модель tiny гораздо проще и удобнее для изучения, будем на ее примере рассматривать работу стека.
В tiny все сегментные регистры указывают на один сегмент, в который загружена программа. Например это сегмент под номером 1234, тогда значения сементных будут содержать адрес этого сегмента. На код указывает регистр CS, а регистр-указатель IP указывает на строку кода, который выполняется:
cs:0100 mov ax,03
cs:0103 int 10h
cs:0105 mov ah,9
.....
то есть сначала регистр IP будет содержать значение 0100, затем 0103, по мере выполнения программы в него загрузится значение 0105. Инструкции выполняются последовательно сверху вниз, словно мы читаем книгу. Стек же "растет" наоборот снизу вверх - от конца сегмента к началу. Со стеком работают сегментный регистр SS и регистр-указатель SP, также есть еще регистр базы BP, он указывает на какой-то определенный адрес стека и не меняет своего значения в процессе работы.
ss:fffe 0b800h
ss:fffc 0c000h
ss:fffa 505h
.....
то есть сначала регистр SP будет содержать значение FFFE, затем FFFC, после загрузки в стек очередной переменной, значение поменяется на FFFA.
Загружаются значения в стек командой PUSH, а "выталкиваются" из стека командой POP.
Напишем небольшую программу и рассмотрим ее в отладчике.
код:
org 100h
nop
nop
nop
nop
push 0aaaah
push 0bbbbh
push 0cccch
mov bp,sp
push 1111h
push 2222h
push 3333h
nop
nop
nop
nop
int 20h
Команда NOP не делает ничего, она используется для организации задержки при обращении к устройствам компьютера, ее я добавил только для наглядности.
Стек в отладчике находится в правом нижнем окне, здесь программа отображена в самом начале работы и в стеке пока только одно значение, все сегментные регистры указывают на один сегмент, в BP пока 0. В следующем скриншоте программа отоброжена уже в конце своей работы:
в SP значение FFF2, BP указывает на FFF8, а IP на 0118.
Казалось бы, со стеком очень неудобно работать: сначала необходимо загрузить в стек значения, затем извлекать их в обратной последовательности, одна ошибка приводит к краху работы программы, почему бы не сохранять значения в переменных, ведь память одна и та же? Почему же программисты предпочитают стек, прямой адресации? Все дело в том, что стек работает гораздо быстрее, к тому же для работы со стеком требуется меньше памяти, а для програмистов работающих на ассемблере, скорость и компактность программ являются важнейшими критериями.
В си-подобных программах весь код основан на API-функциях, они тоже используют стек. Например чтобы вывести информационное окно используется функция MessageBox, на си это так:
MessageBox(0,"Information","title",0)
на ассемблере можно записать так:
push 0
push _title
push _info
push 0
call [MessageBox]
_title db "title"
_info db "information"
В общем то все так и происходит во время выполнения кода на компьютере, обратите внимание - значения записываются справа налево, а извлекаются в обратом порядке.
Можно воспользоваться директивой INVOKE, что упрощает запись:
invoke MessageBox, 0, "information", "title",0
И напоследок код так называемой "невозможной" программы, "невозможными" называют те программы, которые можно написать только на языке ассемблера и ни на каком другом.
В строке 3 мы устанавливаем указатель стека SP на метку OK, затем, выталкиваем в стек двойное значение 90h, 90h - это машинный код команды NOP, той самой, которая ничего не делает. Прерывание int 20h в строке 11 должно завершить выполнение программы, то есть до вывода третьего сообщения в строке 13. Попробуйте догодаться сами чем завершается выполнение этой программы.
В том случае, если захотите запустить эту программу самостоятельно, то воспользуйтесь компилятором fasm, а для эмуляции можно воспользоваться программой DOSBox.
#ассемблер #стек