masm model small .data fls db 5 fld db ? .code start: ... mov al,fls mov fld,al ... end start |
mov ax,ds mov es,ax |
Но есть и другой, более красивый способ выполнения данной операции — использование стека и команд push и pop:
push ds ;поместить значение регистра ds в стек pop es ;записать в es число из стека |
xchg ax,bx ;обменять содержимое регистров ax и bx xchg ax,word ptr [si] ;обменять содержимое регистра ax ;и слова в памяти по адресу в [si] |
Как видно из рис. 1, самым нижним уровнем является уровень BIOS, на котором работа с оборудованием ведется напрямую через порты. Тем самым реализуется концепция независимости от оборудования. При замене оборудования необходимо будет лишь подправить соответствующие функции BIOS, переориентировав их на новые адреса и логику работы портов.
Принципиально управлять устройствами напрямую через порты
несложно. Сведения о номерах портов, их разрядности, формате управляющей
информации приводятся в техническом описании устройства. Необходимо знать
лишь конечную цель своих действий, алгоритм, в соответствии с которым работает
конкретное устройство, и порядок программирования его портов. То есть,
фактически, нужно знать, что и в какой последовательности нужно послать
в порт (при записи в него) или считать из него (при чтении) и как следует
трактовать эту информацию. Для этого достаточно всего двух команд, присутствующих
в системе команд микропроцессора:
in
аккумулятор,номер_порта — ввод в аккумулятор из
порта с номером номер_порта;
out
порт,аккумулятор — вывод содержимого аккумулятора
в порт с номером номер_порта.
Команда lea похожа на команду mov тем, что она также производит пересылку. Однако, обратите внимание, команда lea производит пересылку не данных, а эффективного адреса данных (то есть смещения данных относительно начала сегмента данных) в регистр, указанный операндом назначение.
Часто для выполнения некоторых действий в программе недостаточно
знать значение одного лишь эффективного адреса данных, а необходимо иметь
полный указатель на данные. Вы помните, что полный указатель на данные
состоит из сегментной составляющей и смещения.
Все остальные команды этой группы позволяют получить
в паре регистров такой полный указатель на операнд в памяти. При этом имя
сегментного регистра, в который помещается сегментная составляющая
адреса, определяется кодом операции. Соответственно, смещение помещается
в регистр общего назначения, указанный операндом назначение.
Но не все так просто с операндом источник. На
самом деле, в команде в качестве источника нельзя указывать непосредственно
имя операнда в памяти, на который мы бы хотели получить указатель.
Предварительно необходимо получить само значение полного
указателя в некоторой области памяти и указать в команде получения полного
адреса имя этой области. Для выполнения этого действия необходимо вспомнить
директивы резервирования и инициализации памяти.
При применении этих директив возможен частный случай,
когда в поле операндов указывается имя другой директивы определения данных
(фактически, имя переменной). В этом случае в памяти формируется адрес
этой переменной. Какой адрес будет сформирован (эффективный или полный),
зависит от применяемой директивы. Если это dw, то в памяти формируется
только 16-битное значение эффективного адреса, если же dd — в память записывается
полный адрес. Размещение этого адреса в памяти следующее: в младшем слове
находится смещение, в старшем — 16-битная сегментная составляющая адреса.
Например, при организации работы с цепочкой символов удобно
поместить ее начальный адрес в некоторый регистр и далее в цикле модифицировать
это значение для последовательного доступа к элементам цепочки. В листинге
1 производится копирование строки байт str_1 в строку байт str_2.
В строках 12 и 13 в регистры si и di загружаются значения
эффективных адресов переменных str_1 и str_2.
В строках 16 и 17 производится пересылка очередного байта
из одной строки в другую. Указатели на позиции байтов в строках определяются
содержимым регистров si и di. Для пересылки очередного байта необходимо
увеличить на единицу регистры si и di, что и делается командами сложения
inc (строки 18, 19). После этого программу необходимо зациклить до обработки
всех символов строки.
Листинг 1. Копирование строки <1>;---------Prg_7_2.asm--------------- <2> masm <3> model small <4> .data <5> ... <6> str_1 db ‘Ассемблер — базовый язык компьютера’ <7> str_2 db 50 dup (‘ ‘) <8> full_pnt dd str_1 <9> ... <10> .code <11> start: <12> ... <13> lea si,str_1 <14> lea di,str_2 <15> les bx,full_pnt ;полный указатель на str1 в пару es:bx <16> m1: <17> mov al,[si] <18> mov [di],al <19> inc si <20> inc di <21> ;цикл на метку m1 до пересылки всех символов <22> ... <23> end start |
Это очень интересная и полезная команда. Ее действие заключается
в том, что она замещает значение в регистре al другим байтом из таблицы
в памяти, расположенной по адресу, указанному операндом адрес_таблицы_перекодировки.
Слово “таблица” весьма условно — по сути это просто строка
байт. Адрес байта в строке, которым будет производиться замещение содержимого
регистра al, определяется суммой (bx) + (al), то есть содержимое al выполняет
роль индекса в байтовом массиве.
При работе с командой xlat обратите внимание на следующий тонкий момент. Несмотря на то, что в команде указывается адрес строки байт, из которой должно быть извлечено новое значение, этот адрес должен быть предварительно загружен (например, с помощью команды lea) в регистр bx. Таким образом, операнд адрес_таблицы_перекодировки на самом деле не нужен (необязательность операнда показана заключением его в квадратные скобки). Что касается строки байт (таблицы перекодировки), то она представляет собой область памяти размером от 1 до 255 байт (диапазон числа без знака в 8-битном регистре).
В качестве иллюстрации работы данной команды мы рассмотрим программу, которая преобразует двузначное шестнадцатеричное число, вводимое с клавиатуры (то есть в символьном виде), в эквивалентное двоичное представление в регистре al. Ниже (листинг 2) приведен вариант этой программы с использованием команды xlat.
Листинг 2. Использование таблицы перекодировки <1>;---------Prg_7_3.asm---------------------- <2>;Программа преобразования двузначного шестнадцатеричного числа <3>;в двоичное представление с использованием команды xlat. <4>;Вход: исходное шестнадцатеричное число; вводится с клавиатуры. <5>;Выход: результат преобразования в регистре al. <6>.data ;сегмент данных <7> message db ‘Введите две шестнадцатеричные цифры,$’ <8> tabl db 48 dup (0),0,1,2,3,4,5,6,7,8,9, 8 dup (0), <9> db 0ah,0bh,0ch,odh,0eh,0fh,27 dup (0) <10> db 0ah,0bh,0ch,odh,0eh,0fh, 153 dup (0) <11> .stack 256 ;сегмент стека <12> .code <13> ;начало сегмента кода <14> proc main ;начало процедуры main <15> mov ax,@data ;физический адрес сегмента данных в регистр ax <16> mov ds,ax ;ax записываем в ds <17> lea bx,tabl ;загрузка адреса строки байт в регистр bx <18> mov ah,9 <19> mov dx,offset message <20> int 21h ;вывести приглашение к вводу <21> xor ax,ax ;очистить регистр ax <22> mov ah,1h ;значение 1h в регистр ah <23> int 21h ;вводим первую цифру в al <24> xlat ;перекодировка первого введенного символа в al <25> mov dl,al <26> shl dl,4 ;сдвиг dl влево для освобождения места для младшей цифры <27> int 21h ;ввод второго символа в al <28> xlat ;перекодировка второго введенного символа в al <29> add al,dl ;складываем для получения результата <30> mov ax,4c00h ;пересылка 4c00h в регистр ax <31> int 21h ;завершение программы <32> endp main ;конец процедуры main <33> code ends ;конец сегмента кода <34> endmain ;конец программы с точкой входа main |
Стек — это область памяти, специально выделяемая для временного хранения данных программы. Важность стека определяется тем, что для него в структуре программы предусмотрен отдельный сегмент. На тот случай, если программист забыл описать сегмент стека в своей программе, компоновщик tlink выдаст предупреждающее сообщение.
Для работы со стеком предназначены три регистра:
Перечислим еще некоторые особенности работы со стеком:
Для работы со стеком предназначены регистры ss, esp/sp
и ebp/bp.
Эти регистры используются комплексно, и каждый из них
имеет свое функциональное назначение.
Регистр esp/sp всегда указывает на вершину стека, то
есть содержит смещение, по которому в стек был занесен последний элемент.
Команды работы со стеком неявно изменяют этот регистр так, чтобы он указывал
всегда на последний записанный в стек элемент. Если стек пуст, то значение
esp равно адресу последнего байта сегмента, выделенного под стек.
При занесении элемента в стек процессор уменьшает значение
регистра esp, а затем записывает элемент по адресу новой вершины.
При извлечении данных из стека процессор копирует элемент,
расположенный по адресу вершины, а затем увеличивает значение регистра
указателя стека esp.
Таким образом, получается, что стек растет вниз, в сторону
уменьшения адресов.
Что делать, если нам необходимо получить доступ к элементам
не на вершине, а внутри стека?
Для этого применяют регистр ebp. Регистр ebp —
регистр указателя базы кадра стека.
Например, типичным приемом при входе в подпрограмму является
передача нужных параметров путем записи их в стек. Если подпрограмма тоже
активно работает со стеком, то доступ к этим параметрам становится проблематичным.
Выход в том, чтобы после записи нужных данных в стек сохранить адрес вершины
стека в указателе кадра (базы) стека — регистре ebp. Значение в ebp в дальнейшем
можно использовать для доступа к переданным параметрам.
Начало стека расположено в старших адресах памяти. На
рис. 2 этот адрес обозначен парой ss:ffff. Смещение ffff приведено здесь
условно. Реально это значение определяется величиной, которую программист
задает при описании сегмента стека в своей программе.
К примеру, для программы в листинге
2 началу стека будет соответствовать пара ss:0100h. Адресная пара ss:ffff
— это максимальное для реального режима значение адреса начала стека, так
как размер сегмента в нем ограничен величиной 64 Кбайт (0ffffh).
Для организации работы со стеком существуют специальные
команды записи и чтения.
push
источник — запись значения источник в вершину стека.
Интерес представляет алгоритм работы этой команды, который
включает следующие действия (рис. 3):
pop
назначение — запись значения из вершины стека по месту, указанному
операндом назначение. Значение при этом “снимается” с вершины стека.
Алгоритм работы команды pop обратен алгоритму команды
push (рис. 4):
pusha
— команда групповой записи в стек.
По этой команде в стек последовательно записываются регистры
ax, cx, dx, bx, sp, bp, si, di. Заметим, что записывается оригинальное
содержимое sp, то есть то, которое было до выдачи команды pusha (рис. 5).
pushaw — почти синоним команды pusha. В чем разница?
На уроке 5 мы обсуждали один из атрибутов сегмента — атрибут разрядности.
Он может принимать значение use16 или use32.
Рассмотрим работу команд pusha и pushaw при каждом из
этих атрибутов:
pushad — выполняется аналогично команде pusha, но есть некоторые особенности, которые вы можете узнать из “Справочника команд”.
Следующие три команды выполняют действия, обратные вышеописанным
командам:
popa;
popaw;
popad.
Группа команд, описанная ниже, позволяет сохранить в стеке
регистр флагов и записать слово или двойное слово в стеке. Отметим, что
перечисленные ниже команды — единственные в системе команд микропроцессора,
которые позволяют получить доступ (и которые нуждаются в этом доступе)
ко всему содержимому регистра флагов.
pushf
— сохраняет регистр флагов в стеке.
Работа этой команды зависит от атрибута размера сегмента:
pushfw — сохранение в стеке регистра флагов размером
в слово. Всегда работает как pushf с атрибутом use16.
pushfd
— сохранение в стеке регистра флагов flags или eflags в зависимости от
атрибута разрядности сегмента (то есть то же, что и pushf).
Аналогично, следующие три команды выполняют действия,
обратные рассмотренным выше операциям:
popf
popfw
popfd
И в заключение отметим основные виды операции, когда использование стека практически неизбежно: