ЗАМЕЧАНИЕ
Логический сдвиг влево и вправо позволяет выполнить быстрое умножение и
деление беззнакового целого числа на делители, представляющие собой
степени числа 2, не требующее накладных расходов команды div.
Для подобного умножения и деления чисел со знаком можно применять
специально предусмотренные в системе команд ассемблера x86
арифметические сдвиги: sal — арифметический сдвиг влево, sar —
арифметический сдвиг вправо (рис. 22).
Рис. 22
Особенность циклических сдвигов заключается в том, что разряды
двигаются по кольцу. Причем существует две разновидности сдвигов в
зависимости от использования флага процессора CF: rol — циклический
сдвиг влево, ror — циклический сдвиг вправо (рис. 23).
132
Рис. 23
В команде rol, как показано на рисунке, бывший самым левым бит
заносится в качестве правого (младшего) и одновременно копируется в CF.
Команды сдвига через перенос rcl — циклический сдвиг влево и rcr —
циклический сдвиг вправо работают следующим образом (рис. 24).
Рис. 24
Поскольку речь зашла о флагах процессора, стоит рассмотреть их
подробнее. Обычно они используются при выполнении команд условного
перехода. В процессорах x86 используются следующие основные флаги:
ZF
—
флаг нуля, устанавливается в единицу, если результат равен 0;
SF
—
флаг положительного/отрицательного значения, устанавливается,
если результат меньше 0;
CF
—
бит переноса из старшего разряда результатов;
OF
—
флаг переполнения, устанавливается, если результат не
поместился в разрядную сетку ЭВМ;
PF
—
флаг четности, устанавливается, если в 8 младших байтах
результата содержится четное количество двоичных единиц.
Значения флагов определяются результатом предыдущей выполненной
команды преобразования данных. Кроме этого, сформировать флаги
можно с помощью специальной команды сравнения операндов cmp:
cmp
ax,bx; сравнение АХ и ВХ
В зависимости от флагов можно выполнить условный переход к метке. В
языке ассемблера x86 существует богатый набор команд переходов, часть
из которых приведены далее, причем допустимы две формы
мнемонической записи (указаны через дробь), что, вообще говоря, — не
лучший подход с точки зрения соблюдения стиля программирования:
JE/JZ
—
переход по нулю (JE — переход по равенству после cmp);
JNE/JNZ
—
если не ноль (не равны аргументы предыдущей cmp);
JG/JNLE
—
переход, если больше для знаковых;
JGE/JNL
—
переход, если больше или равно для знаковых;
JAE/JNBE
—
переход, если больше или равно для беззнаковых;
133
JC/JNAE
—
переход, если бит
СF установлен в единицу.
Переходы необходимы для изменения линейного порядка выполнения
действий в программе. Для этого место в программе, куда передается
управление (с которого продолжается выполнение программы), должно
быть помечено. Рассмотрим пример нахождения максимума среди чисел
без знака:
; максимум среди чисел X и Y → в ячейку Z
mov
ax,X; берем в AX значение числа из ячейки с именем X
cmp
ax,Y; сравнение — формирование флагов
jae
M; переход к метке М если >=
mov
ax,Y;
M: mov
z,ax
Помимо команд условного перехода в программах на языке ассемблера
используются безусловный переход и вызов подпрограммы с возвратом.
Команда безусловного перехода записывается так:
jmp
M1; продолжить с метки M1
Если требуется, например, в целях информационной безопасности
максимально затруднить понимание логики программы, например, при ее
дизассемблировании
злоумышленниками,
можно
насытить
ее
беспорядочными переходами с помощью jmp.
При исполнении команды вызова подпрограммы call происходит
следующее. В системный стек заносится текущее значение регистра —
счетчика команд процессора (IP — Instruction Pointer) и управление
передается по указанному в команде адресу (метке). По окончании
подпрограммы в ней должна встретиться команда ret, при выполнении
которой производится обратное действие — извлекается значение с
вершины стека и управление передается в вызвавшую программу:
; вызов подпрограммы pecat
call pecat
; сюда возвращается управление после исполнения подпрограммы
; …
; где-то в другом месте исходного текста
; подпрограмма печати — комментарий необходим
pecat:; … действия
; действия
ret ; возврат из подпрограммы
Отметим, что требование хорошего стиля программирования
подразумевает, как и при написании программ на языках высокого уровня,
что следует снабжать подпрограмму комментарием, раскрывающим
134
выполняемые ею действия.
Система команд процессоров x86 довольно богата — в ней имеются
специальные команды для организации циклов. Рассмотрим пример с
использованием команды loop. Пусть некий фрагмент программы нужно
повторить N раз. Реализовать это можно, например, следующим образом:
mov
cx,N; регистр CX — счетчик, аналог переменной цикла
L:
;начало тела цикла
;… действия цикла
;…
;…
dec
cx; CX=CX-1
cmp
cx,0; CX=0?
jne
L; CX еще не ноль — возврат к метке L
Как видите, в конце цикла используется стандартная тройка команд. Ее
можно заменить одной — loop! Переписать программу в этом случае
можно следующим образом:
mov
cx,N; регистр CX — счетчик, аналог переменной цикла
L:
;начало тела цикла
;… действия цикла
;…
;…
loop
L; цикл, использует регистр CX по умолчанию
Видно, что современные ассемблеры и архитектуры ЭВМ предоставляют
довольно гибкие возможности — главное, умело ими пользоваться.
Контрольные вопросы
1.
К какому поколению языков программирования относится ассемблер?
2.
В чем разница между ассемблером и автокодом? Откуда произошло
название «автокод»?
3.
Как вы оцениваете перспективы ассемблера в XXI веке?
4.
Каковы области применения ассемблера в современных сложных
программных проектах?
5.
Как работает команда xchg? Удобно ли использовать ее на ассемблере
и как она могла бы повлиять на языки программирования высокого
уровня?
6.
В чем заключается разница между арифметическим и логическим
сдвигами?
7.
Что такое машинное слово?
135
8.
В чем заключается механизм работы индексной адресации?
9.
Какие разновидности машинных команд существуют?
10.
Как работают команды условного перехода? Зачем они нужны?
11.
Для чего применяется команда loop?
Сумма нечетных на ассемблере
Приведенной ранее информации достаточно для того, чтобы приступить к
написанию программы суммирования нечетных элементов числовой
последовательности, проходящей красной нитью через настоящую книгу,
на языке ассемблера x86. Будем пользоваться бесплатно распространяемой
версией Flat Assembler. Рассмотрим возможный вариант решения:
; программа подсчета суммы нечетных в массиве chisla
include 'win32ax.inc'
.data
chisla dw 1,2,3,4,5,0,0,0,0,0
N
dw 10
.code
start: ;
начальная метка программы, в конце должнj быть .end start
mov esi,chisla
mov cx
,[N] ; количество элементов в таблице
mov dx,0 ; в dx будем накапливать сумму нечетных, сначала
обнуляем
l1: mov ax,[esi]
; очередное число заносим в АX
test
ax,1 ; проверяем на нечетность – младший бит на 1
jz l2
; если младший бит двоичного числа равен нулю –
четное!
add dx,ax ; прибавляем очередной нечетный элемент
l2: add esi,2
loop
l1 ; цикл по счетчику cx, пока не дойдет до нуля
; вызываем outint для вывода целого значения — результата
mov ax,dx
call outint
invoke ExitProcess,0
Данная программа предполагает, что у нас есть возможность вывода на
экран результата — мы формируем его в регистре процессора AX. Первая
строка — комментарий, который, как и положено, описывает суть нашей
программы. Строка с include нужна, чтобы мы смогли использовать в
программе системный вызов invoke ExitProcess,0 для корректного
136
завершения. Директива .data FlatAssembler указывает на начало сегмента
описания данных в программе, директива .code — начало исполняемого
блока команд.
В секции описания данных у находится директива chisla dw
1,2,3,4,5,0,0,0,0,0
, с помощью которой мы заносим в память
набор обрабатываемых чисел. В следующей строке — описание ячейки N,
содержащей размер массива, в нашем случае 10.
Правила FlatAssembler подразумевают, что начало исполняемой
программы снабжается меткой start (последней строчкой программы
должна в этом случае быть директива .end start). Итак, вначале в
регистр ESI заносится адрес начала обрабатываемого массива в памяти.
Регистр ESI — так называемая расширенная (32-разрядная) версия
регистра процессора SI, ее использование в данном случае обусловлено
использованием современного компьютера. В следующей строке заносим в
регистр CX, используемый как счетчик, количество чисел. Квадратные
скобки необходимы, чтобы указать на необходимость взять в качестве
аргумента число по адресу, а не сам адрес ячейки, — косвенную
адресацию. Затем мы обнуляем регистр DX — в нем будем накапливать
сумму нечетных чисел.
Со следующей строки начинается тело цикла, поэтому она имеет метку l1.
В цикле мы первым делом извлекаем из памяти по адресу, на который
указывает ESI, очередное число (косвенную адресацию дают квадратные
скобки) и заносим его в регистр AX. Проверка нечетности числа
осуществляется путем вполне уместного при программировании на
ассемблере использования низкоуровневых возможностей — команды
битового тестирования по маске test ax,1. Вспомним, что данные
хранятся в памяти ЭВМ в двоичном виде в позиционном коде. Младший
разряд числа отвечает при этом за степень 2
0
и равен нулю в случае, если
число делится без остатка на 2, и единице — в противном случае.
Соответственно, для проверки на нечетность нет необходимости
выполнять операцию деления на 2. Достаточно проверить содержимое
младшего бита (маска 1). Следующая команда условного перехода jz
обеспечивает переход к метке l2 в случае, если результат нулевой (число
четное), с помощью чего производится обход команд программы,
производящих суммирование.
Следующая операция, которая выполняется либо после суммирования
естественным путем, либо после перехода по команде jz l2, — переход к
следующему числу в памяти. Для этого мы увеличиваем на 2 значение
регистра ESI. Увеличение на 2 связано с тем, что мы храним
обрабатываемые числа в машинных словах, а минимальная единица
137
адресации процессора x86 — 1 байт. В случае если счетчик не дошел до
нуля, с помощью специальной команды цикла loop выполняется
переход к повтору цикла с метки l1.
После выполнения программы сумма нечетных чисел будет накоплена в
регистре DX. Нам было бы интересно получить это значение на экране. К
сожалению, в ассемблере нет удобных средств, подобных writeln в
Паскале или cout << в С++, позволяющих печатать значения
производного типа. Flat Assembler предоставляет системный вызов
MessageBox
, отображающий на экране окно сообщения, но в качестве
аргументов он требует строку. Поэтому нам необходимо преобразовать
число — сумму нечетных — в строку, иными словами, в его запись с
Достарыңызбен бөлісу: |