7_Язык Ассемблера (975804), страница 9
Текст из файла (страница 9)
В наших предыдущих программах мы делали это таким образом:stack segment stackdw 64 dup (?)stack endsИмя сегмента стека и способ резервирования памяти может быть любым, например, стек такогоже размера можно описать так:st_1 segment stackdb128 dup (?)st_1 endsТо, что этот сегмент будет при запуске программы на счёт использоваться именно как начальный сегмент стека, указывает не имя стека, а его параметр stack в директиве segment.
Этот параметр является служебным словом языка Ассемблера и, вообще говоря, не должен употребляться ни вкаком другом смысле.1В нашем последнем примере размер сегмента стека установлен в 64 слова или 128 байта, поэтому в начале работы регистр SP будет иметь значение 128, т.е., как мы и говорили ранее, он указывает на первое слово за концом стека. Области памяти в стеке обычно не имеют имён, так как доступ к ним, как правило, производится только с использованием адресов на регистрах.Обратим здесь внимание на одно важное обстоятельство.
Перед началом работы со стеком необходимо загрузить в регистры SS и SP требуемые значения, однако сама программа это сделать неможет, т.к. при выполнении самой первой команды программы стек уже должен быть доступен (почему это так мы узнаем в нашем курсе позже, когда будем изучать механизм прерываний). Поэтомув рассмотренных выше примерах программ мы сами не загружали в регистры SS и SP никаких начальных значений. Как мы узнаем позже, перед началом выполнения нашей программы этим регистрам присвоит нужные значения специальная системная программа загрузчик, которая размещаетнашу программу в памяти и передаёт управление на команду, помеченную той меткой, которая указана в конце нашего модуля в качестве параметра директивы end. Как видим, программа загрузчикавыполняет, в частности, те же функции начальной установки значений необходимых регистров, которые в нашей учебной машине выполняло устройство ввода при нажатии кнопки ПУСК.
Разумеется, позже при работе программы мы в принципе и сами можем загрузить в регистры SS и SP новыезначения, это будет переключением на другой сегмент стека.2Теперь приступим к изучению команд, которые работают со стеком (т.е. читают из него и пишутв него слова).
Рассмотрим сначала те команды работы со стеком, которые не являются командамиперехода. Командаpush op1где единственный операнд op1 может иметь форматы r16, m16, CS, DS, SS, ES, записывает в вершину стека слово, определяемое своим операндом. Это команда выполняется по правилу:SP := (SP–2)mod 216 ; <SS,SP> := op1Здесь, как и ранее, регистровая пара <SS,SP> обозначает в стеке слово с адресом, вычисляемым поформулеАфиз = (SS*16 + SP)mod 220Особым случаем является командаpush SPВ младших моделях нашего семейства она выполняется, как описано выше, а в старших – по схеме<SS,SP> := SP; SP := (SP–2)mod 2161Иногда в наших примерах мы, следуя учебнику [5], называли так же и сам сегмент стека. Некоторыекомпиляторы с Ассемблера (например, MASM-4.0) допускают это, если по контексту могут определить, что этоименно имя пользователя, а не служебное слово.
Другие компиляторы (например, Турбо-Ассемблер [17]) подходят к этому вопросу более строго и не допускают использования служебных слов в качестве имён пользователя. Заметим, что все служебные слова Ассемблера, за исключением имён регистров, мы, как это принято вПаскале, выделяем в нашей книге жирным шрифтом.2Замечание для продвинутых учащихся: такое переключение необходимо выполнять в так называемомрежиме запрета прерываний, о чём мы будем говорить в главе, посвящённой системе прерываний.29Следовательно, если мы хотим, чтобы наша программа правильно работала на всех моделях семейства, надо с осторожностью использовать в программе команду push SP .Командаpop op1где op1 может иметь форматы r16, m16, SS, DS, ES, читает из вершины стека слово и записываетего в место памяти, определяемое своим операндом.
Это команда выполняется по правилу:op1 := <SS,SP>; SP := (SP+2)mod 216Команда без явного параметраpushfзаписывает в стек регистр флагов FLAGS, а командаpopfнаоборот, читает из стека слово и записывает его в регистр флагов FLAGS. Эти команды удобны длясохранения в стеке и восстановления значения регистра флагов.В старших моделях нашего семейства появились две новые удобные команды работы со стеком.Командаpushaпоследовательно записывает в стек регистры AX,CX,DX,BX,SP (этот регистр записывается до егоизменения), BP,SI и DI. Командаpopaпоследовательно считывает из стека и записывает значения в эти же регистры (но, естественно, в обратном порядке).
Эти команды предназначены для сохранения в стеке и восстановления значенийсразу всех этих регистров.Машинные команды записи в стек не проверяют того, что стек уже полон, для надёжного программирования это должен делать сам программист. Например, для проверки того, что стек уже полон, и писать в него нельзя, можно использовать команду сравненияcmp SP,0; стек уже полон ?и выполнить условный переход, если регистр SP равен нулю.
Особым случаем здесь будет стек максимального размера 216 байт, для него значение регистра SP будет равно нулю, как для полного, таки для пустого стека (обязательно понять это!), поэтому, как уже говорилось, не рекомендуется использовать в программе стек максимального размера.При проверке переполнения стека следует иметь в виде следующее важное обстоятельство. Встеке всегда должен оставаться некоторый "неприкосновенный запас" свободного места, так как влюбой момент времени сам компьютер (его центральный процессор) может записать в стек некоторые данные (подробно про это мы будем говорить при изучении системы прерываний). Исходя изэтого, в программе следует проверять переполнение стека, например, такими командами:cmp SP,Kgb errВ наших предыдущих примерах под такой "неприкосновенный запас" K мы отводили 64 или 128байт (посмотрите на описание сегмента стека в этих примерах).Для проверки того, что стек уже пуст, и читать из него нельзя, следует использовать командусравненияcmp SP,N; стек пуст ?где N – чётное число, равное размеру стека в байтах.
Если размер стека в байтах нечётный, то стекполон при SP=1, т.е. в общем случае необходима проверка SP<2. Обычно избегают задавать стекинечётной длины, так как обмен со стеком производится только словами (по два байта), и один байтникогда не будет использоваться. Кроме того, для стеков нечётной длины труднее проверить переполнение и пустоту стека.1В качестве примера использования стека рассмотрим программу для решения следующей задачи.
Необходимо вводить целые беззнаковые числа до тех пор, пока не будет введено число ноль(признак конца ввода). Затем следует вывести в обратном порядке те из введённых чисел, которые1Важность контроля правильной работы со стеком можно проиллюстрировать тем фактом, что в программах на Турбо-Паскале всегда включён контроль пустоты и переполнения стека, т.е. этот контроль невозможно выключить никакой директивой транслятору, в отличие от, например, контроля выхода за допустимыйдиапазон при работе с целыми числами или выхода индекса за границы массива.30принадлежат диапазону [2..100] (сделаем спецификацию, что таких чисел может быть, например,не более 300). Ниже приведено возможное решение этой задачи.include io.asmstack segment stackdb128 dup (?); это запас для системных нуждdw300 dup (?); это для хранения наших чиселstack endscode segmentassume cs:code,ds:code,ss:stackT1db'Вводите числа до нуля$'T2db'Числа в обратном порядке:',10,13,'$'T3db'Ошибка – много чисел!',10,13,'$'program_start:movax,codemovds,axmovdx,offset T1; Приглашение к вводуoutstrnewlinesubcx,cx; хороший способ для cx:=0L:inint axcmpax,0; проверка конца вводаjePech; на вывод результатаcmpax,2jbLcmpax,100jaL;проверка диапазона [2..100]cmpcx,300; в стеке уже 300 чисел ?jeErrpush ax; запись числа в стекinccx; счетчик количества чисел в стекеjmpLPech: jcxz Kon; нет чисел в стекеmovdx,offset T2outstrL1:popaxoutword ax,10; ширина поля вывода=10loop L1Kon: finishErr: movdx,T3outstrfinishcode endsend program_startЗаметим, что в нашей программе нет собственно переменных, а только строковые константы,поэтому мы не описали отдельный сегмент данных, а разместили эти строковые константы в кодовом сегменте, на начало которого установили сегментный регистр CS.
Можно считать, что сегментыданных и кода в нашей программе в некотором смысле совмещены. Мы разместили строковые константы в самом начале сегмента кода, перед входной точкой программы, но с таким же успехомможно разместить эти строки и в конце кодового сегмента после последней макрокоманды finish.Обратите внимание, как мы выбрали размер стека: 128 байт мы зарезервировали для системныхнужд (как уже упоминалось, стеком будут пользоваться и другие программы, подробнее об этом будет рассказано далее) и 300 слов мы отвели для хранения вводимых нами чисел. При реализацииэтой программы, анализируя переполнение стека, мы использовали команды:cmp cx,300; в стеке уже 300 чисел ?jeErrКак уже отмечалось выше, это можно сделать и командами31cmp SP,128; только "неприкосновенный запас"?jbErrТеперь, после того, как мы научились работать с командами записи слов в стек и чтения слов изстека, вернёмся к дальнейшему рассмотрению команд перехода.7.9.