- Оглавление
- Что такое язык ассемблера
- Ассемблеры и редакторы
- Структура кода
- Регистры и флаги
- Инструкции на языке ассемблера
- Работа с переменными
- Принятие пользовательского ввода
- Отображение вывода
- Ветвление или использование условий
- Использование циклов
- Цикл for
- Цикл while
- Цикл do-while
- Использование синтаксиса LOOP
- Директива включения
- Дополнительно: Задача об обратном треугольнике
- Резюме
Оглавление
- Что такое язык ассемблера
- Ассемблеры и редакторы
- Структура кода
- Регистры и флаги
- Инструкции языка ассемблера
- Работа с переменными
- Принятие пользовательского ввода
- Отображение вывода
- Ветвление или использование условий
- Использование циклов
- Цикл For
- цикл While
- цикл Do-while
- Использование синтаксиса LOOP
- Директива Include
- Дополнительно: Проблема обратного треугольника
- Резюме
Что такое язык ассемблера
Язык ассемблера — это низкоуровневый язык программирования, который является очень быстрым, использует меньше ресурсов по сравнению с языками более высокого уровня и может быть выполнен путем прямого перевода на машинный язык с помощью ассемблера. Согласно Википедии:
В компьютерном программировании язык ассемблера — это любой низкоуровневый язык программирования с очень сильным соответствием между инструкциями языка и инструкциями машинного кода архитектуры.
Мы знаем, что процессор (также известный как CPU — Central Processing Unit) выполняет все типы операций, фактически работая как мозг компьютера. Однако он распознает только строки из 0 и 1. Как вы можете себе представить, кодировать на машинном языке очень сложно. Поэтому для определенного семейства процессоров был разработан низкоуровневый язык ассемблера, который представляет различные инструкции в символическом коде, что гораздо проще для понимания человеком. Но, как вы уже догадались, разрабатывать на языке ассемблера сложно и несколько неудобно.
Итак, зачем нам изучать язык ассемблера в современном мире?
Вы можете подумать о следующих моментах, чтобы решить, изучать его или нет.
- Повышение квалификации.
- Выучить самый быстрый язык, помимо машинного.
- Встроить язык ассемблера в язык более высокого уровня, чтобы использовать функции, не поддерживаемые языком более высокого уровня, или для повышения производительности.
- Восполнить пробел в знаниях, чтобы понять, как появились языки более высокого уровня.
Ассемблеры и редакторы
Ассемблеры — это программы, которые переводят код языка ассемблера в эквивалентный ему код машинного языка. Сегодня на рынке существует множество ассемблеров, предназначенных для различных микропроцессоров, таких как MASM, TASM, NASM и др. Список различных ассемблеров можно найти на этой странице Википедии.
Редакторы кода — это программное обеспечение, в котором вы можете писать код, изменять и сохранять его в файл. Некоторые редакторы, поддерживающие язык ассемблера, это VS code, DOSBox, emu8086 и так далее. Также доступны онлайн-ассемблеры, например, популярный онлайн-редактор Ideone. Мы будем использовать emu8086, который поставляется со средой, необходимой для начала нашего путешествия по языку ассемблера.
Структура кода
Мы можем просто написать ассемблерный код и эмулировать его в emu8086, и он будет запущен. Однако, без вызова оператора выхода или инструкции halt
, программа будет продолжать выполнять следующую инструкцию в памяти, пока ее не остановит ОС или сам emu8086. Ассемблерный код сохраняется в файле типа .asm
.
Существует также несколько хороших практик, таких как определение модели и размера стековой памяти в самом начале. Для модели small
определите сегмент данных и кода после стека. Сегмент кода содержит код для выполнения. В приведенном здесь примере структуры я создал процедуру main
(также называемую функцией или методами в других языках программирования), в которой начинается выполнение кода. В конце ее я вызвал определенный предопределенный оператор с прерыванием, чтобы указать на завершение выполнения кода.
.model small
.stack 100H
; Data segment
.data ; if there is nothing in the data segment, you can omit this line.
; Code segment
.code
main PROC
; Write your code here
exit:
MOV AH, 4CH
INT 21H
main ENDP
END main
Первая строка, .model small
, определяет модель памяти, которую следует использовать. Некоторые известные модели памяти: tiny, small, medium, compact, large и так далее. Модель памяти small
поддерживает один сегмент данных и один сегмент кода, чего обычно достаточно для написания небольших программ. Следующая строка .stack 100H
определяет размер стека в шестнадцатеричных числах. Эквивалентным десятичным числом является 256
. Строки, начинающиеся с или часть строки после ;
, являются комментариями, которые ассемблер игнорирует.
Регистры и флаги
Регистры — это сверхбыстрая память, напрямую связанная с процессором. emu8086 может эмулировать все внутренние регистры микропроцессора Intel 8086. Все эти регистры имеют длину 16 бит и сгруппированы в несколько категорий следующим образом,
- Регистры общего назначения: Существует четыре регистра общего назначения, каждый из которых делится на две подгруппы — низкий и высокий уровень. Например, AX делится на AL и AH, каждый длиной 8 бит.
- Аккумулятор (AX)
- База (BX)
- Счетчик (CX)
- Данные (DX)
- Сегментные регистры: Имеется также четыре сегментных регистра.
- Сегмент кода (CS)
- Сегмент данных (DS)
- Сегмент стека (SS)
- Дополнительный сегмент (ES)
- Регистры специального назначения: Имеются два индексных регистра и три регистра указателей.
- Индекс источника (SI)
- Индекс назначения (DI)
- Базовый указатель (BP)
- Указатель стека (SP)
- Указатель инструкции (IP)
-
Регистр флагов: Это 16-битный регистр, из которых 9 бит используются 8086 для индикации текущего состояния процессора. Девять флагов делятся на две группы.
- Флаги состояния: Шесть флагов состояния указывают на статус выполняемой в данный момент инструкции.
- Флаг переноса (CF)
- Флаг четности (PF)
- Вспомогательный флаг (AF)
- Флаг нуля (ZF)
- Флаг знака (SF)
- Флаг переполнения (OF)
- Управляющие флаги: Существует три управляющих флага, которые контролируют определенные операции процессора.
- Флаг прерывания (IF)
- Флаг направления (DF)
- Флаг ловушки (TF)
Чтобы узнать больше об этих регистрах и о том, для чего они используются, посетите эту страницу.
Инструкции на языке ассемблера
Для микропроцессора Intel 8086 доступно в общей сложности 116 инструкций. Все эти инструкции с соответствующими примерами приведены по этой ссылке.
В этой статье я остановлюсь только на нескольких инструкциях, необходимых для понимания последующих частей.
- Копирование данных (MOV): Эта инструкция копирует байт (8-битный) или слово (16-битное) из источника в место назначения. Оба операнда должны быть одного типа (байт или слово). Синтаксис этой инструкции следующий:
MOV destination, source
Операндом destination
может быть любой регистр или область памяти, в то время как операндом source
может быть регистр, адрес памяти или постоянное/промежуточное значение.
- Сложение (ADD) и вычитание (SUB): ADD складывает данные операндов
destination
иsource
и сохраняет результат вdestination
. Оба операнда должны быть одного типа (слова или байты), иначе ассемблер выдаст ошибку. Инструкция вычитания вычитаетsource
изdestination
и сохраняет результат вdestination
.
; Addition
ADD destination, source
ADD BL, 10
; Subtraction
SUB destination, source
SUB BL, 10
-
Метка: Метка — это символическое имя адреса команды, которое указывается сразу после объявления метки. Она может быть помещена в начало оператора и служить операндом инструкции. Использованный ранее
exit:
является меткой. Метки бывают двух типов.- Символьные метки: Символьная метка состоит из идентификатора или символа, за которым следует двоеточие (
:
). Они должны быть определены только один раз, так как имеют глобальную область применения и отображаются в таблице символов объектного файла. - Числовые метки: Числовая метка состоит из одной цифры в диапазоне от нуля (
0
) до девяти (9
), за которой следует двоеточие (:
). Они используются только для локальных ссылок и исключены из таблицы символов объектного файла. Следовательно, они имеют ограниченную область применения и могут быть многократно переопределены.
- Символьные метки: Символьная метка состоит из идентификатора или символа, за которым следует двоеточие (
; Symbolic label
label:
MOV AX, 5
; Numeric label
1:
MOV AX, 5
- Compare (CMP): эта инструкция берет два операнда и вычитает один из другого, затем устанавливает флаги OF, SF, ZF, AF, PF и CF соответственно. Результат нигде не хранится.
CMP operand1, operand2
Операнд operand1
может быть адресом регистра или памяти, а operand2
может быть регистром, памятью или непосредственным значением.
- Инструкции перехода: Инструкции перехода передают управление программой на новый набор инструкций, на который указывает метка, представленная в качестве операнда. Существует два типа инструкций перехода.
- Безусловный переход (JMP): непосредственно переходит к предоставленной метке.
- Условный переход: Эти инструкции используются для перехода только при выполнении условия и вызываются после инструкции
CMP
. Эта инструкция сначала оценивает, удовлетворяется ли условие с помощью флагов, а затем переходит на метку, заданную в качестве операнда. Это очень похоже на операторыif
в других языках программирования. В языке ассемблера 8086 доступна 31 инструкция условного перехода.
Работа с переменными
В программе на ассемблере все переменные объявляются в сегменте data
. Язык emu8086 предоставляет несколько директив define для объявления переменных. В частности, в этой статье мы будем использовать директивы DB
(define byte) и DW
(define word), которые выделяют 1 байт и 2 байта соответственно.
[variable-name] define-directive initial-value [,initial-value]...
Здесь variable-name
является идентификатором для каждого пространства хранения. Ассемблер ассоциирует значение смещения для каждого имени переменной, определенного в сегменте данных.
Ниже приведен пример объявления переменной, где мы инициализируем num
и char
значением, которое может быть изменено позже. output
инициализируется строкой и имеет символ доллара ($
) в конце, чтобы указать конец строки. input_char
объявляется без начального значения. Мы можем использовать ?
, чтобы указать, что значение в данный момент неизвестно.
; Data segment
.data
num DB 31H
char DB 'A'
output DW "Hello, World!!$"
input_char DB ?
Мы пока не можем использовать переменные в сегменте кода! Для использования этих переменных в сегменте кода, мы должны сначала переместить адрес сегмента данных в регистр DS
(сегмент данных). Используйте эту строку в начале сегмента кода для импорта всех переменных.
; Storing all variables in data segment
MOV AX, @data
MOV DS, AX
Принятие пользовательского ввода
Ассемблер emu8086 поддерживает ввод данных пользователем, устанавливая предопределенное значение 01
или 01H
в регистре AH
и затем вызывая прерывание (INT
). Оно примет от пользователя один символ и сохранит значение ASCII этого символа в регистре AL
. Эмулятор emu8086 отображает все значения в шестнадцатеричном виде.
; input a character from user
MOV AH, 1
INT 21h ; the input will be stored in AL register
Отображение вывода
Эмулятор emu8086 поддерживает вывод одного символа. Он также позволяет выводить многосимвольные символы или строки. Как и при вводе, мы должны указать предопределенное значение в регистре AH
и вызвать прерывание. Для вывода одного символа предопределено значение 02
или 02H
, а для вывода строки 09
или 09H
. Выходное значение должно быть сохранено в регистре данных общего назначения до вызова прерывания.
; Output a character
MOV AH, 2
MOV DL, 35
INT 21H
; Output a string
MOV AH, 9
LEA DX, output
INT 21H
Как показано в коде, для вывода одного символа мы сохраняем значение в регистре DL
, поскольку длина символа составляет один байт или 8 бит. Однако для вывода строки все немного иначе. Мы должны загрузить эффективный адрес (адрес со смещением) строковой переменной в регистр DX
с помощью инструкции LEA
. Строковая переменная должна быть определена в сегменте данных.
Полный код, содержащий объявление переменной, ввод и вывод, представлен на GitHub.
Ветвление или использование условий
Мы можем имитировать условия if-else, поддерживаемые языками программирования более высокого уровня, используя CMP
и инструкции перехода. Вот некоторые часто используемые инструкции условного перехода,
Инструкция | Переход if | Аналогично |
---|---|---|
JE | равно | == |
JL | меньше | < |
JLE | меньше или равно | <= |
JG | больше | > |
JGE | больше или равно | >= |
Существует также инструкция JMP
, которая работает аналогично операторам else
, встречающимся в языках более высокого уровня. Ниже приведен ассемблерный код, который сравнивает значение регистра AL
с 5
и устанавливает соответствующее значение в регистре BL
.
; setting a test value
MOV AL, 5
; Compare
CMP AL, 5
JG greater ; if greater
JE equal ; else if equal
JMP less ; else
greater:
MOV BL, 'G'
JMP after
equal:
MOV BL, 'E'
JMP after
less:
MOV BL, 'L'
after:
; Other codes
; Note: BL will contain 'E' at this point
Полный код доступен в этом репозитории GitHub.
Использование циклов
Мы также можем использовать циклы в языке ассемблера. Однако, в отличие от языка более высокого уровня, он не предоставляет различных типов циклов. Хотя эмулятор emu8086 поддерживает пять типов синтаксиса циклов, LOOP
, LOOPE
, LOOPNE
, LOOPNZ
, LOOPZ
, они недостаточно гибкие для многих ситуаций. Мы можем создавать свои собственные циклы, используя операторы условия и перехода. Ниже приведены различные типы циклов, реализованные на языке ассемблера, все они эквивалентны.
Цикл for
Цикл for имеет секцию инициализации, где инициализируются переменные цикла, секцию условия цикла и, наконец, секцию увеличения/уменьшения для выполнения вычислений или изменения переменных цикла перед следующей итерацией. Ниже приведен пример цикла на языке C
.
char bl = '0';
for (int cl = 0; cl < 5; cl++) {
// body
bl++;
}
Эквивалентный код на ассемблере выглядит следующим образом:
MOV BL, '0'
init_for:
; initialize loop variables
MOV CL, 0
for:
; condition
CMP CL, 5
JGE outside_for
; body
INC BL
; increment/decrement and next iteration
INC CL
JMP for
outside_for:
; other codes
Цикл while
В отличие от цикла for, цикл while не имеет секции инициализации. В нем есть только секция условия цикла, при выполнении которой выполняется часть тела. В основной части мы можем выполнить некоторые вычисления перед следующей итерацией. Ниже приведен пример цикла while на языке C
.
char bl = '0';
int cl = 0;
while (cl < 5) {
// body
bl++;
cl++;
}
Идентичный ассемблерный код:
MOV CL, 0
MOV BL, '0'
while:
; condition
CMP CL, 5
JGE outside_while
; body
INC BL
INC CL
; next iteration
JMP while
outside_while:
; other codes
Цикл do-while
Подобно циклу while, цикл do-while имеет секцию условия цикла и тело. Единственное отличие заключается в том, что код в теле цикла выполняется хотя бы один раз, даже если условие оценивается как false
. Ниже приведен пример цикла do-while на языке C
.
char bl = '0';
int cl = 0;
do {
// body
bl++;
cl++;
} while (cl < 5);
Соответствующий ассемблерный код выглядит следующим образом,
MOV CL, 0
MOV BL, '0'
do_while:
; body
INC BL
INC CL
; condition
CMP CL, 5
JL do_while
; other codes
Использование синтаксиса LOOP
Мы можем использовать предопределенный синтаксис цикла, используя регистр CX
в качестве счетчика. Ниже приведен пример синтаксиса цикла, который делает то же самое, что и предыдущие циклы.
MOV BL, '0'
; initialize counter
MOV CX, 5
loop1:
INC BL
LOOP loop1
Полный код, содержащий различные циклы, доступен на GitHub.
Директива включения
Директива Include используется для доступа и использования процедур и макросов, определенных в других файлах. Синтаксис: include
, за которым следует имя файла с расширением.
include file_name
Ассемблер автоматически ищет файл в двух местах и выдает ошибку, если не может его найти. Этими местами являются:
- Папка, в которой находится исходный файл
- Папка
Inc
.
В папке Inc находится файл emu8086.inc, который определяет некоторые полезные процедуры и макросы, облегчающие кодирование. Мы должны включить этот файл в начало нашего исходного кода, чтобы использовать эти функциональные возможности.
include 'emu8086.inc'
Теперь мы можем использовать эти макросы в сегменте кода. Некоторые из этих макросов и процедур, которые я считаю наиболее полезными, следующие:
- Макрос PRINT для печати строки. Пример использования:
PRINT output
. - Макрос PUTC для печати ASCII-символа. Пример использования:
PUTC char
. - Процедура GET_STRING для получения от пользователя строки с нулевым окончанием, пока не будет нажата клавиша
Enter
. Для использования этой процедуры объявитеDEFINE_GET_STRING
перед директивойEND
. - Процедура CLEAR_SCREEN для очистки всего экрана и установки позиции курсора в начало. Объявите
DEFINE_CLEAR_SCREEN
перед директивойEND
, чтобы использовать эту процедуру.
Чтобы узнать больше о макросах и процедурах внутри файла emu8086.inc
, посетите эту страницу.
Дополнительно: Задача об обратном треугольнике
Давайте решим задачу, в которой используется все, что мы узнали до сих пор. Задача состоит в том, чтобы ввести число (1-9) от пользователя и вывести в консоль форму обратного треугольника, используя #
. Также должны быть выведены соответствующие сообщения об ошибках, если пользователь введет недопустимый символ. Демонстрационный вывод показан на рисунке.
Попробуйте сначала сами, и если вы не сможете решить эту задачу, то читайте дальше.
Чтобы решить эту проблему, мы должны выполнить следующие задачи:
- Ввести число от пользователя
- Проверить правильность ввода
- Вывести удобные для пользователя сообщения
- Теперь начинается самое сложное. Мы не можем использовать один цикл for для печати формы обратного треугольника. Для этого мы должны использовать два цикла, один внутри другого, также известные как вложенные циклы. Во внешнем цикле мы можем проверить, сколько строк должно быть напечатано, а также напечатать новую строку в начале или в конце. Внутренний цикл можно использовать для печати
#
.
Ниже приведен демонстрационный код вложенного цикла:
; Initialize outer loop counter
MOV BL, 0 ; counts line number starting from 0
outer_loop: ; using while loop format
CMP BL, x ; assuming x contains user input
JE outside_loop
; Print new-line
; Initialize inner loop counter
MOV CH, 0
MOV CL, x
SUB CL, BL ; subtract current line number from x
inner_loop:
; Print #
LOOP inner_loop
; Increment outer loop counter
INC BL
JMP outer_loop
outside_loop:
; other codes
Конечный результат моего кода выглядит следующим образом:
Полное решение доступно в моем репозитории GitHub.
Резюме
В этой статье мы рассмотрели очень много материалов. Во-первых, мы поняли, что такое язык ассемблера и названия некоторых ассемблеров. Затем мы поняли структуру кода и узнали все регистры и флаги микропроцессора 8086. Разобравшись с некоторыми инструкциями ассемблера, мы узнали, как определить переменную, как принять ввод от пользователя, а также как вывести что-то на экран. Затем мы узнали об условиях и циклах, и, наконец, в завершение, мы решили задачу с помощью языка ассемблера.