Offset в ассемблере: Оператор OFFSET

Содержание

Оператор OFFSET

Главная / Ассемблер / Для чайников / Введение в Ассемблер /


Английский за 3 месяца

Хотим мы того или нет, но английский язык стал международным средством общения. В Европе он входит в школьную программу, причём на таком уровне, что после школы все дети умеют достаточно хорошо говорить на английском. Это надо просто принять как данность и использовать. Лишать себя возможности быть частью мирового сообщества, это, как минимум, недальновидно. Подробнее…

OFF SET можно перевести как “вне набора”. Применительно к языку ассемблера ещё более вольный перевод может звучать так: за пределами набора команд. Почему этот оператор назвали именно так, поймёте, прочитав эту статью.

Ну а вообще слово OFFSET переводится как “смещение”. И это, конечно, настоящий перевод. Но я позволил себе немного пофилософствовать )))

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

В защищённом режиме работы процессора смещения всегда являются 32-разрядными числами без знака. В реальном и виртуальном режимах адресации смещения всегда 16-разрядные.

С помощью оператора OFFSET в ассемблере можно объявлять переменные, то есть связывать адрес в памяти с именем переменной. По этой ссылке вы найдёте пример объявления строки. Но переменные могут быть, разумеется, не только строковыми.

Пример:


.model tiny
.code
ORG    100h

start:
  MOV AX, wVar        ; AX = 65535
  MOV DX, 
OFFSET
wVar ; DX = 107 RET wVar DD 65535 ; Объявляем переменную типа WORD END start

Здесь мы объявили переменную wVar и назначили сразу ей какой-то значение (в нашем случае 65535). В программе мы записали ЗНАЧЕНИЕ этой переменной в регистр АХ. А вот потом, с помощью оператора OFFSET, мы получаем адрес (смещение) переменной wVar, относительно начала сегмена (в нашем случае это 100h). И в нашем случае смещение будет равно 107 в шестнадцатеричной системе, потому что:

  • Сегмент начинается с адреса 100h (ORG 100h)
  • Команда MOV AX, wVar занимает 3 байта с адресами: 100h, 101h, 102h
  • Команда MOV DX, OFFSET wVar занимает следующие 3 байта с адресами: 103h, 104h, 105h
  • Команда RET занимает 1 байт по адресу: 106h

Ну и получается, что наша переменная wVar находится по адресу 107h.

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


.model tiny
.code
ORG    100h

start:
  MOV	AH,	09h                ;Номер функции 09h
  MOV	DX,	OFFSET stroka+7    ;Адрес строки записываем в DX
  INT	21h
  RET
stroka	DB	'Hello, World!!!$' ;Строка для вывода
  END 	start

END	start

Здесь мы выводим не всю строку, а начиная с 8-го символа, потому что к смещению адреса переменной

stroka мы прибавили 7. Но прибавили мы 7, а не 8, потому что адресация начинается с нуля, а не с единицы. Таким образом на экран будет выведено:

World!!!

то есть только нужная нам часть строки.

На этом пока всё. Подключайтесь к группе Основы программирования в Телеграм, или к другим каналам (ссылки ниже), чтобы ничего не пропустить.


Подписаться на канал в РУТуб

Вступить в группу «Основы программирования»

Подписаться на рассылки по программированию


Первые шаги в программирование

Главный вопрос начинающего программиста – с чего начать? Вроде бы есть желание, но иногда «не знаешь, как начать думать, чтобы до такого додуматься».

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


ASSEMBLER-Учебник по основам языка программирования ASSEMBLER

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

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

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

mov mem, AX

в качестве операндов используется обозначение ячейки памяти mem, a также обозначение регистра АХ. В то же время, для соответствующей машинной команды операндами являются содержимое ячейки памяти и содержимое регистра. Было бы правильнее говорить об операндах машинных команд и о параметрах, или аргументах команд языка ассемблера.
По отношению к командам ассемблера было бы правильнее использовать термин «параметры», оставив за термином «операнд» обозначение тех физических объектов, с которыми имеет дело процессор при выполнении машинной команды, однако обычно эти тонкости не принимают в расчет, и говоря об операндах команд языка, понимают в действительности операнды машинных команд.

В архитектуре современных 32-разрядных процессоров Intel предусмотрены довольно изощренные способы адресации; в МП 86 способов адресации меньше. В настоящем разделе будут описаны режимы адресации, используемые в МП 86.
В книгах, посвященных языку ассемблера, можно встретить разные подходы к описанию способов адресации: не только названия этих режимов, но даже и их количество могут различаться. Разумеется, способов адресации существует в точности столько, сколько их реализовано в процессоре; однако, режимы адресации можно объединять в группы по разным признакам, отчего и создается некоторая путаница, в том числе и в количестве имеющихся режимов. Мы будем придерживаться распространенной, но не единственно возможной терминологии.
Регистровая адресация. Операнд (байт или слово) находится в регистре. Этот способ адресации применим ко всем программно-адресуемым регистрам процессора.

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

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

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

Команда mov, использованная в последнем предложении, имеет два операнда; первый операнд определяется с помощью регистровой адресации, второй — с помощью непосредственной.


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

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

В приведенном примере относительный адрес строки mes, т.е. расстояние в байтах первого байта этой строки от начала сегмента, в котором она находится, заносится в регистр DX.
Прямая адресация памяти. Адресуется память; адрес ячейки памяти (слова или байта) указывается в команде (обычно в символической форме) и поступает в код команды:

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

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

assume DS:data

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

assume ES:data

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

mov AX,CS:mem

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

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

Настроив регистр ES на сегментный адрес видеобуфера BS00h, мы пересылаем код знака «!» сначала по относительному адресу 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, что имеет смысл. Таким образом, обозначение сегментного регистра с двоеточием перед операндом говорит о том, что операнд является адресом. В дальнейшем мы еще столкнемся с этим важным правилом.
Мы рассмотрели три важнейших способа адресации: регистровую, непосредственную и прямое обращение к памяти. Все остальные режимы адресации относятся к группе косвенной адресации памяти, когда в определении адреса ячейки памяти участвует один или несколько регистров процессора. Рассмотрим последовательно эти режимы.
Регистровая косвенная (базовая и индексная). Адресуется память (байт или слово). Относительный адрес ячейки памяти находится в регистре, обозначение которого заключается в прямые скобки. В МП 86 косвенная адресация допустима только через регистры ВХ, ВР, SI и DI. При использовании регистров ВХ или ВР адресацию называют базовой, при использовании регистров SI или DI — индексной.
Преобразуем приведенный выше пример, чтобы продемонстрировать использование косвенной адресации через регистр.

mov AX,0B800h ;Сегментный адрес
mov ES,AX ; видеобуфера в ES
mov BX,2000 ;Смещение к середине экрана
mov byte ptr ES:[ВХ], ‘ ! ‘ ;Символ на экран

Настроив ES, мы засылаем в регистр ВХ требуемое смещение (для разнообразия к середине видеобуфера, который имеет объем точно 4000 байт), и в последней команде засылаем код в видеобуфер с помощью косвенной базовой адресации через пару регистров ES:BX с указанием замены сегмента (ES:).
Если косвенная адресация осуществляется через один из регистров ВХ, SI или DI, то подразумевается сегмент, адресуемый через DS, поэтому при адресации через этот регистр обозначение DS: можно опустить:

mov AX,0B800h ;Сегментный адрес
mov DS,AX ;видеобуфера в DS
mov BX,2000 ;Смещение к середине экрана
mov byte ptr [ВХ], ‘ ! ‘ ;Символ на экран

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

mov D1,2000 ;Смещение к середине экрана
mov byte ptr [DI] , ‘ ! ‘ ;Символ на экран

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

mov BX,2000 ;Смещение к середине экрана
mov byte ptr ES: [BX] , ‘ ! ‘ ;Символ на экран

можно использовать одну

mov byte ptr ES:2000,’!’ ;Выведем символ в середину экрана

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

mov AX,0B800h ;Сегментный адрес
mov ES,AX ;видеобуфера в ES
mov DI, 80*2*24 ;Смещение к нижней строке экрана
mov byte ptr ES: [DI] ,’О’ ;Символ на экран
mov byte ptr ES:2[DI],’К’ ;Запишем символ в следующую позицию
mov byte ptr ES:4[DI],’ ! ‘ ;Запишем символ в следующую позицию

В этом примере в качестве базового выбран регистр DI; в него заносится базовый относительный адрес памяти, в данном случае смещение в видеобуфере к началу последней строки экрана. Модификация этого адреса с целью получить смещение по строке экрана осуществляется с помощью констант 2 и 4, которые при вычислении процессором исполнительного адреса прибавляются к содержимому базового регистра DI.
Иногда можно встретиться с альтернативными обозначениями того же способа адресации, которые допускает ассемблер. Вместо, например, 4[ВХ] можно с таким же успехом написать [ВХ+4], 4+[ВХ] или [ВХ]+4. Такая неоднозначность языка ничего, кроме путаницы, не приносит, однако ее надо иметь в виду, так как с этими обозначениями можно столкнуться, например, рассматривая текст деассемблированной программы.
Рассмотрим теперь пример использования базовой адресации со смещением при обращении к стеку:

;Основная программа
push DS ;В стек загружаются значения
push ES ;трех регистров,
push SI ;передаваемых подпрограмме
call mysub ;Вызов подпрограммы mysub,
;использующей эти параметры
;Подпрограмма mysub
mov BP,SP ;Поместим в ВР текущий адрес вершины стека
mov АХ,2[ВР], ;Читаем в АХ последний параметр (SI)
mov ВХ,4[ВР] ;Читаем в ВХ предыдущий параметр (ES)
mov CX,6[BP] ;Читаем в СХ первый параметр (DS)

Здесь продемонстрирован классический прием чтения содержимого стека без извлечения из него этого содержимого. После того, как основная, программа сохранила в стеке три параметра, которые потребуются подпрограмме, командой call вызывается подпрограмма mysub. Эта команда сохраняет в стеке адрес возврата (адрес следующего за call предложения основной программы) и осуществляет переход на подпрограмму. Состояние стека при входе в подпрограмму приведено на рис. 2.15.

Рис.2.15. Состояние стека после загрузки в него трех параметров и перехода на подпрограмму

Если бы подпрограмма просто сняла со стека находящиеся там параметры, она первым делом изъяла бы из стека адрес возврата, и лишила бы себя возможности вернуться в основную программу (подробнее вопросы вызова подпрограммы и возврата из нее будут обсуждаться в последующих разделах). Поэтому в данном случае вместо команд pop удобнее воспользоваться командами mov. Подпрограмма копирует в ВР содержимое трех параметров и перехода на мое SP и использует затем этот адрес в качестве базового, модифицируя его с помощью базовой адресации со смещением.
Кстати, мы опять сталкиваемся здесь с той весьма обычной ситуацией, когда программист не имеет возможности обращаться по наглядным символическим адресам, которых в стеке, естественно, нет, а вынужден определять «вручную» смещения к интересующим его элементам стека. При этом необходимо учесть и алгоритм выполнения команды call, которая, сохраняя в стеке адрес возврата в основную программу, смещает указатель стека еще на одно слово.
В нашем фрагментарном примере мы не рассматриваем вопрос возврата в основную программу. Вдумчивый читатель мог также усомниться в правильности или, лучше сказать, в разумности текста подпрограммы. Ведь перенося параметры из стека в регистры общего назначения, подпрограмма затирает их исходное содержимое. Если же они не содержали ничего нужного, то ими можно было воспользоваться для передачи параметров в подпрограмму, а не связываться с мало наглядными операциями со стеком. Действительно, ради краткости мы опустили операции, практически необходимые в любой подпрограмме — сохранение в стеке (опять в стеке!) тех регистров, которые будут использоваться в подпрограмме. Кстати, это относится и к регистру ВР. В реальной подпрограмме эти действия следовало выполнить, что привело бы к изменению смещений при регистре ВХ, которые приняли бы значения (с учетом сохранения 4 регистров) 10, 12 и 14.
Во всех приведенных выше примерах регистр использовался для хранения базового адреса, а смещение, если оно требовалось, указывалось в виде константы. Возможна и обратная ситуация, когда в качестве смещения выступает адрес массива, а в регистре находится индекс адресуемого элемента в этом массиве. Рассмотрим относительно реальный пример такого рода.
Пусть нам надо заполнить массив из 10000 слов натуральным рядом чисел. Зарезервируем в сегменте данных место под этот массив, а в сегменте команд организуем цикл занесения в последовательные слова массива ряда нарастающих чисел. Нам придется воспользоваться несколькими новым командами (inc, add и loop), которые в дальнейшем будут рассмотрены более подробно.

;Сегмент данных
array dw 10000
;Сегмент команд
mov SI, 0 ;Начальное значение индекса элемента в массиве
mov АХ, 0 ;Первое число-заполнитель
mov СХ,10000;Число шагов в цикле (всегда в СХ)
fill: mov array[SI],AX ;Занесение числа в элемент массива
inc AX ;Инкремент числа-заполнителя
add SI,2 ;Смещение в массиве к следующему слову
loop fill ;Возврат на метку fill (СХ раз)

Цикл начинается с команды, помеченной меткой fill (правила образования имен меток такие же, как и для имен полей данных). В этой команде содержимое АХ, поначалу равное 0, переносится в ячейку памяти, адрес которой вычисляется, как сумма адреса массива array и содержимого индексного регистра SI, в котором в первом шаге никла тоже 0. В результате в первое слово массива заносится 0. Далее содержимое регистра АХ увеличивается на 1, содержимое регистра SI — на 2 (из-за того, что массив состоит из слов), и командой loop осуществляется переход на метку fill, после чего тело цикла повторяется при новых значениях регистров АХ и SI. Число шагов в цикле, отсчитываемое командой loop, определяется исходным содержимым регистра СХ.
Базово-индексная адресация. Адресуется память (байт или слово). Относительный адрес операнда определяется, как сумма содержимого следующих пар регистров:

[ВХ] [SI] (подразумевается DS:[BX][SI])
[ВХ][DI] (подразумевается DS:[BX][DI])
[ВР] [SI] (подразумевается SS:[BP][SI])
[ВР] [DI] (подразумевается SS:[BP][DI])

Это чрезвычайно распространенный способ адресации, особенно, при работе с массивами. В нем используются два регистра, при этом одним из них должен быть базовый (ВХ или ВР), а другим — индексный (SI или DI). Как правило, в одном из регистров находится адрес массива, а в другом — индекс в нем, при этом совершенно безразлично, в каком что. Трансформируем предыдущий пример, введя в него более эффективную базово-индексную адресацию.

;Сегмент данных
array dw 10000
;Сегмент команд
mov BX,offset array ;Базовый адрес массива в
;базовом регистре
mov SI, 0 ;Начальное значение индекса
;элемента в массиве
mov АХ, 0 ;Первое число-заполнитель
mov CX,10000 ;Число шагов в цикле
fill: mov [BX][SI],AX ;Отправим число в массив
inc AX ;Инкремент числа-заполнителя
add SI, 2 ;Смещение в массиве к следующему слову
loop fill ;На метку fill (CX раз)

Повышение эффективности достигается за счет того, что команда занесения числа в элемент массива оказывается короче (так как в нее не входит адрес массива) и выполняется быстрее, так как этот адрес не надо каждый раз считывать из памяти.
Базово-индексная адресация со смещением. Адресуется память (байт или слово). Относительный адрес операнда определяется как сумма содержимого двух регистров и смещения.
Это способ адресации является развитием предыдущего. В нем используются те же пары регистров, но полученный с их помощью результирующий адрес можно еще сместить на значение указанной в команде константы. Как и в случае базово-индексной адресации, константа может представлять собой индекс (и тогда в одном из регистров должен содержаться базовый адрес памяти), но может быть и базовым адресом. В последнем случае регистры могут использоваться для хранения составляющих индекса. Приведем формальный пример рассматриваемого режима адресации.
Пусть в сегменте данных определен массив из 24 байтов, в котором записаны коды латинских и русских символов верхнего ряда клавиатуры:

sims db «QWERTYUIOP{}’
db «ЙЦУКЕНПШЦЗХЪ’

Последовательность команд
mov BX,12 ;Число байтов в строке
mov SI, 6
mov DL,syms[BX][SI]

загрузит в регистр DL элемент с индексом 6 из второго ряда, т. е. код ASCII буквы Г. Тот же результат можно получить, загрузив в один из регистров не индекс, а адрес массива:

mov BX, off set sym
mov SI,6
mov DL, 12 [BX] [SI]

Использование оператора OFFSET для массива в сборке x86?

Задавать вопрос

спросил

Изменено 7 лет, 10 месяцев назад

Просмотрено 9к раз

Сейчас я прохожу 9Язык ассемблера 0013 для процессоров x86, 6-е издание , автор Кип Р. Ирвин. Это довольно приятно, но что-то меня смущает.

В начале книги показан следующий код:

 list BYTE 10,20,30,40
ListSize = ($ - список)
 

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

Однако книга позже делает:

 .data
массивB БАЙТ 10h,20h,30h
.код
mov esi, OFFSET arrayB
мов ал,[esi]
вкл ЕСИ
мов ал,[esi]
вкл ЕСИ
мов ал,[esi]
 

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

Так в чем же разница между использованием OFFSET для массива и простым вызовом переменной массива? Раньше меня убеждали, что простой вызов переменной массива также даст мне ее адрес.

  • массивы
  • сборка
  • x86
  • смещение

1

 . данные
Номер дд 3
.код
mov eax,Число
mov ebx,смещение Номер
 

EAX прочитает память по определенному адресу и сохранит число 3

EBX сохранит этот определенный адрес.

 mov ebx,смещение Номер
 

в данном случае эквивалентен

 lea ebx,Number
 

1

Зарегистрируйтесь или войдите в систему

Зарегистрируйтесь с помощью Google

Зарегистрироваться через Facebook

Зарегистрируйтесь, используя электронную почту и пароль

Опубликовать как гость

Электронная почта

Требуется, но не отображается

Опубликовать как гость

Электронная почта

Требуется, но не отображается

Конспект лекций — Глава 12

Конспект лекций — Глава 12 — Сборка
 ПРОЦЕСС СБОРКИ
--------------------
 -- компьютер понимает машинный код
 -- люди (и компиляторы) пишут на ассемблере
 сборка ------------------ машина
 источник --> | ассемблер | --> код
 код ------------------
ассемблер — это программа — очень детерминированная программа —
 он переводит каждую инструкцию в свой машинный код. Раньше между ними существовала прямая переписка.
 инструкции языка ассемблера и инструкции машинного языка.
 это уже не так. Ассемблеры в настоящее время сделаны больше
 мощный и может «переделывать» код.
 Pentium (основанный на 8086) имеет один к одному
 соответствие между инструкциями на языке ассемблера и
 инструкции машинного языка.
СБОРКА
---------
 работа сборщика состоит в том, чтобы
 1. назначить адреса
 2. сгенерировать машинный код
 ассемблер будет
 -- назначить адреса
 -- сгенерировать машинный код
 -- сгенерировать образ того, как должна выглядеть память для
 программа, которую нужно выполнить.
 простой ассемблер сделает 2 полных прохода по данным
 (исходный код) для выполнения этой задачи.
 проход 1: создать полную ТАБЛИЦУ СИМВОЛОВ
генерировать машинный код для инструкций, отличных от
ветки, прыжки, зов, леа и т. д. (эти инструкции
которые полагаются на адрес для своего машинного кода).
 проход 2: полный машинный код для инструкций, которые не получили
закончился в проходе 1. ассемблер начинается в начале исходного кода программы,
и СКАНЫ. Он ищет
 -- директивы (.data .code .stack .486 и т.д.)
 -- инструкции
 ВАЖНЫЙ:
 есть отдельные области памяти для данных и инструкций.
 ассемблер размещает их в ПОСЛЕДОВАТЕЛЬНОМ ПОРЯДКЕ по мере сканирования
 через исходный код программы.
 начальные адреса фиксированы -- будет собрана ЛЮБАЯ программа
 иметь данные и инструкции, которые начинаются с одного и того же адреса.
Генерация машинного кода для инструкции
---------------------------------------------------------
Это сложно из-за большого разнообразия адресации.
режимов в сочетании с большим количеством инструкций.
Чаще всего машинный код инструкции состоит из
 1) 8-битный код операции
 (выбор опкода будет несколько зависеть от адресации
 режимы, используемые для операндов)
 с последующим
 2) один или несколько байтов, описывающих режимы адресации для
 операнды.
ПРИМЕР ИНСТРУКЦИИ:
 добавить еакс, 24
 Найдите Приложение C. Именно там находится весь этот машинный код. материал указан.
 Для инструкции добавления в таблице перечислены:
 добавить рег, р/м 03 /р
об/м, рег 01 /об
р/м, иммед 81/0 id 
 Единственный, который будет соответствовать типам операндов, это
 третий в списке:
 добавить r/m, immed 81/0 id
 Итак, это тот, который мы выбираем.
 81 — это 8-битный код операции.
 За опкодом следует информация об адресации
 режим 2-х операндов (у сложения всегда ровно 2 операнда
 и режим адресации каждого должен быть явно указан)
 Обычно оба операнда описываются (точно)
 кодирование одного байта, который Intel называет байтом ModR/M.
 В описании машинного кода (идентификатор 81/0)
 Символ /0 описывает часть этого байта ModR/M.
 /0 можно найти в таблице пояснений на странице 352.
В нем говорится, что поле reg байта ModR/M равно 000.
Байт ModR/M:
 Этот байт описывает режим адресации операндов.
 Он разделен на 3 поля следующим образом
 БИТЫ 7 6 5 4 3 2 1 0
мод reg/opcode r/m
 Для этого примера инструкции биты 5, 4, 3 установлены на 000,
 давать
 БИТЫ 7 6 5 4 3 2 1 0
мод reg/opcode r/m
0 0 0
 Это говорит о том, что второй операнд является немедленным. Описание первого операнда будет сделано с помощью
 поля режима и r/m байта ModR/M.
 Посмотрите в таблице (стр. 353), чтобы найти режим регистрации,
 используя регистр EAX (поскольку он есть в примере инструкции).
 В таблице указано, что Mod равен 11, R/M равен 000, что дает
 БИТЫ 7 6 5 4 3 2 1 0
мод reg/opcode r/m
 1 1 0 0 0 0 0 0
 Последний шаг — получить часть идентификатора. Из объяснения
 table (стр. 352), id описан как 32-битный непосредственный.
 Поэтому id соответствует 32-битному представлению с дополнением до двух.
 стоимости 24.
 Это 0000 0000 0000 0000 0000 0000 0001 1000
 В шестнадцатеричном формате это 0x00000018.
Собрав все это воедино, мы получаем машинный код для
пример инструкции ( добавить eax, 24 )
Обратите внимание, что все в шестнадцатеричном формате.
И непосредственные значения указываются младшим значащим байтом первым!
 код операции
 81
 ModR/M байт
 с0
 немедленный
 18
 00
 00
 00
Написано слева направо:
 81 с0 18 00 00 00
Еще один пример генерации машинного кода. Машинный код для инструкции Pentium 
 dec двойное слово ptr [EDX]
 Со страницы 349, нам нужна форма инструкции декремента
 дек р/м фф /1
 Код операции ff, и он описывает, что будет один операнд,
 и это общего вида. /1 говорит, что
 Поле Reg байта ModR/M будет равно 001.
 БИТЫ 7 6 5 4 3 2 1 0
Mod Reg/Opcode R/M
0 0 1
 В таблице на стр. 353 описаны поля Mod и R/M.
 Найдите режим прямой адресации регистра, используя регистр EDX
 в таблице. Это дает Mod 00 и R/M 010.
 БИТЫ 7 6 5 4 3 2 1 0
Mod Reg/Opcode R/M
 0 0 0 0 1 0 1 0 
 В шестнадцатеричном формате это 0a.
 Теперь машинный код завершен: ff 0a.
БОЛЬШОЙ ПРИМЕР:
 .данные
а1 дд 4
а2 дд?
а3 дд 5 дуп(0)
 .код
главная: mov ecx, 20
 мов акс, 15
мов эдкс, 0
jz target_label
loop1: добавить edx, eax
 имул [эбп + 8]
дек экх
 джг петля1
target_label:
 сделанный
Первый шаг: сборка раздела данных.
При сканировании исходного кода считывается токен .data.
Это директива, которая сообщает ассемблеру, что последующее
выделяется в разделе данных программы. Помните, что директива — это «команда» для ассемблера.
 о том, как собрать исходный код.
Следующая встреченная лексема — это метка a1.
Этот символ (метка) еще не находится в своей таблице символов, поэтому
ассемблер присваивает адрес и помещает его в символ
стол.
 Помните, ассемблер назначает первый доступный адрес
 в разделе данных.
 Таблица символов
 адрес символа
 ---------------------
 a1 0040 0000 (я придумал этот адрес, потому что нам нужно
начальный адрес раздела данных.)
Следующий токен — dd. Это позволяет ассемблеру знать, что нужно выделить
одно двойное слово пробела по текущему адресу.
Следующий токен равен 4. Он сообщает ассемблеру, что значение
выделенное пространство должно иметь значение 4.
Следующая строка делает то же самое,
 размещение a2 в таблице символов по следующему доступному адресу
 (0x0040 0004)
 выделение 1 двойного слова
 не помещая что-то конкретное в выделенное пространство
Когда закончите с разделом данных, у нас будет
 таблица символов
 адрес символа
 ---------------------
 а1 0040 0000
 а2 0040 0004
 а3 0040 0008
 0040 000с
 0040 0010
 0040 0014
 0040 0018
 0040 001c (следующий доступный адрес в
раздел данных. НЕ ЧАСТЬ ТАБЛИЦЫ.)
и,
 карта памяти раздела данных
адрес содержание примечания
шестигранник
0040 0000 0000 0004 для а1
0040 0004 0000 0000 для a2 (по умолчанию 0)
0040 0008 0000 0000 5 двойных слов для a3
0040 000с 0000 0000
0040 0010 0000 0000
0040 0014 0000 0000
0040 0018 0000 0000
При встрече с директивой .code ассемблер знает, что
следующие адреса, которые он назначает, будут находиться в разделе кода
программы (отдельно от данных).
Предположим, что код будет собран таким образом, что первый
инструкция размещается по адресу 0x0000 0000.
Код (повторяется):
 .код
главная: mov ecx, 20
 мов акс, 15
движение акс, 0
jz target_label
loop1: добавить edx, eax
 имул [эбп + 8]
дек экх
 джг петля1
target_label:
 сделанный
Первый токен, полученный после директивы .code
является основным ярлыком. (Как и для ВСЕХ символов) ассемблер
проверяет, находится ли этот символ уже в таблице символов.
Это не так, поэтому ассемблер назначает первый доступный
адрес и помещает его в таблицу символов. таблица символов
 адрес символа
 ---------------------
 а1 0040 0000
 а2 0040 0004
 а3 0040 0008
 0040 000с
 0040 0010
 0040 0014
 0040 0018
 0040 001c (следующий доступный адрес в
раздел данных. НЕ ЧАСТЬ ТАБЛИЦЫ.)
 основной 0000 0000
Далее ассемблер подбирает токен mov. Он знает, что
это инструкция, и читает остальную часть инструкции
чтобы сгенерировать машинный код для этой инструкции.
 мов экх, 20
 мов рег, immed b8 + rd
 Байт ModR/M не требуется, так как регистр встроен
 в байт кода операции, и немедленное должно следовать.
 rd (из таблицы на стр. 352) равно 1, b8+1=b9Непосредственный 0x00000014.
 Итак, машинный код будет
 б9 14 00 00 00
 Эти 5 байт размещаются по адресу 0x0000 0000, и
 следующий доступный адрес для инструкции становится
 0x0000 0005.  Ассемблер готов к следующему токену. Это будет
вторая инструкция mov в программе. Он знает, что
это инструкция, и читает остальную часть инструкции
чтобы сгенерировать машинный код для этой инструкции. мов акс, 15
 мов рег, immed b8 + rd
 Байт ModR/M не требуется, так как регистр встроен
 в байт кода операции, и немедленное должно следовать.
 rd (из таблицы на стр. 352) равно 0, b8+0=b8
 Непосредственный 0x0000000f.
 Итак, машинный код будет
 б8 10 00 00 00
 Эти 5 байт размещаются по адресу 0x0000 0005, и
 следующий доступный адрес для инструкции становится
 0x0000 000а.  Ассемблер готов к следующему токену. Это будет
инструкция третьего хода в программе. Он знает, что
это инструкция, и читает остальную часть инструкции
чтобы сгенерировать машинный код для этой инструкции.
 мов эдкс, 0
 мов рег, immed b8 + rd
 Байт ModR/M не требуется, так как регистр встроен
 в байт кода операции, и немедленное должно следовать.
 rd (из таблицы на стр. 352) равно 2, b8+2=ba
 Непосредственный 0x00000000.
 Итак, машинный код будет
 ба 00 00 00 00
 Эти 5 байт размещаются по адресу 0x0000 000a, и
 следующий доступный адрес для инструкции становится
 0x0000 000f. Ассемблер готов к следующему токену. Это будет
jz инструкция в программе. Он знает, что
это инструкция, и читает остальную часть инструкции
чтобы сгенерировать машинный код для этой инструкции.
 jz target_label
 jz rel32 0f 84 "кд"
 «cd» — это 32-битное смещение кода. Это должно быть
 разница между тем, каким будет ПК при выполнении этого
 code и адрес, назначенный для метки target_label.
 Проблема в том, что target_label еще не
 присвоен адрес. Итак, ассемблеру нужно
 подождите, пока выяснится часть смещения 32-битного кода
 этой инструкции до второго прохода ассемблера.
 Ассемблер знает, что эта инструкция будет
 ровно 6 байт, так что можно продолжить сборку
 по адресу 0x0000 0015.
Карта памяти текстового раздела на данный момент:
 карта памяти текстового раздела
содержимое адреса  0000 0000 б914 00 00 00
0000 0005 b8 0f 00 00 00
0000 000а ба 00 00 00 00
0000 000f 0f 84 ?? ?? ?? ??
0000 0015
Ассемблер готов к следующему токену. Это будет
метка петля1. Ассемблер проверяет, находится ли этот символ в
таблица символов. Это не так, поэтому ассемблер присваивает адрес
и помещает символ в таблицу.
 таблица символов
 адрес символа
 ---------------------
 а1 0040 0000
 а2 0040 0004
 а3 0040 0008
 0040 000с
 0040 0010
 0040 0014
 0040 0018
 0040 001c (следующий доступный адрес в
раздел данных. НЕ ЧАСТЬ ТАБЛИЦЫ.)
 основной 0000 0000
 петля1 0000 0015
Ассемблер готов к следующему токену. Это будет
добавить инструкцию в программу. Он знает, что
это инструкция, и читает остальную часть инструкции
чтобы сгенерировать машинный код для этой инструкции.
 добавить edx, eax
добавить рег, р/м 03 /р
или
добавить р/м, reg 01 /р
Мне все равно, какой из них выбран. Они
одинаковая длина. Выбрал первый.
/r означает, что байт ModR/M имеет регистр
операнд и операнд R/M.
 БИТЫ 7 6 5 4 3 2 1 0
Mod Reg/Opcode R/M
 1 1 0 1 0 0 0 0 
 В шестнадцатеричном формате это d0. Итак, машинный код инструкции — 03 d0.
 Эти 2 байта размещаются по адресу 0x0000 0015.
 Следующий доступный адрес для кода будет 0x0000 0017.
Карта памяти текстового раздела на данный момент:
 карта памяти текстового раздела
содержимое адреса  0000 0000 б914 00 00 00
0000 0005 b8 0f 00 00 00
0000 000а ба 00 00 00 00
0000 000f 0f 84 ?? ?? ?? ??
0000 0015 03 д0
0000 0017
К следующей инструкции.
 имул [эбп + 8]
импульс р/м f7/5
/5 означает, что байт ModR/M имеет регистр
поле 101
Режим адресации для [ebp + 8] находится под disp32[EBP]
в таблице на стр. 353.
 БИТЫ 7 6 5 4 3 2 1 0
Mod Reg/Opcode R/M
 1 0 1 0 1 1 0 1 
 В шестнадцатеричном формате это объявление.
 32-битное смещение следует за байтом ModR/M. Это содержит
 32-битное кодирование с дополнением до 2 для значения 8.
 0x 00 00 00 08
 Машинный код этой инструкции — f7 ad 08 00 00 00.
 Эти 6 байт размещаются по адресу 0x0000 0017.
 Следующий доступный адрес для кода будет 0x0000 0019..
Карта памяти текстового раздела на данный момент:
 карта памяти раздела кода
содержимое адреса  0000 0000 b9 14 00 00 00
0000 0005 b8 10 00 00 00
0000 000а ба 00 00 00 00
0000 000f 0f 84 ?? ?? ?? ??
0000 0015 03 д0
0000 0017 f7 объявление 08 00 00 00
0000 001д
Следующая инструкция проста. дек экх
декабрь обр. 48 + рд
 rd равен 1 для ecx. Таким образом, машинный код — это один байт 49.
 карта памяти раздела кода
содержимое адреса  0000 0000 b9 14 00 00 00
0000 0005 b8 10 00 00 00
0000 000а ба 00 00 00 00
0000 000f 0f 84 ?? ?? ?? ??
0000 0015 03 д0
0000 0017 f7 объявление 08 00 00 00
0000 001д 4

001е За командой декремента следует джг петля1 jg rel32 0f 8f "кд" Как и другая инструкция управления: "cd" - это 32-битное смещение кода. Это должно быть разница между тем, каким будет ПК при выполнении этого code и адрес, назначенный для метки target_label. Ассемблер знает, что эта инструкция будет ровно 6 байт. Чтобы вычислить "cd", во время выполнения (для принятой управляющей инструкции): содержимое поля PC + offset --> PC ПК указывает на инструкцию после управляющей инструкции при добавлении смещения. во время сборки: смещение байта = целевой адрес - ( адрес инструкции после условного управляющий адрес ) = адрес петли 1 - (6 + 0x0000 001e) (взято из таблицы символов) = 0x0000 0015 - 0x0000 0024 Обратите внимание, что это будет отрицательное число.

То есть OK -- сгенерировать 32-битное значение дополнения до 2. 0000 0000 0000 0000 0000 0000 0001 0101 - 0000 0000 0000 0000 0000 0000 0010 0100 --------------------------------------------------------- становится 0000 0000 0000 0000 0000 0000 0001 0101 + 1111 1111 1111 1111 1111 1111 1101 1100 --------------------------------------------------------- 1111 1111 1111 1111 1111 1111 1111 0001 в шестнадцатеричном формате 0x ff ff ff f1 это значение "cd", давая машинный код 0f 8f f1 ff ff ff (Помните, что младший байт идет первым.) Снова, карта памяти раздела кода (пока) содержимое адреса 0000 0000 б914 00 00 00 0000 0005 b8 10 00 00 00 0000 000а ба 00 00 00 00 0000 000f 0f 84 ?? ?? ?? ?? 0000 0015 03 д0 0000 0017 f7 объявление 80 00 00 00 0000 001д 49 0000 001e 0f 8f f1 ff ff ff 0000 0024 Последнее, о чем мы будем беспокоиться в таблице, это следующая метка: target_addr Он помещается в таблицу символов в следующий доступный адрес, 0x0000 0024. Теперь у нас есть заполненная таблица символов: адрес символа --------------------- а1 0040 0000 а2 0040 0004 а3 0040 0008 0040 000с 0040 0010 0040 0014 0040 0018 0040 001c (следующий доступный адрес в раздел данных. НЕ ЧАСТЬ ТАБЛИЦЫ.) основной 0000 0000 петля1 0000 0015 target_addr0000 0024 После того, как этот первый проход ассемблера выполнен, ВСЕ ярлыкам были даны адреса. Во время этого второго прохода ассемблера все оставшиеся код, оставшийся для завершения, завершен. Для этого примера фрагмент кода, то есть инструкция jz по адресу 0x0000 000f. Остается только вычислить смещение. Это работает так же, как расчет для другой команды управления. смещение байта = целевой адрес - ( адрес инструкции после условного управляющий адрес ) = адрес целевой_адрес - (6 + 0x0000 000f) (взято из таблицы символов) = 0x0000 0024 - 0x0000 0015 0000 0000 0000 0000 0000 0000 0010 0100 - 0000 0000 0000 0000 0000 0000 0001 0101 --------------------------------------------------------- 0000 0000 0000 0000 0000 0000 0000 1111 Обратите внимание, что это смещение является положительным числом.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *