понедельник, 15 июня 2009 г.

Косвенная адресация со смещением

Теперь скомбинируем два предыдущих метода адресации. Адресуется память (байт или слово). До процессора 80386 относительный адрес операнда определялся, как сумма содержимого регистра BX, BP, SI или DI и указанной в команде константы, иногда называемой смещением. Смещение может быть числом или адресом. При использовании регистров BX, SI и DI подразумевается сегмент, адресуемый через DS, а при использовании ВР подразумевается сегмент стека и, соответственно, регистр SS.
Следующая команда:

mov ax,[bx+2]

помещает в регистр AX слово, находящееся в сегменте, указанном в DS, со смещением на 2 большим, чем число, находящееся в BX.
Такая форма адресации используется в тех случаях, когда в регистре находится адрес начала структуры данных, а доступ надо осуществить к какому-нибудь элементу этой структуры. Другое важное применение косвенной адресации со сдвигом — доступ из подпрограммы к параметрам, переданным в стеке, используя регистр BP (EBP) в качестве базы и номер параметра в качестве смещения.
Другие допустимые формы записи этого способа адресации:

mov ax,[bp]+2
mov ax,2[bp]
mov ax,2+[bp]

Начиная с 80386 и старше, процессоры Intel позволяют дополнительно использовать EAX, EBX, ECX, EDX, EBP, ESP, ESI и EDI, так же как и для обычной косвенной адресации. С помощью этого метода можно организовывать доступ к одномерным массивам байт: смещение соответствует адресу начала массива, а число в регистре — индексу элемента массива, который надо считать. Очевидно, что, если массив состоит не из байт, а из слов, придется умножать базовый регистр на два, а если из двойных слов — на четыре. Для этого предусмотрен специальный метод адресации – косвенная адресация с масштабированием.

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

Небольшое замечание: в четные адреса видеобуфера заносятся символы, в нечетные (после символа) заносится атрибут символа (цвет, фонт и т.п.).

Рассмотрим пример использования косвенной адресации со смещением при обращении к стеку:

И, как водится, листинг:

Обращаем внимание на данные и на то, что команда push offset bt1, транслировалась в 5 машинных команд.

Теперь медитация в отладчике... Скрин ниже сделан после выполнения первых двух команд инициализации регистра DS.

Обращаем пристальное внимание на стек. Хотя мы туда еще ни чего не вносили, там уже полно хлама - это поработал отладчик. Но как видим SP указывает, как ему и положено, на первое слово за "дном" стека.
Так же видим, что одна команда push offset bt1 транслировалась в 5 машинных команд, соответственно и две другие команды, заносящие смещение наших символов в стек, так же транслировались каждая в 5 машинных инструкций. Обращаем внимание на смещение в этик командах, указывающее на наши символы в сегменте данных.
Едем дальше... Следующий скрин сделан после исполнения команды push offset bt1, вернее соответствующих ей 5 машинных команд:

Обращаем внимание на регистр SP, а так же на то, что смещение к символу "О", равное 0000h было занесено в стек.
Теперь выполняем следующую команду push offset bt2...

Опять следим за SP. Видим что смещение 0001h было занесено в стек. Выполняем следующую команду push offset bt3...

Теперь и смещение 0002h помещено в стек. Теперь, на команде call жмем F7, чтобы войти в подпрограмму.

Теперь мы в подпрограмме. Обращаем внимание, что команда call поместила в стек адрес возврата (0026h) из подпрограммы, то есть смещение к следующей команде после себя. И так, сейчас у нас SP указывает на смещение возврата из подпрограммы и в стеке, так же, находятся смещения ко всем символам которые нам надо вывести на экран.
Следующие три команды производят очистку экрана. Команда mov bp,sp поместит содержимое SP. Если бы подпрограмма просто сняла со стека находящиеся там параметры, она первым делом изъяла бы из стека адрес возврата, и лишила бы себя возможности вернуться в основную программу. Поэтому в данном случае вместо команд pop удобнее воспользоваться командами mov для получения нужных значений из стека. Следующие две команды настраивают сегментный регистр ES на работу с видеобуфером. Затем в регистр DI помещается смещение к первому знакоместу экрана. А вот следующую команду mov si,6[bp] рассмотрим более подробно.
Следующий скриншот сделан после выполнения трех команд:

mov si,6[bp]
mov cx,[si]
mov byte ptr es:[di],cl

Смотрим и просветляемся...

Команда mov si,6[bp], поместила в регистр SI смещение (0000h) в сегменте данных к коду символа "O" - 4Fh. Вспоминаем что BP работает в парочке с регистром SS, а SI с регистром DS. На скриншоте приведен простой расчет F8h+06h=FEh. Вспоминаем где мы видели этот адресок 00FEh... Следующая команда mov cx,[si] помещает содержимое ячейки памяти по адресу DS:SI в регистр CX, считывая целое слово. И код нашего символа оказывается в CL. Далее команда mov byte ptr es:[di],cl помещает этот код (4Fh) в видеобуфер, что можно видеть в дампе сегмента адресуемого через регистр ES. Итак на экран выведен символ "O". Далее, подобным образом, выводятся остальные символы. Стоит обратить внимание что значение регистра SP не меняется. Команда ret извлекает из стека по адресу SS:SP адрес возврата 0026h и помещает его в регистр IP. Затем выполняются две команды завершения программы.

воскресенье, 14 июня 2009 г.

Косвенная адресация

По аналогии с регистровыми и непосредственными операндами адрес операнда в памяти также можно не указывать непосредственно, а хранить в любом регистре. До 80386 для этого можно было использовать только BX, SI, DI и BP, но потом эти ограничения были сняты и адрес операнда разрешили считывать также и из EAX, EBX, ECX, EDX, ESI, EDI, EBP и ESP (но не из AX, CX, DX или SP напрямую — надо использовать EAX, ECX, EDX, ESP соответственно или предварительно скопировать смещение в BX, SI, DI или BP). Например, следующая команда помещает в регистр AX слово из ячейки памяти, селектор сегмента которой находится в DS, а смещение — в BX:

mov ax,[bx]

Как и в случае прямой адресации, DS используется по умолчанию, но не во всех случаях: если смещение берут из регистров ESP, EBP или BP, то в качестве сегментного регистра используется SS. В реальном режиме можно свободно пользоваться всеми 32-битными регистрами, надо только следить, чтобы их содержимое не превышало границ 16-битного слова.
И небольшой примерчик для медитации...




суббота, 13 июня 2009 г.

Прямая адресация

Данный вид адресации иногда, так же, называют - прямая адресация памяти.

Адресуется память; адрес ячейки памяти (слова или байта) указывается в команде (обычно в символической форме) и поступает в код команды:

;Сегмент данных
meml dw 0 ;Слово памяти содержит 0
mem2 db 230 ;Байт памяти содержит 230
;Сегмент команд
inc meml ;Содержимое слова meml увеличивается на 1
mov DX,meml ; Содержимое слова с именем menu загружается в DX
mov AL,mem2 ; Содержимое байта с именем mem2 загружается в АL

Сравнивая этот пример с предыдущим, мы видим, что указание в команде имени ячейки памяти обозначает, что операндом является содержимое этой ячейки; указание имени ячейки с описателем offset - что операндом является адрес ячейки.
Прямая адресация памяти на первой взгляд кажется простой и наглядной. Если мы хотим обратиться, например, к ячейке mem1, мы просто указываем ее имя в программе. В действительности, однако, дело обстоит сложнее. Вспомним, что адрес любой ячейки состоит из двух компонентов: сегментного адреса и смещения. Обозначения mem1 и mem2 в предыдущем примере, очевидно, являются смещениями. Сегментные же адреса хранятся в сегментных регистрах. Однако сегментных регистров четыре: DS, ES, CS и SS. Каким образом процессор узнает, из какого регистра взять сегментный адрес, и как сообщить ему об этом в программе?
Процессор различает группу кодов, носящих название префиксов. Имеется несколько групп префиксов: повторения, размера адреса, размера операнда, замены сегмента. Здесь нас будут интересовать префиксы замены сегмента.
Команды процессора, обращающиеся к памяти, могут в качестве первого байта своего кода содержать префикс замены сегмента, с помощью которого процессор определяет, из какого сегментного регистра взять сегментный адрес. Для сегментного регистра ES код префикса составляет 26h, для SS - 36h, для CS - 2Eh. Если префикс отсутствует, сегментный адрес берется из регистра DS (хотя для него тоже предусмотрен свой префикс).
Если в начале программы с помощью директивы assume указано соответствие сегменту данных сегментного регистра DS

assume DS:data

то команды обращения к памяти транслируются без какого-либо префикса, а процессор при выполнении этих команд берет сегментный адрес из регистра DS.
Если в директиве assume указано соответствие сегмента данных регистру ES

assume ES:data

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

И его листинг...

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

mov BX,CS:mem

В этом случае транслятор включит в код команды префикс замены для сегмента CS (2Eh).

До сих пор мы обсуждали адресацию ячеек, содержащихся в сегментах данных программы. Однако часто бывает нужно обратиться к памяти вне пределов программы: к векторам прерываний, системным таблицам, видеобуферу и т.д. Разумеется, такое обращение возможно только если мы знаем абсолютный адрес интересующей нас ячейки. В этом случае необходимо сначала настроить один из сегментных регистров на начато интересующей нас области, после чего можно адресоваться к ячейкам по их смещениям.
Пусть требуется вывести в левый верхний угол экрана несколько символов, например, два восклицательных знака. Эту операцию можно реализовать с помощью следующих команд:

mov AX,0B800h ;Сегментный адрес видеобуфера
mov ES,AX ;Отправим его в ES
mov byte ptr ES:0,'!' ;Отправим символ на 1-е знакоместо экрана
mov byte ptr ES:2,'!' ;Отправим символ на 2-е знакоместо экрана

Настроив регистр ES на сегментный адрес видеобуфера B800h, мы пересылаем код знака "!" сначала по относительному адресу 0 (в самое начало видеобуфера, в байт со смещением 0), а затем на следующее знакоместо, имеющее смещение 2 (в нечетных байтах видеобуфера хранятся атрибуты символов, т.е. цвет символов и фона под ними). В обеих командах необходимо с помощью обозначения ES: указать сегментный регистр, который используется для адресации памяти. Встретившись с этим обозначением, транслятор включит в код команды префикс замены сегмента, в данном случае код 26h.
Примерчик для закрепления материала...

И его листинг...

В приведенном примере мы снова столкнулись с использованием атрибутивного оператора byte ptr, который позволяет в явной форме задать размер операнда. Однако если раньше этот оператор использовался, чтобы извлечь байт из данного, объявленного, как слово, то здесь его назначение иное. Транслятор, обрабатывая команду

mov byte ptr ES:0,'!'

не имеет возможности определить размер операнда-приемника. Разумеется, видеобуфер, как и любая память, состоит из байтов, однако надо ли рассматривать эту память, как последовательность байтов или слов? Команда без явного задания размера операнда

mov ES:0,'!'

вызовет ошибку трансляции, так как ассемблер не сможет определить, надо ли транслировать это предложение, как команду пересылки в видеобуфер байта 21h, или как команду пересылки слова 0021h.
Между прочим, на первый взгляд может показаться, что в обсуждаемой команде достаточно ясно указан размер правого операнда, так как символ (в данном случае "!") всегда занимает один байт. Однако транслятор, встретив обозначение "!", сразу же преобразует его в код ASCII этого символа, т.е. в число 21h, и уже не знает, откуда это число произошло и какой размер оно имеет.
Стоит еще отметить, что указание в команде описателя word ptr

mov word ptr ES:0,'!'

не вызовет ошибки трансляции, но приведет к неприятным результатам. В этом случае в видеобуфер будет записано слово 002lh, которое заполнит байт 0 видеобуфера кодом 21h, а байт 1 кодом 00h. Однако атрибут 00h обозначает черный цвет на черном фоне, и символ на экране виден не будет (хотя и будет записан в видеобуфер).
При желании можно избавиться от необходимости вводить описатель размера операнда. Для этого надо пересылать не непосредственное данное, а содержимое регистра:

mov AL,'!'
mov ES:0,AL

Здесь операндом-источником служит регистр AL, размер которого (1 байт) известен, и размер операнда-приемника определять не надо. Разумеется, команда

mov ES:0,AX

заполнит в видеобуфере не байт, а слово.
Для адресации к видеобуферу в вышеприведенном примере использовался сегментный регистр дополнительных данных ES. Это вполне естественно, так как обычно регистр DS служит для обращения к полям данных программы, а регистр ES как раз и предназначен для адресации всего остального. Однако при необходимости можно было воспользоваться для записи в видеобуфер регистром DS:

mov AX,0B800h ;Сегментный адрес
mov DS,AX ; видеобуфера в DS
mov byte ptr DS:0, '!' ;Символ в видеобуфер

Любопытно, что хотя обозначение DS: здесь необходимо, транслятор не включит в код команды префикс замены сегмента, так как команда без префикса выполняет адресацию по умолчанию через DS.
Если, однако, по умолчанию выполняется адресация через DS, то нельзя ли опустить в последней команде обозначение сегментного регистра? Нельзя, так как обозначение DS: число указывает, что число является не непосредственным операндом, а адресом операнда. Команда (неправильная)

mov 6,10

должна была бы переслать число 10 в число 6, что, разумеется, лишено смысла и выполнено быть не может. Команда же

mov DS:6,10

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

пятница, 12 июня 2009 г.

Непосредственная адресация

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

mov АН,40h ;Число 40h загружается в АН
mov AL,'*' ;Код ASCII символа "*' загружается в AL
int 21h ;Команда прерывания с аргументом 21h
limit = 528 ;Число 528 получает обозначение limit
mov CX,limit ;Число, обозначенное limit, загружается в СХ

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

; Сегмент данных
mes db 'Урок 1' ;Строка символов
;Сегмент команд
mov DX,offset mes ;Адрес строки засылается в DX

В приведенном примере относительный адрес строки mes, т.е. расстояние в байтах первого байта этой строки от начала сегмента, в котором она находится, заносится в регистр DX.

четверг, 11 июня 2009 г.

Регистровая адресация

Операнд находится в регистре. Этот способ адресации применим ко всем программно-адресуемым регистрам процессора.

inc СН ;Плюс 1 к содержимому СН
push DS ;DS сохраняется в стеке
xchg ВХ,ВР ;ВХ и ВР обмениваются содержимым
mov ES,АХ ;Содержимое АХ пересылается в ES

среда, 10 июня 2009 г.

Способы адресации

Большинство команд процессора вызывается с аргументами, которые в ассемблере принято называть операндами. Способом, или режимом адресации называют процедуру нахождения операнда для выполняемой команды. Если команда использует два операнда, то для каждого из них должен быть задан способ адресации, причем режимы адресации первого и второго операнда могут как совпадать, так и различаться. Операнды команды могут находиться в разных местах: непосредственно в составе кода команды, в каком-либо регистре, в ячейке памяти; в последнем случае существует несколько возможностей указания его адреса. Строго говоря, способы адресации являются элементом архитектуры процессора, отражая заложенные в нем возможности поиска операндов. С другой стороны, различные способы адресации определенным образом обозначаются в языке ассемблера и в этом смысле являются разделом языка.
Следует отметить неоднозначность термина "операнд" применительно к программам, написанным на языке ассемблера. Для машинной команды операндами являются те данные (в сущности, двоичные числа), с которыми она имеет дело. Эти данные могут, как уже отмечалось, находиться в регистрах или в памяти. Если же рассматривать команду языка ассемблера, то для нее операндами (или, лучше сказать, параметрами) являются те обозначения, которые позволяют сначала транслятору, а потом процессору определить местонахождение операндов машинной команды. Так, для команды ассемблера

mov mem, AX

в качестве операндов используется обозначение ячейки памяти mem, a также обозначение регистра АХ. В то же время, для соответствующей машинной команды операндами являются содержимое ячейки памяти и содержимое регистра. Было бы правильнее говорить об операндах машинных команд и о параметрах, или аргументах команд языка ассемблера.
По отношению к командам ассемблера было бы правильнее использовать термин "параметры", оставив за термином "операнд" обозначение тех физических объектов, с которыми имеет дело процессор при выполнении машинной команды, однако обычно эти тонкости не принимают в расчет, и говоря об операндах команд языка, понимают в действительности операнды машинных команд.
В архитектуре современных 32-разрядных процессоров Intel предусмотрены довольно изощренные способы адресации; в МП 86 способов адресации меньше. Пока будем разбираться с режимами адресации, используемыми в МП 86.
В книгах, посвященных языку ассемблера, можно встретить разные подходы к описанию способов адресации: не только названия этих режимов, но даже и их количество могут различаться. Разумеется, способов адресации существует в точности столько, сколько их реализовано в процессоре; однако, режимы адресации можно объединять в группы по разным признакам, отчего и создается некоторая путаница, в том числе и в количестве имеющихся режимов.
И так начнем разбираться...

вторник, 9 июня 2009 г.

Данные сложного типа. Записи.

Запись — структурный тип данных, состоящий из фиксированного числа элементов длиной от одного до нескольких бит.
При описании записи для каждого элемента указывается его длина в битах и, что необязательно, некоторое значение.
Суммарный размер записи определяется суммой размеров ее полей и не может быть более 8, 16 или 32 бит.
Если суммарный размер записи меньше указанных значений, то все поля записи “прижимаются” к младшим разрядам.
В отличие от структур, дающих имена байтам, словам, двойным словам или целым массивам, в записях определяются строки битов внутри байтов, слов или двойных слов.

Использование записей в программе, так же, как и структур, организуется в три этапа:
  1. Задание шаблона записи, то есть определение набора битовых полей, их длин и, при необходимости, инициализация полей.
  2. Определение экземпляра записи. Так же, как и для структур, этот этап подразумевает инициализацию конкретной переменной типом заранее определенной с помощью шаблона записи.
  3. Организация обращения к элементам записи.
Компилятор TASM, кроме стандартных средств обработки записей, поддерживает также и некоторые дополнительные возможности их обработки.

Описание записи

Описание шаблона записи имеет следующий синтаксис:

имя_записи RECORD <описание элементов>

Здесь:
<описание элементов> представляет собой последовательность описаний отдельных элементов записи согласно синтаксической диаграмме (см. рис.):

При описании шаблона память не выделяется, так как это всего лишь информация для транслятора ассемблера о структуре записи.
Пример:

iotest record i1:1,i2:2=11,i3:1,i4:2=11,i5:2=00

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

Определение экземпляра записи

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

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

Если инициализировать поля не требуется, то достаточно указать ? при определении экземпляра записи:

flag iotest ?

Если вы составите и исследуете в отладчике тестовый пример с данным определением записи, то увидите, что все поля переменной типа запись flag обнуляются. Это происходит несмотря на то, что в определении записи заданы начальные значения полей.

Если требуется частичная инициализация элементов, то они заключаются в угловые (< и >) или фигурные ({ и }) скобки.
Различие здесь в том, что в угловых скобках элементы должны быть заданы в том же порядке, что и в определении записи. Если значение некоторого элемента совпадает с начальным, то его можно не указывать, но обязательно обозначить его запятой. Для последних элементов идущие подряд запятые можно опустить.
К примеру, согласиться со значениями по умолчанию можно так:

flag iotest <> ;согласились со значением по умолчанию

Изменить значение поля i2 можно так:

flag iotest <,10,> ; переопределили i2

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

flag iotest {i2=10} ;переопределили i2,
;не обращая внимания на порядок
;следования других компонентов записи

Работа с записями
Как организовать работу с отдельными элементами записи? Обычные механизмы адресации здесь бессильны, так как они работают на уровне ячеек памяти, то есть байтов, а не отдельных битов. Здесь программисту нужно приложить некоторые усилия.
Прежде всего для понимания проблемы нужно усвоить несколько моментов:
  • Каждому имени элемента записи ассемблер присваивает числовое значение, равное количеству сдвигов вправо, которые нужно произвести для того, чтобы этот элемент оказался “прижатым” к началу ячейки. Это дает нам возможность локализовать его и работать с ним. Но для этого нужно знать длину элемента в битах.

    mov ah,i2 ;получаем количество сдвигов в право

  • Сдвиг вправо производится с помощью команды сдвига shr.
  • Ассемблер содержит оператор width, который позволяет узнать размер элемента записи в битах или полностью размер записи. Варианты применения оператора width:

    width имя_элемента_записи ;значением оператора
    ;будет размер элемента в битах

    width имя_экземпляра_записи
    или
    width имя_типа_записи ;значением оператора
    ;будет размер всей записи в битах.

    mov al,width i2
    ...
    mov ax,width iotest

  • Ассемблер содержит оператор mask, который позволяет локализовать биты нужного элемента записи. Эта локализация производится путем создания маски, размер которой совпадает с размером записи. В этой маске обнулены биты на всех позициях, за исключением тех, которые занимает элемент в записи.
  • Сами действия по преобразованию элементов записи производятся с помощью логических команд.
И так, поскольку в системе команд МП 86 практически нет средств работы с битовыми полями, поэтому непосредственно обратиться к элементу записи невозможно. Чтобы произвести обработку интересующего нас элемента, его нужно сначала выделить, сдвинуть, при необходимости, к младшим разрядам, выполнить необходимые действия и поместить его обратно на свое место в записи. Это приходится осуществлять с помощью команд сдвигов и логических операций - and, or, xor и not.

Выделение элемента записи:
  • Поместить запись во временную память — регистр (8, 16 или 32-битный в зависимости от размера записи).
  • Получить битовую маску, соответствующую элементу записи, с помощью оператора mask.
  • Локализовать биты в регистре с помощью маски и команды and.
  • Сдвинуть биты элемента к младшим разрядам регистра командой shr. Число разрядов для сдвига получить с использованием имени элемента записи.
В результате этих действий элемент записи будет локализован в начале рабочего регистра и далее с ним можно производить любые действия.

Работа с элементом записи:

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

Помещение измененного элемента на его место в запись:
  • Используя имя элемента записи в качестве счетчика сдвигов, сдвинуть влево биты элемента записи (команда shl).
  • Если вы не уверены в том, что разрядность результата преобразований не превысила исходную, можно выполнить “обрезание” лишних битов, используя команду and и маску элемента.
  • Подготовить исходную запись к вставке измененного элемента путем обнуления битов в записи на месте этого элемента. Это можно сделать путем наложения командой and инвертированной маски элемента записи на исходную запись.
  • С помощью команды or наложить значение в регистре на исходную запись.
Ну и примерчик для медитации...
Програмулина ни чего не выводит на экран, так как предназначена для медитации на нее под отладчиком. В программе создается экземпляр записи iotest с именем flag, имеющий двоичное значение (по умолчанию) - 10001100 (h). Программа устанавливает третий слева бит (младший бит i2) в единичку. Соответственно, после этих изменений, в переменной flag должно быть значение ACh (10101100).

Теперь листинг программы...

И немного отладчика...

Этот скрин сделан после выполнения первых двух команд программы. Следующий уже после выполнения команды or.

понедельник, 8 июня 2009 г.

Данные сложного типа. Объединения.

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

Объединение — тип данных, позволяющий трактовать одну и ту же область памяти как имеющую разные типы и имена.

Описание объединений в программе напоминает описание структур, то есть сначала описывается шаблон, в котором с помощью директив описания данных перечисляются имена и типы полей:

имя_объединения UNION
<описание полей>
имя_объединения ENDS

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

воскресенье, 7 июня 2009 г.

Данные сложного типа. Стуктуры.

Рассмотренные нами массивы представляют собой совокупность однотипных элементов. Но часто в приложениях возникает необходимость рассматривать некоторую совокупность данных разного типа как некоторый единый тип.
Это очень актуально, например, для программ баз данных, где необходимо связывать совокупность данных разного типа с одним объектом. Особенно если база данных пишется на ассемблере :)
По определению структура — это тип данных, состоящий из фиксированного числа элементов разного типа.

Для использования структур в программе необходимо выполнить три действия:
  1. Задать шаблон структуры.
    По смыслу это означает определение нового типа данных, который впоследствии можно использовать для определения переменных этого типа.
  2. Определить экземпляр структуры.
    Этот этап подразумевает инициализацию конкретной переменной заранее определенной (с помощью шаблона) структурой.
  3. Организовать обращение к элементам структуры.
Очень важно, чтобы вы с самого начала уяснили, в чем разница между описанием структуры в программе и ее определением.
Описать структуру в программе означает лишь указать ее схему или шаблон; память при этом не выделяется. Этот шаблон можно рассматривать лишь как информацию для транслятора о расположении полей и их значении по умолчанию.
Определить структуру — значит, дать указание транслятору выделить память и присвоить этой области памяти символическое имя.
Описать структуру в программе можно только один раз, а определить — любое количество раз.

Описание шаблона структуры

Описание шаблона структуры имеет следующий синтаксис:

имя_структуры STRUC
<описание полей>
имя_структуры ENDS

Здесь <описание полей> представляет собой последовательность директив описания данных db, dw, dd, dq и dt.
Их операнды определяют размер полей и, при необходимости, начальные значения. Этими значениями будут, возможно, инициализироваться соответствующие поля при определении структуры.
Как мы уже отметили при описании шаблона, память не выделяется, так как это всего лишь информация для транслятора.
Местоположение описания шаблона в программе может быть произвольным, но, следуя логике работы однопроходного транслятора, он должен быть расположен до того места, где определяется переменная с типом данной структуры. То есть при описании в сегменте данных переменной с типом некоторой структуры ее шаблон необходимо поместить в начале сегмента данных либо перед ним.

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

;описали шаблон стуктуры
worker struc ;информация о сотруднике
nam db 30 dup (' ') ;фамилия, имя, отчество
sex db 'м';пол, по умолчанию 'м' - мужской
position db 30 dup (' ') ;должность
age db 2 dup(' ') ;возраст
standing db 2 dup(' ') ;стаж
salary db 4 dup(' ') ;оклад
birthdate db 8 dup(' ') ;дата рождения
worker ends

Определение данных с типом структуры

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

[имя переменной] имя_структуры <[список значений]>

Здесь:
  • имя переменной — идентификатор переменной данного структурного типа.
    Задание имени переменной необязательно. Если его не указать, будет просто выделена область памяти размером в сумму длин всех элементов структуры.
  • список значений — заключенный в угловые скобки список начальных значений элементов структуры, разделенных запятыми.
    Его задание также необязательно.
    Если список указан не полностью, то все поля структуры для данной переменной инициализируются значениями из шаблона, если таковые заданы.
    Допускается инициализация отдельных полей, но в этом случае пропущенные поля должны отделяться запятыми. Пропущенные поля будут инициализированы значениями из шаблона структуры. Если при определении новой переменной с типом данной структуры мы согласны со всеми значениями полей в ее шаблоне (то есть заданными по умолчанию), то нужно просто написать угловые скобки.
    К примеру: victor worker <>.
Для примера определим несколько переменных с типом описанной выше структуры.

sotr1 worker <'Гурко Андрей Вячеславович',,'художник','33','15','1800','26.01.64'>
sotr2 worker <'Михайлова Наталья Геннадьевна','ж','программист','30','10','1680','27.10.58'>
sotr3 worker <'Степанов Юрий Лонгинович',,'художник','38','20','1750','01.01.58'>
sotr4 worker <'Юрова Елена Александровна','ж','свяэист','32','2',,'09.01.66'>
sotr5 worker <> ;здесь все значения по умолчанию


Методы работы со структурой

Идея введения структурного типа в любой язык программирования состоит в объединении разнотипных переменных в один объект.
В языке должны быть средства доступа к этим переменным внутри конкретного экземпляра структуры. Для того чтобы сослаться в команде на поле некоторой структуры, используется специальный оператор — символ "." (точка). Он используется в следующей синтаксической конструкции:

адресное_выражение.имя_поля_структуры

Здесь:

  • адресное_выражение — идентификатор переменной некоторого структурного типа или выражение в скобках указывающее на ее адрес
  • имя_поля_структуры — имя поля из шаблона структуры. Это, на самом деле, тоже адрес, а точнее, смещение поля от начала структуры.
Теперь примерчик...

Эта программа уже выводит на экран информацию о первом сотруднике. Запускаем, анализируем, просветляемся...

Давайте представим, что сотрудников не четверо, а намного больше, и к тому же их число и информация о них постоянно меняются. В этом случае теряется смысл явного определения переменных с типом worker для конкретных личностей.
Язык ассемблера разрешает определять не только отдельную переменную с типом структуры, но и массив структур.
К примеру, определим массив из 10 структур типа worker:

mas_sotr worker 10 dup (<>)

Дальнейшая работа с массивом структур производится так же, как и с одномерным массивом. Здесь возникает несколько вопросов:
Как быть с размером и как организовать индексацию элементов массива?

Аналогично другим идентификаторам, определенным в программе, транслятор назначает имени типа структуры и имени переменной с типом структуры атрибут типа. Значением этого атрибута является размер в байтах, занимаемый полями этой структуры. Извлечь это значение можно с помощью оператор type.
После того как стал известен размер экземпляра структуры, организовать индексацию в массиве структур не представляет особой сложности.
К примеру, программа выводит на экран содержимое поля sex всех структур worker в массиве mas_sotr:

суббота, 6 июня 2009 г.

Данные сложного типа. Массивы.

Массив - структурированный тип данных, состоящий из некоторого числа элементов одного типа.

Для того чтобы разобраться в возможностях и особенностях обработки массивов в программах на ассемблере, нужно ответить на следующие вопросы:
  • Как описать массив в программе?
  • Как инициализировать массив, то есть как задать начальные значения его элементов?
  • Как организовать доступ к элементам массива?
  • Как организовать массивы с размерностью более одной?
  • Как организовать выполнение типовых операций с массивами?
Описание и инициализация массива в программе

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

1) Перечислением элементов массива в поле операндов одной из директив описания данных. При перечислении элементы разделяются запятыми. К примеру:

;массив из 5 элементов.Размер каждого
элемента 4 байта:
mas dd 1,2,3,4,5

2) Используя оператор повторения dup. К примеру:

;массив из 5 нулевых элементов.
;Размер каждого элемента 2 байта:
mas dw 5 dup (0)

Такой способ определения используется для резервирования памяти с целью размещения и инициализации элементов массива.

3) Используя директивы label и rept. Пара этих директив может облегчить описание больших массивов в памяти и повысить наглядность такого описания. Директива rept относится к макросредствам языка ассемблера и вызывает повторение указанное число раз строк, заключенных между директивой и строкой endm. К примеру, определим массив байт в области памяти, обозначенной идентификатором mas_b. В данном случае директива label определяет символическое имя mas_b, аналогично тому, как это делают директивы резервирования и инициализации памяти. Достоинство директивы label в том, что она не резервирует память, а лишь определяет характеристики объекта. В данном случае объект — это ячейка памяти. Используя несколько директив label, записанных одна за другой, можно присвоить одной и той же области памяти разные имена и разный тип, что и сделано в следующем фрагменте:

...
n=0
...
mas_b label byte
mas_w label word
rept 4
dw 0f1f0h
endm

В результате в памяти будет создана последовательность из четырех слов f1f0. Эту последовательность можно трактовать как массив байт или слов в зависимости от того, какое имя области мы будем использовать в программе — mas_b или mas_w.

4) Использование цикла для инициализации значениями области памяти, которую можно будет впоследствии трактовать как массив.
Посмотрим на примере, каким образом это делается.

;prg_12_1.asm
MASM
MODEL small
STACK 256
.data
mes db 0ah,0dh,'Массив- ','$'
mas db 10 dup (?) ;исходный массив
i db 0
.code
main:
mov ax,@data
mov ds,ax
xor ax,ax ;обнуление ax
mov cx,10 ;значение счетчика цикла в cx
mov si,0 ;индекс начального элемента в cx
go: ;цикл инициализации
mov bh,i ;i в bh
mov mas[si],bh ;запись в массив i
inc i ;инкремент i
inc si ;продвижение к следующему
;элементу массива
loop go ;повторить цикл
;вывод на экран получившегося массива
mov cx,10
mov si,0
mov ah,09h
lea dx,mes
int 21h
show:
mov ah,02h ;функция вывода значения
;из al на экран
mov dl,mas[si]
add dl,30h ;преобразование числа в символ
int 21h
inc si
loop show
exit:
mov ax,4c00h ;стандартный выход
int 21h
end main ;конец программы


Доступ к элементам массива

При работе с массивами необходимо четко представлять себе, что все элементы массива располагаются в памяти компьютера последовательно.
Само по себе такое расположение ничего не говорит о назначении и порядке использования этих элементов. И только лишь программист с помощью составленного им алгоритма обработки определяет, как нужно трактовать эту последовательность байт, составляющих массив. Так, одну и ту же область памяти можно трактовать как одномерный массив, и одновременно те же самые данные могут трактоваться как двухмерный массив. Все зависит только от алгоритма обработки этих данных в конкретной программе. Сами по себе данные не несут никакой информации о своем “смысловом”, или логическом, типе. Помните об этом принципиальном моменте.
Эти же соображения можно распространить и на индексы элементов массива. Ассемблер не подозревает об их существовании и ему абсолютно все равно, каковы их численные смысловые значения.
Для того чтобы локализовать определенный элемент массива, к его имени нужно добавить индекс. Так как мы моделируем массив, то должны позаботиться и о моделировании индекса. В языке ассемблера индексы массивов — это обычные адреса, но с ними работают особым образом. Другими словами, когда при программировании на ассемблере мы говорим об индексе, то скорее подразумеваем под этим не номер элемента в массиве, а некоторый адрес.

Давайте еще раз обратимся к описанию массива. К примеру, в программе статически определена последовательность данных:

dim dw 0011h,2233h,4455h,6677h,8899h

Пусть эта последовательность чисел трактуется как одномерный массив. Размерность каждого элемента определяется директивой dw, то есть она равна 2 байта. Чтобы получить доступ к числу 6677h, нужно к адресу массива прибавить 6. Нумерация элементов массива в ассемблере начинается с нуля.
В общем случае для получения адреса элемента в массиве необходимо начальный (базовый) адрес массива сложить с произведением индекса этого элемента на размер элемента массива:

база + (индекс*размер элемента)

Архитектура микропроцессора предоставляет достаточно удобные программно-аппаратные средства для работы с массивами. К ним относятся базовые и индексные регистры, позволяющие реализовать несколько режимов адресации данных. Используя данные режимы адресации, можно организовать эффективную работу с массивами в памяти.
Для более глубокого понимания можно помедитировать на эту програмку:

Эта программа ни чего не выводит на экран. Предназначена просто для медитации на нее под отладчиком. Внимание!!! При отладке, чтобы увидеть работу цикла, на команде loop go, нужно нажимать клавишу F7.
Зарисовка из медитации на эту прогу:

Двухмерные массивы

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

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

Если последовательность однотипных элементов в памяти трактуется как двухмерный массив, расположенный по строкам, то адрес элемента (i, j) вычисляется по формуле

(база + количество_элементов_в_строке * размер_элемента * i+j)

Здесь i = 0...n–1 указывает номер строки, а j = 0...m–1 указывает номер столбца.

Например, пусть имеется массив чисел (размером в 1 байт) mas(i, j) с размерностью 4 на 4
(i= 0...3, j = 0...3):

23 04 05 67
05 06 07 99
67 08 09 23
87 09 00 08

В памяти элементы этого массива будут расположены в следующей последовательности:

23 04 05 67 05 06 07 99 67 08 09 23 87 09 00 08

Если мы хотим трактовать эту последовательность как двухмерный массив, приведенный выше, и извлечь, например, элемент
mas(2, 3) = 23, то проведя нехитрый подсчет, убедимся в правильности наших рассуждений:

Эффективный адрес mas(2, 3) = mas + 4 * 1 * 2 + 3 = mas + 11

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

Эта программа, тоже ни чего не выводит, так как предназначена для выполнения ее под отладчиком, для пущего научения.

пятница, 5 июня 2009 г.

Операторы

Возможные типы операндов мы уже рассмотрели. Перечислим теперь возможные типы операторов ассемблера и синтаксические правила формирования выражений ассемблера.
  • Арифметические операторы
  • Операторы сдвига
  • Операторы сравнения
  • Логические операторы
  • Индексный оператор
  • Оператор переопределения типа
  • Оператор переопределения сегмента
  • Оператор именования типа структуры
  • Оператор получения сегментной составляющей адреса выражения
  • Оператор получения смещения выражения
В табл. 2 приведены поддерживаемые языком ассемблера операторы и перечислены их приоритеты. Дадим краткую характеристику операторов:

Арифметические операторы. К ним относятся:
  • унарные “+” и “–”;
  • бинарные “+” и “–”;
  • умножения “*”;
  • целочисленного деления “/”;
  • получения остатка от деления “mod”.
Эти операторы расположены на уровнях приоритета 6, 7, 8 в табл. 2. Например,

tab_size equ 50 ;размер массива в байтах
size_el equ 2 ;размер элементов

;вычисляется число элементов массива
;и заносится в
регистр cx
mov cx,tab_size / size_el ;оператор “/”

Операторы сдвига выполняют сдвиг выражения на указанное количество разрядов.

Пример:
mask_b equ 10111011

mov al,mask_b shr 3 ;al=00010111

Операторы сравнения (возвращают значение “истина” или “ложь”) предназначены для формирования логических выражений (см. табл.1). Логическое значение “истина” соответствует цифровой единице, а “ложь” — нулю.

Пример:

tab_size equ 30 ;размер таблицы

mov al,tab_size ge 50 ;загрузка размера
;таблицы в al
cmp al,0 ;если tab_size меньше 50, то
je m1 ;переход на m1

m1: …

В этом примере если значение tab_size больше или равно 50, то результат в al равен 0ffh, а если tab_size меньше 50, то al равно 00h. Команда cmp сравнивает значение al с нулем и устанавливает соответствующие флаги в flags/eflags. Команда je на основе анализа этих флагов передает или не передает управление на метку m1.

Таблица 1. Операторы сравнения
Оператор Значение
eq ИСТИНА, если выражение_1 равно выражение_2
ne ИСТИНА, если выражение_1 не равно выражение_2
lt ИСТИНА, если выражение_1 меньше выражение_2>ИСТИНА, если выражение_1 не равно выражение_2
le ИСТИНА, если выражение_1 меньше или равно выражение_2
gt ИСТИНА, если выражение_1 больше выражение_2
ge ИСТИНА, если выражение_1 больше или равно выражение_2

Логические операторы выполняют над выражениями побитовые операции. Выражения должны быть абсолютными, то есть такими, численное значение которых может быть вычислено транслятором.

Пример:

flags equ 10010011

mov al,flags xor 01h ;
al=10010010;пересылка в al поля flags с
;инвертированным правым битом

Индексный оператор [ ]. Не удивляйтесь, но скобки тоже являются оператором, и транслятор их наличие воспринимает как указание сложить значение выражение_1 за этими скобками с выражение_2, заключенным в скобки.

Пример:

mov ax,mas[si] ;пересылка слова по адресу
;mas+(si) в регистр ax

Заметим, что в литературе по ассемблеру принято следующее обозначение: когда в тексте речь идет о содержимом регистра, то его название берут в круглые скобки. Мы также будем придерживаться этого обозначения.
К примеру, в нашем случае запись в комментариях последнего фрагмента программы mas + (si) означает вычисление следующего выражения: значение смещения символического имени mas плюс содержимое регистра si.

Оператор переопределения типа ptr применяется для переопределения или уточнения типа метки или переменной, определяемых выражением. Тип может принимать одно из следующих значений: byte, word, dword, qword, tbyte, near, far.

Пример:

d_wrd dd 0
...
mov al,byte ptr d_wrd+1 ;пересылка второго байта
;из двойного слова

Поясним этот фрагмент программы. Переменная d_wrd имеет тип двойного слова. Что делать, если возникнет необходимость обращения не ко всей переменной, а только к одному из входящих в нее байтов (например, ко второму)? Если попытаться сделать это командой
mov al,d_wrd+1, то транслятор выдаст сообщение о несовпадении типов операндов. Оператор ptr позволяет непосредственно в команде переопределить тип и выполнить команду.

Оператор переопределения сегмента : (двоеточие) заставляет вычислять физический адрес относительно конкретно задаваемой сегментной составляющей: “имя сегментного регистра”, “имя сегмента” из соответствующей директивы SEGMENT или “имя группы”.

Этот момент очень важен, поэтому поясню его подробнее. При обсуждении сегментации мы говорили о том, что микропроцессор на аппаратном уровне поддерживает три типа сегментов — кода, стека и данных. В чем заключается такая аппаратная поддержка? К примеру, для выборки на выполнение очередной команды микропроцессор должен обязательно посмотреть содержимое сегментного регистра cs и только его. А в этом регистре, как мы знаем, содержится (пока еще не сдвинутый) физический адрес начала сегмента команд. Для получения адреса конкретной команды микропроцессору остается умножить содержимое cs на 16 (что означает сдвиг на четыре разряда) и сложить полученное 20-битное значение с 16-битным содержимым регистра ip. Примерно то же самое происходит и тогда, когда микропроцессор обрабатывает операнды в машинной команде. Если он видит, что операнд — это адрес (эффективный адрес, который является только частью физического адреса), то он знает, в каком сегменте его искать — по умолчанию это сегмент, адрес начала которого записан в сегментном регистре ds.

А что же с сегментом стека? Вспоминаем что уже писалось про регистры. В контексте нашего рассмотрения нас интересуют регистры sp и bp. Если микропроцессор видит в качестве операнда (или его части, если операнд — выражение) один из этих регистров, то по умолчанию он формирует физический адрес операнда используя в качестве его сегментной составляющей содержимое регистра ss. Что подразумевает термин “по умолчанию”? Это набор микропрограмм в блоке микропрограммного управления, каждая из которых выполняет одну из команд в системе машинных команд микропроцессора. Каждая микропрограмма работает по своему алгоритму. Изменить его, конечно же, нельзя, но можно чуть-чуть подкорректировать. Делается это с помощью необязательного поля префикса машинной команды. Если мы согласны с тем, как работает команда, то это поле отсутствует. Если же мы хотим внести поправку (если, конечно, она допустима для конкретной команды) в алгоритм работы команды, то необходимо сформировать соответствующий префикс.
Префикс представляет собой однобайтовую величину, численное значение которой определяет ее назначение. Микропроцессор распознает по указанному значению, что этот байт является префиксом, и дальнейшая работа микропрограммы выполняется с учетом поступившего указания на корректировку ее работы. Сейчас нас интересует один из них - префикс замены (переопределения) сегмента. Его назначение состоит в том, чтобы указать микропроцессору (а по сути, микропрограмме) на то, что мы не хотим использовать сегмент по умолчанию. Возможности для подобного переопределения, конечно, ограничены. Сегмент команд переопределить нельзя, адрес очередной исполняемой команды однозначно определяется парой cs:ip. А вот сегменты стека и данных — можно. Для этого и предназначен оператор “:”. Транслятор ассемблера, обрабатывая этот оператор, формирует соответствующий однобайтовый префикс замены сегмента.

Пример:

.code
...
jmp met1 ;обход обязателен, иначе поле ind
;будет трактоваться как очередная команда
ind db 5 ;описание поля данных в сегменте команд
met1:
...
mov al,cs:ind ;переопределение сегмента
;позволяет работать с
данными,
;определенными внутри сегмента кода


Оператор именования типа . (точка) также заставляет транслятор производить определенные вычисления, если он встречается в выражении.

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

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

.data
pole dw 5
...
.code
...
mov ax,seg pole
mov es,ax
mov dx,offset pole ;теперь в паре es:dx
;полный адрес pole

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

Таблица 2. Операторы и их приоритет
Оператор Приоритет
length, size, width, mask, (, ), [, ], <, > 1
. 2
: 3
ptr, offset, seg, type, this 4
high, low 5
+, - (унарные) 6
*, /, mod, shl, shr 7
+, -, (бинарные) 8
eq, ne, lt, le, gt, ge 9
not 10
and 11
or, xor 12
short, type 13