Asm 2006
.pdf
6
ставления целых чисел из-за принципа программной совместимости старших моделей нашего семейства ЭВМ с младшими, несмотря на то, что эти младшие модели уже давно не выпускаются.1
6.5. Сегментация памяти
Память нашей ЭВМ имеет уже знакомую нам сегментную организацию. В любой момент времени для младшей модели определены четыре сегмента (хотя для старших моделей число сегментов больше). Это означает, что есть четыре сегментных регистра, которые указывают на определённые области памяти. Каждый сегментный регистр имеет длину 16 разрядов, а в то же время для адресации любого места нашей памяти необходимо, как мы уже говорили, 20 разрядов. Для того чтобы сегмент мог располагаться на любом месте оперативной памяти, адрес начала сегмента получается после умножения значения сегментного регистра на число 16. Правда, как легко понять, при таком способе задания начала сегмента, он может начинаться не с любого места оперативной памяти, а только с адресов, кратных 16 (в некоторых книгах по Ассемблеру такие участки памяти называются парагра-
фами).
Итак, производить обмен с памятью можно только относительно одного из них этих сегментных регистров. Таким образом, физический адрес числа или команды вычисляется центральным процессором по формуле
A |
:= (SEG*16 + A)mod 220, |
|
|
|
|
|
физ |
|
|
|
|
|
|
где SEG – значение сегментного регистра, а A –заданное в команде смещение. Физический адрес бе- |
||||||
рётся по модулю 220, чтобы он не вышел за максимальный адрес памяти. |
|
|
|
|
|
|
В качестве мнемонических обозначений сегментных регистров выбраны |
|
|
|
|
|
|
следующие двухбуквенные служебные имена:2 кодовый сегментный регистр |
DS |
|
|
|||
(CS), сегментный регистр данных (DS), сегментный регистр стека (SS) и до- |
|
|
|
|
|
|
|
|
|
|
|
||
полнительный сегментный регистр (ES). Смысл этих названий скоро прояс- |
|
|
|
|
|
|
ниться. Каждый из этих регистров может адресовать сегмент памяти длиной |
CS |
|
|
|||
от 1 до 216 байт (напомним, что вся память состоит из 220 ячеек), при этом |
|
|
||||
|
|
|
|
|
||
|
|
|
|
|
||
"настоящая" длина сегмента известна только программисту. Так как физиче- |
|
|
|
|
|
|
ский адрес в приведённой выше формуле берётся по модулю 220, то, очевид- |
SS |
|
|
|||
но, что память, как и в нашей учебной ЭВМ, "замкнута в кольцо". Другими |
|
|
|
|
|
|
|
|
|
|
|
||
словами, в одном сегменте могут находиться ячейки с самыми большими и |
|
|
|
|
|
|
|
|
|
|
|
||
самыми маленькими адресами основной памяти. На рис. 6.1 показан пример |
ES |
|
|
|||
расположения сегментов в памяти. |
|
|
|
|
|
|
Стоит отметить, что сегментные регистры являются специализированны- |
|
|
|
|
|
|
ми, предназначенными только для хранения адресов сегментов, поэтому |
|
|
|
|
|
|
арифметические операции (сложение, вычитание и др.) над их содержимым не |
|
|
|
|
|
|
|
|
|
|
|
||
предусмотрены. |
|
Рис. 6.1. Пример |
||||
Заметим, что даже если все сегменты не перекрываются и имеют макси- |
|
расположения сег- |
||||
мальный размер, то и в этом случае центральный процессор в каждый момент |
|
ментов в памяти. |
||||
времени имеет доступ только к одной четвёртой от общего объёма оператив- |
|
|
|
|
|
|
ной памяти. |
|
|
|
|
|
|
6.6.Мнемонические обозначения регистров
Всилу того, что в ЭВМ все регистры имеют безликие двоичные обозначения, программисты предпочитают использовать в своих программах мнемонические названия регистров. Регистры общего назначения, каждый из которых может складывать, вычитать и просто хранить данные, а некото-
рые – ещё умножать и делить, обозначают следующими служебными именами: AX, BX, CX, DX. Для обеспечения многообразия форматов данных каждый из них разбит на две части по 8 бит (биты нумеруются немного непривычно справа налево, начиная с нуля):
1 Для любознательных студентов отметим, что, например, в семейства ЭВМ IBM-360/370, которые начали выпускать свою первую модель позже, чем в нашем семействе, уже принят "естественный" для программиста формат целого числа, где крайний левый бит задаёт знак числа, а затем записывается прямой код модуля.
2 Эти имена (как и имена всех остальных регистров нашей ЭВМ) являются служебными в языке Ассемблера. Напомним, что служебные имена нельзя использовать ни в каком другом смысле, кроме того, который определён в языке.
7
|
15 |
7 |
0 |
|
AX |
AH |
|
|
AL |
BX |
BH |
|
|
BL |
CX |
CH |
|
|
CL |
DX |
DH |
|
|
DL |
|
|
16 бит |
|
|
Каждый из регистров AH, AL, BH, BL, CH, CL, DH и DL может быть использован в машинных командах как самостоятельный регистр, на них можно выполнять операции сложения и вычитания. В дальнейшем условное обозначение r8 мы будем использовать для обозначения любого короткого (8- разрядного) адресуемого регистра.
Существуют также четыре регистра с именами SI, DI, SP и BP, которые также могут использоваться для проведения сложения и вычитания, но они уже не делятся на половинки:
15 |
0 |
SI
DI
SP
BP
16 бит
В основном эти четыре регистра используются как индексные, т.е. на них обычно храниться положение конкретного элемента в некотором массиве. Условное обозначение r16 будем использовать для указания любого из регистров AX, BX, CX, DX, SI, DI, SP и BP.
Кроме перечисленных выше регистров программист имеет дело с регистром IP (instruction pointer), который называется счётчиком адреса (в учебной машине мы обозначали его как RA). Этот регистр содержит адрес следующей исполняемой команды (точнее, содержит смещение этой команды относительно начала кодового сегмента, адрес начала этого сегмента равен значению сегментного регистра CS, умноженному на 16).
16 бит
IP
И, наконец, как уже упоминалось, архитектурой изучаемой ЭВМ предусмотрен регистр флагов с именем FLAGS. Он содержит шестнадцать одноразрядных флагов, например, ранее упоминавшиеся флаги CF и OF. Конкретные номера битов, содержащих тот или иной флаг, для понимания архитектуры несущественны, и приводиться здесь не будут. Заметим также, что эти номера нам не надо будет знать и при программировании на языке Ассемблера.
16 бит
FLAGS … CF … OF …
Как уже показывалось на рисунках, все биты в регистрах пронумерованы справа налево: в шестнадцатибитных – от 0 до 15, в восьмибитных – от 0 до 7. Как и в Паскале, в языке Ассемблера принято такое же соглашение по семантике всех имён: регистр символов не различается. Таким образом, имена можно писать как большими, так и маленькими буквами, например, имена AX,Ax,aX и ax обозначают в нашем языке Ассемблера один и тот же регистр.
Рассмотрим теперь особенности хранения чисел в регистровой и основной памяти ЭВМ. Запишем, например, шестнадцатеричное число 1234h в какой-нибудь 16-тиразрядный регистр (каждая шестнадцатеричная цифра занимает по 4 бита):
12 3 4
Теперь перешлём это число в память в ячейки с адресами, например, 100 и 101. Так вот: в ячейку с адресом 100 при такой пересылке запишется число 34h, а в ячейку с адресом 101 запишется число 12h. Говорят, что целое число представлено в основной памяти (в отличие от регистров) в перевёрнутом виде. Это связано с тем, что в младших моделях ЭВМ при каждом обращении к памяти в центральный процессор читался всего один байт. Таким образом, для того, чтобы считать двухбайтное целое число, было необходимо дважды обратиться к памяти, поэтому было удобно (напри-
8
мер, для проведения операция сложения "в столбик") получать из памяти сначала младшие цифры числа, а затем – старшие. В современной архитектуре за одно обращение из памяти получают сразу 8 или 16 байт, но из-за совместимости моделей семейства пришлось оставить перевёрнутое представление чисел, что, конечно, неудобно для программистов. Заметим, что в отличие от чисел, команды хранятся в памяти в обычном (не перевернутом) виде.
6.7. Структура команд
Теперь рассмотрим структуру машинных команд самых распространённых форматов регистррегистр и регистр-память.
•Формат регистр–регистр.
6 бит |
1 бит |
1 бит |
1 бит |
1 бит |
3 бита |
3 бита |
КОП |
d |
w |
1 |
1 |
R1 |
R2 |
Команды этого формата занимают 2 байта. Первая часть команды – код операции – занимает в команде 6 бит, что позволяет задавать до 64 различных операций. Далее следуют однобитные поля с именами d и w, где d – так называемый бит направления, а w – бит размера аргумента, последующие два бита для этого формата всегда равны 11, а последние две части (по 3 бита каждая) задают номера регистров-операндов команды.
Стоит подробнее рассмотреть назначение битов d и w. Бит d задаёт направление выполнения команды, а именно:
<R1> |
:= |
<R1> |
|
<R2> при d |
= |
0 |
<R2> |
:= |
<R2> |
|
<R1> при d |
= |
1. |
Бит w задаёт размер регистров-операндов, имена которых можно определить по следующей схе-
ме:
R1,2 |
w = 1 |
w = 0 |
000 |
AX |
AL |
001 |
CX |
CL |
010 |
DX |
DL |
011 |
BX |
BL |
100 |
SP |
AH |
101 |
BP |
CH |
110 |
SI |
DH |
111 |
DI |
BH |
В младших моделях ЭВМ нашего семейства в таком формате возможна работа лишь с упомянутыми в таблице регистрами. В последующих же моделях возможности этого формата были расширены, но за счёт увеличения длины команды. В своих программах мы будем пользоваться и другими видами формата регистр-регистр, например, командой mov ds,ax , но выписывать их внутреннее представление не будем.
Как видно из приведённой выше таблицы, архитектурой нашего компьютера не предусмотрены операции формата КОП r8,r16 , т.е. операции над регистрами разной длины запрещены, например, команды типа add AL,BX являются неправильными. Исходя из этого, для проведения операций над чис-
лами разной длины появляется необходимость преобразования типов из короткого целого в длинное, и из длинного в сверхдлинное (и наоборот). Такое преобразование, как можно (и нужно!) понять, зависит от знаковой или беззнаковой трактовки числа.
Беззнаковое число всегда расширяется из короткого формата в более длинный слева нулями, а для знакового числа слева размножается его знаковый бит. Таким образом, Вам необходимо понять, что для знаковых чисел незначащими двоичными цифрами будут 0 для неотрицательных и 1 для отрицательных значений. Для преобразования знаковых целых чисел из более короткого формата в более длинный в языке машины предусмотрены безадресные команды, имеющие в Ассемблере такую мнемонику:
cbw |
(convert byte to word) |
и |
(convert word to double), |
cwd |
9
которые производят знаковое расширение соответственно значения регистра AL до AX и AX до значения пары регистров <DX,AX> (так называемой регистровой пары), которые в этом случае рассматриваются как один длинный 32-х битный регистр.
Преобразование целого значения из более длинного формата в более короткий (усечение) производится путём отбрасывания соответствующего числа левых битов целого числа. Усечённое число получится правильным, т.е. будет иметь то же значение, что и исходной число, если слева будут отброшены только незначащие биты. Вам нужно обязательно понять, что для беззнаковых чисел незначащими будут всегда нулевые биты, а для знаковых – это биты, совпадающие по значению со знаковым битом усечённого числа.
•Формат регистр–память (и память-регистр).
КОП |
R1 |
A2 |
Второй операнд A2 может в этом формате иметь один из приведённых ниже трёх видов:
1. A2 = A,
2.A2 = A[M1],
3.A2 = A[M1][M2].
Здесь A – задаваемое в команде число (смещение) длиной 1 или 2 байта (заметим, что нулевое смещение иногда может не задаваться и совсем не занимать места в команде), M1 и M2 – так называемые регистры-модификаторы. Как мы сейчас увидим, значение адреса второго операнда A2 будет вычисляться по определённым правилам, поэтому этот адрес часто называют исполнительным адресом.
Стоит отметить один факт. До сих пор в учебных ЭВМ адресом мы обычно называли физический номер ячейки в памяти машины. В нашей новой машине адресом принято называть смещение ячейки относительно начала того сегмента, в котором она находится. Для обозначения же полного адреса будем употреблять термин физический адрес.
Рассмотрим подробнее каждый их трёх возможных видов второго операнда A2. При A2 = A физический адрес операнда вычисляется центральным процессором по формуле:
Aфиз := (SEG*16 + A)mod 220,
где SEG обозначает значение одного из сегментных регистров (как для команды выбирается один конкретный из четырёх существующих сегментных регистров мы будем говорить далее).
Запись A2 = A[M1] означает использование регистра-модификатора, которым может быть любым из следующих регистров: BP, BX, SI, DI. Адрес операнда в сегменте при этом будем назы-
вать испольнительным адресом Aисп , он вычисляется так:
Aисп := (A + <M1>)mod 216,
где вместо <M1> подставляется содержимое одного из четырёх указанных выше регистров модификаторов. В этом случае физический адрес операнда A2 будет вычисляться по формуле
Aфиз := (SEG*16 + Aисп)mod 220
Запись A2 = A[M1][M2] обозначает использование в команде двух регистров-модификаторов1 и вычисление исполнительного и физического адресов по формулам:
Aисп := (A + <M1> + <M2>)mod 216) Aфиз := (SEG*16 + Aисп)mod 220
На месте M1 можно указывать любой из регистров BX или BP, а на месте M2 – любой из регистров SI или DI (вообще говоря, можно сделать и наоборот, на месте M1 указывать SI или DI, а на месте M2 указывать BX или BP). Заметьте однако, что использование, например, регистров BX и BP (как и SI и DI) одновременно в качестве модификаторов запрещено (мы вскоре поймём, почему так сделано). Таким образом, смещение операнда в сегменте вычисляется как сумма двух или трёх чисел (взятая по модулю 216). Такой адрес, как мы указали выше, называется исполнительным адресом операнда и по аналогии с физическим адресом обычно обозначается как Aисп. Скажем также, что в
1В нашем языке Ассемблера допускается эквивалентная запись выражения A[M1][M2] в виде A[M1+M2]
идаже, к сожалению, в виде [A+M1+M2], в то время, как, например, запись A[M1,M2] запрещена, что вносит сумятицу в мысли студентов.
10
старших моделях нашего семейства ЭВМ почти все ограничения на использование регистров в качестве модификаторов также были сняты (за счёт увеличения длины команды).
Возвращаясь к способу вычисления исполнительного и физического адресов можно заметить, что память сегмента, как и вся оперативная память, как бы замкнута в кольцо. Другими словами, при последовательном увеличении исполнительного адреса мы с последнего байта сегмента попадаем в начало этого же сегмента (на его нулевой байт).1 Как уже отмечалось, это же касается и всей оперативной памяти, которая тоже может, как и в нашей учебной машине, считаться замкнутой в кольцо.
Регистры-модификаторы имеют и другое широко используемое название – индексные регистры, так как эти регистры часто используются для доступа к элементам массивов (как говорят, для индексации элементов массивов). При этом сам способ задания адресов с использованием индексных регистров называется индексированием. По сути, базирование и индексирование, – очень похожие способы адресации, однако, они преследуют разные цели. Как мы уже говорили, базирование используется для уменьшения объёма программного кода, в то время как индексирование предоставляет программисту удобный инструмент для работы с массивами. Заметим, что при использовании индексирования отпадает необходимость делать самомодифицирующиеся программы для обработки массивов. Теперь, изменяя значение индексного регистра, мы получаем доступ к нужным нам элементам массивов без изменения внешнего вида самой команды.
В качестве примера вычислим физический адрес второго операнда команды сложения формата RX, на языке Ассемблера эту команду можно записать в виде add ax,6[bx][di] . Пусть регистры имеют следующие значения (эти значения, как это часто делается в Ассемблере, записаны в шестнадцатеричном виде, напомним, что в этом случае перед числом записывается цифра ноль, если оно начинается с цифр A–F):
bx = 0FA00h, di = 0880h, ds = 2000h
Тогда
Aисп := (6 + 0FA00h + 0880h)mod 216 = 0286h
Aфиз := (2000h*16 + Aисп)mod 220 = (20000h + 0286h)mod 220 = 20286h
Если, например, в байте с адресом 20286h хранится число 56h, а в байте с адресом 20287h – число 32h, то наша команда реализует операцию сложения ax:=ax+3256h.
Рассмотрим теперь внутреннее машинное представление формата команды регистр-память. Длина этой команды 2, 3 или 4 байта:
|
8 бит |
2 бита |
3 бита |
3 бита |
8 бит |
8 бит |
||
КОП |
|
D |
w |
mod |
R1 |
mem |
a8 |
a8->a16 |
где mod – двух битовое поле, называемой полем модификатора, mem – трёх битовое поле способа адресации, a8 и a16 – это обозначение одноили двухбайтного смещения. Биты d и w уже знакомы нам из предыдущего формата регистр-регистр и имеют тот же смысл. Все возможные комбинации значения полей mod и mem приведены в таблице 6.1.
Таблица 6.1. Значения полей mod и mem в формате регистр-память.
mem \ mod |
00 |
01 |
10 |
11 |
|
|
0 доп. Байт. |
1 доп. байт |
2 доп. байта |
Это |
|
000 |
[BX+SI] |
[BX+SI]+a8 |
[BX+SI]+a16 |
||
|
|||||
001 |
[BX+DI] |
[BX+DI]+a8 |
[BX+DI]+a16 |
уже |
|
010 |
[BP+SI] |
[BP+SI]+a8 |
[BP+SI]+a16 |
||
011 |
[BP+DI] |
[BP+DI]+a8 |
[BP+DI]+a16 |
формат |
|
|
|||||
100 |
[SI] |
[SI]+a8 |
[SI]+a16 |
|
|
101 |
[DI] |
[DI]+a8 |
[DI]+a16 |
RR |
|
110 |
a16 |
[BP]+a8 |
[BP]+a16 |
||
111 |
[BX] |
[BX]+a8 |
[BX]+a16 |
|
Данная таблица показывает, как зависит способ адресации от значения полей mem и mod. Видно, что поле с именем mod фактически определяет, сколько байт в команде отводится под запись собст-
1 Важно понять, что это касается только сегментов максимальной длины 216 байт. В языке Ассемблера можно описывать и использовать сегменты и меньшей длины, которые, конечно, уже нельзя считать замкнутыми в кольцо.
11
венно смещения A (0, 1 или два байта). Как видим, эта таблица полностью объясняет ограничения на выбор регистров-модификаторов, которые мы сформулировали ранее. Мы не будем рассматривать машинный вид остальных форматов команд, будем изучать их только на языке Ассемблера. Напомним только, что рассматриваемые нами форматы команд имеют следующие мнемонические обозначения:
•RR – (регистр – регистр);
•RX – (регистр – память или память – регистр, в зависимости от значения бита d в команде);
•RI – (регистр – непосредственный операнд в команде);
•SI – (память – непосредственный операнд в команде);
•SS – (память – память, т.е. оба операнда в основной памяти).
6.8. Команды языка машины
Далее мы будем изучать синтаксис машинных команд и семантику их выполнения центральным процессором. Для удобства команды будем записывать так, как это принято в языке Ассемблер (можно считать, что мы уже начали понемногу изучать этот язык низкого уровня).
6.8.1. Команды пересылки
Команды пересылки – одни из самых распространённых команд в языке машины. Все они пересылают значение одного или двух байт из одного места памяти в другое. Для более компактного описания синтаксиса машинных команд введём следующие условные обозначения (с некоторыми из них мы уже знакомы):
r8 – любой короткий регистр AH,AL,BH,BL,CH,CL,DH,DL;
r16 – любой из длинных регистров AX,BX,CX,DX,SI,DI,SP,BP;
m8, m16, m32 – операнды, расположенные в основной памяти длиной 1,2 и 4 байта; i8, i16, i32 – непосредственные операнды в самой команде длиной 1, 2 и 4 байта; SR – один из трёх сегментных регистров SS, DS, ES;
CS – кодовый сегментный регистр.
Общий вид команды пересылки в нашей двухадресной ЭВМ такой (как уже говорилось, после точки с запятой будем записывать, как это принято в Ассемблере, комментарий к команде):
mov op1,op2; op1 := op2
Здесь в комментарии мы указали семантику выполнения этой команды: второй операнд пересылается на место первого операнда. Существуют следующие допустимые форматы первого и второго операндов команды пересылки, запишем их в виде таблицы, где во второй колонке перечислены все возможные вторые операнды, допустимые для операнда из первой колонки:
op1 |
|
|
op2 |
r8 |
r8, |
m8, |
i8 |
r16 |
r16, m16, i16, SR, CS |
||
m8 |
r8, |
i8 |
|
m16 |
r16, i16, SR, CS |
||
SR |
r16, m16 |
|
|
Любопытно отметить, что запрещена команда пересылки вида mov CS,op2 , так как по своей
сути это будет уже команда передачи управления, а это уже совсем другой класс команд машины.
Команды пересылок не меняют флаги в регистре FLAGS. Как видим, в языке машины существует несколько десятков команд пересылок различных форматов. Из приведённой выше таблицы следует, что команды пересылок с кодом операции mov бывают форматов RR, RX (и XR), RI и SI. Существует также команда пересылки формата SS (память-память), но она имеет другой код операции и будет изучаться в разделе, посвящённом так называемым строковым (или цепочечным) командам.
Отметим также полезную команду обмена содержимым двух операндов xchg op1,op2; обмен значениями операндов: op1 ↔ op2.
Таблица допустимых операндов для этой команды:
op1 |
op2 |
r8 |
r8,m8 |
m8 |
r8 |
12
r16 r16, m16
M16 r16
Эта команда также не меняет флаги.
6.8.2. Арифметические команды
Изучение команд для выполнения арифметических операций начнём с самых распространённых команд сложения и вычитания целых чисел. Определим вид и допустимые операнды у этих двухадресных команд сложения и вычитания:
КОП op1,op2, где КОП = add, sub, adc, sbb.
Команды с кодами операций add (сложение) и sub (вычитание) выполняются по схеме: op1 := op1 ± op2
Команды с кодами операций adc (сложение с учётом флага переноса) и sbb (вычитание с учётом флага переноса) имеют три операнда, два из которых задаются в команде явно, а третий по умолчанию является значением флага переноса CF:
op1 := op1 ± op2 ± CF
Таблица допустимых операндов для этих команд:
op1 |
|
|
|
op2 |
r8 |
r8, |
m8, |
i8 |
|
m8 |
r8, |
i8 |
|
|
r16 |
r16, |
m16, |
i16 |
|
m16 |
r16, |
i16 |
|
|
В результате выполнения всех этих операций всегда изменяются флаги CF, OF, ZF, SF, которые отмечают соответственно за перенос, переполнение, нулевой результат и знак результата (флагу SF всегда присваивается знаковый бит результата). Эти команды меняют и некоторые другие флаги (см. учебники [5,9]), но это нас здесь интересовать не будет.
Далее рассмотрим команды умножения и деления целых чисел. Формат этих команд накладывает сильные ограничения на месторасположение их операндов, эти команды очень похожи на команды одноадресной ЭВМ. Первый операнд всех команд этого класса явно в команде не указывается и находится в фиксированном регистре, заданном по умолчанию. В младшей модели семейства есть следующие команды умножения и деления, в них, как и в уже знакомой нам одноадресной ЭВМ, явно задаётся только второй операнд (т.е. второй сомножитель или делитель):
mul op2; беззнаковое умножение, imul op2; знаковое умножение,
div op2; беззнаковое целочисленное деление, idiv op2; знаковое целочисленное деление.
Как видим, в самой команде явно задаётся только второй операнд (т.е. второй сомножитель или делитель). Этот операнд op2 может быть форматов r8 и m8 (соответственно говорят о коротком умножении или делении) или форматов r16 и m16 (это длинное умножение и деление). Обратите особое внимание на то, что операнд op2 не может быть форматов i8 и i16 (это типичная ошибка начинающих программистов). Как можно заметить, в отличие от команд сложения и вычитания, умножение и деление знаковых и беззнаковых целых чисел выполняются разными командами (по разным алгоритмам). Это легко понять, если, например, вспомнить знакомое нам ещё со школы правило умножения знаковых чисел "минус на минус даёт плюс".
В случае с короткими целыми операндами r8 и m8 при умножении вычисление производится по формуле:
AX := AL * op2
При делении на короткий операнд форматов r8 и m8 производятся следующие действия (операции div и mod понимаются в смысле языка Паскаль):
AL := AX div op2 AH := AX mod op2
В случае с длинным вторым операндом формата r16 и m16 при умножении вычисление производится по формуле:
<DX,AX> := AX * op2
13
Как видим, произведение располагается сразу в двух регистрах <DX,AX> (как мы уже упомина-
ли, это называется регистровой парой).
При делении на длинный операнд формата r16 и m16 вычисление производится по формулам:
AX := <DX,AX> div op2 DX := <DX,AX> mod op2
В этих командах операнд запись <DX,AX> обозначает 32-разрядное целое число, расположенное сразу в двух регистрах DX и AX, а op2, как уже говорилось, может иметь формат r16 или m16. Заметьте, что команды деления одновременно получают два результата (частное и остаток).
Как видим, команды умножения всегда дают точный результат, так как под хранение произведения выделяется в два раза больше места, чем под каждый из сомножителей. В то же время команды деления могут вызывать аварийную ситуацию, если частное не помещается в отведённое для него место, т.е. в регистры AL и AX соответственно. Такая ситуация называется целочисленным переполнением, при этом происходит аварийное прекращение выполнения программы. Разумеется, похожую аварийную ситуацию вызывает и деление на ноль. В то же время заметим, что остаток от деления всегда помещается в отводимое для него место на регистрах AH или DX соответственно (докажите это!).
После выполнения команд умножения устанавливаются некоторые флаги, из которых для программиста представляют интерес только флаги переполнения и переноса (CF и OF). Эти флаги устанавливаются по следующему правилу. CF=OF=1, если в произведении столько значащих (двоичных) цифр, что они не помещаются в младшей половине произведения. На практике это означает, что при значениях флагов CF=OF=1 произведение коротких целых чисел не помещается в регистр AL и частично "переползает" в регистр AH. Аналогично произведение длинных целых чисел – не помещается в регистре AX и "на самом деле" занимает оба регистра <DX,AX>. И наоборот, если CF=OF=0, то в старшей половине произведения (соответственно в регистрах AH и DX) находятся только незначащие двоичные цифры произведения (это двоичные нули для положительных и двоичные единицы для отрицательных произведений). Другими словами, при CF=OF=0 в качестве результата произведения можно взять только его младшую половину, что может оказаться полезным при программировании.
Команды деления после своего выполнения как-то устанавливают некоторые флаги, но никакой полезной информации из значения этих флагов программист извлечь не может. Можно сказать, что деление "портит" определённые флаги (в частности, портятся "полезные" флаги CF,OF,ZF,SF).
Для написания программ на Ассемблере нам будут нужны также следующие унарные арифметические операции.
neg op1; взятие обратной величины знакового числа, op1:=-op1; inc op1; увеличение (инкремент) аргумента на единицу, op1:=op1+1; dec op1; уменьшение (декремент) аргумента на единицу, op1:=op1-1;
Здесь операнд op1 может быть форматов r8, m8, r16 и m16. Применение этих команд вместо соответствующих по действию команд вычитания и сложения приводит к более компактным программам. Необходимо однако отметить, что компактные команды inc op1 и dec op1 , в отличие от эквивалентных им более длинных команд add op1,1 и sub op1,1 никогда не меняют флаг CF.1
1 Это получилось потому, что схемы центрального процессора, выполняющие команды inc и dec (которые, как говорят, производят инкремент и декремент своего операнда) для экономии аппаратуры используются и при выполнении некоторых других команд, которые не должны менять этот флаг.
Глава 7. Язык Ассемблера
7.1. Понятие о языке Ассемблера
Наличие большого количества форматов данных и команд в архитектурах некоторых современных ЭВМ приводит к существенным трудностям при программировании на машинном языке. Для упрощения процесса написания программ для ЭВМ был разработан язык-посредник, названный Ассемблером,1 который, с одной стороны, должен быть машинно-ориентированным (допускать написание любых машинных программ), а с другой стороны – позволять автоматизировать процесс составления программ в машинном коде. Для перевода с языка Ассемблера на язык машины2 используется специальная программа-переводчик, также называемая Ассемблером (от английского слова "assembler" – "сборщик"). В зависимости от контекста, мы, если это не будет вызывать неоднозначности, в разных случаях под словом "Ассемблер" будет пониматься или сам язык программирования, или программа-переводчик с этого языка на язык машины.
В нашем курсе мы не будем рассматривать все особенности языка Ассемблера, для этого надо обязательно изучить хотя бы один из учебников [5–8]. Заметим также, что для целей изучения архитектуры ЭВМ нам понадобится только некоторое достаточно небольшое подмножество языка Ассемблера, только оно и будет использоваться во всех наших примерах.
Рассмотрим, что, например, должна делать программа Ассемблер при переводе с языка Ассемблера на язык машины:
•заменять мнемонические обозначения кодов операций на соответствующие машинные
коды операций (например, для нашей учебной машины, ВЧЦ → 002);
•автоматически распределять память под хранение переменных, что позволяет программисту не заботиться о конкретном адресе переменной, если ему всё равно, где она будет расположена (внутри заданного сегмента при сегментной организации памяти);
•подставлять в программе вместо имён переменных их значения (обычно значение имени переменной – это адрес этой переменной в некотором сегменте);
•преобразовывать числа, написанные в программе в различных системах счисления во внутреннее машинное представление (в машинную систему счисления).
Вконкретном Ассемблере обычно существует много полезных возможностей для более удобного написания программ, что возлагает на Ассемблер дополнительные функции, однако при этом должны выполняться следующие требования (они вытекают из принципов Фон Неймана):
•возможность помещать в любое определённое программистом место памяти любую команду или любые данные;
•возможность выполнять любые данные как команды и работать с командами, как с данными (например, складывать команды как числа);
•возможность выполнить любую команду из языка машины.3
7.2. Применение языка Ассемблера
Общеизвестно, что программировать на Ассемблере трудно. Как Вы знаете, сейчас существует много различных языков высокого уровня, которые позволяют затрачивать много меньше усилий при написании программ. Так, считается, что на один оператор языка высокого уровня при программировании на Ассемблере приходится тратить примерно 10 предложений машинного языка. Кроме того,
1Как уже говорилось ранее, предшественниками языков Ассемблер были так называемые языки псевкокодов, такой язык псевдокода мы использовали при изучении архитектур учебных машин.
2Как мы узнаем позже, на самом деле производится перевод не на язык машины, а на специальный промежуточный язык, который называется объектным языком.
3Современные ЭВМ могут работать в так называемом привилегированном (или защищённом) режиме
[18].В этом режиме программы обычных пользователей, не имеющие соответствующих привилегий, не могут выполнять некоторое подмножество особых (привилегированных) команд из языка машины. Аналогично на современных ЭВМ существует так называемый режим защиты памяти, при этом одна программа не может иметь доступ (писать и читать) в память другой программы. Привилегированный режим работы и защиту памяти мы будем изучать в нашем курсе в главе, посвящённой мультипрограммному режиму работы ЭВМ.
2
отлаживать и модифицировать программы на Ассемблере значительно труднее, чем на языках высокого уровня.
На рис. 7.1 показана взаимосвязь языков программирования высокого уровня (их ещё называют машинно-независимыми), языков низкого уровня (машинно-ориентированных) и языка машины.
Языки высокого уровня
Трансляция (перевод)
Машинно-ориентированные языки (Ассемблеры)
Трансляция (перевод)
Язык машины
Рис. 7.1. Взаимосвязь языков программрования разных уровней.
Естественно, возникает вопрос, когда у программиста может появиться необходимость при написании своих программ использовать не более удобный язык программирования высокого уровня, а перейти на язык низкого уровня (Ассемблер). В настоящее время можно указать две области, в которых использование языка Ассемблера оправдано, а зачастую и необходимо.
Во-первых, это так называемые машинно-зависимые системные программы, обычно они управляют различными устройствами компьютера (такие программы, как правило, называются драйверами). В этих системных программах используются специальные машинные команды, которые нет необходимости применять в обычных (или, как говорят прикладных) программах. Эти команды невозможно или весьма затруднительно задать в программе на языке высокого уровня. Кроме того, обычно от драйверов требуется, чтобы они были компактными и выполняли свою работу за минимально возможное время.
Вторая область применения Ассемблера связана с оптимизацией времени выполнения больших программ. Очень часто программы-переводчики (трансляторы) с языков высокого уровня дают весьма неэффективную программу на машинном языке. Особенно это касается программ вычислительного характера, в которых большую часть времени выполняется очень небольшой по длине (порядка 2- 5%) участок программы (обычно называемый главным циклом). Для повышения эффективности выполнения таких программ могут использоваться так называемые многоязыковые системы программирования, которые позволяют записывать части программы на разных языках. Обычно основная часть оптимизируемой программы записывается на языке программирования высокого уровня (Фортране, Паскале, С и т.д.), а критические по времени выполнения участки программы – на Ассемблере. Скорость работы всей программы при этом может значительно увеличиться. Заметим, что часто это единственный способ заставить программу дать результат за приемлемое время.
7.3. Структура программы на Ассемблере
При дальнейшем изучения архитектуры компьютера нам придётся писать как фрагменты, так и полные программы на языке Ассемблер. Для написания этих программ мы будем использовать одну из версий языка Ассемблер, так называемый Макроассемблер версии 4.0 (MASM-4.0).1 Достаточно полное описание этого языка приведено в учебнике [5], изучения этого учебника (или аналогичных учебников по языку Ассемблера [6-8]) является обязательным для хорошего понимания материала по нашему курсу и успешной сдачи экзамена. На лекциях мы подробно будем изучать только те осо-
1 Эта одна из первых версий Ассемблера, в настоящее время существуют и более "продвинутые" версии этого языка. Причина, по которой мы не взяли для изучения более современную версию языка Ассемблер (например, MASM-6.0 или Турбо-Ассемблер) заключается в следующем. В новых версиях этого языка существуют развитые возможности по автоматизации процесса составления программ. Можно сказать, что эти возможности повышают уровень языка, их использование скрывает от программиста многие тонкости архитектуры нашего компьютера и позволяет ему не задумываться о некоторых особенностях в использовании машинных команд и сегментной памяти. В нашей же версии MASM-4.0 такие "продвинутые" возможности отсутствуют и студентам приходится всё делать вручную, что благотворно сказывается на усвоении учебного материала.
