В мир информатики # 97 (1–15 ноября).
Эксперименты

Как процессор обменивается данными с памятью

Е.А. Еремин,
г. Пермь

Продолжение. Начало см. “В мир информатики” № 96 / “Информатика” № 20/2007

Рассмотрим теперь более сложную ситуацию, когда однотипные данные (в нашем случае 16-битовые целые числа) последовательно хранятся в памяти в виде массива. В этом случае имеет место заполнение памяти, изображенное на рис. 1.

Как видно из рис. 1, адрес начального байта любого числа можно вычислить по формуле:

 

Ai = A0 + Di = A0 + 2i,

— где A0 — адрес нулевого элемента, i — индекс элемента, значение которого ищется (в нашем эксперименте выберем A0 = 200, а  i = 0, 1, …, 8).

Обязательно обратите внимание на тот факт, что нумерацию в массиве удобно начинать с нуля: именно в этом случае расчетная формула выглядит наиболее просто. Простота объясняется тем, что фактически для адресации необходимого элемента приходится вычислять объем памяти, занятый всеми предшествующими ему элементами. Таким образом, например, пятому элементу удобнее всего ставить в соответствие номер 4, тогда первому, соответственно, 0 (т.е. ему “ничего не предшествует”).

Рис. 1. Организация памяти в виде одномрного массива

В языках программирования высокого уровня для удобства человека индексы часто все-таки начинают с единицы. В наиболее развитых версиях языка BASIC даже предусмотрен специальный оператор OPTION BASE, который позволяет устанавливать начальное значение индексов массива 0 или 1 (по умолчанию — всегда 0).
В случае, когда индексы начинаются не с 0, а с 1, при вычислении смещения Di следует вместо переменной i написать (i – 1).

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

Итак, пусть в памяти, начиная с адреса 200h, располагается массив из 9 двухбайтовых целых чисел, который изображен на рис. 1. Значения чисел, разумеется, можно было задать произвольно, но для удобства проверки правильности работы программ мы занесем в элементы массива их индексы (от 0 до 8).

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

Для доступа к элементам массива в процессорах фирмы Intel предусмотрен большой ассортимент методов адресации (см. способы 5–11 в таблице приложения 1 в первой части статьи). Доступ к массивам основывается на двух методах — базовом и индексном, остальные, как видно из названий, являются их модификациями. О весьма тонком отличии между базовым и индексным методами мы поговорим позднее, а пока познакомимся с индексным способом адресации.

Для хранения индексов в рассматриваемом семействе процессоров предусмотрены специальные индексные регистры SI и DI3. Хранящееся в любом из них значение индекса может автоматически складываться с начальным адресом массива, который должен быть указан явным образом в виде константы. Запись 200[SI], например, означает, что при обращении к памяти будет выбран адрес, равный сумме начального адреса массива 200 и индексного смещения, находящегося в данный момент в регистре SI. Подчеркнем, что величина смещения не есть значение индекса: в нашем примере она вдвое превышает последнее, поскольку каждое число занимает два байта. Вместо умножения на 2 хорошие программисты, как правило, используют сдвиг на один бит влево; в частности, в нашей программе второй командой стоит SHL SI,1, что как раз и обеспечивает требуемый сдвиг влево (SHL — от англ. SHift Left).

Примечание. Сдвиг влево формально эквивалентен приписыванию после числа дополнительного нуля. В повседневной десятичной системе это приводит к увеличению значения в 10 раз (из 12 получается 120), а в двоичной, соответственно, вдвое.

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

Протокол 2

При разборе протокола 2 непременно обратите внимание на следующие детали (соответствующие места в тексте, как обычно, подчеркнуты):

— индексный операнд 200[SI] отладчик Debug расшифровывает “по-своему”: [SI+200];

— сдвиг влево преобразует значение SI из 4 в 8, т.е. действительно удваивает;

— в случае, когда выводимая на экран команда работает с памятью, Debug вычисляет адрес данных с учетом текущего значения регистров (в нашем случае SI) и выводит соответствующее содержимое справа от ассемблерного представления инструкции.

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

Внимательное изучение таблицы приложения 1 показывает, что в ходе некоторых методов адресации процессор самостоятельно способен умножать индекс на размер данных. В частности, вас должен был заинтересовать масштабированный индексный метод адресации (строка 7 в таблице), хорошо подходящий к нашей задаче. Главная проблема применения выбранного метода заключается в том, что он работает только в 32-битном режиме и, следовательно, с расширенными регистрами, а их-то Debug как раз и не поддерживает. Трудность, к счастью, состоит не в том, что при работе с Debug нельзя использовать 32-разрядные инструкции, а в том, что их не удается ввести напрямую. В связи с этим предлагаю воспользоваться “обходным” путем: предварительно узнав коды необходимых инструкций, введем их в память как набор байтов.

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

Для получения кодов инструкций я воспользовался программой Flat Assembler (автор Tomasz Grysztar) [1].

Обратим внимание читателей на то, что 66 и 67 — это префиксы, переводящие данную команду из 16-разрядного в 32-разрядный режим выполнения, причем первый влияет на размер данных (число 4 заносится в 32-разрядный регистр ESI), а второй — на длину адреса, что, собственно, и позволяет применять изучаемый нами сейчас масштабированный индексный метод адресации.

Дальнейшие действия уже понятны из протокола 3. Подчеркнем, что он предполагается продолжением предыдущего эксперимента по поискам пятого элемента массива, поэтому последний заново вводить не требуется. Зато придется очистить регистр AX и “сбросить” на начало программы счетчик команд IP (см. в протоколе команды rax и rip).

Протокол 3 (продолжение протокола 2)

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

Остается разобрать уже упоминавшийся ранее базовый метод адресации. Его суть состоит в том, что адрес данных получается в результате суммирования содержимого базового регистра (BX или BP) с некоторой константой, например, [BX+30]. Это на первый взгляд мало чем отличается от изученного нами подробно индексирования, когда адрес также являлся суммой содержимого регистра и константы. И тем не менее некоторое тонкое отличие все-таки есть.

Согласно общей теории адресации [2–3], дело обстоит следующим образом. Как мы уже знаем, адрес элемента массива является суммой двух слагаемых — A0 и Di. Так вот, когда A0 помещается в виде константы в команду, а Di — в регистр, то адресация называется индексной (в регистре находится индекс, что, кстати, подчеркивается записью 200[SI]), а при обратном размещении слагаемых (в регистре тогда будет база, т.е. A0) — базовой. У процессоров с универсальными регистрами, допускающими любые методы адресации, отличие чисто смысловое; в тексте программы на ассемблере оно еще заметно по способу записи, но на уровне машинных кодов разница между индексной и базовой адресацией весьма условна4 (в обоих случаях адрес есть сумма содержимого указанного регистра и константы!).

Что касается процессора Intel, то у него регистры неравноценны. В частности, регистры BP и BX называются базовыми, а SI и DI — индексными, отсюда адресация по BP и BX “обречена” быть базовой, а по SI и DI — индексной. Мы видим, что терминология строится на принципиально другой основе, нежели в теории методов адресации, что дополнительно запутывает и без того нетривиальную классификацию.

Эксперименты с Debug подтверждают изложенную выше точку зрения следующим весьма наглядным образом. Согласно принятым в языке ассемблер правилам, индексная адресация записывается в виде 200[SI], а базовая — [BP+200]. Тем не менее отладчик, кроме стандартного 200[SI], нормально воспринимает набор [SI+200], более того, как мы видели в протоколе 2, при выводе на экран он использует именно последнюю (базовую!) форму записи. Наоборот — вместо базовой формы [BP+200] можно смело вводить индексную 200[BP] с тем же самым результатом. С точки зрения правил ассемблера путаница полная!

Задания для самостоятельной работы

1. В таблице приложения 1 (в первой части статьи) найдите в последнем столбце в строках 5, 6, 8 и 10 разнообразные допустимые варианты записи индексного и базового методов адресации. (Заметим, что в таблице приведены не все, а лишь сколько-нибудь логичные варианты; на практике вы можете набирать и совсем экзотические выражения типа [SI]30[BX], и Debug послушно преобразует их в осмысленную сумму.) Введите несколько форм записи одной и той же команды и убедитесь в тождественности кодов, которые Debug занесет в память.

2. Опробуйте базовую адресацию по аналогии с тем, как это делалось в протоколе 2.

3. В программе из протокола 3 команду mov esi,4 замените на mov esi,4000000 с кодом 66 BE 00 00 00 04. Убедитесь, что данная команда, выводящая адрес данных за пределы сегмента в 64 Кб, вызывает срабатывание защиты памяти и принудительное (без всякого предупреждения!) завершение сеанса Debug.

Литература

1. http://fastassembler.net.

2. Брусенцов Н.П. Микрокомпьютеры. М.: Наука, 1985.

3. Информатика в понятиях и терминах. / Г.А. Бордовский, В.А. Извозчиков, Ю.В. Исаев, В.В. Морозов. Под ред. В.А. Извозчикова. М.: Просвещение, 1991.

Продолжение — в следующем выпуске.


4 В частном случае A0 = Di оба метода практически совпадают.