Введение в Ассемблер. Работа с регистрами. Адресация и команды пересылки данных. Арифметические операции с целыми числами. Способы адресации операндов

Операнд – объект, над которым выполняется машинная команда.

Операнды ассембле­ра описываются выражениями с числовыми и текстовыми константами, мет­ками и идентификаторами переменных с использованием знаков операций и некоторых зарезервированных слов.

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

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

Под способами адресации понимаются существующие способы задания адреса хранения операндов:


Операнд задается на микропрограммном уровне (операнд по умолчанию) : в этом случае команда явно не содержит операнда, алгоритм выполнения команды использует некоторые объекты по умолчанию (регистры, признаки и т.д.).

mul ebx ; eax = eax*ebx, неявно использует регистр eax


Операнд задается в самой команде (непосредственный операнд) : операнд является частью кода команды. Для хранения такого операнда в команде выделяется поле длиной до 32 бит. Непосредственный операнд может быть только вторым операндом (источником). Операнд-получатель может находиться либо в памяти, либо в регистре.

mov eax , 5 ; eax = 5;
add ebx , 2 ; ebx = ebx + 2;


Операнд находится в одном из регистров (регистровый операнд) : в коде команды указываются именами регистров. В качестве регистров могут использоваться:

  • 32-разрядные регистры ЕАХ, ЕВХ, ЕСХ, EDX, ESI, EDI, ESP, EBP;
  • 16-разрядные регистры АХ, ВХ, СХ, DX, SI, DI, SP, ВР;
  • 8-разрядные регистры АН, AL, BH, BL, CH, CL, DH, DL;
  • сегментные регистры CS, DS,SS, ES, FS, GS.

add eax, ebx ; eах = eax + ebх
dec esi ; esi = esi — 1

Операнд располагается в памяти . Данный способ позволяет реализовать два основных вида адресации:

  • прямую адресацию;
  • косвенную адресацию.

Прямая адресация : эффективный адрес определяется непосредственно полем смещения машинной команды, которое может иметь размер 8, 16 или 32 бита.

mov eax, sum ; eax = sum

Ассемблер заменяет sum на соответствующий адрес, хранящийся в сегменте данных (по умолчанию адресуется регистром ds ) и значение, хранящееся по адресу sum , помещает в регистр eax .

Косвенная адресация в свою очередь имеет следующие виды:

  • косвенная базовая (регистровая) адресация;
  • косвенная базовая (регистровая) адресация со смещением;
  • косвенная индексная адресация;
  • косвенная базовая индексная адресация.

Косвенная базовая (регистровая) адресация. При такой адресации эффективный адрес операнда может находиться в любом из регистров общего назначения, кроме sp/esp и bp/ebp (это специфические регистры для работы с сегментом стека). Синтаксически в команде этот режим адресации выражается заключением имени регистра в квадратные скобки .

mov eax , ; eax = *esi; *esi значение по адресу esi

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

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

mov eax , ; eax = *(esi+4)

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

mov eax , mas

Значение эффективного адреса второго операнда вычисляется выражением mas+(esi *4) и представляет собой смещение относительно начала сегмента данных.

Наличие возможности масштабирования существенно помогает в решении проблемы индексации при условии, что размер элементов массива постоянен и составляет 1, 2, 4 или 8 байт.

Данный вид адресации также может использоваться со смещением.

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

mov eax ,

Эффективный адрес второго операнда формируется как esi+edx . Значение по этому адресу помещается в регистр eax.

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


Операндом является порт ввода-вывода .
Помимо адресного пространства оперативной памяти микропроцессор поддерживает адресное пространство ввода-вывода, которое используется для доступа к устройствам ввода-вывода. Объем адресного пространства ввода-вывода составляет 64 Кбайт. Для любого устройства компьютера в этом пространстве выделяются адреса. Конкретное значение адреса в пределах этого пространства называется портом ввода-вывода. Физически порту ввода-вывода соответствует аппаратный регистр (не путать с регистром микропроцессора), доступ к которому осуществляется с помощью специальных команд ассемблера in и out .

in al ,60h ; ввести байт из порта 60h

Регистры, адресуемые с помощью порта ввода-вывода, могут иметь разрядность 8, 16 или 32 бит, но для конкретного порта разрядность регистра фиксирована. В качестве источника информации или получателя применяются регистры-аккумуляторы eax , ax , al . Выбор регистра определяется разрядностью порта. Номер порта может задаваться непосредственным операндом в командах in и out или значением в регистре dx . Последний способ позволяет динамически определить номер порта в программе.

mov dx ,20h ; записать номер порта 20h в регистр dx
mov al ,21h ; записать значение 21h в регистр al
out dx ,al ; вывести значение 21h в порт 20h


Счетчик адреса – специфический вид операнда. Он обозначается знаком $. Специфика этого операнда в том, что когда транслятор ассемблера встречает в исходной программе этот символ, он подставляет вместо него текущее значение счетчика адреса (регистр EIP ). Значение счетчика адреса представляет собой смещение текущей машин­ной команды относительно начала сегмента кода, адресуемого сегментным регистром CS . При обработке транслятором очередной команды ассемблера счетчик адреса увеличивается на длину сформированной машинной команды. Обработка директив ассемблера не вле­чет за собой изменения счетчика. В качестве примера использования в команде значения счетчика адреса можно привести следующий фрагмент:

jmp $+3 ;безусловный переход на команду mov
nop ; длина команды nop составляет 1 байт
mov al ,1

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


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

Записи (аналогично структурному типу) используются для доступа к битовому полю некоторой записи. Для доступа к битовому полю записи используется директива RECORD .

Операторы в языке ассемблера

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

Приоритет Оператор
1 length, size, width, mask, (), , < >
2 .
3 :
4 ptr, offset, seg, this
5 high, low
6 +, — (унарные)
7 *, /, mod, shl, shr
8 +, -, (бинарные)
9 eq, ne, lt, le, gt, ge
10 not
11 and
12 or, xor
13 short, type

Характеристика основных операторов.

Арифметические операторы . К ним относятся унарные операторы + и , бинарные + и , операторы умножения * , целочисленного деления / , получения остатка от деления mod . Например,

size equ 48 ;размер массива в байтах
el equ 4 ;размер элемента
;вычисляется количество элементов
mov ecx , size / el ;оператор /

Операторы сдвига выполняют сдвиг выражения на указанное количество разрядов. Например,

msk equ 10111011 ; константа
mov al , msk shr 3 ; al=00010111 /

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

size equ 30 ;размер таблицы

mov al , tab_size ge 50 ;al = 0
cmp al , 0 ;если size < 50, то
je m1 ;переход на m1

m1: …

Если значение size больше или равно 50, то результат в аl равен 1, в противном случае — 0. Команда cmp сравнивает значение аl с нулем и устанавливает соответствующие флаги в EFLAGS . Команда je на основе анализа этих флагов передает или не передает управление на метку m1 .

Назначение операторов сравнения приведено в таблице

Оператор Условие
eq ==
ne !=
lt <
le <=
gt >
ge >=

Логические операторы выполняют над выражениями побитовые операции. Выражения должны быть константными. Например,

L1 equ 10010011

mov al , L1
xor al , 01h ;al=10010010

Индексный оператор . Транслятор воспринимает наличие квадратных скобок как указание сложить значение выражения за со значением выражения , заключенным в скобки. Например,

mov eax , mas ;eax=*(mas+(esi))

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

Оператор переопределения типа ptr применяется для переопределения или уточнения типа метки или переменной, определяемых выражением. Тип может принимать одно из следующих значений.

Тип Пояснение Назначение
byte 1 байт переменная
word 2 байта переменная
dword 4 байта переменная
qword 8 байт переменная
tword 10 байт переменная
near ближний указатель функция
far дальний указатель функция

Например,

str1 db «Привет» , 0

lea esi , str1

cmp byte ptr , 0 ; ==0?

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

Оператор переопределения сегмента : (двоеточие) вычисляет физический адрес относительно конкретно задаваемой сегментной составляющей, в качестве которой могут выступать:

  • имя сегментного регистра,
  • имя сегмента из соответствующей директивы SEGMENT
  • имя группы.

Для выборки на выполнение очередной команды микропроцессор анализирует содержимое сегментного регистра CS , в котором содержится физический адрес начала сегмента кода. Для получения адреса конкретной команды микропроцессор складывает промасштабированное (умноженное на 16) значение сегментного регистра CS с содержимым регистра EIP . Запись CS:EIP содержит адрес текущей выполняемой команды. Аналогично обрабатываются операнды в машинных командах.

Оператор именования типа структуры . (точка) также заставляет транслятор производить определенные вычисления, если встречается в выражении.

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

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

Data
str1 db «Привет» ,0
.code
mov esi, offset str1
mov al , ; al = ‘П’

Оператор определения длины массива length возвращает число элементов, определенных операндом dup . Если операнд dup отсутствует, то оператор length возвращает значение 1.Например,

tabl dw 10 dup (?)

mov edx , length tabl ; edx=10

Оператор type возвращает число байтов, соответствующее определению указанной переменной:

fldb db ?
tabl dw 10 dup (?)

mov eax , type fldb ;eax = 1
mov eax , type tabl ;eax = 2

Оператор size возвращает произведение длины length и типа type и используется при ссылках на переменную с операндом dup .
Для предыдущего примера

mov edx , size tabl ;edx = 20 байт

Оператор short –модификация атрибута near в команде jmp, если переход не превышает границы +127 и -128 байт. Например,

jmp short метка

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

Оператор width возвращает размер в битах объекта типа RECORD или его поля.

Цели:

    закрепить знания о регистрах общего назначения 32-разрядных процессоров INTEL;

    научиться использовать косвенную адресацию для работы с оперативной памятью;

    научиться использовать команды умножения и деления целых чисел.

Основная нагрузка при работе компьютера ложится на процессор и память. Процессор выполняет команды, хранящиеся в памяти. В памяти хранятся также и данные. Между процессором и памятью происходит непрерывный обмен информацией. Процессор имеет свою небольшую память, состоящую из регистров . Команда процессора, использующая находящиеся в регистрах данные, выполняется много быстрее аналогичных команд над данными в памяти. Поэтому часто для того, чтобы выполнить какую-либо команду, данные для неё предварительно помещают в регистры. Результат команды можно при необходимости поместить обратно в память. Обмен данными между памятью и регистрами осуществляют команды пересылки . Кроме этого, можно обмениваться данными между регистрами, посылать и получать данные от внешних устройств. В регистр и ячейку памяти можно посылать и непосредственный операнд – число. Кроме этого имеются команды, с помощью которых можно помещать и извлекать данные из стека специальной области памяти, используемой для хранения адресов возврата из функций, передаваемых в функцию параметров и локальных переменных.

Адресация и выделение памяти

Для процессора вся память представляет собой последовательность однобайтовых ячеек, каждая из которых имеет свой адрес. Для того, чтобы оперировать большими числами, пары ячеек объединяют в слова, пары слов – в двойные слова, пары двойных слов – в учетверенные слова. Чаще всего в программах оперируют байтами , словами и двойными словами (в соответствии с одно-, двух- и четырехбайтовыми регистрами процессоров). Адресом слова и двойного слова является адрес их младшего байта.

На листинге 1 представлен пример доступа к памяти при помощи косвенной адресации. Рассмотрим подробно. Прежде всего, отметим, что в программу включен заголовочный файл , который содержит заголовки всех основных API -функций ОС Windows, а также определение большого количества структур, типов переменных (в частности, определение типа DWORD , который сводится просто к unsigned int ). В ассемблерных командах используются переменные, определенные средствами языка Си. Это связано с тем, что встроенный в Си ассемблер не позволяет осуществлять резервирование памяти. Адресация памяти с помощью переменных называют также прямой адресацией . Косвенная адресация состоит в следующем. Если адрес ячейки содержится в регистре, например, EAX , то для того, чтобы послать туда число 100, нужно написать MOV BYTE PTR , 100 . Префикс BYTE PTR указывает, что в операции участвует однобайтовая ячейка памяти (можно использовать WORD PTR , DWORD PTR – это будет соответствовать двух- и четырехбайтовому операнду). Чтобы получить адрес ячейки памяти, используется команда LEA .

/* подключаемые заголовочные файлы */ #include #include #include /* глобальные переменные */ BYTE a= 10 ; // 8-битное беззнаковое целое число DWORD addressRet; // переменная для хранения адреса /* главная функция */ void main // (в 32-разрядной ОС адрес ячейки памяти занимает 4 байта, // поэтому для хранения адреса надо использовать расширенные // регистры) MOV addressRet, EAX; // помещаем в переменную addressRet адрес переменной // а, хранящийся в регистре EAX. Обратите внимание: // этот адрес меняется при каждом запуске программы MOV BYTE PTR [ EAX] , 100 ; // помещаем по адресу, хранящемуся в регистре EAX // число 100 - фактически, присваиваем переменной а // значение 100 } ; printf ("address of variable a is %u\n " , addressRet) ; // выводим адрес переменной a printf ("value of variable a = %u\n " , a) ; // выводим значение переменной а _getch() ; }

Листинг 1.

Здесь используется доступ к переменной типа BYTE по указателю – структура BYTE PTR . Немного позже мы увидим, как этот прием используется при написании программ.

Задания.

    Попробуйте записать по адресу переменной а , хранящемуся в регистре ЕАХ , число 260 . Какой ответ вы получили? Почему?

    Задайте переменную b типа WORD и переменную c типа DWORD . Используя косвенную адресацию, запишите в эти переменные числа 1023 и 70000 , соответственно.

    Поместите в переменную с число 70000 , используя указатель типа BYTE :

LEA EAX, c; MOV BYTE PTR [ EAX] , 70000 ;

Объясните полученный результат (напоминаем, что адресом слова или двойного слова является адрес их младшего байта). Проделайте то же самое, используя указатель типа WORD .

    На листинге 2 представлена программа, иллюстрирующая способы доступа к переменным по указателям. Наберите эту программу. Разберитесь с комментариями. Попробуйте поменять элементы массива. Попробуйте выводить результаты в шестнадцатеричной системе (вместо %u в строке формата функции printf() используйте %x ).

/* использование косвенной адресации */ #include // необходим для работы printf #include // необходим для работы _getch(); #include // содержит определение типов BYTE, WORD, DWORD; BYTE ar[ 6 ] = { 1 , 12 , 128 , 50 , 200 , 10 } ; // статический массив типа BYTE BYTE a1, a2, a3, a4, a5; // 8-битные беззнаковые числа WORD b1, b2; // 16-битные беззнаковые числа DWORD c; // 32-битное беззнаковое число void main // ar в регистр EAX MOV AL, BYTE PTR [ EBX] ; // помещаем в регистр AL число (типа BYTE) // число, записанное по адресу, хранящемуся // в регистре EBX, то есть первый элемент массива MOV a1, AL; // записываем содержимое регистра AL в переменную a /*помещаем в переменную a2 число, записанное по адресу "начало массива плюс 1 байт", то есть по адресу второго элемента массива*/ MOV AL, BYTE PTR [ EBX] + 1 ; MOV a2, AL; /*помещаем в переменную a3 число, записанное по адресу "число, записанное в регистре EBX плюс 1", то есть по адресу второго элемента массива*/ MOV AL, BYTE PTR [ EBX+ 1 ] ; MOV a3, AL; /*помещаем в переменную a4 число, записанное по адресу "номер, хранящийся в регистре EDX, начиная с номера, записанного регистре EBX", то есть второй элемент массива*/ MOV EDX, 1 ; MOV AL, BYTE PTR [ EBX] [ EDX] ; MOV a4, AL; /*помещаем в переменную a5 число, записанное по адресу "сумма чисел, записанных в регистрах EBX и EDX", то есть второй элемент массива*/ MOV AL, BYTE PTR [ EBX+ EDX] ; MOV a5, AL; /*помещаем в переменную b1 2 и 1 элементы массива*/ MOV AX, WORD PTR [ EBX] ; MOV b1, AX; /*помещаем в переменную b2 4 и 3 элементы массива*/ MOV AX, WORD PTR [ EBX] + 2 ; MOV b2, AX; /*помещаем в переменную с 6, 5, 4 и 3 элементы массива*/ MOV EAX, DWORD PTR [ EBX] + 2 ; MOV c, EAX; } ; printf ("first element of array a1 = %u \n " , a1) ; printf ("second element of array a2 = %u \n " , a2) ; printf ("second element of array (another way) a3 = %u \n " , a3) ; printf ("second element of array (base addressation) a4 = %u \n " , a4) ; printf ("second element of array (base addr. - another way) a5 = %u \n " , a5) ; printf ("1, 2 elements of array b1 = %u \n " , b1) ; printf ("3, 4 elements of array b2 = %u \n " , b2) ; printf ("3, 4, 5, 6 elements of array c = %u \n " , c) ; _getch() ; }

Листинг 2.

Доступ к переменной по указателю используется и в языках высокого уровня (очень часто – при создании динамических массивов).

Указатель – это переменная, которая содержит адрес другой переменной (говорят, что указатель указывает на переменную того типа, адрес которой он содержит). Существует одноместная (унарная, т.е. для одного операнда) операция взятия адреса переменной & (амперсанд, как в названии мультфильма Tom&Jerry). Если имеем объявление int a , то можно определить адрес этой переменной: &a . Если Pa – указатель, который будет указывать на переменную типа int , то можно записать: Pa=&a . Существует унарная операция * (она называется операцией разыменования ), которая действует на переменную, содержащую адрес объекта, т.е. на указатель. При этом извлекается содержимое переменной, адрес которой находится в указателе. Если Pa=&a , то, воздействуя на обе части операцией * получим (по определению этой операции): *Pa=a . Исходя из этого, указатель объявляется так:

< тип переменной> * < имя указателя>

Это и есть правило объявления указателя: указатель на переменную какого-то типа – это такая переменная, при воздействии на которую операцией разыменования получаем значение переменной того же типа. На листинге 3 приведен пример использования указателя в языке Си.

/* получение адреса переменной - сравнение С и Assembler */ #include // необходим для работы printf #include // необходим для работы _getch(); #include // содержит определение типов BYTE, WORD, DWORD; BYTE a= 10 ; BYTE * cAddr; DWORD asmAddr; BYTE b; void main MOV asmAddr, EBX; // помещаем в переменную asmAddr содержимое регистра EBX, // т.е. адрес переменной a } ; cAddr=& a; // записываем в переменную типа BYTE* адрес переменной типа BYTE b=* cAddr; // осуществляем разыменование указателя на переменную а printf ("Assembler: address of a is %u\n " , asmAddr) ; printf ("C: address of a is %u\n " , cAddr) ; printf ("C: value of a is %u\n " , b) ; _getch() ; }

Листинг 3.

На листинге 4 представлена программа, позволяющая получать адреса элементов массивов разных типов средствами Cи. Обратите внимание на значения соседних адресов элементов массива.

/* адресация в массивах */ #include // необходим для работы printf #include // необходим для работы _getch(); #include // содержит определение типов BYTE, WORD, DWORD; unsigned int mas[ 4 ] ; // массив 4-байтовых целых чисел unsigned int * ptrMas; // указатель на переменную типа unsigned int unsigned short int masShort[ 4 ] ; // массив 2-байтовых целых чисел unsigned short int * ptrMasShort; // указатель на переменную типа unsigned short int BYTE masBYTE[ 4 ] ; // массив 1-байтовых целых чисел BYTE * ptrMasBYTE; // указатель на переменную типа BYTE void main() { ptrMas = mas; // помещаем в указатель адрес первого элемента массива ptrMasShort = masShort; ptrMasBYTE = masBYTE; printf ("array of int \n " ) ; for (int i= 0 ; i< 4 ; i++ ) printf ("int pointer+%u = %u\n " , i, ptrMas+ i) ; printf ("\n array of short int \n " ) ; for (int i= 0 ; i< 4 ; i++ ) printf ("short pointer+%u = %u\n " , i, ptrMasShort+ i) ; printf ("\n array of BYTE \n " ) ; for (int i= 0 ; i< 4 ; i++ ) printf ("byte pointer+%u = %u\n " , i, ptrMasBYTE+ i) ; _getch() ; }

Листинг 4.

Один из наиболее часто встречающихся случаев – использование указателей для динамического выделения памяти при создании массивов (листинг 5).

/* динамическое выделение памяти */ #include // необходим для работы printf #include #include // содержит определение типов BYTE, WORD, DWORD #include #include // необходим для работы malloc() void main() { int * ptint; // указатель на переменную типа int /* Выделяем память под массив. Аргумент функции malloc() - число байт. Нам нужен массив из 10 целых чисел. Поэтому общее число байт - размер числа типа int (определяется функцией sizeof()), умноженный на число элементов массива. Стоящая перед malloc() конструкция (int*) осуществляет приведение к типу int* (то есть теперь выделенная память будет рассматриваться компилятором как совокупность 4 байтных ячеек, в которых хранятся числа типа int) */ ptint = (int * ) malloc (10 * sizeof (int ) ) ; /* заполняем массив */ for (int i= 0 ; i< 10 ; i++ ) ptint[ i] = i; /*выводим элементы массива*/ for (int i= 0 ; i< 10 ; i++ ) printf ("%d " , ptint[ i] ) ; free (ptint) ; // освобождаем память _getch() ; }

Листинг 5.

Задание. Выведите на экран адреса элементов массива, созданного в программе, показанной на листинге 5. Попробуйте создать динамический массив типа double , заполнить его, вывести на печать элементы массива и их адреса.

Арифметические операции над целыми числами

Сложение и вычитание целых чисел

Рассмотрим 3 основные команды сложения. Команда INC осуществляет инкремент, т.е. увеличение содержимого операнда на 1, например, INC EAX . Команда INC устанавливает флаги OF, SF, ZF, AF, PF в зависимости от результатов сложения. Команда ADD осуществляет сложение двух операндов. Результат пишется в первый операнд (приемник) . Первый операнд может быть регистром или переменной. Второй операнд – регистром, переменной или числом. Невозможно, однако, осуществлять операцию сложения одновременно над двумя переменными. Команда действует на флаги CF, OF, SF, ZF, AF, PF . Её можно использовать для знаковых и для беззнаковых чисел. Команда ADC осуществляет сложение двух операндов подобно команде ADD и флага (бита) переноса. С её помощью можно осуществлять сложение чисел, размер которых превышает 32 бита или изначально длина операндов превышает 32 бита.

/* сложение целых чисел */ #include #include // необходим для работы _getch() #include // содержит определение типов BYTE, WORD, DWORD; int a, b, c; DWORD d, e, f, m, n, l, k; void main() { a= 100 ; b=- 200 ; f= 0 ; d= 0xffffffff ; e= 0x00000010 ; m= 0x12345678 ; n= 0xeeeeeeee ; l= 0x11111111 ; k= 0x22222222 ; __asm{ /* сложение положительного и отрицательного чисел */ MOV EAX, a; ADD EAX, b; MOV c, EAX; /* сложение двух больших чисел */ MOV EAX, e; // EAX = 0x00000010 ADD d, EAX; // результат превышает 4 байта, поэтому флаг CF // устанавливается в 1: // 0xffffffff // + 0x00000010 // ---------- // 0x0000000f (и 1 должна переноситься в следующий разряд, // но его нет, поэтому устанавливается флаг CF) ADC f, 0 ; // осуществляет сложение двух операндов (подобно команде ADD) и // флага (бита) переноса CF. Вначале f=0, второй операнд также 0, // поэтому в данном случае выполнение команды сводится к помещению в // переменную f значения CF /* сложение двух больших чисел, расположенных в паре регистров */ MOV EDX, m; // поместили в EDX старшие 4 байта первого числа, //EDX=0x12345678 MOV EAX, n; // поместили в EAX младшие 4 байта первого числа, // EAX=0xeeeeeeee MOV ECX, l; // поместили в ECX старшие 4 байта второго числа, // ECX=0x11111111 MOV EBX, k; // поместили в EBX младшие 4 байта первого числа, // EBX=0x22222222 ADD EAX, EBX; // сложили младшие 4 байта MOV n, EAX; ADC EDX, ECX; // сложили старшие 4 байта MOV m, EDX; } ; printf ("c=a+b=%d\n " , c) ; printf ("f=d+e=%x%x\n " , f, d) ; printf ("sum of lowest 4 bytes = %x\n " , n) ; printf ("sum of highest 4 bytes = %x\n " , m) ; _getch() ; }

Листинг 6.

/*вычитание целых чисел*/ #include // необходим для работы printf() #include // необходим для работы _getch() #include // содержит определение типов BYTE, WORD, DWORD; int a, b, c; __int64 i, j, k; void main() { a= 100 ; b=- 200 ; i= 0x1ffffffff ; j= 0x1fffffffb ; __asm{ /* вычитание 32-битных чисел */ MOV EAX, a; SUB EAX, b; MOV c, EAX; /* вычитание 64-битных чисел */ MOV EAX, DWORD PTR i; // поместили в EAX адрес младших 4 байт числа i. // По этому адресу записано число 0xffffffff MOV EDX, DWORD PTR i+ 4 ; // поместили в EDX адрес старших 4 байт числа i. MOV EBX, DWORD PTR j; // поместили в EBX адрес младших 4 байт числа j. // По этому адресу записано число 0xfffffffb MOV ECX, DWORD PTR j+ 4 ; // поместили в ECX адрес старших 4 байт числа j. // По этому адресу записано число 0x00000001 SUB EAX, EBX; // вычитаем из младших 4 байт числа i младшие 4 байта // числа j. Эта операция влияет на флаг CF SBB EDX, ECX; // вычитаем из старших 4 байт числа i старшие 4 байта // числа j, а также флаг CF MOV DWORD PTR k, EAX; // помещаем в память младшие 4 байта результата MOV DWORD PTR k+ 4 , EDX; // помещаем в память старшие 4 байта результата } ; printf ("c=a+b=%d\n " , c) ; printf ("k=i-j=%I64x\n " , k) ; _getch() ; }

Листинг 7.

Умножение целых чисел

В отличие от сложения и вычитания умножение чувствительно к знаку числа, поэтому существует две команды умножения: MUL – для умножения беззнаковых чисел, IMUL – для умножения чисел со знаком . Единственным оператором команды MUL может быть регистр или переменная. Здесь важен размер этого операнда (источника).

    Если операнд однобайтовый , то он будет умножаться на AL , соответственно, результат будет помещен в регистр AX независимо от того, превосходит он один байт или нет. Если результат не превышает 1 байт, то флаги OF и CF будут равны 0, в противном случае – 1.

    Если операнд двухбайтовый , то он будет умножаться на AX , и результат будет помещен в пару регистров DX:AX (а не в EAX , как могло бы показаться логичным). Соответственно, если результат поместится целиком в AX , т.е. содержимое DX будет равно 0, то нулю будут равны и флаги CF и OF .

    Наконец, если оператор-источник будет иметь длину четыре байта , то он будет умножаться на EAX , а результат должен быть помещен в пару регистров EDX:EAX . Если содержимое EDX после умножения окажется равным нулю, то нулевое значение будет и у флагов CF и OF .

Команда IMUL имеет 3 различных формата. Первый формат аналогичен команде MUL . Остановимся на двух других форматах.

IMUL operand1, operand2

operand1 должен быть регистр, operand2 может быть числом, регистром или переменной. В результате выполнения умножения (operand1 умножается на operand2 , и результат помещается в operand1 ) может получиться число, не помещающееся в приемнике. В этом случае флаги CF и AF будут равны 1 (0 в противном случае).

IMUL operand1, operand2, operand3

В данном случае operand2 (регистр или переменная) умножается на operand3 (число) и результат заносится в operand1 (регистр). Если при умножении возникнет переполнение, т.е. результат не поместится в приемник, то будут установлены флаги CF и OF . Применение команд умножения приведено на листинге 8.

#include // необходим для работы printf() #include // необходим для работы _getch() #include // содержит определение типов BYTE, WORD, DWORD; DWORD a= 100000 ; __int64 b; int c=- 1000 ; int e; void main() { __asm{ /* беззнаковое умножение */ MOV EAX, 100000 ; // поместили в EAX число, превышающее 2 байта MUL DWORD PTR a; // умножаем содержимое регистра EAX на a, // результат будет помещен в пару регистров // EDX:EAX MOV DWORD PTR b, EAX; // помещаем в младшие 4 байта // 8-байтной переменной b младшие 4 байта результата MOV DWORD PTR b+ 4 , EDX; // помещаем в старшие 4 байта // 8-байтной переменной b старшие 4 байта результата /* знаковое умножение */ IMUL EAX, c, 1000 ; // умножаем с на 1000 и результат помещаем в EAX MOV e, EAX; // помещаем результат умножения в переменную e } ; printf ("a*100000 = %I64d\n " , b) ; // интерпретируем выводимое число как __int64 printf ("e = %d\n " , e) ; _getch() ; }

Листинг 8. Применение команд умножения

Деление целых чисел

Деление беззнаковых чисел осуществляется с помощью команды DIV . Команда имеет только один операнд – это делитель. Делитель может быть регистром или ячейкой памяти. В зависимости от размера делителя выбирается и делимое.

    Делитель имеет размер 1 байт . В этом случае делимое помещается в регистре AX AL , в регистре AH будет остаток от деления.

    Делитель имеет размер 2 байта DX:AX . Результат деления (частное) содержится в регистре AX , в регистре DX будет остаток от деления.

    Делитель имеет размер 4 байта . В этом случае делимое помещается в паре регистров EDX:EAX . Результат деления (частное) содержится в регистре EAX , в регистре EDX будет остаток от деления.

Команда знакового деления IDIV полностью аналогична команде DIV . Существенно, что для команд деления значения флагов арифметических операций не определены. В результате деления может возникнуть либо переполнение, либо деление на 0. Обработку исключения должна обеспечить операционная система.

#include // необходим для работы printf() #include // необходим для работы _getch() #include // содержит определение типов BYTE, WORD, DWORD; DWORD a, b, c; void main() { a= 100000 ; // делимое - 4 байта __asm{ /* беззнаковое деление */ MOV EAX, a; // поместили младшие 4 байта делимого в регистр EAX MOV EDX, 0 ; // поместили старшие 4 байта делимого в регистр EDX MOV EBX, 30 ; // поместили в EBX делитель (4 байта!) DIV EBX; // выполнили деление содержимого EDX:EAX на // содержимое EBX MOV b, EAX; // помещаем в b частное
  • NASM/YASM требует word , когда размер операнда не подразумевается другим операндом. (В противном случае в порядке).
  • Для MASM/TASM требуется word ptr , когда размер операнда не подразумевается другим операндом. (В противном случае в порядке).

Каждый из них задыхается от другого синтаксиса.

ПРЕДУПРЕЖДЕНИЕ: Это очень странная область без каких-либо стандартов ИСО или легкодоступных таблиц BNF; и я не специалист по прохождению через минные поля проприетарного синтаксиса MASM.

В вашем случае это может быть не разница, но оператор PTR может означать в других случаях:

В общем случае выражение оператора PTR принудительно обрабатывается как указатель указанного типа:

.DATA num DWORD 0 .CODE mov ax, WORD PTR ; Load a word-size value from a DWORD

Я думаю, что существуют также специфические требования для ассемблера (nasm/tasm/other asm), а использование "байтового ptr" более переносимо.

Также проверьте раздел 4.2.16 в книга из Индии и разделы 8.12.3 (и 8.11.3 "Типы конфликтов") в "Программе программирования языка программирования".

ОБНОВЛЕНИЕ: спасибо Фрэнку Котлеру, похоже, что NASM "использует вариант синтаксиса сборки Intel" (wiki), который не включает операцию PTR.

UPDATE1: существует оригинальная "ASM86 LANGUAGE REFERENCE MANUAL" от Intel, 1981-1983, оператор PTR определен на стр. 4-15

Оператор PTR

Синтаксис: введите имя PTR

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

MOV WORD PTR , 5 ;set word pointed to by BX = 5 INC DS:BYTE PTR 10 ;increment byte at offset 10 ;from DS

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

MOV CL, BYTE PTR AWORD ;get first byte MOV CL, BYTE PTR AWORD + 1 ;get second byte

Значения полей:

type Это поле может иметь одно из следующих значений: BYTE, WORD, DWORD, QWORD, TBYTE, NEAR, FAR

name Это поле может быть: 1. Имя переменной. 2. Имя метки. 3. Адрес или регистр. 4. Целое число, которое представляет смещение.

UPDATE2: Благодаря Uni из Stuttgart битрейдер! Существует оригинальное руководство MACRO-86 от Microsoft (1981). Страница 3-7:

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

MOV ,FOO

Вы можете обратиться к FOO как к байту немедленно. В этом случае вы можете ввести любой из операторов (они эквивалентны):

MOV BYTE PTR ,FOO MOV ,BYTE PTR FOO

Эти утверждения указывают MACRO-86, что FOO является байтом немедленно. Создается меньшая команда.

И страница 3-16:

Операторы переопределения

Эти операторы используются для переопределения сегмента, смещения, типа или расстояния между переменными и метками.

Указатель (PTR)

PTR

Оператор PTR переопределяет тип (BYTE, WORD, DWORD) или расстояние (NEAR, FAR) операнда.

- новый атрибут; новый тип или новое расстояние.

- это операнд, атрибут которого должен быть переопределен.

Самое важное и частое использование для PTR заключается в том, чтобы убедиться, что MACRO-86 понимает, какой атрибут должен иметь выражение. Это особенно верно для атрибута type. Всякий раз, когда вы размещаете ссылки в своей программе, PTR очищает расстояние или тип выражения. Таким образом, вы можете избежать фазовых ошибок.

Второе использование PTR заключается в доступе к данным по типу, отличному от типа в определении переменной. Чаще всего это происходит в структурах. Если структура определена как WORD, но вы хотите получить доступ к элементу в виде байта, PTR является оператором для этого. Однако гораздо более простой способ - ввести второй оператор, который также определяет структуру в байтах. Это устраняет необходимость использования PTR для каждой ссылки на структуру. См. Директиву LABEL в разделе 4.2.1 "Директивы памяти".

CALL WORD PTR MOV BYTE PTR ARRAY, (something) ADD BYTE PTR FOO,9

После прочтения этого и поиска некоторых определений синтаксиса из этих документов я считаю, что запись PTR является обязательной. Использование mov BYTE , 0 неверно в соответствии с руководством MACRO-86.

Питер, 2003. - 629 c.
Скачать (прямая ссылка): assembler2003.djvu Предыдущая 1 .. 40 > .. >> Следующая

О Оператор переопределения типа ptr применяется для переопределения или уточнения типа метки или переменной, определяемых выражением (рис. 5.10). Тип может принимать одно из следующих значений: byte, word, dword, qword, tbyte, near, far. Что означают эти значения, вы узнаете далее на этом уроке. Например:

d wrd dd 0 * * *

пюу al.byte ptr d_wrd+l ;пересылка второго байта из двойного

;словаПоясним этот фрагмент программы. Переменная djwrd имеет тип двойного слова. Что делать, если возникнет необходимость обращения не ко всему значению переменной, а только к одному из входящих в нее байтов (например, ко второму)? Если попытаться сделать это командой mov al. d_wrd+l, то транслятор выдаст сообщение о несовпадении типов операндов. Оператор ptr позволяет непосредственно в команде переопределить тип и выполнить команду.

ЧТип!-(ptr)-Выражение -

Рис. 5.10. Синтаксис оператора переопределения типа

О Оператор переопределения сегмента: (двоеточие) заставляет вычислять физический адрес относительно конкретно задаваемой сегментной составляющей: «имя сегментного регистра», «имя сегмента» из соответствующей директивы SEGMENT или «имя группы» (рис. 5.11).

Этот момент очень важен, поэтому поясним его подробнее. При обсуждении сегментации мы говорили о том, что микропроцессор на аппаратном уровне поддерживает три типа сегментов - кода, стека и данных. В чем заключается такая аппаратная поддержка? К примеру, для выборки на выполнение очередной команды микропроцессор должен обязательно посмотреть содержимое сегментного регистра es и только его. А в этом регистре, как мы знаем, содержится (пока еще не сдвинутый) физический адрес начала сегмента команд. Для получения адреса конкретной команды микропроцессору остается умножить содержимое es на 16 (что означает сдвиг на четыре разряда) и сложить полученное 20-битное значение с 16-битным содержимым регистра і р. Примерно то же самое происходит и тогда, когда микропроцессор обрабатывает операнды в машинной команде. Если он видит, что операнд - это адрес (эффективный адрес, который является только частью физического адреса), то он знает, в каком сегменте его искать, - по умолчанию это сегмент, адрес начала которого записан в сегментном регистре ds.

А что же с сегментом стека? Посмотрите урок 2 там, где мы описывали назначение регистров общего назначения. В контексте нашего рассмотрения нас интересуют регистры sp и bp. Если микропроцессор видит в качестве операнда (или его части, если операнд - выражение) один из этих регистров, то по умолчанию он формирует физический адрес операнда, используя в качестве его сегментной составляющей содержимое регистра ss. Что подразумевает термин «по умолчанию»? Вспомните «рефлексы», о которых мы говорили на уроке 1. Это набор микропрограмм в блоке микропрограммного управления, каждая из которых выполняет одну из команд в системе машинных команд микропроцессора. Каждая микропрограмма работает по своему алгоритму. Изменить его, конечно же, нельзя, но можно чуть-чуть подкорректировать. Делается это с помощью необязательного поЛя префикса машинной команды (см. формат команд в уроке* 2), Если мы согласны с тем, как работает команда, то это поле отсутствует. Если же мы хотим внести поправку (если, конечно, она допустима для конкретной команды) в алгоритм работы команды, то необходимо сформировать соответствующий префикс. Префикс представляет собой однобайтовую величину, численное значе-ниє которой определяет ее назначение. Микропроцессор распознает по указанному значению, что этот байт является префиксом, и дальнейшая работа микропрограммы выполняется с учетом поступившего указания на корректировку ее работы. По ходу обсуждения материала книги мы познакомимся с большинством возможных префиксов. Сейчас нас интересует один из них - префикс замены (переопределения) сегмента. Его назначение состоит в том, чтобы указать микропроцессору (а по сути, микропрограмме) на то, что мы не хотим использовать сегмент по умолчанию. Возможности для подобного переопределения, конечно, ограничены. Сегмент команд переопределить нельзя, адрес очередной исполняемой команды однозначно определяется парой cs: і р. А вот сегменты стека и данных - можно. Для этого и предназначен оператор «:». Транслятор ассемблера, обрабатывая этот оператор, формирует соответствующий однобайтовый префикс замены сегмента. Например:

jmp metl ;обход обязателен, иначе поле ind будет трактоваться;как очередная команда ind db 5 ;описание поля данных в сегменте команд

mov al,cs:ind !переопределение сегмента позволяет работать

;с данными, определенными внутри сегмента кода

Продолжим перечисление операторов.

О Оператор именования типа структуры. (точка) также заставляет транслятор производить определенные вычисления, если он встречается в выражении. Подробно мы обсудим этот оператор, когда введем понятие сложных типов данных на уроке 12.

О Оператор получения сегментной составляющей адреса выражения seg возвращает физический адрес сегмента для выражения (рис. 5.12), в качестве которого могут выступать метка, переменная, имя сегмента, имя группы или некоторое символическое имя.

Арифметические операции - ADD, SUB, MUL, DIV. Многие опкоды делают вычисления. Вы можете узнать многие из них по их названиям: add (addition - добавление), sub (substraction - вычитание), mul (multiply - умножение), div (divide - деление).

Опкод add имеет следующий синтаксис:

Add приемник, источник

Выполняет вычисление: приемник = приемник + источник.

Имеются также другие формы:

приемник источник пример
регистр регистр add ecx, edx
регистр память add ecx, dword ptr / add ecx,
регистр значение add eax, 102
память значение add dword ptr , 80
память регистр add dword ptr , edx

Эта команда очень проста. Она добавляет значение источника к значение приемника и помещает результат в приемник. Другие математические команды:

Sub приемник, источник (приемник = приемник - источник)
mul множимое, множитель (множимое = множимое * множитель)
div делитель (eax = eax / делитель, edx = остаток)

Поскольку регистры могут содержать только целочисленные значения (то есть числа, не, с плавающей запятой), результат деления разбит на частное и остаток. Теперь, в зависимости от размера источника, частное сохраняется в eax, а остаток в edx:
* = Например: если dx = 2030h, а ax = 0040h, dx: ax = 20300040h. Dx:ax - значение dword, где dx представляет старшее word, а ax - младшее. Edx:eax - значение quadword (64 бита), где старшее dword в edx и младшее в eax.

Источник операции деления может быть:

  1. 8-бит регистр (al, ah, cl,...)
  2. 16-бит регистр (ax, dx, ...)
  3. 32-бит регистр (eax, edx, ecx...)
  4. 8-бит значение из памяти (byte ptr )
  5. 16-бит значение из памяти (word ptr )
  6. 6a 32-бит значение памяти (dword ptr )

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

Логические операции с битами - OR, XOR, AND, NOT. Эти команды работают с приемником и источником, исключение команда "NOT". Каждый бит в приемнике сравнивается с тем же самым битом в источнике, и в зависимости от команды, 0 или 1 помещается в бит приемника:

AND (логическое И) устанавливает бит результата в 1, если оба бита, бит источника и бит приемника установлены в 1.
OR (логическое ИЛИ) устанавливает бит результата в 1, если один из битов, бит источника или бит приемника установлен в 1.
XOR (НЕ ИЛИ) устанавливает бит результата в 1, если бит источника отличается от бита приемника.
NOT инвертирует бит источника.

Пример:

Mov ax, 3406d
mov dx, 13EAh
xor ax, dx

Ax = 3406 (десятичное), в двоичном - 0000110101001110.

Dx = 13EA (шестнадцатиричное), в двоичном - 0001001111101010.

Выполнение операции XOR на этими битами:

Источник = 0001001111101010 (dx)

Приемник = 0000110101001110 (ax)

Результат = 0001111010100101 (новое значение в ax)

Новое значение в ax, после выполнения команды - 0001111010100101 (7845 - в десятичном, 1EA5 - в шестнадцатиричном).

Другой пример:

Mov ecx, FFFF0000h
not ecx

FFFF0000 в двоичном это -
Если вы выполните инверсию каждого бита, то получите:
, в шестнадцатиричном это 0000FFFF
Значит после операции NOT, ecx будет содержать 0000FFFFh.

Увеличение/Уменьшение - INC/DEC. Есть 2 очень простые команды, DEC и INC. Эти команды увеличивают или уменьшают содержимое памяти или регистра на единицу. Просто поместите:

Inc регистр; регистр = регистр + 1
dec регистр; регистр = регистр - 1
inc dword ptr ; значение в будет увеличено на 1.
dec dword ptr ; значение в будет уменьшено на 1.

Ещё одна команда сравнения - test. Команда Test выполняет операцию AND (логическое И) с двумя операндами и в зависимости от результата устанавливает или сбрасывает соответствующие флаги. Результат не сохраняется. Test используется для проверки бит, например в регистре:

Test eax, 100b
jnz смещение

Команда jnz выполнит переход, если в регистре eax третий бит справа - установлен. Очень часто комманду test используют для проверки, равен ли регистр нулю:

Test ecx, ecx
jz смещение

Команда jz выполнит переход, если ecx = 0.

Ничего не делающая команда - nop. Эта команда не делает абсолютно ничего (пустая команда). Она только занимает пространство и время. Используется для резервирования места в сегменте кода или организации программной задержки.

Обмен значениями - XCHG. Команда XCHG также весьма проста. Назначение: обмен двух значений между регистрами или между регистрами и памятью:

Mov eax , 237h
mov ecx, 978h
xchg eax, ecx
в результате:
eax = 978h
ecx = 237h

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