Постигаем си глубже, используя ассемблер. Что будем использовать

14.04.2019

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

От читающих потребуются хотя бы базовые знания в следующих вещах:

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

Что будем использовать?

  1. Нам понадобится компилятор Си, который поддерживает современный стандарт. Можно воспользоваться онлайн компилятором на сайте ideone.com .
  2. Так же нам нужен декомпилятор, опять же, можно воспользоваться онлайн декомпилятором на сайте godbolt.org .
  3. Можно так же взять компилятор для ассемблера, который есть на ideone по ссылке выше.
Почему у нас все онлайн? Потому что это удобно для разрешения спорных ситуаций из-за различных версий и операционных систем. Компиляторов много, декомпиляторов так же хватает, не хотелось бы в дискуссии учитывать особенности каждого.

При более основательном подходе к изучению, лучше пользоваться оффлайн версиями компиляторов, можете взять связку из актуального gcc, OllyDbg и NASM. Отличия должны быть минимальны.

Простейшая программа

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

Первое, что нужно усвоить, компилятор даже при оптимизации нулевого уровня (-O0), может вырезать код, написанный программистом. Поэтому код следующего вида:

Int main(void) { 5 + 3; return 0; }
Ничем не будет отличаться от:

Int main(void) { return 0; }
Поэтому придется писать таким образом, чтобы при декомпиляции мы, все же, увидели превращение нашего кода во что-то осмысленное, поэтому примеры могут выглядеть, как минимум странно.

Второе, нам нужны флаги компиляции. Достаточно двух: -O0 и -m32 . Этим мы задаем нулевой уровень оптимизации и 32-битный режим. С оптимизаций должно быть очевидно: нам не хочется видеть интерпретацию нашего кода в asm, а не оптимизированного. С режимом тоже должно быть очевидно: меньше регистров - больше внимания к сути. Хотя эти флаги я буду периодически менять, чтобы углубляться в материал.

Таким образом, если вы пользуетесь gcc, то компиляция может выглядеть так:

Gcc source.c -O0 -m32 -o source

Соответственно, если вы пользуетесь godbolt, то вам нужно указать эти флаги в строку ввода рядом с выбором компилятора. (Первые примеры я демонстрирую на gcc 4.4.7, потом поменяю на более поздний)

Теперь, можно посмотреть первый пример:

Int main(void) { register int a = 1; //записываем в регистровую переменную 1 return a; //возвращаем значение из регистровой переменной }
Итак, следующий код соответствует этому:

Push ebp mov ebp, esp push ebx mov ebx, 1 mov eax, ebx pop ebx pop ebp ret
Первые две строчки соответствую прологу функции (точнее три, но третью хочу пояснить сейчас), и мы их разберем в статье о функциях. Сейчас просто не обращайте на них внимание, тоже самое касается последних 3х строчек. Если вы не знаете asm, давайте смотреть, что означают эти команды.

Инструкции ассемблера имеют вид:

Mnemonic dst, src
т. е.

Инструкция получатель, источник

Тут нужно оговориться, что AT&T-синтаксис имеет другой порядок, и потом мы к нему еще вернемся, но сейчас нас интересует синтаксис схожий с NASM.

Начнем с инструкции mov . Эта инструкция перемещает из памяти в регистры или из регистров в память. В нашем случае она перемещает число 1 в регистр ebx.

Давайте кратко о регистрах: в архитектуре x86 восемь 32х битных регистров общего назначения, это значит, что эти регистры могут быть использованы программистом (в нашем случае компилятором) при написании программ. Регистры ebp, esp, esi и edi компилятор будет использовать в особых случаях, которые мы рассмотрим позже, а регистры eax, ebx, ecx и edx компилятор будет использовать для всех остальных нужд.

Таким образом mov ebx, 1 , прямо соответствует строке register int a = 1;

И означает, что в регистр ebx было перемещено значение 1.

А строчка mov eax, ebx , будет означать, что в регистр eax будет перемещено значение из регистра ebx.

Есть еще две строчки push ebx и pop ebx . Если вы знакомы с понятием «стек», то догадываетесь, что сначала компилятор поместил ebx в стек, тем самым запомнил старое значение регистра, а после окончания работы программы, вернул из стека это значение обратно в регистр ebx.

Почему компилятор помещает значение 1 из регистра ebx в eax? Это связано с соглашением о вызовах функций языка Си. Там несколько пунктов, все они нас сейчас не интересуют. Важно то, что результат возвращается в eax, если это возможно. Таким образом понятно, почему единица в итоге оказывается в eax.

Но теперь логичный вопрос, а зачем понадобился ebx? Почему нельзя было написать сразу mov eax, 1 ? Все дело в уровне оптимизации. Я же говорил: компилятор не должен вырезать наш код, а мы написали не return 1 , мы использовали регистровую переменную. Т. е. компилятор сначала поместил значение в регистр, а затем, следуя соглашению, вернул результат. Поменяйте уровень оптимизации на любой другой, и вы увидите, что регистр ebx, действительно, не нужен.

Кстати, если вы пользуетесь godbolt, то вы можете наводить мышкой на строку в Си, и вам подсветится соответствующий этой строке код в asm, при условии, что эта строка выделена цветом.

Стек

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

Int main(void) { int a = 1; //записываем в переменную 1 int b = a + 5; //прибавим к "a" 5 и сораним в "b" return b; //возвращаем значение из переменной }
ASM:

Push ebp mov ebp, esp sub esp, 16 mov DWORD PTR , 1 mov eax, DWORD PTR add eax, 5 mov DWORD PTR , eax mov eax, DWORD PTR leave ret
Опять же, пропустим верхние 3 строчки и нижние 2. Теперь у нас переменная а локальная, следовательно память ей выделяется на стеке. Поэтому мы видим следующую магию: DWORD PTR , что же она означает? DWORD PTR - это переменная типа двойного слова. Слово - это 16 бит. Термин получил распространение в эпоху 16-ти битных процессоров, тогда в регистр помещалось ровно 16 бит. Такой объем информации стали называть словом (word). Т. е. в нашем случае dword (double word) 2*16 = 32 бита = 4 байта (обычный int).

В регистре ebp содержится адрес на вершину стека для текущей функции (мы к этому еще вернемся, потом), поэтому он смещается на 4 байта, чтобы не затереть сам адрес и дописывает значение нашей переменной. Только, в нашем случае он смещается на 8 байт для переменной a . Но если вы посмотрите на код ниже, то увидите, что переменная b лежит со смещением в 4 байта. Квадратные скобки означают адрес. Т. е. это строка работает следующим образом: на основе адреса, хранящегося в ebp, компилятор помещает значение 1 по адресу ebp-8 размера 4 байта. Почему минус восемь, а не плюс. Потому что плюсу бы соответствовали параметры, переданные в эту функцию, но опять же, обсудим это позже.

Следующая строка перемещает значение 1 в регистр eax. Думаю, это не нуждается в подробных объяснениях.

После этого нужно переместить значение 6 в переменную b , что и делается следующей строкой (переменная b находится в стеке по смещению 4).

Наконец, нам нужно вернуть значение переменной b, следовательно нужно переместить
значение в регистр eax (mov eax, DWORD PTR ).

Если с предыдущим все понятно, то можно переходить, к более сложному.

Что произойдет, если мы напишем следующее: int var = 2.5;

Каждый из вас, я думаю, ответит верно, что в var будет значение 2. Но что произойдет с дробной частью? Она отбросится, проигнорируется, будет ли преобразование типа? Давайте посмотрим:

ASM:
mov DWORD PTR , 2
Компилятор сам отбросил дробную часть за ненадобностью.

Что произойдет, если написать так: int var = 2 + 3;

ASM:
mov DWORD PTR , 5
И мы узнаем, что компилятор сам способен вычислять константы. А в данном случае: так как 2 и 3 являются константами, то их сумму можно вычислить на этапе компиляции. Поэтому можно не забивать себе голову вычислением таких констант, компилятор может сделать работу за вас. Например, перевод в секунды из часов можно записать, как hours * 60 * 60. Но скорее, в пример тут стоит поставить операции над константами, которые объявлены в коде.

Что произойдет, если напишем такой код:

Int a = 1; int b = a * 2;
mov DWORD PTR , 1 mov eax, DWORD PTR add eax, eax mov DWORD PTR , eax
Интересно, не правда ли? Компилятор решил не пользоваться операцией умножения, а просто сложил два числа, что и есть - умножить на 2. (Я уже не буду подробно описывать эти строки, вы должны понять их, исходя из предыдущего материала)

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

Но усложним ему задачу и напишем так:

Int a = 1; int b = a * 3;
ASM

Mov DWORD PTR , 1 mov edx, DWORD PTR mov eax, edx add eax, eax add eax, edx mov DWORD PTR , eax
Пусть вас не вводит в заблуждение использование нового регистра edx, он ничем не хуже eax или ebx. Может понадобиться время, но вы должны увидеть, что единица попадает в регистр edx, затем в регистр eax, после чего значение eax складывается само с собой и после уже добавляется еще одна единица из edx. Таким образом, мы получили 1+1+1.

Знаете, бесконечно он так делать не будет, уже на *4, компилятор выдаст следующее:

Mov DWORD PTR , 1 mov eax, DWORD PTR sal eax, 2 mov DWORD PTR , eax mov eax, 0
Итак, у нас новая инструкция sal , что же она делает? Это двоичный сдвиг влево. Эквивалентно следующему коду в Си:

Int a = 1; int b = a << 2;
Для тех, кто не очень понимает, как работает этот оператор:

0001 сдвигаем влево (или добавляем справа) на два нуля: 0100 (т. е. 4 в 10ой системе счисления). По своей сути сдвиг влево на 2 разряда - это умножение на 4.

Забавно, что если вы умножите на 5, то компилятор сделает один sal и один add, можете сами потестировать разные числа.

На 22, компилятор на godbolt.org сдается и использует умножение, но до этого числа он пытается выкрутиться самыми разными способами. Даже вычитание использует и еще некоторые инструкции, которые мы еще не обсуждали.

Ладно, это были цветочки, а что вы думаете по поводу следующего кода:

Int a = 2; int b = a / 2;
Если вы ожидаете вычитания, то увы - нет. Компилятор будет выдавать более изощренные методы. Операция «деление» еще медленнее умножения, поэтому компилятор будет также выкручиваться:

Mov DWORD PTR , 2 mov eax, DWORD PTR mov edx, eax shr edx, 31 add eax, edx sar eax mov DWORD PTR , eax
Следует сказать, что для этого кода я выбрал компилятор существенно более поздней версии (gcc 7.2), до этого я приводил в пример gcc 4.4.7. Для ранних примеров существенных отличий не было, для этого примера они используют разные инструкции в 5ой строчке кода. И пример, сгенерированный 7.2, мне сейчас легче вам объяснить.

Стоит обратить внимание, что теперь переменная a находится в стеке по смещению 4, а не 8 и сразу же забыть об этом незначительном отличии. Ключевые моменты начинаются с mov edx, eax . Но пока пропустим значение этой строки. Инструкция shr осуществляет двоичный сдвиг вправо (т. е. деление на 2, если бы было shr edx, 1 ). И тут некоторые смогут подумать, а почему, действительно, не написать shr edx, 1 , это же то, что делает код в Си? Но не все так просто.

Давайте проведем небольшую оптимизацию и посмотрим на что это повлияет. В действительности, мы нашим кодом выполняем целочисленное деление. Так как переменная «a» является целочисленным типом и 2 константа типа int, то результат никак не может получиться дробным по логике Си. И это хорошо, так как делить целочисленные числа быстрее и проще, но у нас знаковые числа, а это значит, что отрицательное число при делении инструкцией shr может отличаться на единицу от правильного ответа. (Это все из-за того, что 0 влезает по середине диапазона для знаковых типов). Если мы заменим знаковое деление на unsigned:

Unsigned int a = 2; unsigned int b = a / 2;
То получим ожидаемое. Стоит учесть, что godbolt опустит единицу в инструкции shr, и это не скомпилируется в NASM, но она там подразумевается. Измените 2 на 4, и вы увидите второй операнд в виде 2.

Теперь посмотрим на предыдущий код. В нем мы видим sar eax , это то же самое, что и shr, только для знаковых чисел. Остальной же код просто учитывает эту единицу, когда мы делим отрицательное число (или на отрицательное число, хотя код немного изменится). Если вы знаете, как представляются отрицательные числа в компьютере, вам будет не трудно догадаться, почему мы делаем сдвиг вправо на 31 разряд и добавляем это значение к исходному числу.

С делением на большие числа, все еще проще. Там деление заменяется на умножение, в качестве второго операнда вычисляется константа. Если вам будет интересно как, можете поломать над этим голову самостоятельно, там нет ничего сложного. Нужно просто понимать, как представляются вещественные числа в памяти.

Заключение

Для первой статьи материала уже больше, чем достаточно. Пора закруглятся и подводить итоги. Мы ознакомились с базовым синтаксисом ассемблера, выяснили, что компилятор может брать на себя простейшие оптимизации при вычислениях. Увидели разницу между регистровыми и стековыми переменными. И некоторые другие вещи. Это была вводная статья, пришлось много времени уделять очевидным вещам, но они очевидны не для всех, в будущем мы постигнем больше тонкостей языка Си.

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

Четырьмя основными логическими командами являются AND (и), OR (или), XOR (исключающее или), NOT (не). Эти четыре команды работают непосредственно с нулями и единицами двоичного кода. Простейшая функция выполняется командой NOT. Эта команда основывается на определении единицы и нуля, как истины (TRUE) и лжи (FALSE) соответственно. Предложение NOT TRUE (не истина) - это FALSE (ложь), а предложение NOT FALSE (не ложь) - это TRUE (истина). Команда NOT инвертирует все биты числа данных. Иначе говоря, команда NOT эквивалентна вычитанию данных из величины, состоящей из всех единиц. действует на единственный бит.

Значение Not

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

1001 0110 and 0000 1111 = 0000 0110 = 6h

1001 0110 or 0000 1111 = 1001 1111 = 9Fh

1001 0110 xor 0000 1111 = 1001 1001 = 99h

13. Команды пересылки данных. Синтаксис. Примеры. Оператор ptr.

Mov – замещает приемник на источник. Значение источника сохраняется. В зависимости от описания операторов пересылается слово или байт. Команда не воздействует на флаги процессора.

Приемник – регистр, переменная.

Источник – регистр, переменная, число.

movs – пересылка данных из строки в строку

movsb байта

movsw слова

mowsd двойного слова

mov пр., ист. – синтаксис

MOV АХ,500 ; АX:=500

MOV BL,DH ; BL:=DH

PTR – задания типа переменной или метки. Если операнды описаны по разному или режим адресации не позволяет однозначно определит размер операнда.

Тип PTR выражение – синтаксис

Выражение – любой операнд

Тип – byte, word, dword, qword (для операндов), near, far или proc (для меток или имен процедур).

mov byte ptr es:,3

mov копирует источник в приемник.

оператор указания типа PTR (от pointer, указатель), который записывается следующим образом:

<тип> PTR <выражение>

где <тип> - это BYTE, WORD или DWORD (есть и другие варианты, но мы их пока не рассматриваем), а выражение может быть константным или адресным.

Если указано константное выражение, то оператор "говорит", что значение этого выражения (число) должно рассматриваться ассемблером как величина указанного типа (размера); например, BYTE PTR 0 - это ноль как байт, a WORD PTR 0 - это ноль как слово (запись BYTE PTR 300 ошибочна, т. к. число 300 не может быть байтом). Отметим, что в этом случае оператор PTR относится к константным выражениям.

Если же в PTR указано адресное выражение, то оператор "говорит", что адрес, являющийся значением выражения, должен восприниматься ассемблером как адрес ячейки указанного типа (размера); например: WORD PTR A - адрес А обозначает слово (байты с адресами А и А+1). В данном случае оператор PTR относится к адресным выражениям.

Арифметические операции - 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

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

Цели:

    закрепить знания о регистрах общего назначения 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 частное

Добро пожаловать на наш сайт!


5.6. Система команд

Под программной моделью понимается набор внутренних регистров и флагов процессора, которые доступны программисту. Будем использовать программную модель процессора 8086, которая является базовой для всех микропроцессоров фирмы Intel, вплоть до процессора Pentium IV. Эта модель была описана ранее в разд. 2.6

Современные процессоры фирмы Intel имеют развитую систему машинных команд. Книга с описанием всех ассемблерных команд имеет объем более 1000 листов, но начинающему программисту в его программах понадобится от силы 10 – 15 машинных команд. Ни один программист никогда не помнит все эти машинные команды наизусть. Опытный программист просто помнит, «что такая команда есть» и, когда она ему понадобится, обращается к справочнику. Остановимся только на командах, с которыми сразу столкнется в своей работе новичок.

Команда MOV -приемник, источник .

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

Важным является следующий момент: в качестве источника и приемника в одной команде не могут одновременно выступать две ячейки памяти !! То есть команда mov perem , [ bx +4] заставит транслятор сформировать сообщение об ошибке. Правильно надо было писать, например, так:

mov ax , [ bx + 4]

mov perem , ax .

Отметим также, что все, что сказано выше об источнике и приемнике, справедливо и для всех остальных команд процессора. Приведем еще один пример:

mov [ si ], 7 ;команда заносит в память по адресу;Аф = (ds )*16+(si ) число 7.

Синтаксически команда написана правильно, а транслятор выдает предупреждение: Argument needs type override . Дело в том, что транслятор не может по такой записи понять, что надо передавать в память ‑ байт или слово? Он может сформировать неверный код операции. О каком формате числа идет речь в такой команде, транслятору должен сообщить программист, написав:

mov byte ptr [ si ], 7 ;(указатель на байт) речь идет о байте.

mov word ptr [ si ], 7 ; речь идет о слове.

Команды INC -приемник и DEC -приемник .

Команда inc (инкремент) прибавляет единицу к содержимому приемника. Команда dec (декремент) вычитает единицу из содержимого приемника. Например:

inc cl ; содержимое регистра cl увеличивается на;единицу;

dec di ; содержимое регистра di уменьшается на;единицу;

inc word ptr

dec perem .

Команды ADD -приемник, источник и SUB -приемник, источник .

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

add ah , 32 ; прибавить 32 к содержимому регистра ah

sub dl , ch ; вычесть содержимое ch из содержимого dl ;(результат в dl )

sub perem, bx

add ax,

add byte ptr , 3.

Команда CMP -приемник, источник .

Команда cmp (сравнение) вычитает содержимое источника из содержимого приемника, но, в отличие от команды sub , результат вычитания никуда не заносится. Результатом работы команды cmp является установка соответствующих флагов в регистре флагов. Команда cmp всегда используется в паре с одной из команд «условного перехода» (je -метка – « перейти, если равно», jne -метка – «перейти, если не равно» и др.). Например:

cmp al, 0

cmp ax, bx

jne not_equal

cmp byte ptr , 0ffh

je exit .

Команда безусловного перехода JMP -метка .

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

_ m 1: cmp ah , 3 ; в ah тройка?

jne _ m 2 ; если нет, прыгаем на _ m 2.

jmp _ m 1 ; прыгаем на _ m 1_ m 2:

add bx , 32

Команды условных переходов:

je -метка ;переход, если равно;

jz -метка ;переход, если результат равен нулю (флаг;zf установлен в единицу). Собственно это;другая запись команды je

jne -метка; переход, если не равно (эквивалентная;команда jnz );

ja -метка; переход, если больше;

jae -метка; переход, если больше или равно;

jb -метка; переход, если меньше;

jbe -метка; переход если меньше или равно.

Например:

sub ax , 40 ; вычитаем из ax 40

jnz m 17 ; если результат не равен нулю,
; прыгаем на m 17

cmp al , bh

jae povtor ; если содержимое al больше или равно;содержимому bh , прыгаем на povtor .

Достаточно часто приходится сталкиваться со случаем, когда на синтаксически правильной команде условного перехода транслятор выдает ошибку: Relative jump out of range . Связано это с тем, что команда условного перехода может обеспечить прыжок только на плюс/минус 128 байт, т.е. приблизительно на 30 – 40 команд (вперед или назад по программе). А если надо прыгнуть на большее расстояние? Применяют команды безусловного перехода (jmp ), обеспечивающие прыжок на плюс/минус 64 Кбайта. Например, рассмотрим фрагмент программы:

cmp ax , 0 ; в ax ноль?

je m 100 ; если да, прыгаем на m 100, если нет,
;идем на следующую команду

mov bx , 40

На команде je m 100 транслятор выдает вышеуказанную ошибку. Перепишем этот фрагмент:

cmp ax , 0 ; в ax ноль?

jne m 200 ; если нет, прыгаем на m 200 , если да,
;идем на следующую команду

jmp m 100 ; прыгаем на m 100

m 200: mov bx , 40

Логика программы не изменилась, а вот ошибки больше не будет.

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

Команда LOOP -метка .

Команда loop (цикл) вычитает единицу из содержимого регистра cx и, если в результате получился «не ноль», переходит на указанную метку. В качестве примера рассмотрим следующий фрагмент:

mov dh , 0

mov cx , 11 ; число повторений цикла

mov al, dh .

Данный фрагмент выполняется следующим образом: сначала в dh загружается 0. Затем в цикле к dh 11 раз прибавляется единица. В результате этого фрагмента мы будем иметь: cx = 0, dh = 11, al = 11 . Конечно, тех же результатов можно было бы достичь проще:

mov cx , 0

mov dh , 11

mov al , dh ,

но здесь нет цикла.

Распространенной ошибкой, приводящей к самым плачевным последствиям, является написание бесконечного цикла. Например, следующий фрагмент приведет к зависанию программы:

mov dh , 0

m 1:

mov cx, 11

loop m1 .

В cx занесется 11, команда loop вычтет из cx единицу, получится «не ноль» и произойдет переход на метку m 1 , В cx снова занесется 11 и так до бесконечности. Метка m 1 поставлена не там, где нужно (правильный вариант смотри выше).

Еще одной менее очевидной, но не менее неприятной по последствиям, ошибкой является занесение внутри цикла (по забывчивости программиста) в регистр cx новой информации, которая портит текущее значение счетчика цикла. Если же изменение cx внутри цикла нам «жизненно необходимо», то надо предварительно запомнить текущее содержимое cx (например, в стеке командой push cx ), а затем восстановить это содержимое (pop cx ) перед выполнением команды loop .

Команды IN al , адрес порта и OUT адрес порта, al .

Команда in передает байт из заданного в команде порта в регистр al . Команда out передает байт из регистра al в заданный в команде порт. В качестве адреса порта может выступать любое число, лежащее в диапазоне 0 – 255 (0 –ffh). Порт – это регистр, которому в системе присвоен адрес. Например, контроллер клавиатуры имеет 2 порта с адресами 20h и 21h, таймер – 4 порта с адресами 40h, 41h, 42h и 43h и.т.д. Приведем примеры команд:

in al , 60 h ; читаем скэн-код нажатой клавиши из;порта клавиатуры

out 40 h , al ; заносим байт коэффициента пересчета
;
в 0-й канал таймера.

Обратите внимание, что обмен информацией с портами ведется только через регистр al (это не совсем правильно, поскольку имеются и другие варианты команд in и out , но для начинающего программиста проще использовать только рассмотренные выше команды).

Команда AND - приемник, источник .

Команда and (логическое И) производит поразрядное логическое умножение содержимого приемника на содержимое источника. Результат заносится в приемник. Например:

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

in al , 61 h ; читаем 61 порт

and al , 11111100 b ; обнуляем два младших бита

out 61 h , al ; записываем обратно в 61 порт.

Эти три команды запрещают звучание встроенного динамика (спикера). Сначала считываем содержимое порта 61 h в регистр al . Затем обнуляем два младших бита al (запрещаем звук). При этом все остальные биты оставляем в их исходном состоянии, чтобы ненароком не нарушить работу системы. После этого отправляем измененную информацию обратно в порт 61 h .

Команда and также часто используется, когда надо проверить значение конкретного бита в байте или слове. Например, надо проверить, установлен ли 1-й бит регистра al в единицу. Эту проверку можно организовать так:

and al , 00000010 b

jnz m 99 .

Если в 1-м бите стоял 0, в результате выполнения первой команды получится ноль. Вторая команда совершает прыжок на m 99, если результатом первой команды был «не ноль», то есть если 1-й бит был установлен в единицу. Недостаток такой проверки – после нее содержимое al будет испорчено.

Команда OR - приемник, источник .

Команда or (логическое ИЛИ) производит поразрядное логическое сложение содержимого источника и содержимого приемника. Результат заносится в приемник. Например:

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

in al, 61h

or al, 00000011b

out 61 h , al .

Команда XOR - приемник, источник .

Команда xor (исключающее ИЛИ) производит поразрядное сложение по модулю 2 содержимого приемника и содержимого источника. Результат заносится в приемник. Например: