Chapter_07 (1110559), страница 9
Текст из файла (страница 9)
Это так называемое правило "последний пришёл – первый вышел" (английское сокращение LIFO – Last In First Out). Вообще говоря, это же правило можно записать и как "первый пришёл – последний вышел" (английское сокращение FILO –First In Last Out). В литературе встречаются оба этих правила и их сокращения. Будем изображатьстек в нашей архитектуре "растущим" снизу-вверх, от конца к началу сегмента стека. Как следствиеполучается, что конец стека фиксирован и будет расположен на рисунках снизу, а вершина двигаетсявверх (при записи в стек) и вниз (при чтении из стека).В любой момент времени регистр SP указывает на вершину стека – это последнее слово, записанное в стек. Таким образом, регистр SP является специализированным регистром, на что указывает и его название (Stack Pointer), следовательно, хотя на нём и можно выполнять арифметические операции сложения и вычитания, но делать это следует только тогда, когда мы хотим изменитьположение вершины стека.
Будем изображать стек, как показано на рис. 7.3.Начало стека SS →Вершина стека SP →Конец стека →SP для пустого стека →Рис. 7.3. Так мы будем изображать стек.На этом рисунке, как это реализовано в нашей архитектуре, стек растёт снизу-вверх, занятаячасть стека закрашена. В начале работы программы, когда стек пустой, регистр SP указывает на первое слово за концом стека.
Особым является случай, когда стек имеет максимальный размер 216байт, в этом случае значение регистра SP для пустого стека равно нулю, т.е. совпадает со значениемэтого регистра и для полного стека. Это происходит из-за того, что сегмент максимального размера,28как мы знаем, как бы склеен в кольцо. Именно поэтому стеки максимального размера использовать впрограммах не рекомендуется, так как будет затруднён контроль пустоты и переполнения стека.Обычно для резервирования памяти под стек на языке Ассемблера описывается специальныйсегмент стека. В наших предыдущих программах мы делали это таким образом: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).