Хотелось бы рассмотреть что-то интересное и полезное вплане использования, поэтому выбор пал на ассемблер, а именно на создание примитивной графики.
Язык ассемблер - это низкоуровневый язык программирования или же программа, которая исходный текст программы, написанный на языке ассемблера, переводит в программу на машинный язык. Язык, по некоторым меркам сложный, но ведь создание примитивов графики берет начало именно тут. Я же хочу рассмотреть ассемблер под Windows, а именно MASM , который, на ряду с Visual Studio, не так давно использовал для создания графических примитивов. Об этом с иллюстрациями и подробностями далее.
Я же отступлю кое-где и кое-как, но в целом у нас должна получиться отличная программа, которая нарисует нам довольно интересный таки примитив. Рассмотрим пример структуры программы на Ассемблере (см. Листинг 1)
Include lib ; подключение inc
includelib lib ; подключение lib
DATA ; иницилизиpуемые данные
; имя класса и окна
DATA? ; неиницилизиpуемые данные
; дескриптор пpогpаммы
CODE ; здесь начинается код программы
Обычно используют один из трех методов:
А) рабочая область может быть восстановлена, если ее содержимое формируется с помощью каких-либо вычислений;
б) последовательность событий, формирующих рабочую область, может быть сохранена, а затем «проиграна» сколь угодно раз;
в) можно создавать виртуальное окно и направлять весь вывод в виртуальное окно, а при получении основным окном сообщения WM_PAINT копировать содержимое виртуального окна в основное (будет использовано для демонстрации написанного позже приложения).
WINGDIAPI BOOL WINAPI MoveToEx(HDC, int, int, LPPOINT);
Первый аргумент - это контекст устройства, второй и третий - координаты точки, в которую устанавливается текущая графическая позиция. Последний аргумент - указатель на структуру типа POINT, в которую функция запишет координаты старой текущей позиции.
WINGDIAPI BOOL WINAPI LineTo(HDC, int, int);
Первый аргумент - контекст устройства, второй и третий аргументы - координаты точек.
WINGDIAPI BOOL WINAPI Rectangle(HDC, int, int, int, int);
Первый аргумент - это контекст устройства, все же остальные аргументы - координаты верхнего левого и нижнего правого углов прямоугольника.
WINGDIAPI BOOL WINAPI Ellipse(HDC, int, int, int, int);
Первый аргумент - это контекст устройства.
Примечание: эллипс ограничен прямоугольником и именно через координаты этого прямоугольника и определяется прорисовываемый эллипс. Второй и третий аргументы - координаты левого верхнего угла прямоугольника, четвертый и пятый аргументы - координаты нижнего правого угла.
WINGDIAPI BOOL WINAPI RoundRect(HDC, int, int, int, int, int, int);
Первые пять аргументов полностью идентичны аргументам функции Rectangle(). Последние два аргумента содержат ширину и высоту эллипса, определяющего дуги.
Для написания примитива рассмотрим шаги, которые необходимы для создания и отрисовки графики:
1) получение дескриптора для программы;
2) регистрация класса окна;
3) создание окна;
4) отображение окна на экpане;
5) обновление содержимого экpана в окне;
6) выход из пpогpаммы.
Приступим к созданию, но для начала создадим новый проект в Visual Studio: File -> New Project
Выбираем пустой прокт: Empty project
Создаем новый файл: правой кнопкой по Source -> Add -> New Item
Создаем новый файл (.asm):
1-ый способ - дописать при создании нового файла file.asm (я таким способом создавал)
2-ой способ - изменить расширение файлу после его создания (file.txt -> rename -> file.asm)
Используем masm в Visual Studio: нажимаем правой кнопкой по преокту -> Build Customization
Задаем этот самый masm: ставим галочку напротив masm
Приступаем к написанию этого самого примитива, а сам листинг смотрите ниже.
Includelib kernel32.lib
include kernel32.inc
includelib user32.lib
include user32.inc
include windows.inc
include gdi32.inc
Hwnd dd 0
hInst dd 0
szTitleName db "АиПОС. Лабороторная работа №6", 0
szClassName db "Приложение Win32", 0
msg MONMSGSTRUCT >
wc WNDCLASS >
ps PAINTSTRUCT >
Main PROC
invoke GetModuleHandle, 0 ;получение значения баз. адреса,
mov hInst, eax ;по которому загружен модуль.
mov wc.style, CS_HREDRAW + CS_VREDRAW + CS_GLOBALCLASS
mov wc.lpfnWndProc, offset WndProc ;адрес оконной процедуры
mov wc.cbClsExtra, 0
mov wc.cbWndExtra, 0
mov eax, hInst ;дескриптор приложения
mov wc.hInstance, eax ;в поле hInstance
invoke LoadIcon, 0, IDI_APPLICATION
mov wc.hIcon, eax ;дескриптор значка в поле hIcon
invoke LoadCursorA, 0, IDC_ARROW
mov wc.hCursor, eax ;дескриптор курсора в поле hCursor
mov wc.hbrBackground, WHITE_BRUSH ;цвет бекграунда окна белый
mov dword ptr wc.lpszMenuName, 0 ;главного меню нет
mov dword ptr wc.lpszClassName, offset szClassName ;имя класса окна
invoke RegisterClassA, offset wc ;регистрация класас окна
invoke CreateWindowEx, 0, offset szClassName, offset szTitleName, \
WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, \
CW_USEDEFAULT, CW_USEDEFAULT, 0, 0, hInst, 0
mov hwnd, eax ;создание окна
invoke ShowWindow, hwnd, SW_SHOWNORMAL ;показ окна
invoke UpdateWindow, hwnd ;перерисовывка содержимого окна
cycle1: ;цикл сообщений
invoke GetMessage, offset msg, 0, 0, 0
cmp ax, 0
je end_c
invoke TranslateMessage, offset msg ;трансляция ввода с клавиатуры
invoke DispatchMessage, offset msg ;отправляем сообщение
;оконной процедуре
jmp cycle1
end_c:
invoke ExitProcess, 0 ;выход из приложения
Main ENDP
WndProc PROC USES ebx edi esi, _hwnd:DWORD, _wmsg:DWORD, _wparam:DWORD, _lparam:DWORD
local _hdc:DWORD
cmp _wmsg, WM_DESTROY
je wmdestroy
cmp _wmsg, WM_PAINT
je wmpaint
invoke DefWindowProcA, _hwnd, _wmsg, _wparam, _lparam ;обраб. по умолчанию
jmp exit_proc
wmpaint:
invoke BeginPaint, _hwnd, offset ps ;получаем контекст устройства
mov _hdc, eax
invoke Rectangle, _hdc, 170, 120, 310, 260 ;тело
invoke Rectangle, _hdc, 120, 120, 170, 140 ;левая лапа
invoke Rectangle, _hdc, 310, 120, 360, 140 ;правая лапа
invoke Rectangle, _hdc, 170, 260, 190, 310 ;левая ноголапа
invoke Rectangle, _hdc, 290, 260, 310, 310 ;правая ноголапа
invoke Rectangle, _hdc, 210, 80, 270, 120 ;башка
invoke Rectangle, _hdc, 220, 85, 225, 90 ;левый глаз
invoke Rectangle, _hdc, 250, 85, 255, 90 ;правый глаз
invoke Rectangle, _hdc, 225, 105, 255, 120 ;рот
invoke EndPaint, _hdc, offset ps ;освобождаем контекст
jmp exit_proc
wmdestroy:
invoke PostQuitMessage, 0 ;послать сообщение WM_QUIT
mov eax, 0 ;возвращаемое значение - 0
exit_proc:
ret
WndProc ENDP
END Main
По ходу действий и написания кода проставлял комментарии, но чтобы понять полную суть, рассмотрю подробнее все, что сделал и написал.
Далее идет инициализация класса окна - оно опpеделяет некотоpые важные хаpактеpистики окна, такие как иконка, куpсоp, функцию, ответственную за окно и так далее. Тут же и описываем дескриптор самого приложения, дескриптор значка и дескриптор курсора. Дескриптора меню в реализованном приложении нет, поскольку это увеличило бы код программы, а функциональности ему не добавило, тем более, что это примитив и он тут вовсе не нужен. Параметры, которые могут или были использованы для создания окна:
1) cbSize: задает размеp общей стpуктуpы WDNCLASSEX в байтах;
2) style: задает стиль окона;
3) cbClsExtra: задается количество дополнительных байтов, котоpые нужно будет зарезервировать для самой программы;
4) hInstance: задает дескриптор модуля;
5) hIcon: задает дескриптор иконки, а его получение просходит посредством обращения функции LoadIcon;
6) hCursor: задает дескриптор куpсоpа, а его получение просходит посредством обращения функции LoadCursor;
7) hbrBackground: задает цвет фона;
8) lpszMenuName: задается дескриптор меню для окон;
9) lpszClassName: задается имя класса окна.
После pегистpации класса окна функцией RegisterClassEx, происходит вызов CreateWindowEx, чтобы создать наше окно, основанное на этом класе.
Основной и немаловажной является процедура WndProc PROC USES ebx edi esi, _hwnd:DWORD, _wmsg:DWORD, _wparam:DWORD, _lparam:DWORD.Не обязательно ее было называть ее WndProc, где пеpвый паpаметp, _hwnd - это хэндл окна, котоpому пpедназначается сообщение,_wmsg - передаваемое сообщение. Стоит сказать, что _wmsg - это не msg стpуктуpа, но это всего лишь число. _wparam и _lparam - это дополнительные паpаметpы, которые используются некоторыми сообщениями.
В конце концов подошли к заключительной части, где и описываются задаваемые фигуры, их координаты и возвращаемые значения. Это ключевая часть, поскольку именно здесь pасполагается логика действий пpогpаммы. Тут же описываем освобождение контекста и возравщаем значения, где далее посылаем сообщение о завершении. Единственное сообщение, которое осталось обработать - wmdestroy - это сообщение будет посылаться окну, когда оно закpывается. В то вpемя, когда пpоцедуpа окна его получает, окно уже исчезло с экpана. После выполнения wmdestroy вызывается PostQuitMessage, котоpый посылает сообщение о выходе и это вынуждает GetMessage веpнуть нулевое значение в eax, а это уже выход из программы.
Общий формат оператора Ассемблера имеет следующий вид:
[Метка[:]] Мнемоника [Операнд1 [{,Операнд2}]] [;Комментарий]
Здесь элементы, указанные в квадратных скобках, могут отсутствовать, а элементы в фигурных скобках могут повторяться 1 или более раз. Пробелы вводятся произвольно, но как минимум один пробел должен следовать после кода операции.
Метка - это идентификатор, связанный с адресом первого байта того оператора, в котором она появляется. Мнемоника - это мнемоническое обозначение соответствующей команды процессора или директивы Ассемблера. Комментарий - это любая последовательность символов, начиная с символа ";" до конца строки, которая поясняет соответствующий оператор.
Метки используются как операнды в операторах программы для ссылки на адреса команд (например, при условных и безусловных переходах) и данных (например, переменных, массивов, структур). Имена меток могут состоять из следующих символов: "A" - "Z", "a" - "z", "_", "@", "$", "?", "0" - "9". Символы "0" - "9" не могут использоваться в качестве первых символов имени метки. Символы "$" и "?" имеют специальное значение, поэтому их не следует использовать в именах пользовательских меток. Имена меток не должны совпадать с именами регистров, мнемониками команд процессора, а также с ключевыми словами Ассемблера (встроенными переменными, операциями, директивами).
Каждая метка должна определяться только один раз, то есть имена меток должны быть уникальными (исключением являются локальные метки, см. далее). Как операнды метки могут использоваться любое число раз.
Метка может занимать всю строку. В этом случае значением метки является адрес команды или директивы, которая должна следовать в следующей строке программы. При определении метки следует завершать ее двоеточием в случае, если после метки следует команда процессора. Если же метка определяется для директивы (при описании данных, сегментов, подпрограмм), то двоеточие не ставится.
Основным полем в строке программы на Ассемблере является поле мнемоники. Мнемоники команды компилируются непосредственно в те команды процессора Intel 8086, которым они соответствуют. В отличие от мнемоник команд, директивы не компилируются в исполняемый код, они лишь управляют различными аспектами работы компилятора - от типа генерируемого кода (для процессоров 8086, 80286, 80386 и т. д.) до определения сегментов и формата создаваемых файлов листингов, и, таким образом, обеспечивают высокоуровневые средства программирования на Ассемблере.
Операнды оператора Ассемблера описываются выражениями. Выражения конструируются на основе операций над целочисленными и символьными константами, метками переменных и именами регистров с использованием знаков и имен операций.
Для программиста ключевым ресурсом вычислительной машины является система реализуемых ей команд. Каждая машинная команда разделяется на группы бит (поля): поле кода операции и одно или несколько полей операндов. Код операции показывает, что нужно делать, а операнды определяют необходимую команде информацию и могут содержать данное, адрес данного, косвенный указатель на данное или другую информацию, относящуюся к обрабатываемым командой данным. Каждой команде сопоставлена мнемоника. Процессор Intel 8086 реализует следующую систему команд, которые в соответствии с выполняемыми ими функциями можно разделить по группам.
Программа Emu8086 платная. Однако в течение 30 дней вы можете использовать её для ознакомления бесплатно.
Итак, вы скачали и установили программу Emu8086 на свой компьютер. Запускаем её и создаём новый файл через меню FILE – NEW – COM TEMPLATE (Файл – Новый – Шаблон файла COM). В редакторе исходного кода после этого мы увидим следующее:
Рис. 1.1. Создание нового файла в Emu8086.
Здесь надо отметить, что программы, создаваемые с помощью Ассемблеров для компьютеров под управлением Windows, бывают двух типов: COM и EXE. Отличия между этими файлами мы рассмотрим позже, а пока вам достаточно знать, что на первое время мы будем создавать исполняемые файлы с расширением COM, так как они более простые.
После создания файла в Emu8086 описанным выше способом в редакторе исходного кода вы увидите строку «add your code hear» - «добавьте ваш код здесь» (рис. 1.1). Эту строку мы удаляем и вставляем вместо неё следующий текст:
MOV AH, 02h MOV DL, 41h INT 21h INT 20h Таким образом, полный текст программы будет выглядеть так: ORG 100h MOV AH, 02h MOV DL, 41h INT 21h INT 20h RET Кроме этого в верхней части ещё имеются комментарии (на рис. 1.1 – это текст зелёного цвета). Комментарий в языке Ассемблера начинается с символа; (точка с запятой) и продолжается до конца строки. Если вы не знаете, что такое комментарии и зачем они нужны, см. книгу Как стать программистом . Как я уже говорил, здесь мы не будем растолковать азы программирования, так как книга, которую вы сейчас читаете, рассчитана на людей, знакомых с основами программирования.
Также отметим, что регистр символов в языке ассемблера роли не играет. Вы можете написать RET, ret или Ret – это будет одна и та же команда.
Вы можете сохранить этот файл куда-нибудь на диск. Но можете и не сохранять. Чтобы выполнить программу, нажмите кнопку EMULATE (с зелёным треугольником) или клавишу F5. Откроется два окна: окно эмулятора и окно исходного кода (рис. 1.2).
Рис. 1.2. Окно эмулятора Emu8086.
В окне эмулятора отображаются регистры и находятся кнопки управления программой. В окне исходного кода отображается исходный текст вашей программы, где подсвечивается строка, которая выполняется в данный момент. Всё это очень удобно для изучения и отладки программ. Но нам это пока не надо.
В окне эмулятора вы можете запустить вашу программу на выполнение целиком (кнопка RUN) либо в пошаговом режиме (кнопка SINGLE STEP). Пошаговый режим удобен для отладки. Ну а мы сейчас запустим программу на выполнение кнопкой RUN. После этого (если вы не сделали ошибок в тексте программы) вы увидите сообщение о завершении программы (рис. 1.3). Здесь вам сообщают о том, что программа передала управление операционной системе, то есть программа была успешно завершена. Нажмите кнопку ОК в этом окне и вы увидите, наконец, результат работы вашей первой программы на языке ассемблера (рис. 1.4).
Рис. 1.3. Сообщение о завершении программы.
Рис. 1.4. Ваша первая программа выполнена.
Как мы уже говорили, наша первая программа выводит на экран английскую букву «А». Результат оправдал наши ожидания – буква «А» выведена на экран.
Здесь стоит отметить, что Emu8086 – это ЭМУЛЯТОР, то есть он эмулирует работу компьютера с процессором 8086. Поэтому в описанном выше примере программа выполняется не операционной системой, а эмулятором. Emu8086 может создавать и реальные программы, которые могут самостоятельно выполняться на компьютере. Но описание работы с Emu8086 не входит в наши планы. Читайте справку и экспериментируйте – всё у вас получится.
В нашем случае пока не важно, как выполняется программа – эмулятором или операционной системой. Главное – разобраться с вопросом создания программ на языке ассемблера. Поэтому разберём нашу простенькую программку подробно.
#make_COM# – 1-ая строка. Эта команда – специфическая для Emu8086. Она используется для определения типа создаваемого файла. В нашем случае это файл с расширением.COM.
ORG 100h – 2-ая строка. Эта команда устанавливает значение программного счетчика в 100h, потому что при загрузке СОМ-файла в память, DOS выделяет под блок данных PSP первые 256 байт (десятичное число 256 равно шестнадцатеричному 100). Код программы располагается только после этого блока. Все программы, которые компилируются в файлы типа СОМ, должны начинаться с этой директивы.
MOV AH, 02h – 3-я строка. Инструкция (или команда) MOV помещает значение второго операнда в первый операнд. То есть значение 02h помещается в регистр АН. Для чего это делается? 02h – это ДОСовская функция, которая выводит символ на экран. Мы пишем программу для DOS, поэтому используем команды этой операционной системы (ОС). А записываем мы эту функцию (а точнее ее номер) именно в регистр АН, потому что прерывание 21h использует именно этот регистр.
MOV DL, 41h – 4-я строка. Код символа «A» заносится в регистр DL. Код символа «A» по стандарту ASCII – это 41h.
INT 21h – 5-я строка. Это и есть то самое прерывание 21h – команда, которая вызывает системную функцию DOS, заданную в регистре АН (в нашем примере это функция 02h). Команда INT 21h – основное средство взаимодействия программ с ОС.
INT 20h – 6-я строка. Это прерывание, которое сообщает операционной системе о выходе из программы и о передаче управления консольному приложению. Значит, при использовании INT 20h в нашем примере, управление будет передаваться программе Emu8086. А в том случае, если программа уже откомпилирована и запущена из ОС, то команда INT 20h вернет нас в ОС (например, в DOS). В принципе, в случае с Emu8086 эту команду можно было бы пропустить, так как эту же функцию выполняет команда RET, которая вставляется в исходный текст автоматически при создании нового файла по шаблону (как это сделали мы ранее). Но я решил использовать INT 20h и здесь для совместимости с другими ассемблерами.
Тем, кому не все понятно из этих объяснений, рекомендую почитать книгу Как стать программистом , а также следующие главы.
Для того, чтобы писать программы на ассемблере, нам необходимо знать, какие регистры процессора существуют и как их можно использовать. Все процессоры архитектуры x86 (даже многоядерные, большие и сложные) являются дальними потомками древнего Intel 8086 и совместимы с его архитектурой. Это значит, что программы на ассемблере 8086 будут работать и на всех современных процессорах x86.
Все внутренние регистры процессора Intel 8086 являются 16-битными:
Всего процессор содержит 12 программно-доступных регистров, а также регистр флагов (FLAGS) и указатель команд (IP).
Регистры общего назначения (РОН) AX, BX, CX и DX используются для хранения данных и выполнения различных арифметических и логических операций. Кроме того, каждый из этих регистров поделён на 2 части по 8-бит, с которыми можно работать как с 8-битными регистрами (AH, AL, BH, BL, CH, CL, DH, DL). Младшие части регистров имеют в названии букву L (от слова Low ), а старшие H (от слова High ). Некоторые команды неявно используют определённый регистр, например, CX может выполнять роль счетчика цикла.
Индексные регистры предназначены для хранения индексов при работе с массивами. SI (Source Index ) содержит индекс источника, а DI (Destination Index ) - индекс приёмника, хотя их можно использовать и как регистры общего назначения.
Регистры-указатели BP и SP используются для работы со стеком. BP (Base Pointer ) позволяет работать с переменными в стеке. Его также можно использовать в других целях. SP (Stack Pointer ) указывает на вершину стека. Он используется командами, которые работают со стеком. (Про стек я подробно расскажу в отдельной части учебного курса)
Сегментные регистры CS (Code Segment ), DS (Data Segment ), SS (Stack Segment ) и ES (Enhanced Segment ) предназначены для обеспечения сегментной адресации. Код находится в сегменте кода, данные - в сегменте данных, стек - в сегменте стека и есть еще дополнительный сегмент данных. Реальный физический адрес получется путём сдвига содержимого сегментного регистра на 4 бита влево и прибавления к нему смещения (относительного адреса внутри сегмента). Подробнее о сегментной адресации рассказывается в части 31 .
COM-программа всегда находится в одном сегменте, который является одновременно сегментом кода, данных и стека. При запуске COM-программы сегментные регистры будут содержать одинаковые значения.
Указатель команд IP (Instruction Pointer ) содержит адрес команды (в сегменте кода). Напрямую изменять его содержимое нельзя, но процессор делает это сам. При выполнении обычных команд значение IP увеличивается на размер выполненной команды. Существуют также команды передачи управления, которые изменяют значение IP для осуществления переходов внутри программы.
Регистр флагов FLAGS содержит отдельные биты: флаги управления и признаки результата. Флаги управления меняют режим работы процессора:
Признаки результата устанавливаются после выполнения арифметических и логических команд:
Не волнуйтесь, если что-то показалось непонятным. Из дальнейшего объяснения станет ясно, что к чему и как всем этим пользоваться 🙂
Методичка содержит материал для выполнения первых лабораторных работ по работе с языком ассемблера
Операторы языка ассемблера ПЭВМ имеют следующий формат:
[<метка> :]<префикс> <код операции > [<спиcок операндов >1 [<комментарии>].
Запись программы выполняется по свободному формату, т. е. специально не оговариваются правила заполнения позиций строки. Точка с запятой в начале строки означает, что данная строка является строкой комментария.
Программа может записываться как заглавными, так и строчными буквами. Метку произвольной длины следует записывать с начала строки и отдалять от кода операции двоеточием, за которым может следовать произвольное количество пробелов (вплоть до конца строки).
Код операции должен отделяться от списка операндов хотя бы одним пробелом. Операнды отделяются один от другого запятой.
Для определения данных в основной памяти и резервирования полей памяти под данные, размещаемые в основной памяти в процессе выполнения программы, используются следующие операторы:
DB - определить однобайтовое поле, DW - определить слово (двухбайтовое поле), DD - определить двойное слово (четырехбайтовое поле).
Формат команды:
DB
[<имя поля>] DW [< количество > DUP (]{ <список чисел >}[)]
DD ?
где <количество >- количество полей памяти указанной длины, которое определяется данной командой (указывается, если определяется не одно поле памяти); ? - используется при резервировании памяти.
Примечание. При записи слов в память младший байт записывается в поле с младшим адресом. Например, в примере 3, если запись выполнялась по адресу 100, то по адресу 100 будет записано 34H, а по адресу 101 - 12H.
Операнды команд ассемблера могут определяться непосредственно в команде, находиться в регистрах или в основной памяти,
Данные, непосредственно записанные в команде, называются литералами. Так, в команде
mov ah, 3
3 - литерал.
Если операнды команд ассемблера находятся в регистрах, то в соответствующих командах указываются имена регистров (если используемые регистры особо не оговариваются для данной команды. Например, в приведенном выше примере аh - имя регистра аккумулятора.
Адресация операндов, расположенных в основной памяти, может быть прямой и косвенной.При использовании прямой адресации в команде указывается символическое имя поля памяти, содержащего необходимые данные, например:
inc OPND
Здесь OPND - символическое имя поля памяти, определенного оператором ассемблера
OPND dw ?
При трансляции программы ассемблер заменит символическое имя на исполнительный адрес указанного поля памяти (смещение относительно начала сегмента) и занесет этот адрес на место индексного смещения. Адресация а этом случае выполняется по схеме: BР + <индексное смещение>, но содержимое регистра ВР при вычислении исполнительного адреса не используется (частный случай).
В отличие от прямого косвенный адрес определяет не местоположение данных в основной памяти, а местоположение компонентов адреса этих данных. В этом случае в команде указываются один или два регистра в соответствии с допустимыми схемами адресации и индексное смещение, которое может задаваться числом или символическим именем. Косвенный адрес заключается в квадратные скобки весь или частично, например:
OPND
OPND +
+
Приведенные выше формы записи косвенного адреса интерпретируются одинаково.
При трансляции программы ассемблер определяет используемую схему адресации и соответствующим образом формирует машинную команду, при этом символическое имя заменяется смещением относительно начала сегмента так же, как в случае прямой адресации.
Примечание. При использовании косвенной адресации по схеме ВР + <индексное смещение> индексное смещение не может быть опущено, так как частный случай адресации по данной схеме с нулевой длиной индексного смещения используется для организации прямой адресации. Следовательно, при отсутствии индексного смещения в команде следует указывать нулевое индексное смещение, т.е. [ВР + 0] .
Приведем два примера:
и
+ +6
.
В первом случае исполнительный адрес операнда определяется суммой содержимого регистра bх и индексного смещения, заданного символическим именем "а", а во втором - суммой содержимого регистров bp, si и индексного смещения, равного 6.
Длина операнда может определяться:
MOV <адрес приемника> ,< адрес источника>
используется для пересылки данных длиной I или 2 байта из регистра в регистр, из регистра в основную память, из основной памяти в регистр, а также для записи в регистр или основную память данных, непосредственно записанных в команде. Все возможные пересылки представлены на рис. 6.
Для загрузки "прямого" адреса в сегментный регистр используются две команды пересылки:
mov ax, code
mov ds, ax
ХCHG <операнд 1> , <операнд 2>
организует обмен содержимого двух регистров (кроме сегментных) или регистра и поля основной памяти. Например:
xchg bx, cx - обмен содержимого регистров bx и сх.
LEA < операнд l > , < операнд 2 >
вычисляет исполнительный адрес второго операнда и помещает его в поле, на которое указывает первый операнд. Приведем примеры:
LDS < регистр > ,<операнд 2>
LЕS < регистр > ,<операнд 2>
Команда LDS загружает в регистры DS:< регистр> указатель (< адрес сегмента > : < исполнительный адрес >), расположенный по адресу, указанному во втором операнде.
Команда LЕS загружает указатель по адресу, расположенному во втором операнде, в регистры ЕS:< регистр> .
Например:
lds si, exword
т.e. слово (2 байта) по адресу exword загружается в si, а по адресу exword+ 2 - в ds.
PUSH < операнд>
организует запись в стек слова, адрес которого указан в операнде. Например;
push dx - запомнить содержимое регистра dx в стеке.
POP < операнд>
организует чтение из стека последнего слова и помещает его по адресу, указанному во втором операнде. Например:
pop dx - восстановить содержимое регистра dx из стека.
ADD <операнд 1> , <операнд 2>
ADC <операнд 1> , <операнд 2>
устанавливают флаги четности, знака результата, наличия переноса, наличия переполнения.
Ilo команде ADD выполняется сложение двух операндов. Результат записывается по адресу первого операнда. По команде АDC также выполнятся сложение двух операндов, но к ним добавляется еще значение, записанное в бите переноса, установленном предыдущей командой сложения.
На рис. 7 показаны возможные способы размещения слагаемых, где а -операнды - слова, б - операнды - байты.
Приведем пример сложения двух 32-разрядных чисел:
mov ax, value1
add value2, ax
mov ax, value1+2
adc value2+2, ax
Исходные числа находится в основной памяти по адресам value1 и value2, а результат записывается по адресу value1.
SUB <уменьшаемое-результат> , <вычитаемое>
SBB <уменьшаемое-результат>, <вычитаемое>
устанавливают флаги четности, знака результата, наличия заема, наличия переполнения.
При выполнении операции по команде SUB заем не учитывается, а по команде SBB - учитывается. Ограничения на местоположение операндов такие же, как и у команды сложения.
NEG <операнд>
знак операнда изменяется на противоположный.
INC <операнд>
значение операнда увеличивается на единицу.
DEC <операнд>
значение операнда уменьшается на единицу.
СМP <операнд 1> , < операнд 2>
выполняется операция вычитания без записи результата и устанавливаются признаки во флажковом регистре.
MUL <операнд>
IМUL <операнд>
устанавливают флаги наличия переноса или переполнения.
По команде MUL числа перемножаются без учета, и по команде - IМUL с учетом знака (в дополнительном коде).
На рис. 8 (где а - операнды - слова, б - операнды - байты) приведены возможные способы размещения сомножителей и результата (один из сомножителей всегда расположен в регистре-аккумуляторе.
Рассмотрим пример:
imul word ptr c
Здесь содержимое основной памяти по адресу "с" длиной слово умножается на содержимое регистра ax. Младшая часть результата операции записывается в регистр aх, а старшая часть - и регистр dx.
DIV <операнд-делитель>
IDIV <операнд-делитель>
По команде DIV операция деления выполняется без учета, а по команде IDIV - с учетом знака (в дополнительном коде).
На рис. 9 приведены возможные способы размещения делимого, делителя и результата (а - операнды - слова, б - операнды - байты).
CBW
CWD
По команде CBW число из al переписывается в ax (дополнение выполняется знаковыми разрядами). Аналогично по команде CWD число из ax переписывается в два регистра dx и ax (dx:ax).
JMP <адрес перехода>
имеет три модификации в зависимости от длины ее адресной части:
При указании перехода к командам, предшествующим команде перехода, ассемблер сам определяет расстояние до метки перехода и строит адрес нужной длины. При указании перехода к последующим частям программы необходимо ставить указатели short, near ptr и far ptr.
В качестве адреса команды перехода используются метки трех видов:
<мнемоническая команда> <адрес перехода>
Мнемоника команд условного перехода:
Все команды имеют однобайтовое поле адреса, следовательно, смешение не должно превышать -128...127 байт. Если смещение выходит за указанные пределы, то используется специальный прием: (читайте в методичке)
В качестве счетчика цикла во всех командах циклической обработки используется содержимое регистра cx
.
LOOP < адрес перехода >
при каждом выполнении уменьшает содержимое регистра cx на единицу и передает управление по указанному адресу, если cx не равно 0:
begin_loop:
; ... тело цикла...
loop begin_loop
Примечание. Если перед началом цикла в регистр cx загружен 0, то цикл выполняется 35536 раз.
JCXZ <адрес перехода>
передает управление по указанному адресу, если содержимое регистра cx равно 0. Например:
mov cx, loop_count;
загрузка счетчика
jcxz end_of_loop;
проверка счетчика
begin_loop:
; ... тело цикла...
loop begin_loop
end_of_loop:
...
LООРE <адрес перехода>
LOOPNE <адрес перехода>
уменьшают содержимое на единицу и передают управление по указанному адресу при условии, что содержимое cx отлично от нуля, но LООРE дополнительно требует наличия признака "равно", а LOOPNE - ""не равно", формируемых командами сравнения. Например:
mov cx, loop_count ; загрузка счетчика
jcxz end_of_loop ; проверка счетчика
begin_loop:
; ... тело цикла...
cmp al, 100 ; проверка содержимого al
loopne begin_loop ; возврат в цикл, если cx0 и al100
end_of_loop: ...
CALL <адрес процедуры>
осуществляет передачу управления по указанному адресу, предварительно записав в стек адрес возврата.
При указании адреса процедуры так же как и при указании адреса перехода в командах безусловного перехода, возникает необходимость определить удаленности процедуры от места вызова:
Например:
call near ptr p - вызов подпрограммы "р".
Текст процедуры должен быть оформлен в виде:
< имя процедуры> ргос < указатель удаленности>
... тело процедуры...
<имя процедуры> end
Здесь указатель удаленности также служит дли определения длины адресов, используемых при обращении к процедуре: near - при использовании двухбайтовых адресов, far - при использовании четырехбайтовых адресов.
RET [<число>]
извлекает из стека адрес возврата и передает управление по указанному адресу.
Если в команде указано значение счетчика, то после восстановления адреса возврата указанное число добавляется к содержимому регистра-указателя стека. Последний вариант команды позволяет удалить из стека параметры, передаваемые в процедуру через стек.
Команды обработки строк используются для организации циклической обработки последовательностей элементов длиной I или 2 байта. Адресация операндов при этом выполняется с помощью пар регистров: DS:SI - источник, ES:DI - приемник. Команды имеют встроенную корректировку адреса операндов согласно флагу направления D: 1 - уменьшение адреса на длину элемента, 0 - увеличение адреса на длину элемента. Корректировка выполняется после выполнения операции.
Установка требуемого значения флага направления выполняется специальными командами:
STD
- установка флага направления в единицу,
CLD
- сброс флага направления в ноль.
Команда загружает байт в АL или слово в AX. Для адресации операнда используются регистры DS:SI
STOSB
(запись байта),
STOSW
(запись слова)
записывает в основную память содержимое AL или АX соответственно. Для адресации операнда используются регистры ES:DI.
MOVSB
(пересылка байта),
МОVSW
(пересылки слова)
пересылает элемент строки из области, адресуемой регистрами DS:SI, в область, адресуемую регистрами ЕS:DI.
SCASB
(поиск байта),
SCASW
(поиск слова).
По команде содержимое регистра AL или АХ сравниваются с элементом строки, адресуемым регистрами DS:SI и устанавливается значение флажков в соответствии с результатом - AL или -AX.
СMPSB
(сравнение байт),
СMPSW
(сравнение слов)
элементы строк, адресуемых парами регистров DS:SI и ES:DI, сравниваются и устанавливаются значения флажков в соответствии с результатом -.
REP <команда>
позволяет организовать повторение указанной команды CX раз. Например:
rep stosb
Здесь поле, адресуемое парой регистров ES:DI длиной CX заполняется содержимым AL .
REPE < команда >
REPNE < команда >
Префиксные команды используются совместно с командами СMPS и SCAS. Префикс REPE означает повторять, пока содержимое регистра СХ не равно нулю и значение флажка нуля равно единице, a REPNE - повторять, пока содержимое регистра CX не равно нулю и значение флажка нуля равно нулю.
Операнды байты или слова.
Пример. Выделить из числа в AL первый бит:
and al, 10000000B
<код операции> <операнд>, <счетчик>
Счетчик записывается в регистр СL. Если счетчик равен 1, то его можно записать в команду.
Коды команд сдвига
Пример. Умножить число в AX на 10:
mov bx, ax
shl ax, 1
shl ax, 1
add ax, bx
shl ax, 1
Обмен данными с внешней средой осуществляемся с помощью следующих команд:
В качестве регистра можно указать AL или AX (соответственно будет обрабатываться байт или два байта). Порт отождествляется с некоторым внешним устройством (0...255).
Однако при организации ввода - вывода помимо самой операции необходимо осуществить ряд дополнительных действий, например, проверить готовность устройства. В связи с этим для типовых устройств разработаны стандартные программы организации ввода - вывода, которые вызываются по команде прерывания int 21h.
В таблице 1 приведен перечень основные функции, реализуемые подпрограммами ввода - вывода, и их коды. Код функции должен передаваться в подпрограмму в регистре AH.
Примеры:
Ниже приведен пример текста программы, которая на языке ассемблер в заданном массиве определяет элемент с максимальной величиной.
SGSTACK SEGMENT PARA STACK "STACK"
DB 32 DUP(?)
SGSTACK ENDS
DATA SEGMENT PARA PUBLIC "DATA"
MAX DW ?
ARRAY DW 10H, 20H, 30H, 0D0H,0A0H
DATA ENDS
CODE SEGMENT PARA PUBLIC "CODE"
ASSUME CS:CODE, DS:DATA, SS:SGSTACK
START: MOV AX, DATA ; загрузить в DS
MOV DS, AX ;селектор сегмента данных
LEA BX, ARRAY ; загрузить в BX начальный адрес массива
MOV CX, 4 ; инициализировать счетчик
MOV AX, ; инициализировать начальное значение max
CYCLE: ADD BX, 2 ; перейти к следующему элементу массива
CMP , AX ; сравнить два значения
JBE BE ; переход если равен или ниже
MOV AX, ; сохранить большее значение
BE: LOOP CYCLE ; проверка на выход из цикла (--CX при CX=0)
MOV MAX, AX ; сохранение максимального значения
EXIT: XOR AL, AL ; выход в OS
MOV AH, 4CH
INT 21H
CODE ENDS
END START
Для выполнения лабораторной работы необходимо: