Руководство для начинающих по языку ассемблера с использованием emu8086


Оглавление

  • Что такое язык ассемблера
  • Ассемблеры и редакторы
  • Структура кода
  • Регистры и флаги
  • Инструкции языка ассемблера
  • Работа с переменными
  • Принятие пользовательского ввода
  • Отображение вывода
  • Ветвление или использование условий
  • Использование циклов
    • Цикл 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
Вход в полноэкранный режим Выход из полноэкранного режима

Ассемблер автоматически ищет файл в двух местах и выдает ошибку, если не может его найти. Этими местами являются:

  1. Папка, в которой находится исходный файл
  2. Папка 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. Разобравшись с некоторыми инструкциями ассемблера, мы узнали, как определить переменную, как принять ввод от пользователя, а также как вывести что-то на экран. Затем мы узнали об условиях и циклах, и, наконец, в завершение, мы решили задачу с помощью языка ассемблера.

Оцените статью
devanswers.ru
Добавить комментарий