В.Г. Баула - Введение в архитектуру ЭВМ и системы программирования (1110549), страница 15
Текст из файла (страница 15)
Эту формулу легко понять, учитывая, что матрица хранится в памяти по строкам (сначала первая строка, затем вторая и т.д.), и каждая строка имеет длину 2*M байт. Буквальное вычисление адресом элементов по приведённой выше формуле (а именно так чаще всего и делает Паскаль-машина) приводит к весьма неэффективной программе. При программировании на Ассемблере лучше всего разделить функции счётчика цикла и индекса элементов. В качестве счётчика лучше всего использовать регистр cx (он и специализирован для этой цели), а адреса лучше хранить в индексных регистрах (bx, si и di). Исходя из этих соображений, можно так переписать программу на Паскале, предвидя её будущий перенос на Ассемблер.
Const N=20; M=30;
Var X: array[1..N,1..M] of integer;
Sum,cx,oldcx: integer; bx: ↑integer;
. . .
{ Ввод матрицы X }
Sum:=0; bx:=↑X[1,1]; {Так в Паскале нельзя}
for cx:=N downto 1 do
if bx↑<0 then begin oldcx:=cx;
for cx:=M downto 1 do begin
Sum:=Sum+bx↑; bx:=bx+2 {Так в Паскале нельзя}
end;
cx:=oldcx
end
else bx:=bx+2*M {Так в Паскале нельзя}
Теперь осталось переписать этот фрагмент программы на Ассемблере:
N equ 20
M equ 30
oldcx equ di
Data segment
X dw N*M dup (?)
Sum dw ?
. . .
Data ends
. . .
; Ввод матрицы X
mov Sum,0
mov bx,offset X; Адрес X[1,1]
mov cx,N
L1: cmp word ptr [bx],0
jge L3
mov oldcx,cx
mov cx,M
L2: mov ax,[bx]
add Sum,ax
add bx,2
loop L2
mov cx,oldcx
jmp L4
L3: add bx,2*M
L4: loop L1
Приведённый пример очень хорошо иллюстрирует стиль мышления программиста на Ассемблере. Для доступа к элементам обрабатываемых данных применяются указатели (ссылочные переменные, адреса), и используются операции над этими адресами (адресная арифметика). Получающиеся программы могут максимально эффективно использовать все особенности архитектуры используемого компьютера. Применение адресом и адресной арифметики свойственно и некоторым языкам высокого уровня (например, языку С), который ориентирован на использование особенности машинной архитектуры для написания более эффективных программ.
Вернёмся к описанию команд цикла. В языке Ассемблера есть и другие команды цикла, которые могут производить досрочный (до исчерпания счётчика цикла) выход из цикла. Как и для команд условного перехода, для мнемонических имен некоторых из них существуют синонимы, которые мы будем разделять в описании этих команд символом /.
Команда
loopz/loope L
выполняется по схеме
Dec(CX); if (CX<>0) and (ZF=1) then goto L;
А команда
loopnz/loopne L
выполняется по схеме
Dec(CX); if (CX<>0) and (ZF=0) then goto L;
В этих командах необходимо учитывать, что операция Dec(CX) является частью команды цикла и не меняет флага ZF.
Как видим, досрочный выход из таких циклов может произойти при соответствующих значениях флага нуля ZF. Такие команды используются в основном при работе с массивами, для усвоения этого материала Вам необходимо изучить соответствующий раздел учебника по Ассемблеру.
7.7. Работа со стеком
Прежде, чем двигаться дальше в описании команд перехода, нам необходимо изучить понятие стека и рассмотреть команды работы со стеком.
Стеком называется сегмент памяти, на начало которого указывает сегментный регистр SS. При работе программы в регистр SS можно последовательно загружать адреса начал нескольких сегментов, поэтому иногда говорят, что в программе несколько стеков. Однако в каждый момент стек только один – тот, на который сейчас указывает регистр SS. Именно этот стек мы и будем иметь в виду.
Кроме начала, у стека есть текущая позиция – вершина стека, её смещение от начала сегмента стека записано в регистре SP (stack pointer). Следовательно, как мы уже знаем, физический адрес вершины стека можно получить по формуле Афиз = (SS*16 + SP)mod 220.
Стек есть аппаратная реализация абстрактной структуры данных стек, с которой Вы познакомились в прошлом семестре. В стек можно записывать (и, соответственно, читать из него) только машинные слова, чтение и запись байтов не предусмотрена в архитектуре рассматриваемого нами компьютера. Это, конечно, не значит, что в стеке нельзя хранить байты, двойные слова и т.д., просто нет машинных команд для записи в стек и чтения из стека данных этих форматов.
В соответствие с определением понятия стек последнее записанное в него слово будет читаться из стека первым. Это так называемое правило "последний пришёл – первый вышел" (английское сокращение LIFO).1 Обычно стек принято изображать "растущим" снизу-вверх. Как следствие получается, что конец стека фиксирован и расположен снизу, а вершина двигается вверх (при записи в стек) и вниз (при чтении из стека).
В каждый момент времени регистр SP указывает на последнее слово, записанное в стек. Обычно стек изображают, как показано на рис. 7.1.
Начало стека SS | |
Вершина стека SP Конец стека | |
SP для пустого стека | |
Рис. 7.1. Так мы будем изображать стек. |
На нашем рисунке, как обычно, стек растёт снизу-вверх, занятая часть стека закрашена. В начале работы программы, когда стек пустой, регистр SP указывает на первое слово за концом стека. Особым является случай, когда стек имеет максимальный размер 216 байт, в этом случае значение регистра SP для пустого стека равно нулю, т.е. совпадает со значением этого регистра и для полного стека, поэтому стеки максимального размера использовать не рекомендуется, так как будет затруднён контроль пустоты и переполнения стека.
Обычно для резервирования памяти под стек на языке Ассемблера описывается специальный сегмент стека. В наших предыдущих программах мы делали это таким образом:
stack segment stack
dw 64 dup (?)
stack ends
Имя сегмента стека и способ резервирования памяти может быть любым, например, можно описать такой стек:
st_1 segment stack
db 128 dup (?)
st_1 ends
То, что этот сегмент будет при выполнении программы использоваться именно как сегмент стека, указывается параметром stack директивы segment. Этот параметр является служебным словом языка Ассемблера и, вообще говоря, не должен употребляться ни в каком другом смысле. 1
В нашем последнем примере размер сегмента стека установлен в 64 слова, поэтому в начале работы регистр SP будет иметь значение 128, т.е., как мы и говорили ранее, указывает на первое слово за концом стека. Области памяти в стеке обычно не имеют имён, так как доступ к ним, как правило, производится только с использованием регистров.
Обратим здесь внимание на важное обстоятельство. Перед началом работы со стеком необходимо загрузить в регистры SS и SP требуемые значения, однако сама программа это сделать не может, т.к. при выполнении самой первой команды программы стек уже должен быть доступен (почему это так мы узнаем в нашем курсе позже, когда будем изучать механизм прерываний). Поэтому в рассмотренных выше примерах программ мы сами не загружали в регистры SS и SP никаких начальных значений. Как мы узнаем позже, перед началом выполнения нашей программы этим регистрам присвоит значения специальная системная программа загрузчик, которая размещает нашу программу в памяти и передаёт управление на команду, помеченную той меткой, которая указана в конце нашего модуля в качестве параметра директивы end. Разумеется, позже при работе программы мы и сами можем загрузить в регистр SS новое значение, это будет переключением на другой сегмент стека.
Рассмотрим сначала те команды работы со стеком, которые не являются командами перехода. Команда
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 216
Следовательно, если мы хотим, чтобы наша программа правильно работала на всех моделях семейства, надо с осторожностью использовать в программе команду 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=0 как для полного, так и для пустого стека (обязательно понять это!), поэтому не рекомендуется использовать стек максимального размера.
Аналогично для проверки того, что стек уже пуст, и читать из него нельзя, следует использовать команду сравнения
cmp SP,K; стек пуст ?
где K – чётное число – размер стека в байтах. Если размер стека в байтах нечётный, то стек полон при SP=1, т.е. в общем случае необходима проверка SP<2. Обычно избегают задавать стеки нечётной длины, для них труднее проверить и пустоту стека.
В качестве примера использования стека рассмотрим программу для решения следующей задачи. Необходимо вводить целые беззнаковые числа до тех пор, пока не будет введено число ноль (признак конца ввода). Затем следует вывести в обратном порядке то из введённых чисел, которые принадлежат диапазону [2..100] (сделаем спецификацию, что таких чисел может быть не более 300). Ниже приведено возможное решение этой задачи.
include io.asm
st segment stack
db 128 dup (?); это для системных нужд
dw 300 dup (?); это для хранения наших чисел
st ends
code segment
assume cs:code,ds:code,ss:st
T1 db ′Вводите числа до нуля$′
T2 db ′Числа в обратном порядке:′,10,13,′$′
T3 db ′Ошибка – много чисел!′,10,13,′$′
program_start:
mov ax,code
mov ds,ax
mov dx, offset T1; Приглашение к вводу
outstr
newline
sub cx,cx; хороший способ для cx:=0
L: inint ax
cmp ax,0; проверка конца ввода
je Pech; на вывод результата
cmp ax,2
jb L
cmp ax,100
ja L; проверка диапазона
cmp cx,300; в стеке уже 300 чисел ?
je Err
push ax; запись числа в стек
inc cx; счетчик количества чисел в стеке
jmp L
Pech: jcxz Kon; нет чисел в стеке