Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Учебное пособие 3000239.doc
Скачиваний:
23
Добавлен:
30.04.2022
Размер:
1.12 Mб
Скачать

4.5. Массивы

В этом разделе рассматриваются способы описания и обработки на языке ассемблера таких составных объектов, как массивы и структуры.

Об индексах элементов массива

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

Пусть имеется массив X из 30 элементов-слов:

X DW 30 DDP(?)

Как видно, при описании массива указывается количество элементов в нем и их тип, но не указывается, как нумеруются (индексируются) его элементы. Поэтому такому описанию может соответствовать и массив, в котором элементы нумеруются с 0, т. е. X[0..29], и массив, в котором нумерация начинается с 1, т. е. X[1..30], и массив с любым другим начальным индексом к, т. е. X[k..29+k]. 'Таким образом, автор программы может «накладывать» на массив различные диапазоны изменения индекса. Какой диапазон выбрать? Иногда границы изменения индекса могут жестко определяться условиями задачи (например, если в задаче явно сказано, что элементы нумеруются с 1). Но если нумерация не навязывается извне, тогда лучше выбрать нумерацию с 0. Почему?

Чтобы ответить на этот вопрос, рассмотрим, как зависит адрес элемента массива от индекса этого элемента. Пусть элементы массива X нумеруются с k:

X DB 30 DUP(?) ;X[k..29+k]

Тогда верно следующее соотношение:

адрес(X[i]) = X+2*(i-k)

или в более общем виде, где явный размер (2) элементов массива завуалирован:

адрес(X[i]) = X+(type X)*(i-k)

Эта зависимость становится наиболее простой при к=0:

адрес(X[i]) = X+(type X)*i

Поэтому обычно и считают при программировании на языке ассемблера, что элементы массива нумеруются с 0:

X DW 30 DUP(?) ;Х[0..29]

Для многомерных массивов ситуация аналогична. Пусть, к примеру, имеется двумерный массив (матрица) А, в котором N строк и М столбцов (N и М - константы) и все элементы - двойные слова, причем строки нумеруются с k1, а столбцы - с k2:

A DD N DUP(M DUP(?)) ;A[kl..N+(k1-1), k2..M+(k2-l)]

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

При этом предположении зависимость адреса элемента матрицы от индексов элемента выглядит так:

адрес(A[i,j]) = A+M*(type А)*(i-k1)+(type A)*(j-k2)

И здесь наиболее простой вид эта зависимость приобретает при нумерации с О, при kl=0 и к2=0:

адрес(А[i,j]) = A+M*(type A)*i+(type A)*j

Реализация переменных с индексом

Следующий вопрос, который возникает при работе с массивами, – это как осуществляется доступ к их элементам, как реализуются переменные с индексом. Чтобы ответить на этот вопрос, надо предварительно познакомиться с такой осо­бенностью ЭВМ, как модификация адресов.

Модификация адресов

До сих пор рассматривались команды, в которых для операндов из памяти указывались их точные адреса (имена), например: MOV СХ, A. Однако в общем случае в команде вместе с адресом может быть указан в квадратных скобках некоторый регистр, например: MOV СХ,А[ВХ]. Тогда команда будет работать не с указанным в ней адресом A, а с так называемым исполнительным (другое название – эффективным) адресом Aисп, который вычисляется по следующей формуле:

Aисп = (А + [BX]) mod 216,

где [ВХ] обозначает содержимое регистра ВХ. Другими словами, прежде чем выполнить команду, центральный процессор прибавит к адресу А, указанному в команде, текущее содержимое регистра ВХ, получит некоторый новый адрес и именно из ячейки с этим адресом возьмет второй операнд. (Сама команда при этом не меняется, суммирование происходит внутри центрального процессора.) Если в результате сложения получилась слишком большая сумма, то от нее берутся только последние 16 битов, на что и указывает операция mod в приведенной формуле. (Следует отметить, что если в команде рядом с адресом не указан регистр, то исполнительный адрес считается равным адресу из команды.)

Замена адреса из команды на исполнительный адрес называется модификацией адреса, а регистр, участвующий в модификации, принято называть регистром-модификатором или просто модификатором. Отметим при этом, что в ПК в качестве модификатора можно использовать не любой регистр, а только один из следующих четырех: ВХ, BP, SI или DI.

Возьмем, к примеру, команду ADD A[SI],5. Здесь в роли модификатора выступает регистр SI. Пусть сейчас в нем находится число 100. Тогд Аисп=A+[SI]=A+100. Значит, по данной команде число 5 будет прибавлено числу из ячейки с адресом А+100, а не из ячейки с адресом А. Если же в SI находится величина -2 (0FFFEh), тогда Аисп=А-2 и потому число 5 будет складываться с числом из ячейки с адресом А-2.

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

Индексирование

Рассмотрим следующий пример: пусть имеется массив

X DW 100 DUP(?) ;Х[0..99]

и требуется записать в регистр АХ сумму его элементов.

Для нахождения суммы надо сначала в АХ записать 0, а затем в цикле выполнять операцию AX:=AX+X[i] при i от 0 до 99. Поскольку адрес элемента X[i] равен X+2*i, то команда, соответствующая этой операции, должна быть следующей:

ADD AX,X+2*i

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

Итак, возникла следующая проблема: по алгоритму наша команда должна работать с разными адресами (как говорят, должна работать с переменным адресом), а правила машинного языка допускают только фиксированный адрес. Именно для устранения этого противоречия и была введена в вычислительные машины модификация адресов, с помощью которой эта проблема решается следующим образом. Разобьем переменный адрес X+2*i на два слагаемых – на постоянное слагаемое X, которое не зависит от индекса i, и на переменное слагаемое 2*i, зависящее от индекса. Постоянное слагаемое записываем в саму команду, а переменное слагаемое заносим в какой-нибудь регистр-модификатор (скажем, в SI) и название этого регистра также записываем в команду в качестве модификатора (рис. 27):

Рис. 27. Схема модификации адреса с помощью регистра SI

Что получилось? Поскольку регистр-модификатор один и тот же, то такая команда имеет фиксированный вид, т. е. удовлетворяет правилам машинного языка. С другой стороны, команда работает с исполнительным адресом, а он получается сложением адреса X из команды с содержимым (2*i) регистра SI, которое может меняться. Поэтому, меняя значение SI, программист заставляет неменяющуюся команду работать с разными адресами. Тем самым удовлетворяются требования как машинного языка, так и алгоритма. Единственное, что осталось сделать, – это правильно менять содержимое регистра SI. Но это уже делается просто: вначале в SI надо заслать 0, а затем увеличивать его значение с шагом 2; в результате команда будет работать с адресами X, Х+2, Х+4, Х+198.

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

С учетом всего сказанного, фрагмент программы нахождения суммы элементов массива X выглядит так:

MOV АХ, 0 ;начальное значение суммы

MOV СХ, 100 ;счетчик цикла

MOV SI, 0 ;начальное значение (удвоенного)

;индекса

L: ADD AX, X[SI] ;AX:=AX+X[i]

ADD SI, 2 ;следующий индекс

LOOP L ;цикл 100 раз

Косвенные ссылки

Рассмотрим еще один случай применения модификации адресов.

Пусть надо решить следующую задачу: имеется некоторая ячейка размером в слово, адрес которой неизвестен, но известно, что этот адрес находится в регистре ВХ, и надо записать, скажем, число 300 в эту ячейку (рис. 28):

Рис. 28. Пример использования косвенных ссылок

Если бы мы заранее знали адрес (x) этой ячейки, то наша задача решалась бы командой MOV x,300. Но в момент составления программы этот адрес неизвестен, а потому и его нельзя указать в команде. Что делать? Вспомним, что команды работают с исполнительными адресами, поэтому нам надо взять такой адрес и такой модификатор, чтобы в сумме они давали этот заранее не известный нам адрес. Легко сообразить, что в данном случае надо взять нулевой адрес и регистр ВХ, поскольку тогда Аисп=0+[ВХ]=0+х=х. Поэтому наша задача решается командой

MOV [ВХ], 300

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

Отметим попутно, что при косвенной ссылке обычно приходится уточнять размер ячейки, на которую она указывает. Если, к примеру, в ячейку, адрес которой находится в регистре ВХ, надо записать число 0, тогда использовать для этого команду MOV [ВХ],0 нельзя, т. к. в ней непонятны размеры операндов: 0 может быть как байтом, так и словом, да и адрес из BX также может быть адресом как байта, так и слова (в приведенном выше примере такой неоднозначности не было, т.к. число 300 может быть только словом). Поэтому с помощью оператора PTR надо указать, операнды какого размера мы имеем в виду:

MOV BYTE PTR [ВХ], 0 ;пересылка байта

MOV WORD PTR [ ВХ ] , 0 ; пересылка слова

Модификация по нескольким регистрам

Идею модификации легко обобщить на случай нескольких модификаторов. Для этого надо в командах вместе с адресом указывать несколько таких регистров. В ПК разрешено указывать сразу два модификатора, причем один из них обязательно должен быть регистром ВХ или BP, другой – регистром SI или DI (модифицировать по парам ВХ и BP или SI и DI нельзя). Возможный пример:

MOV АХ, А[ВХ] [SI]

В данном случае исполнительный адрес вычисляется по формуле

Аисп = (А + [ВХ] + [SI]) mod 216

Модификация по двум регистрам обычно используется при работе с двумерными массивами. Пусть, к примеру, имеется матрица A размером 10x20:

A DB 10 DUP(20 DOP(?)) ;А[0..9,0..19]

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

При расположении элементов матрицы в памяти по строкам (первые 20 байтов – начальная строка матрицы, следующие 20 байтов – вторая строка и т. д.) адрес элемента A[i,j] равен A+20*i+j. Для хранения величины 20*i следует отвести регистр BX, а для хранения j – регистр SI. Тогда А[ВХ] – это начальный адрес i-й строки матрицы, a A[BX][SI] - адрес j-го элемента этой строки.

MOV AL, 0 ;количество искомых строк

;внешний цикл (по строкам)

MOV СХ, 10 ;счетчик внешнего цикла

MOV ВХ, 0 ;смещение от А

;до начала строки (20*i)

L: MOV АН,А[ВХ] ;АН – начальный элемент строки

MOV DX,CX ;спасти СХ внешнего цикла

;внутренний цикл (по столбцам)

MOV СХ, 19 ;счетчик внутреннего цикла

MOV SI, 0 ;индекс элемента внутри строки (j)

L1: INC SI ;j:=j+l

CMP A[BX] [SI] , AH ;A[i,j]=AH?

LOOPNE LI ;цикл, пока A[i,j]OAH,

; но не более 19 раз

JNE L2 ;АН не повторился —> L2

INC AL ;учет строки

;конец внутреннего цикла

L2: MOV CX, DX ;восстановить СХ

;для внешнего цикла

ADD ВХ, 20 ;на начало следующей строки

LOOP L ;цикл 10 раз

Запись модифицируемых адресов в языке ассемблера

Уточним правила записи модифицируемых адресов в языке ассемблера.

Пусть А обозначает адресное выражение, а Е – любое выражение (адресное или константное), тогда в языке ассемблера допустимы следующие три основные формы записи адресов в командах, которые задают следующие исполнительные адреса:

А : Аисп=А

Е[М] : Аисп=(Е+[М]) mod 216 (М: BX,BP,SI,DI)

Е[М1][М2] : Аисп=(Е+[М1] + [М2]) mod 216 (Ml: ВХ,ВР; М2: SI,DI)

(Замечание: если Е=0, то 0 можно опустить: 0[М] = [М].)

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

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

Отметим, что модификация адреса не меняет тип адреса: например, если X – имя байтовой переменной, то TYPE X[SI] = BYTE. Если же перед модификатором указано константное выражение (например, 1[ВХ]), то тип такого адреса считается неопределенным.

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

1) Запись в квадратных скобках имени регистра-модификатора (ВХ, BP, SI или DI) эквивалентна выписыванию содержимого этого регистра (тогда как имя регистра без квадратных скобок обозначает сам регистр).

Примеры:

A DW 99

AA DW A

MOV BX, AA ;в BX – адрес A

MOV CX, [BX] ;CX:=A (CX:=99)

MOV CX, BX ;CX:=BX (CX:=адрес A)

Следует отметить, что заключать в квадратные скобки имена регистров, не являющихся идентификаторами (АХ, SP, DS, AL и т. п.), нельзя.

2) Любое выражение можно заключить в квадратные скобки, от этого его Выел не изменится (в то же время снятие скобок может изменить смысл).

Примеры:

MOV CX, [2] ; = MOV CX, 2

MOV CX, [A] ; = MOV CX, A

MOV CX, [A+2[BX]] ; = MOV CX, A+2[BX]

3) Следующие записи эквивалентны:

[x][y] = [х] + [у] = [х+у]

Другими словами, выписывание рядом двух выражений в квадратных скобках чает сумму этих выражений, эимер:

MOV CX, [BX][SI] ; MOV СХ,[BX]+[SI] = MOV CX,[BX+SI]

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

A+1, [А+1], [А]+[1], [А][1], А[1], 1[А], [А]+1, ...

5[SI], [5][SI], [5]+[SI], [SI+5], [SI]+5, ...

A-2[BX], [A-2]+[BX], [A-2+BX], A[BX-2], A[BX]-2, ...

A[BX][DI], A[BX+DI], [A+BX+DI], A[BX]+[DI], ...

0[BX][SI], [BX][SI], [BX]+[SI], [BX+SI], [SI][BX], ...

Итак, в языке ассемблера один и тот же адрес можно записать разными способами. Однако; при этом следует помнить, что используемая запись должна быть эквивалентной одной из трех основных форм записи таких адресов. В частности, в записи не должно быть:

– суммы двух адресов (А+В);

– суммы, одним из слагаемых которой является регистр (ВХ+2);

– запрещенных сочетаний регистров ([SI+DI]);

– регистров, не являющихся модификаторами (А[СХ], 2[BL]);

– имени или числа непосредственно за [] ([SI]5, [ВХ]А).

Кроме того, адрес не должен сводиться к числу ([5]), т. к. тогда это будет константное выражение, а не адресное.

В остальном ограничений нет, и каждый выбирает ту форму записи, что ему больше нравится. В дальнейшем будет использоваться форма, в которой скобок ставится имя переменной, а остальные слагаемые указываются в квадратных скобках (например, A[SI+3]). Такие записи похожи на привычные обозначения переменных с индексами в языках высокого уровня (A[i+3]).

И еще одно замечание. Адресные выражения с модификаторами можно использовать при записи операндов команд и некоторых директив (EQU, PTR и др.), но ни в коем случае нельзя указывать в директивах определения данных. Например, директива

X DW 1[SI]

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

Команда LEA

При использовании регистров-модификаторов часто приходится записывать в них те или иные адреса. Пусть, к примеру, нам надо занести в регистр BX адрес переменной X:

X DW 88

Чтобы сделать это, можно завести еще одну переменную, значением которой является адрес X:

Y DW X

а затем переслать значение этой переменной в регистр ВХ:

MOV BX, Y

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

Загрузка исполнительного адреса (load effective address):

LEA rl6, A

Эта команда вычисляет исполнительный адрес второго операнда и записывает регистр r16: r16:=Аисп. Флаги команда не меняет. В качестве первого операнда может быть указан любой регистр общего назначения, а в качестве второго – любое адресное выражение (с модификаторами или без них).

Одним из примеров, где полезна команда LEA, является вывод строки по операции OUTSTR. Эта операция, напомним, требует, чтобы начальный адрес выводимой строки находился в регистре DX. Так вот, засылку этого адреса в DX и следует делать по команде LEA:

S DB 'a+b=c','$'

LEA DX, S ;DХ:=адрес S

OUTSTR ;будет выведено: a+b=c

смотрим особенности команды LEA. При этом будем предполагать, что амме имеются следующие описания:

Q DW 45

R DW -8

Прежде всего отметим, что команда LEA очень похожа на команду MOV, но между ними имеется и принципиальное различие: если LEA записывает в регистр сам адрес, указанный в команде, то MOV записывает содержимое с этим адресом:

LEA BX, Q ;ВХ:=адрес Q

MOV BX, Q ; ВХ:=содержимое Q (=45)

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

LEA CХ, 88 ;ошибка

LEA CХ, ВХ ;ошибка

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

MOV SI, 2

LEA AX, Q[SI] ;АХ:=Аисп=Q+[SI]=адрес(Q+2)=адрес(R)

В частности, этим можно воспользоваться для пересылки в какой-либо регистр значения регистра-модификатора, увеличенного или уменьшенного на некоторое число:

MOV BX, 50

LEA CX,[BX+2] ;CX:=[BX]+2=50+2=52

LEA DI,[DI-3] ;DI:=DI-3 (но понятнее: SUB DI,3)

Команда XLAT

Рассмотрим еще одну команду, связанную с модификацией адресов. Это так называемая команда перевода, перекодировки:

Перекодировка (translate): XLAT

Действие этой команды заключается в том, что содержимое байта памяти, адрес которого равен сумме текущих значений регистров ВХ и AL, записывается в регистр AL: AL:=байт по адресу [ВХ+AL]. Флаги не меняются.

Команда XLAT используется для перекодировки символов. Предполагается, что имеется таблица (байтовый массив) размеров до 256 байтов, i-й элемент которой трактуется как новый код символа с кодом i. Начальный адрес этой таблицы должен находиться в регистре ВХ, а исходный код (i) перекодируемого символа – в регистре AL. Команда присваивает регистру AL новый код символа, взятый из i-го элемента таблицы.

В качестве примера рассмотрим, как с помощью этой команды можно преобразовать любую шёстнадцатеричную цифру-число (от 0 до 15) в цифру-символ. Для этого следует определить в программе таблицу DIG16, перечислив в ней все символы, изображающие шестнадцатеричные цифры:

DIG16 DB '0123456789ABCDEF' ;DIG16[0..15]

Пусть в регистре AL находится число от 0 до 15 (скажем, 14). Тогда записать в этот регистр соответствующий символ-цифру ('Е') можно так:

LEA BX, DIG16 ;BX – на начало DIG16

XLAT ;AL:=DIG16[AL]