Представление данных
Поскольку общение с компьютером происходит на машинном уровне, необходимо иметь
представление о том, как сохраняется и обрабатывается информация. Для этого используются
электрические элементы, которые могут принимать только два состояния: ‘‘включено’’ и
‘‘выключено’’. При сохранении данных в устройствах хранения, последовательность
электрических или магнитных зарядов также интерпретируется как состояние ‘‘включено’’ или
‘‘выключено’’, что и составляет содержимое записанной информации.
Двоичные числа
Компьютер сохраняет команды и данные в оперативной памяти как последовательность
заряженных или разряженных ячеек. Образно можно представить состояние каждой ячейки как
переключатель с двумя состояниями: ‘‘включено и выключено’’ или ‘‘истина и ложь’’. Такие
ячейки идеально подходят для хранения двоичных чисел, которые используют базовое число 2,
так как отдельные биты могут принимать только два состояния - 0 или 1. Ячейки памяти,
соответствующие единице, имеют повышенный заряд, а соответствующие нулю - почти
разряжены. На рис. 3.1 условно показано соответствие переключателей и двоичных чисел.
Команды и данные
В языках высокого уровня команды и данные имеют существенное логическое различие, однако в
машине они все представлены одинаково, как наборы нулей и единиц. Например, следующая
последовательность двоичных разрядов может включать первые три символа алфавита,
сохраненные в строковой переменной, или может быть машинной командой.
010000010100001001000011
Числовые системы
Каждая числовая система имеет основание системы счисления, или базовое число -
максимальное значение, которое может быть присвоено отдельной цифре. Для компактного
отображения значений больше 9 используются шестнадцатеричные символы от A до F,
соответствующие десятичным значениям от 10 до 15. Когда записывают двоичное, восьмеричное
или шестнадцатеричное число, к нему добавляют определенный символ, представленный
строчной буквой. Например, шестнадцатеричное число 45 должно быть записано как 45h,
восьмеричное 76 - как 76o, а двоичное 11010011 необходимо записать как 11010011b. Таким
образом ассемблер распознает числовые константы в исходной программе.
Но если это число послать в память видеоадаптера, то он воспримет его как символ, поэтому на
экране увидим букву А. Это происходит потому, что в соответствии с кодировкой ASCII для
символа А выбрано значение 01000001. Таким образом, интерпретация данного значения зависит
от определенных условий, которые и придают ему смысл.
Числа со знаком
Двоичные числа могут быть как со знаком, так и без знака. Числа без знака используют все восемь
битов для получения значения (например, 11111111 = 255). Просуммировав значения всех битов
для преобразования в десятичное число, получим максимально возможное значение, которое
может хранить байт без знака (255). Для слова без знака это значение будет составлять 65535.
Байт со знаком использует только семь битов для получения значения, а старший восьмой бит
зарезервирован для знака, при этом 0 соответствует положительному значению, а 1 -
отрицательному. На представленном ниже рис. 3.6 показано отображение положительного и
отрицательного числа 10.
Дополнение до двух
Чтобы не усложнять процессор, отдельный блок для реализации операции вычитания не делают;
эту операцию выполняет блок суммирования. Перед суммированием отрицательные числа
преобразовываются в дополнительные числа. Это такое число, которое в сумме с исходным
числом дает 0. Например, десятичное –6 будет дополнением к 6, так как 6+(-6)=0. Таким образом,
вместо операции вычитания A–B процессор суммирует с положительным числом A
дополнительное к B: A+(-B). Вместо того чтобы вычесть 4 из 6, процессор просто складывает –4 и
6. При работе с двоичными числами для дополнительного числа используется термин дополнение
до двух (встречается также определение двоичное дополнение). Например, для двоичного
значения 0001 двоичным дополнением до двух будет 1111. Такое число получается из исходного
числа после изменения всех единиц на нули, а нулей на единицы (инверсия) и прибавления к
полученному числу единицы, как показано ниже. Инверсия битов в двоичном числе обозначается
NOT(n). Поэтому двоичное дополнение может быть представлено выражением NOT(n)+1.
Еще несколько примеров преобразования чисел приведено в табл. 3.7 (для дополнения до двух
используем аббревиатуру NEG(n))
Максимальные и минимальные значения
Число со знаком из n разрядов может использовать только n-1 бит для получения значения.
Например, знаковый байт использует только семь битов (от 0 до 127). В табл. 3.8 показаны
максимальные и минимальные значения для байт, слов, двойных и учетверенных слов со знаком.
Наименьшие значения (-128, -32768, -2147483648) являются недопустимыми. Нетрудно убедиться,
что двоичное дополнение до -128 (10000000) будет также 10000000.
Хотя процессор выполняет вычисления без учета знака числа, в программе знак операнда
необходимо обязательно указывать. Сложение операндов со значениями +16 и -23 будет
выглядеть в командах ассемблера следующим образом.
MOV AX,+16
ADD AX,-23
В двоичном выражении число 16 будет выглядеть как 00010000, а -23 - как 11101001. Когда
процессор складывает эти числа, он получает 11111001. Это двоичное число соответствует
десятичному -7, как показано в примере.
Таким образом, сложение чисел со знаком получается корректным. Но в данном слу" чае
двоичное число можно интерпретировать и как десятичное число без знака 249. Именно поэтому
программист должен отслеживать эти величины и четко представлять, какой тип имеет
полученное значение.
Хранение символов
Компьютеры могут хранить только двоичные значения, но нам необходимо работать не только с
численными значениями, но и с символами, такими как ‘‘A’’ или ‘‘$’’. Для этого компьютер
использует схему кодирования символов, которая позволяет преобразовывать символы в числа и
наоборот. Наиболее известная система кодирования для компьютеров обозначается
аббревиатурой ASCII (American Standard Code for Information Interchange). В ASCII каждому
символу присваивается уникальный код, включая контрольные символы, используемые при
печати и передаче данных между компьютерами. Стандартный ASCII" код использует только 7
разрядов в диапазоне 0-127. Значения от 0 до 31 заняты служебными кодами, используемыми
при печати, передаче информации и выводе на экран. В обычном режиме они не отображаются
на экране. Остальные значения, допустимые в байте, - дополнительные, их применяют для
расширения символьного ряда. В операционной системе MS DOS значения 128-255 используются
для получения графических символов и греческих букв. В операционной системе Windows
существует множество наборов символов, и в каждом из них дополнительным значениям
соответствуют различные символы. Строка символов представляет в памяти последовательность
байт. Например, числовым кодам строки ‘‘ABC123’’ будет соответствовать последовательность
значений 41h, 42h, 43h, 31n, 32h и 33h.
Хранение чисел
Как наиболее эффективно сохранять числа в памяти? Это зависит от того, как эти числа будут
использоваться. Если числа используются для вычислений, должно быть применено двоичное
представление числа, и наоборот, лучше хранить коды ASCII, если данные значения будут
использоваться для отображения символов на экране. Например, число 123 можно сохранить в
памяти двумя способами: как последовательность кодов ASCII для чисел 1, 2 и 3 или как один байт
со значением 123, как показано на рис. 3.7.
Двоичное содержимое памяти всегда можно просмотреть, но по одному лишь значению нельзя
определить, что оно представляет. Предположим, два байта памяти имеют значения 01000001 и
01000010. Но что это такое? Данные, код или текст? Это невозможно узнать, пока не
идентифицирован определенный участок памяти. Программа должна отслеживать состояние
данных и тип представления, чтобы избежать конфликтов. Языки высокого уровня не позволят
использовать переменные вместо команд, чего нельзя сказать о языке ассемблера. Ограничения
языка помогают избежать серьезных ошибок, но в языке ассемблера почти нет ограничений, и
программист должен учитывать все, даже незначительные детали.
Регистры процессора
Приведенное ниже описание относится только к процессорам 8086 и 80286, для которых были
характерны 16-разрядные регистры. В современных процессорах типа почти все регистры 32-
или 64-разрядные, что существенно увеличивает возможности компьютера. Однако младшие
половины регистров этих процессоров совпадают и по названиям, и по назначению с 16-
разрядными регистрами процессора 8086. Поэтому программы, написанные для выполнения
под управлением MS-DOS, то есть для 16-разрядного процессора, работают и на 32-разрядном,
равно как и 32-разрядные на 64-, хотя и не используют все его возможности.
mov BL,AH
пересылает старший байт регистра AX в младший байт регистра BX, не затрагивая при этом вторых
байтов этих регистров. Еще раз отметим, что сначала указывается операнд-приемник, а после
запятой — операнд-источник, т.е. команда выполняется как бы справа налево. Во многих случаях
регистры общего назначения вполне эквивалентны, однако предпочтительнее в первую очередь
использовать AX, поскольку многие команды занимают в памяти меньше места и выполняются
быстрее, если их операндом является регистр AX (или его половины AH или AL).
Индексные регистры SI и DI так же, как и регистры данных, могут использоваться произвольным
образом. Однако их основное назначение — хранить индексы (смещения) относительно
некоторой базы (т.е. начала массива) при выборке операндов из памяти. Адрес базы при этом
обычно находится в одном из базовых регистров (BX или BP). Примеры такого рода будут
приведены ниже.
Регистр BP служит указателем базы при работе с данными в стековых структурах, о чем будет речь
впереди, но может использоваться и произвольным образом в большинстве арифметических и
логических операций или просто для временного хранения каких-либо данных.
Регистры SI, DI, BP и SP, в отличие от регистров данных, не допускают побайтовую адресацию.
Четыре сегментных регистра CS, DS, ES и SS хранят начальные адреса сегментов программы и, тем
самым, обеспечивают возможность обращения к этим сегментам.
Регистр CS обеспечивает адресацию к сегменту, в котором находится код команд программы,
регистры DS и ES — к сегментам данных (таким образом, в любой точке программа может иметь
доступ к двум сегментам данных, основному и дополнительному), а регистр SS — к сегменту стека.
Сегментные регистры, естественно, не могут выступать в качестве регистров общего назначения.
Флаг переноса CF (Carry Flag) индицирует перенос или заем при выполнении арифметических
операций, а также служит индикатором ошибки при обращении к системным функциям.
Флаг паритета PF (Parity Flag) устанавливается в 1, если младшие 8 бит результата операции
содержат четное число двоичных единиц.
Флаг нуля ZF (Zero Flag) устанавливается в 1, если результат операции равен нулю.
Флаг знака SF (Sign Flag) показывает знак результата операции, устанавливаясь в 1 при
отрицательном результате.
Флаг переполнения OF (Overflow Flag) фиксирует переполнение, т.е. выход результата операции за
пределы допустимого для данного процессора диапазона значений.
dec AX
содержимое AX станет равно 0, и процессор сразу отметит этот факт, установив в регистре флагов
бит ZF (флаг нуля). Если попытаться сложить два больших числа, например, 58000 и 61000, то
установится флаг переноса CF, так как число 119000, получающееся в результате сложения,
должно занять больше двоичных разрядов, чем помещается в регистрах или ячейках памяти, и
возникает «перенос» старшего бита этого числа в бит CF регистра флагов.
Управляющий флаг разрешения прерываний IF (Interrupt Flag) разрешает (если равен 1) или
запрещает (если равен 0) процессору реагировать на прерывания от внешних устройств.
Таким образом, в отличие от битов состояния, управляющие флаги устанавливает или сбрасывает
программист, если он хочет изменить настройку системы (например, запретить на какое-то время
аппаратные прерывания или изменить направление обработки строк).
CLC ; мнемокод
Любую команду можно сопроводить комментарием, отделяя его от команды точкой с запятой ‘‘;’’.
Ассемблер является языком низкого уровня, потому что его команды, по сути, машинные, т.е.
команды языка ассемблера имеют взаимно однозначное соответствие с машинными кодами. И
наоборот, одно утверждение в языке высокого уровня, таком как Delphi или C#, обычно
транслируется в несколько машинных кодов.
Операнд может быть регистром, переменной, ячейкой памяти или непосредственным значением,
как показано в табл. 3.9.
count DB 50
.STACK 4096
Сложение и вычитание
Команды ADD и SUB выполняют сложение и вычитание байтов или слов, содержащих двоичные
данные. Вычитание выполняется в компьютере по методу сложения с двоичным дополнением:
для второго операнда устанавливаются обратные значения бит и прибавляется 1, а затем
происходит сложение с первым операндом. Во всем, кроме первого шага, операции сложения и
вычитания идентичны.
- сложение/вычитание регистр-регистр;
add ax,bx
add ah,al
- сложение/вычитание память-регистр;
sub [000ah],ax
- сложение/вычитание регистр-память;
add ax,279
sub ch,3
- сложение/вычитание память - непосредственное значение.
sub cs:[000ah],3
Для беззнаковых величин все биты являются битами данных, и вместо ограничения +32767
регистр может содержать числа до +65535. Для знаковых величин левый байт является знаковым
битом. Команды ADD и SUB не делают разницы между знаковыми и беззнаковыми величинами,
они просто складывают и вычитают биты. В следующем примере сложения двух двоичных чисел,
первое число содержит единичный левый бит. Для беззнакового числа биты представляют
положительное число 249, для знакового - отрицательное число -7:
Беззнаковое Знаковое
11111001 249 -7
00000010 2 +2
11111011 251 -5
Состояние "перенос" возникает в том случае, когда имеется пеpенос в знаковый разряд.
Состояние "переполнение" возникает в том случае, когда перенос в знаковый разряд не создает
переноса из разрядной сетки или перенос из разрядной сетки происходит без переноса в
знаковый разряд. При возникновении переноса при сложении беззнаковых чисел, результат
получается неправильный:
Беззнаковое Знаковое CF OF
11111100 252 -4
00000101 5 +5
00000001 1 1 1 0
(неправильно)
Беззнаковое Знаковое CF OF
01111001 121 +121
00001011 11 +11
(неправильно)
Беззнаковое Знаковое CF OF
(неправильно)
Умножение
Операция умножения для беззнаковых данных выполняется командой MUL, а для знаковых -
IMUL. Ответственность за контроль над форматом обрабатываемых чисел и за выбор подходящей
команды умножения лежит на самом программисте. Существуют две основные операции
умножения:
"Байт на байт". Один из множителей находится в регистре AL, а другой в байте памяти или в
однобайтовом регистре. После умножения произведение находится в регистре AX. Операция
игнорирует и стиpает любые данные, которые находились в регистре AH.
"Слово на слово". Один из множителей находится в регистре AX, а другой - в слове памяти или в
регистре. После умножения произведение находится в двойном слове, для которого требуется
два регистра: старшая (левая) часть произведения находится в регистре DX, а младшая (правая)
часть в регистре AX. Операция игнорирует и стирает любые данные, которые находились в
регистре DX.
mul multr
Если поле MULTR определено как байт (DB), то операция предполагает умножение содержимого
AL на значение байта из поля MULTR. Если поле MULTR определено как слово (DW), то операция
предполагает умножение содержимого AX на значение слова из поля MULTR. Если множитель
находится в регистре, то длина регистра определяет тип операции, как это показано ниже:
Пример:
n db 10
...
mov al,2
mov al,26
mov ax,8
mov bx,-1
Таким образом, если множимое и множитель имеет одинаковый знаковый бит, то команды MUL
и IMUL генерируют одинаковый результат. Но, если сомножители имеют разные знаковые биты,
то команда MUL вырабатывает положительный результат умножения, а команда IMUL -
отрицательный.
Повышение эффективности умножения: При умножении на степень числа 2 (2,4,8 и т.д.) более
эффективным является сдвиг влево на требуемое число битов. Сдвиг более чем на 1 требует
загрузки величины сдвига в регистр CL. В следующих примерах предположим, что множимое
находится в регистре AL или AX:
Shl ax,cl
Многословное умножение
Обычно умножение имеет два типа: "байт на байт" и "слово на слово". Как уже было показано,
максимальное знаковое значение в слове ограничено величиной +32767. Умножение больших
чисел требует выполнения некоторых дополнительных действий. Рассматриваемый подход
предполагает умножение каждого слова отдельно и сложение полученных результатов.
Рассмотрим следующее умножение в десятичном формате:
1365
х12
2730
1365
16380
Представим, что десятичная арифметика может умножать только двухзначные числа. Тогда
можно умножить 13 и 65 на 12 раздельно, следующим образом:
13 65
х12 х12
26 130
13 65
156 780
15600
+780
16380
X dd ?
Y dw ?
Z dw ? , ? , ?
...
mov ax,wp x ;
mul y
mov z,ax
mov bx,dx
mul y
add ax,bx
mov z+2,ax
adc dx,0
mov z+4,dx
Деление
Операция деления для беззнаковых данных выполняется командой DIV, a для знаковых - IDIV.
Ответственность за подбор подходящей команды лежит на программисте. Существуют две
основные операции деления:
Деление "слова на байт". Делимое находится в регистре AX, а делитель - в байте памяти или в
однобайтовом регистре. После деления остаток получается в регистре AH, а частное -в AL. Так как
однобайтовое частное очень мало (максимально+255(FFh) для беззнакового деления и +127 (7Fh)
для знакового), то данная операция имеет ограниченное использование.
Деление "двойного слова на слово". Делимое находится в регистровой паре DX:AX, а делитель - в
слове памяти или в регистре. После деления остаток получается в регистре DX, а частное в
регистре AX. Частное в одном слове допускает максимальное значение +32767 (FFFFh) для
беззнакового деления и +16383 (7FFFh) для знакового.
div divisor
Если поле DIVISOR определено как байт (DB), то операция предполагает деление слова на байт.
Если поле DIVISOR определено как слово (DW), то операция предполагает деление двойного слова
на слово.
При делении, например, 13 на 3, получается результат 4 1/3. Частное есть 4, а остаток - 1. Заметим,
что ручной калькулятор (или программа на языке BASIC) выдает в этом случае результат 4,333....
Значение содержит целую часть (4) и дробную часть (,333). Значение 1/3 и 333... есть дробные
части, в то время как 1 есть остаток от деления.
Mov ax,100
Mov bh,2
Переполнения и прерывания
Используя команды DIV и особенно IDIV, очень просто вызвать пеpеполнение. Прерывания
приводят (по крайней маре в системе, используемой при тестировании этих программ) к
непредсказуемым результатам. В операциях деления предполагается, что частное значительно
меньше, чем делимое. Деление на ноль всегда вызывает прерывание. Но деление на 1
генерирует частное, которое равно делимому, что может также легко вызвать прерывание.
Рекомендуется использовать следующее правило: если делитель - байт, то его значение должно
быть меньше, чем левый байт (AH) делителя: если делитель - слово, то его значение должно быть
меньше, чем левое слово (DX) делителя.
В обоих случаях частное превышает возможный размер. Для того чтобы избежать подобных
ситуаций, полезно вставлять перед командами DIV и IDIV соответствующую проверку.
Для команды IDIV данная логика должна учитывать тот факт, что либо делимое, либо делитель
могут быть отрицательными, а так как сравниваются абсолютные значения, то необходимо
использовать команду NEG для временного перевода отрицательного значения в положительное.
Преобразование знака
Команда NEG обеспечивает преобразование знака двоичных чисел из положительного в
отрицательное и наоборот.
Примеры:
Neg ax
Neg bl
Преобразование знака для 35-битового (или большего) числа включает больше шагов.
Предположим, что регистровая пара DX:AX содержит 32-битовое двоичное число. Так как команда
NEG не может обрабатывать два регистра одновременно, то ее использование приведет к
неправильному результату. В следующем примере показано использование команды NOT:
Логические команды
Команды логических операций : and, not,or,xor,test
Логические операции являются важным элементом в проектировании микросхем и имеют много
общего в логике программирования. Команды AND, OR, XOR и TEST - являются командами
логических операций. Эти команды используются для сброса и установки бит и для
арифметических операций в коде ASCII . Все эти команды обрабатывают один байт или одно
слово в регистре или в памяти, и устанавливают флаги CF, OF, PF, SF, ZF.
AND: Если оба из сравниваемых битов равны 1, то результат равен 1; во всех остальных случаях
результат - 0.
Mov al,00110011b
mov al,00110011b
or al,11101110b ; al=11111111b
XOR: Если один из сравниваемых битов равен 0, а другой равен 1, то результат равен 1; если
сравниваемые биты одинаковы(оба - 0 или оба - 1) то результат - 0.
mov al,00110011b
xor al,11101110b
; al=11011101b
mov al,00110011b
Первый операнд в логических командах указывает на один байт или слово в регистре или в
памяти и является единственным значением, которое может изменятся после выполнения
команд.
Значение сдвига на 1 может быть закодировано как непосредcтвенный операнд, значение больше
1 должно находиться в регистре CL.
Команды сдвига
При выполнении команд сдвига флаг CF всегда содержит значение последнего выдвинутого бита.
Существуют следующие команды cдвига:
Первая команда SHR сдвигает содержимое регистра AX вправо на 1 бит. Выдвинутый в результате
один бит попадает в флаг CF, а самый левый бит регистра AX заполняется нулем. Вторая команда
cдвигает содержимое регистра AX еще на три бита. При этом флаг CF последовательно принимает
значения 1, 1, 0, а в три левых бита в регистре AX заносятся нули.
Команда SAR имеет важное отличие от команды SHR: для заполнения левого бита используется
знаковый бит. Таким образом, положительные и отрицательные величины сохраняют свой знак.
В приведенном примере знаковый бит содержит единицу. При сдвигах влево правые биты
заполняются нулями. Таким обpазом, результат команд сдвига SHL и SAL индентичен. Сдвиг влево
часто используется для удваивания чисел, а сдвиг вправо - для деления на 2. Эти операции
осуществляются значительно быстрее, чем команды умножения или деления.
Деление пополам нечетных чисел (например, 5 или 7) образует меньшие значения (2 или 3,
соответственно) и устанавливают флаг CF в 1. Кроме того, если необходимо выполнить сдвиг на 2
бита, то использование двух команд сдвига более эффективно, чем использование одной
команды с загрузкой регистра CL значением 2.
Первая команда ROR при выполнении циклического сдвига переносит правый единичный бит
регистра BX в освободившуюся левую позицию. Вторая команда ROR переносит, таким образом,
три правых бита.
В командах RCR и RCL в сдвиге участвует флаг CF. Выдвигаемый из регистра бит заносится в флаг
CF, а значение CF при этом поступает в освободившуюся позицию.
Здесь команда SHL сдвигает все биты регистра AX влево, причем самый левый бит попадает в флаг
CF. Затем команда RCL сдвигает все биты регистра DX влево и в освободившийся правый бит
заносит значение из флага CF.
Задания
1. В тексте описаны регистры для 16-битной архитектуры, найти и привести описание для
регистров 32- и 64-битной.
2. Описать своими словами/на примере как производится (или может быть произведено)
умножение и деление самим процессором.
3. Составить таблицы сложения и умножения для двоичной и шестнадцатеричной систем
счисления.
4. Пошагово описать решение и ответить, что находится в регистре AX для следующего куска
кода:
MOV AL,AAh
MOV BX,2021h
ADD AL,BL
MUL BH
XOR AX, BX
5. Пошагово описать решение и ответить, что находится в регистре AX для следующего куска
кода:
MOV AX,2021h
MOV BL,AAh
SUB AX,BL
NEG BL
IDIV 10h
SHR AL,01h