Лекция 7. Основы программирования на языке Ассемблера

7.1. Структура команды языка Ассемблера

Давайте рассмотрим команду «загрузить число 0x1234 в регистр АХ». На языке ассемблера она записывается очень просто:

MOV АХ,0x1234

Для процессора каждая команда представляется в виде двоичного числа (пункт 7 концепции фон Неймана). Ее числовое представление называется машинным кодом. Команда MOV АХ, 0x1234 на машинном языке может быть записана так:

0x1111: 0хВ8, 0x34, 0x12

0x1114: следующие команды

В рассматриваемом примере команда помещена по адресу 0x1111. Следующая команда начинается тремя байтами дальше, значит, под команду MOV с операндами отведено 3 байта. Второй и третий байты содержат операнды команды MOV. А что такое 0хВ8? После преобразования 0хВ8 в двоичную систему мы получим значение 10111000b. Первая часть – 1011 – и есть код команды MOV. Встретив код 1011, контроллер «понимает», что перед ним – именно MOV. Следующий разряд (1) означает, что операнды будут 16-разрядными. Три последние цифры определяют регистр назначения. Три нуля соответствуют регистру АХ (или AL, если предыдущий бит был равен 0, указывая таким образом, что операнды будут 8-разрядными).

К счастью, нам не придется записывать команды в машинном коде, поскольку ассемблер разрешает использовать их символические имена.

Общий формат команды языка ассемблера такой:

имя_команды [подсказка] операнды

Необязательная подсказка указывает компилятору требуемый размер операнда. Ее значением может быть слово BYTE (8-битный операнд), WORD (16-битный) или DWORD (32-битный).

Представим инициализацию некоторой «переменной» нулем, то есть запись нулей по адресу переменной. Подсказка сообщит компилятору размер операнда, то есть сколько именно нулевых байтов должно быть записано по этому адресу. Пока мы не изучили команду для записи значения, поэтому будем считать, что записать значение можно так:

mov dword [0x12345678],0; записывает 4 нулевых байта,

; начиная с адреса 0x12 345678

mov word [0x12345678],0; записывает 2 нулевых байта,

; начиная с адреса 0x12345678

mov byte [0x12345678],0; записывает 1 нулевой байт

; по адресу 0x12345 678

7.2. Операнды команд языка Ассемблера

Данные, которые обрабатываются командами, называются операндами. Операнды в языке ассемблера записываются непосредственно после команды, если их несколько, то через запятую.

Одни команды вообще не имеют никаких операндов, другие имеют один или два операнда. В качестве операнда можно указать непосредственное значение (например, 0x123), имя регистра или ссылку на ячейку памяти (так называемые косвенные операнды). Что же касается разрядности, имеются 32-разрядные, 16-разрядные, и 8-разрядные операнды. Почти каждая команда требует, чтобы операнды были одинакового размера (разрядности). Команда

MOV АХ,0x1234

имеет два операнда: операнд регистра и непосредственное значение, и оба они 16-битные.

Последний тип операнда – косвенный тип – адресует данные, находящиеся в памяти, получает их из памяти и использует в качестве значения. Узнать этот операнд очень просто – по наличию в записи квадратных скобок.

В документации по Ассемблеру различные форматы операндов представлены следующими аббревиатурами:

reg8 -операнд – любой 8-разрядный регистр общего назначения;

regl6 -onepaнд – любой 16-разрядный регистр общего назначения;

reg32 -onepaнд – любой 32-разрядный регистр общего назначения;

m – операнд может находиться в памяти;

imm8 – непосредственное 8-разрядное значение;

imml6 – непосредственное 16-разрядное значение;

imm32 – непосредственное 32-разрядное значение;

segreg – операнд должен быть сегментным регистром.

Допускаются неоднозначные типы операндов, например: reg8 / imm8 -onepaнд может быть любым 8-битным регистром общего назначения или любым 8-битным непосредственным значением.

Иногда размер операнда определяется только по последнему типу, например, следующая запись аналогична предыдущей: R / imm8 -onepaнд может быть любым регистром (имеется в виду 8-битный регистр) общего назначения или 8-разрядным значением.

7.3. Способы адресации памяти языка Ассемблера

Адрес, как и сама команда, – это число. Чтобы не запоминать адреса всех «переменных», используемых в программе, этим адресам присваивают символические обозначения, которые называются переменными (иногда их также называют указателями).

В языке Ассемблера используются непосредственная, прямая и косвенная адресации. При использовании косвенного операнда адрес в памяти, по которому находится нужное значение, записывается в квадратных скобках: [адрес]. Если мы используем указатель, то есть символическое представление адреса, например, [ESI], то в листинге машинного кода мы увидим, что указатель был заменен реальным значением адреса. Можно также указать точный адрес памяти, например, [0x594F].

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

Рассмотрим ситуацию, когда регистр ESI содержит адрес первого элемента (нумерация начинается с 0) в массиве байтов. Как получить доступ, например, ко второму элементу (элементу, адрес которого на 1 байт больше) массива?

Процессор поддерживает сложные способы адресации, которые очень нам пригодятся в дальнейшем. В нашем случае, чтобы получить доступ ко второму элементу массива, нужно записать косвенный операнд [ESI + 1].

Имеются даже более сложные типы адресации: [адрес + ЕВХ + 4]. В этом случае процессор складывает адрес, значение 4 и значение, содержащееся в регистре ЕВХ. Результат этого выражения называется эффективным адресом (ЕА, Effective Address) и используется в качестве адреса, по которому фактически находится операнд.

При вычислении эффективного адреса процессор 80386+ также позволяет умножать один член выражения на константу, являющуюся степенью двойки: [адрес + ЕВХ * 4]. Корректным считается даже следующее «сумасшедшее» выражение: [число - 6 + ЕВХ * 8 + ESI].

На практике чаще всего довольствуются только одним регистром [ESI] или суммой регистра и константы, например, [ESI + 4]. В зависимости от режима процессора, мы можем использовать любой 16-разрядный или 32-разрядный регистр общего назначения [ЕАХ], [ЕВХ],... [ЕВР].

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

7.4. Псевдокоманды языка Ассемблера

Некоторые из команд могут работать с операндом, расположенным в памяти. Классический пример – команда MOV AX, [number], загружающая в регистр АХ значение из области памяти, адрес которой представлен символическим обозначением «number». Но пока не ясно, как связать символическое обозначение и реальный адрес в памяти. Как раз для этого и служат псевдокоманды.

Предложения языка ассемблера делятся на команды и псевдокоманды (директивы). Команды ассемблера – это символические имена машинных команд, обработка их компилятором приводит к генерации машинного кода. Псевдокоманды же управляют работой самого компилятора. На одной и той же аппаратной архитектуре могут работать различные ассемблеры: их команды обязательно будут одинаковыми, но псевдокоманды могут быть разными.

Псевдокоманды DB, DW и DD

Чаще всего используется псевдокоманда DB (define byte), позволяющая определить числовые константы и строки. Рассмотрим несколько примеров:

db 0x55; один байт в шестнадцатеричном виде

db 0x55,0x56,0x57; три последовательных байта: 0x55, 0х56, 0x57

db 'а',0x55; можно записать символ в одинарных кавычках

; получится последовательность 0x61, 0x55

db 'Hello',13,10,'$'; можно записать целую строку

; получится 0x48, 0x65, 0х6С, 0х6С,

; 0x6F, 0xD, 0xA, 0x24

Для определения порции данных размера, кратного слову, служит директива DW (define word):

dw 0x1234; 0х34, 0x12

dw 'a'; 0x61, 0x00: второй байт заполняется нулями

Директива DD (define double word) задает значение порции данных размера, кратного двойному слову:

dd 0x12345678; 0х78 0x56 0x34 0x12

dd 1.234567e20; так определяются числа с плавающей точкой

А вот так определяется переменная, тот самый «number»:

number dd 0x1; переменная number инициализирована

; значением 1

Переменная «number» теперь представляет адрес в памяти, по которому записано значение 0x00000001 длиной в двойное слово.

Псевдокоманда EQU

Эта директива определяет константу, известную во время компиляции. В качестве значения константы можно указывать также константное выражение. Директиве EQU должно предшествовать символическое имя:

four EQU 4; тривиальный пример

Псевдокоманды RESB, RESW и RESD

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

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

Для резервирования памяти служат три директивы: RESB (резервирует байт), RESW (резервирует слово) и RESD (резервирует двойное слово). Аргументом этих псевдокоманд является количество резервируемых позиций:

resb 1; резервирует 1 байт

resb 2; резервирует 2 байта

resw 2; резервирует 4 байта (2 слова)

resd 1; резервирует 4 байта

number resd 1; резервирует 4 байта для переменной "number"

buffer resb 64; резервирует 64 байта для переменной "buffer"

Псевдокоманда TIMES

Директива TIMES – это псевдокоманда префиксного типа, то есть она используется только в паре с другой командой. Она повторяет последующую псевдокоманду указанное количество раз, подобно директиве DUP из ассемблера Borland TASM.

Применяется эта директива в тех случаях, когда нужно «забить» некоторую область памяти повторяющимся образцом. Следующий код определяет строку, состоящую из 64 повторяющихся «Hello»:

many_hello: times 64 db 'Hello'

Первый аргумент, указывающий количество повторений, может быть и выражением. Например, задачу «разместить строку в области памяти размером в 32 байта и заполнить пробелами оставшееся место» легко решить с помощью директивы TIMES:

buffer db "Hello"; определяем строку

times 32-($-buffer) db ' '; определяем нужное кол-тво пробелов

Выражение 32-($-buffer) возвратит значение 27, потому что $-buffer равно текущей позиции минус позиция начала строки, то есть 5.

Вместе с TIMES можно использовать не только псевдокоманды, но и команды процессора:

times 5 inc eax; 5 раз выполнить INC EAX


Понравилась статья? Добавь ее в закладку (CTRL+D) и не забудь поделиться с друзьями:  




Подборка статей по вашей теме: