Основы Assembler для i8086

Основы Assembler для i8086

Здравствуйте. На предмете Архитектура ЭВМ в целях ознакомления с принципами работы компьютера нас обучили основам ассемблера под i8086 процессор (большинство современных процессоров с ним совместимы). Я подумал, что не будет лишним рассказать здесь все, о чем узнал.

Общая структура программы

Сегменты

Общепринято программу делить на определенные участки. Как минимум, каждая программа имеет три сегмента:

Сегмент стека Используется для хранения временных данных. Например, в стек записываются параметры, при вызове процедур.

Сегмент данных Используется для хранения статических данных, «вшитых» в программу. Кроме того, в этом сегменте можно определять так называемые переменные, то есть области памяти, предназначенные для временного хранения обрабатываемых данных.

Сегмент кода Как видно из названия, содержит команды. В одном из сегментов кода, должна быть метка, указывающая на начало программы. Эта метка указывается после ключевого слова end, в конце программы.

В реальных приложениях, конечно, обычно встречается множество сегментов каждого из указанных типов. Это объясняется, прежде всего, тем, что размер сегмента ограничен, а также тем, что деление программы на блоки позволяет снизить сложность программы. Более того, при разбиении программы на модули возникновение самостоятельных сегментов неизбежно. О модулях речь пойдет позже.

Определяются сегменты с помощью ключевого слова segment, перед которым ставится имя сегмента. В конце сегмента ставится ключевое слово ends. Вот пример определения сегмента данных с именем Data:

Data segment

;segement data ends

С символа точка с запятой в ассемблере начинаются комментарии. Распространяется это на всю оставшуюся справа строку.

Особый синтаксис в определении сегмента стека. Вот как это выглядит:

Stk segment STACK \"STACK\" ends

Модули

Помимо деления на сегменты, имеющее функциональное значение, программу обычно делят на модули. Каждый модуль содержит данные и код, необходимые для решения определенного круга задач, связанных по смыслу. То есть деление на модули уже имеет больше логических оснований. Кроме того, модули позволяют применять повторное использование кода и помогают разделить работу по созданию программы между членами группы разработчиков.

Код каждого модуля помещается в отдельный файл исходников, компилируются эти файлы тоже по отдельности. Полученные obj-файлы указываются при линковке. Главный модуль должен в списке параметров находиться прежде остальных.

Взаимодействуют модули посредством вызова процедур. В вспомогательном модуле процедура объявляется как экспортируемая путем помещения ее имени с модификатором public в начале файла.

Public ProcName

В файле использующем этот модуль, данные процедуры также объявляются в начале файла с модификатором extrn: extrn ProcName: far

Данные

Объявление данных

При объявлении данных используется следующий синтаксис:

[Имя метки] [Тип данных] [Значение]

Обязателен только тип данных.

Имя

Имя метки является эквивалентом адреса и служит только для удобства программирования. После компиляции программы имен в ней нет.

Тип данных

Под термином “тип данных” понимается в основном только размер. Вот некоторые, наиболее часто употребляемые типы:

Ключевое слово Размер в байтах Расшифровка db 1 data byte dw 2 data word dd 4 data double word

Значение

В качестве значения можно указать число или символ. Более того, можно указать несколько значений через запятую, а в случае символов, их можно объединить в одну строку, заключенную в кавычки. Этот синтаксис эквивалентен множественному определению, и служит для упрощения процесса кодирования.

В качестве значения также может выступать конструкция для резервирования определенного объема памяти. Она имеет следующий формат:

[тип данных] [count] dup(?) где count — число, означающее количество элементов указанного типа, под которые надо зарезервировать место. Размер резервируемого места будет вычисляться по формуле count * размер типа данных.

Эта конструкция обычно используется при определении сегмента стека:

Stk segment STACK \"STACK\" dw 10 dup(?) ends

Способы адресации

Адрес значения, передаваемого команде можно передать по разному. Например, значение может находиться непосредственно в регистре, передаваемом в параметре, а может по адресу, находящемся в этом регистре. Чтобы различать эти тонкости и существуют различные способы адресации. Вот полный список:

Наименование Синтаксис Описание

Прямая push [label] Указывается адрес значения

Непосредственная push 10 Значение непосредственно в команде

Регистровая push Ax Значение в регистре

Регистровая косвенная push [Ax] В регистре адрес значения

Регистровая относительная push label [Ax] Адрес значения получается путем прибавления к содержимому регистра адреса label

Базовая push [Bx] Адрес находится в Bx

Базово-индексная push [Bx+Si] Адрес получается сложением содержимого Bx и Si

Относительная базово-индексная push label [Bx+Si] Адрес равен label + Bx + Si

Код

Регистры

Регистр процессора — сверхбыстрая память внутри процессора, предназначенная прежде всего для хранения промежуточных результатов вычисления.

Процессор i8086 имеет 14 регистров, из которых только 4 можно использовать для нужд программиста (Ax, Bx, Cx, Dx). Да и эти в некоторых случаях бывают нужны для работы определенных команд или прерываний. Вместимость всех регистров два байта. При чем к регистрам общего назначения можно обращаться также отдельно к младшему байту (Al, Bl, Cl, Dl) и к старшему (Ah, Bh, Dh, Ch).

Сегментные регистры

Тут имеются три регистра, соответствующие трем основным сегментам программы:

Имя Возможная расшифровка Описание

Ds Data start Адрес сегмента данных

Cs Code start Адрес сегмента стека

Ss Stack start Адрес сегмента данных

Es Extendet Data Segment Еще один для сегмента данных

Содержимое Ds прибавляется ко всем адресам (кроме базовой и базово-индексной адресации) для получения эффективного адреса. С ним часто приходится работать напрямую, при написании процедур. Если процедура использует свой сегмент данных, то она должна сохранить старое значение, записать адрес нужного сегмента и перед возвратом вернуть все как было.

Индексные и указательные регистры

Индексные регистры Si и Di применяются при базово-индексной адресации. Их удобно использовать в качестве счетчиков в циклах.

Указательных регистра существует три:

Имя Возможная расшифровка Описание

Bp Base pointer Используется при базовой и базово-индексной адресациях

Sp Stack pointer Указывает на вершину стека

Ip Instruction Pointer Содержит адрес следующей команды

Для чтения значений из стека нужно содержимое Sp записать в Bp, и далее применяя базовую и базово-индексную адресации можно получать содержимое стека не делая выборки.

Команды

Первой командой в сегменте кода является не столько команда, сколько директива компилятора. Она указывает текущие сегмент стека, данных и кода. Синтаксис директивы следующий: assume Cs:[имя сегмента кода], Ds:[имя сегмента данных], Ss:[имя сегмента стека]

Однако, на этом приготовления еще не заканчиваются. Необходимо также вручную занести в Ds адрес необходимого сегмента данных, если конечно он вам нужен. Писать напрямую в сегментный регистр нельзя, поэтому делается эта операция через регистр общего назначения: mov Ax, Data; Data-имя сегмента данных mov Ds, Ax

Далее следуют команды вашей программы, каждая на новой строчке. Вот некоторые наиболее употребляемые:

Ключевое слово Пример использования Расшифровка Описание mov mov Ds, Ax move Заносит значение из левого аргумента в правый lea lea Ax, x1 load effective address Загрузка в левый аргумент эффективного адреса метки, передаваемой вторым параметром push push Ax — Занесение значения в стек pop pop Ax — Выборка значения из стека jmp jmp Exit jump Безусловный переход по метке cmp cmp Ax, 10 compare Сравнение значений путем вычитания. Результат проверяется путем установки условных переходов je je Equal jump equal Переход если параметры cmp равны jne jne NotEqual jump not equal Переход если параметры cmp не равны jb jb Bellow jump below Переход если первый параметр cmp меньше jbe jbe BellowOrEqual jump below equal Переход если первый параметр cmp меньше или равен второму call call ReadLine — Вызов процедуры int int 21H interrupt Вызов прерывания add add Ax, Cx — Ax = Ax + Cx sub sum Ax, Cx — Ax = Ax - Cx mul mul 10 multiple Умножение Ax на параметр div div 2 divide Деление Ax на параметр inc inc Ax increment Увеличение на 1 dec dec Ax decrement Уменьшение на 1 ret ret return Возврат из процедуры loop loop CycleStart — Повтор кода от этой команды до метки указанной в параметре Cx раз.

Процедуры

Вызов процедуры в целом похож на безусловный переход, за исключением того, что при этом в стек заносится адрес возврата. Адрес возврата состоит из текущего значения счетчика команд SI если это ближний вызов, и если дальний, то также добавляется значение CS. Помимо этих 2-х или 4-х байт, которые заносятся в стек автоматически командой вызова call, перед вызовом процедуры в стек заносятся значения, необходимые для работы процедуры — параметры. Это делается уже вручную с помощью команды push: lea Ax, x1 push Ax

Получить эти значения процедура может с помощью регистра Bp. В него заносится значение Sp, хранящее адрес вершины стека, и далее адрес нужного значения получается путем прибавления к Bp смещения. Стоит помнить, что при дальнем вызове первый параметр будет находиться по адресу Sp+4, а при ближнем Sp+2.

Объявляется процедура с помощью ключевого слова proc, после перед которым идет имя, а после модификатор, указывающий, какой способ вызова нужно использовать — дальний (far) или ближний (near). После кода идет ключевое слово endp. Возврат из процедуры осуществляет команда ret. Вот пример объявления процедуры с именем ProcName:

ProcName proc far

;код процедуры ret endp

PS

Вот так, коротко о главном. Видимо, программисту об ассемблере больше знать ничего и не надо, т. к. я изложил здесь почти все, что нам дали.

Спасибо за внимание, удачи!


Карта сайта


Информационный сайт Webavtocat.ru