Более подробное обсуждение режимов адресации (16, 32, 64 бит) см. В руководстве Agner Fog «Оптимизация сборки» , раздел 3.3. В этом руководстве гораздо больше подробностей, чем в этом ответе, среди прочего, для перемещения символов и / или 32-битного независимого от позиции кода.
И, конечно же, в руководствах Intel и AMD есть целые разделы с подробностями о кодировании ModRM (и дополнительных байтов SIB и disp8 / disp32), что дает понять, что кодируется и почему существуют ограничения.
См. Также: таблицу синтаксиса AT&T (GNU) и синтаксиса NASM для различных режимов адресации, включая косвенные переходы / вызовы. Также см. Коллекцию ссылок внизу этого ответа.
x86 (32- и 64-битный) имеет несколько режимов адресации на выбор. Все они имеют форму:
[base_reg + index_reg*scale + displacement] ; or a subset of this
[RIP + displacement] ; or RIP-relative: 64bit only. No index reg is allowed
(где масштаб равен 1, 2, 4 или 8, а смещение - 32-битная константа со знаком). Все остальные формы (кроме относящихся к RIP) являются ее подмножествами, в которых не учитывается один или несколько компонентов. Это означает, что вам не нужен обнуленный index_reg
, например, для доступа к [rsi]
.
В исходном коде asm не имеет значения, в каком порядке вы пишете: [5 + rax + rsp + 15*4 + MY_ASSEMBLER_MACRO*2]
работает нормально. (Все вычисления с константами происходят во время сборки, что приводит к единственному постоянному смещению.)
Все регистры должны быть одинакового размера. И того же размера, что и режим, в котором вы находитесь, если только вы используете альтернативный размер адреса, требующий дополнительного байта префикса. Узкие указатели редко используются за пределами x32 ABI (ILP32 в длинном режиме), где вы, возможно, захотите игнорировать верхние 32 бита регистра, например вместо использования movsxd
для расширения знака 32-битного, возможно, отрицательного смещения в регистре до 64-битной ширины указателя.
Если вы хотите использовать al
в качестве индекса массива, для например, вам нужно обнулить его или расширить знаком до ширины указателя. (Иногда возможно, что верхние биты rax
уже обнулены, прежде чем возиться с байтовыми регистрами, и это хороший способ добиться этого.)
Ограничения отражают то, что кодируется в машинном коде, как обычно для языка ассемблера. Коэффициент масштабирования - это 2-битный счетчик сдвига. Байты ModRM (и необязательный SIB) могут кодировать до 2 регистров, но не более, и не имеют никаких режимов, которые вычитают регистры, только добавляют. Базой может быть любой регистр. Индексом может быть любой регистр, кроме ESP / RSP. См. rbp not allowed as SIB base? для получения информации о кодировке, например, почему [rsp]
всегда нужен байт SIB.
Все возможные подмножества общего случая могут быть кодированы, кроме тех, которые используют e/rsp*scale
(очевидно, бесполезно в «нормальном» коде, который всегда сохраняет указатель на стековую память в esp
).
Обычно размер кода кодировок составляет:
- 1B для однорегистровых режимов (mod / rm (Mode / Register-or-memory))
- 2B для двух регистровых режимов (mod / rm + байт SIB (Scale Index Base))
- смещение может составлять 0, 1 или 4 байта (знаковое расширение до 32 или 64, в зависимости от размера адреса). Таким образом, смещения из
[-128 to +127]
могут использовать более компактную кодировку disp8
, экономя 3 байта по сравнению с disp32
.
ModRM присутствует всегда, и его биты сигнализируют, присутствует ли также SIB. Аналогично disp8 / disp32. Исключения размера кода:
[reg*scale]
сам по себе может быть закодирован только с 32-битным смещением (которое, конечно, может быть нулевым). Умные ассемблеры работают над этим, кодируя lea eax, [rdx*2]
как lea eax, [rdx + rdx]
, но этот трюк работает только для масштабирования на 2. В любом случае требуется байт SIB в дополнение к ModRM.
Невозможно закодировать e/rbp
или r13
в качестве базового регистра без байта смещения, поэтому [ebp]
кодируется как [ebp + byte 0]
. Кодировки без смещения с ebp
в качестве базового регистра вместо этого означают, что базового регистра нет (например, для [disp + reg*scale]
).
[e/rsp]
требуется байт SIB, даже если нет индексного регистра. (есть ли смещение). Кодировка mod / rm, которая будет указывать [rsp]
, вместо этого означает, что существует байт SIB.
См. Таблицу 2-5 в справочном руководстве Intel и в соответствующем разделе, где подробно описаны особые случаи. (Они одинаковы в 32-битном и 64-битном режимах. Добавление относительной RIP-кодировки не противоречило любой другой кодировке, даже без префикса REX.)
Для повышения производительности обычно не стоит тратить дополнительную инструкцию только на то, чтобы получить меньший машинный код x86. На процессорах Intel с кеш-памятью uop он меньше, чем L1 I $, и является более ценным ресурсом. Как правило, более важно свести к минимуму число мопов слитных доменов.
Как они используются
(Этот вопрос был помечен как MASM, но часть этого ответа говорит о версии синтаксиса Intel для NASM, особенно там, где они различаются для адресации относительно RIP x86-64. Синтаксис AT&T не рассматривается, но имейте в виду, что это просто еще один синтаксис для того же машинный код, поэтому ограничения те же.)
Эта таблица не совсем соответствует аппаратным кодировкам возможных режимов адресации, поскольку я различаю использование метки (например, глобальных или статических данных) и использование небольшого постоянного смещения. Итак, я рассказываю о режимах аппаратной адресации + поддержке компоновщика символов.
(Примечание: обычно вам нужно movzx eax, byte [esi]
или movsx
, когда источник является байтом, но mov al, byte_src
действительно ассемблируется и часто встречается в старом коде, сливаясь с младшим байтом EAX / RAX. См. Почему GCC не использует частичные регистры? и Как изолировать элементы массива байтов и слов в 64-битном регистре)
Если у вас есть int*
, часто вы использовали бы коэффициент масштабирования для масштабирования индекса по размеру элемента массива, если у вас есть индекс элемента вместо байтового смещения. (Предпочитайте смещения байтов или указатели, чтобы избежать режимов индексированной адресации по причинам размера кода и производительности в некоторых случаях, особенно на процессорах Intel, где это может повредить микрослияние). Но вы можете делать и другие вещи.
Если у вас есть указатель char array*
в esi
:
mov al, esi
: недействительно, не соберется. Без квадратных скобок это вообще не груз. Это ошибка, потому что регистры разного размера.
mov al, [esi]
загружает указанный байт, то есть array[0]
или *array
.
mov al, [esi + ecx]
загружает array[ecx]
.
mov al, [esi + 10]
загружает array[10]
.
mov al, [esi + ecx*8 + 200]
загружает array[ecx*8 + 200]
mov al, [global_array + 10]
загружается из global_array[10]
. В 64-битном режиме это может и должен быть относительный адрес RIP. Рекомендуется использовать NASM DEFAULT REL
, чтобы по умолчанию генерировать адреса, относящиеся к RIP, вместо того, чтобы всегда использовать [rel global_array + 10]
. Думаю, MASM делает это по умолчанию. Невозможно напрямую использовать индексный регистр с относительным адресом RIP. Обычный метод - lea rax, [global_array]
mov al, [rax + rcx*8 + 10]
или аналогичный.
См. Как работают ли ссылки на относительные переменные RIP, такие как [RIP + _a] в синтаксисе Intel GAS x86-64? для получения дополнительных сведений и синтаксиса для GAS .intel_syntax
, NASM и синтаксиса GAS AT&T.
mov al, [global_array + ecx + edx*2 + 10]
загружается из global_array[ecx + edx*2 + 10]
Очевидно, вы можете индексировать статический / глобальный массив с помощью одного регистра. Возможен даже 2D-массив с использованием двух отдельных регистров. (предварительное масштабирование с дополнительной инструкцией для масштабных коэффициентов, отличных от 2, 4 или 8). Обратите внимание, что математика global_array + 10
выполняется во время ссылки. Объектный файл (вывод ассемблера, ввод компоновщика) информирует компоновщик о добавлении +10 к окончательному абсолютному адресу, чтобы поместить правильное 4-байтовое смещение в исполняемый файл (вывод компоновщика). Вот почему вы не можете использовать произвольные выражения для констант времени компоновки, которые не являются константами времени сборки (например, символ адреса).
В 64-битном режиме для этого по-прежнему требуется global_array
как 32-битный абсолютный адрес для части disp32
, которая работает только в позиционно-зависимый исполняемый файл Linux или largeaddressaware = no Windows.
mov al, 0ABh
Совсем не загрузка, а немедленная константа, которая хранилась внутри инструкции. (Обратите внимание, что вам нужно поставить префикс 0
, чтобы ассемблер знал, что это константа, а не символ. Некоторые ассемблеры также принимают 0xAB
, а некоторые из них не принимают 0ABh
: подробнее).
Вы можете использовать символ как непосредственную константу, чтобы получить адрес в регистре:
- NASM:
mov esi, global_array
assembles into a mov esi, imm32
that puts the address into esi.
- MASM:
mov esi, OFFSET global_array
нужно сделать то же самое.
- MASM:
mov esi, global_array
собирается в нагрузку: mov esi, dword [global_array]
.
In 64-bit mode, the standard way to put a symbol address into a register is a RIP-relative LEA. Syntax varies by assembler. MASM does it by default. NASM needs a default rel
directive, or [rel global_array]
. GAS needs it explicitly in every addressing mode. How to load address of function or label into register in GNU Assembler. mov r64, imm64
is usually supported too, for 64-bit absolute addressing, but is normally the slowest option (code size creates front-end bottlenecks). mov rdi, format_string
/ call printf
typically works in NASM, but is not efficient.
В качестве оптимизации, когда адреса могут быть представлены как 32-битные абсолютные (вместо смещения rel32 от текущей позиции), mov reg, imm32
по-прежнему оптимален, как и в 32-битном коде. (Исполняемый файл Linux без PIE или Windows с LargeAddressAware = no). Но обратите внимание, что в 32-битном режиме lea eax, [array]
не эффективен: он тратит байт размера кода (ModRM + absolute disp32) и не может работать на таком количестве портов выполнения, как mov eax, imm32
. 32-битный режим не имеет относительной адресации RIP.
Обратите внимание, что OS X загружает весь код по адресу за пределами младших 32 бит, поэтому 32-битная абсолютная адресация неприменима. Позиционно-независимый код не необходим для исполняемых файлов, но вы можете это сделать, потому что 64-битная абсолютная адресация менее эффективна, чем относительная RIP. Формат объектного файла macho64 не поддерживает перемещения для 32-разрядных абсолютных адресов. как это делает Linux ELF. Убедитесь, что имя метки не используется в качестве 32-битной константы времени компиляции. Эффективный адрес, такой как [global_array + constant]
, подходит, потому что он может быть преобразован в режим адресации, относящейся к RIP. Но [global_array + rcx]
не допускается, потому что RIP не может использоваться с другими регистрами, поэтому он должен быть собран с абсолютным адресом global_array
, жестко запрограммированным как 32-битное смещение (, который будет расширен знаком до 64b).
Любые и все эти режимы адресации можно использовать с LEA
для выполнения целочисленных вычислений с бонус не влияет на флаги, независимо от того, является ли это действительным адресом. Использование LEA для значений, не являющихся адресами / указатели?
[esi*4 + 10]
обычно используется только с LEA (если смещение не является символом, а не маленькой константой). В машинном коде нет кодирования только для масштабированного регистра, поэтому [esi*4]
должен быть ассемблирован в [esi*4 + 0]
с 4 байтами нулей для 32-битного смещения. По-прежнему часто стоит копировать + сдвиг в одной инструкции вместо более короткого mov + shl, потому что обычно пропускная способность uop является более узким местом, чем размер кода, особенно на процессорах с кешем декодированного uop.
Вы можете указать переопределение сегмента, например mov al, fs:[esi]
(синтаксис NASM). Переопределение сегмента просто добавляет префиксный байт перед обычной кодировкой. Все остальное остается прежним, с тем же синтаксисом.
Вы даже можете использовать переопределения сегментов с относительной адресацией RIP. 32-битная абсолютная адресация требует для кодирования на один байт больше, чем относительная RIP, поэтому mov eax, fs:[0]
может наиболее эффективно кодироваться с использованием относительного смещения, которое дает известный абсолютный адрес. т.е. выберите rel32, чтобы RIP + rel32 = 0. YASM сделает это с mov ecx, [fs: rel 0]
, но NASM всегда использует абсолютную адресацию disp32, игнорируя спецификатор rel
. Я не тестировал MASM или газ.
Если размер операнда неоднозначен (например, в инструкции с непосредственным операндом и операндом из памяти), используйте byte
/ word
/ dword
/ qword
, чтобы указать:
mov dword [rsi + 10], 123 ; NASM
mov dword ptr [rsi + 10], 123 ; MASM and GNU .intex_syntax noprefix
movl $123, 10(%rsi) # GNU(AT&T): operand size from mnemonic suffix
См. документацию yasm для эффективных адресов синтаксиса NASM и / или раздел wikipedia x86 о режимах адресации.
На вики-странице указано, что разрешено в 16-битном режиме. Вот еще одна «шпаргалка» для 32-битных режимов адресации.
16-битные режимы адресации
Размер адреса 16 бит не может использовать байт SIB, поэтому все режимы адресации с одним и двумя регистрами кодируются в один байт mod / rm. reg1
может быть BX или BP, а reg2
может быть SI или DI (или вы можете использовать любой из этих 4 регистров самостоятельно). Масштабирование недоступно. 16-битный код устарел по многим причинам, в том числе по этой, и не стоит изучать, если вам не нужно.
Обратите внимание, что 16-битные ограничения применяются в 32-битном коде, когда используется префикс размера адреса, поэтому 16-битная LEA-математика очень ограничительна. Однако вы можете обойти это: lea eax, [edx + ecx*2]
устанавливает ax = dx + cx*2
, потому что мусор в верхних битах исходных регистров не действует.
Также имеется более подробное руководство по режимам адресации для 16 бит а>. 16-разрядная версия имеет ограниченный набор режимов адресации (действительны только несколько регистров и нет масштабных коэффициентов), но вы можете прочитать ее, чтобы понять некоторые основы того, как процессоры x86 используют адреса, потому что некоторые из них не изменились за 32-битный режим.
Похожие темы:
Многие из них также указаны выше, но не все.
- См. Страницу SO x86 tag wiki для получения ссылок на документы и справочные руководства, включая руководства Intel.
- Intel-синтаксис и Синтаксис AT&T вики-страницы посвящены различиям между ними и (для Intel) различным вариантам синтаксиса Intel.
- Микро-слияние и режимы адресации влияние режимов индексированной адресации на производительность на семействе Sandybridge: гашение, за исключением редких случаев.
- Mach-O 64 -битный формат не поддерживает 32-битные абсолютные адреса. NASM Accessing Array 64-разрядная адресация MacOS
- 32-разрядные абсолютные адреса больше не разрешены в x86 -64 Linux? (Linux PIE против исполняемых файлов, зависимых от позиции)
- Как сделать Относительные ссылки на переменные RIP, такие как [RIP + _a] в x86-64 GAS, синтаксис Intel работает? (также относится к NASM и GAS AT&T)
- Как загрузить адрес функции или пометьте регистр в GNU Assembler, как эффективно помещать адреса символов в регистры вместо того, чтобы просто использовать их в режиме адресации напрямую.
- Почему адрес статических переменных является относительным к указателю инструкции? и Почему этот MOVSS в инструкциях используется относительная адресация RIP? - относительная RIP-адресация - это стандартный эффективный способ загрузки / сохранения статических данных, который работает, даже если данные находятся в другом разделе кода (из-за того, как работают компоновщики / загрузчики программ относительное смещение остается постоянным, даже если программа / библиотека в целом не зависят от позиции.)
person
Peter Cordes
schedule
03.12.2015