Это вопрос о префиксах переопределения размера операнда в архитектуре x86-64 (AMD64).
Вот набор инструкций ассемблера (nasm) и их кодировки; под новым я имею в виду регистры r8, …, r15:
67: address-size override prefix
|
| 4x: operand-size override prefix
| |
; Assembler ; | Dst operand | Src operand | -- --
mov eax,ecx ; | 32-bit | 32-bit | 89 C8 |
mov r8d,ecx ; | 32-bit new | 32-bit | 41 89 C8 |
mov eax,r9d ; | 32-bit | 32-bit new | 44 89 C8 |
mov r8d,r9d ; | 32-bit new | 32-bit new | 45 89 C8 |
mov rax,rcx ; | 64-bit | 64-bit | 48 89 C8 |
mov r8,rcx ; | 64-bit new | 64-bit | 49 89 C8 |
mov rax,r9 ; | 64-bit | 64-bit new | 4C 89 C8 |
mov r8,r9 ; | 64-bit new | 64-bit new | 4D 89 C8 |
lea eax,[ecx] ; | 32-bit | 32-bit | 67 8D 01 |
lea r8d,[ecx] ; | 32-bit new | 32-bit | 67 44 8D 01 |
lea eax,[r9d] ; | 32-bit | 32-bit new | 67 41 8D 01 |
lea r8d,[r9d] ; | 32-bit new | 32-bit new | 67 45 8D 01 |
lea rax,[rcx] ; | 64-bit | 64-bit | 48 8D 01 |
lea r8,[rcx] ; | 64-bit new | 64-bit | 4C 8D 01 |
lea rax,[r9] ; | 64-bit | 64-bit new | 49 8D 01 |
lea r8,[r9] ; | 64-bit new | 64-bit new | 4D 8D 01 |
push rax ; | | 64-bit | 50 |
push r8 ; | | 64-bit new | 41 50 |
Изучая эти и те же инструкции с другими регистрами, я пришел к следующему. Между «старым» и «новым» регистрами существует пара. Не исчерпывающе:
AX <--> R8
CX <--> R9
DX <--> R10
BX <--> R11
BP <--> R13
Игнорируя префикс размера, байты инструкции относятся не к конкретным регистрам, а к парам регистров. В качестве примера: байты 89 C8 указывают команду mov из источника, который является либо ecx, rcx, r9d, либо r9, в пункт назначения, который является либо eax, rax, r8d, либо r8. Учитывая, что операнды должны быть 32- или 64-битными, существует восемь допустимых комбинаций. Префикс переопределения размера операнда (или его отсутствие) указывает, какая из этих комбинаций является предполагаемой. Например, если префикс присутствует и равен 44, то исходный операнд должен быть 32-битным новым регистром (в этом примере затем сворачиваться до r9d), а местом назначения должен быть 32-битный старый регистр (здесь тогда сигнализация eax).
Возможно, я не совсем правильно понял, но я думаю, что уловил суть. Тогда может показаться, что префиксы переопределения размера операнда переопределяют тот факт, что без них инструкция использовала бы 32-битные «старые» операнды.
Но наверняка есть кое-что, что ускользнет от меня, иначе: какой смысл тогда говорить о «версии x86-64 с размером операнда по умолчанию 64-бит» (например, здесь)?
Или есть способ, работающий на 64-битной машине, установить размер операнда по умолчанию на 32 или 64, и если это так, и если моя программа настроит машину соответствующим образом, я бы увидел разные кодировки?
Также: когда будет использоваться префикс переопределения размера операнда 66H?
Взгляните на руководства Intel по разработке программного обеспечения. Объясняется кодировка и значение префиксов. Я могу написать настоящий ответ позже. — person user1752563 schedule 07.07.2021
Префиксы от 40h
до 4Fh
называются префиксами REX. Они могут указывать 64-битный размер операнда. Они также могут указывать на использование одного из 8 верхних регистров для источника или назначения. Я считаю, что возможна любая комбинация этих вариантов. — person user1752563 schedule 07.07.2021
Префикс 66 изменяет размер операнда на 16 бит. — person user1752563 schedule 07.07.2021
См., Например, wiki.osdev.org/X86-64_Instruction_Encoding#Encoding бит REX смыслы. — person user1752563 schedule 07.07.2021
Да, в машинном коде по умолчанию используется 32-битная версия для большинства инструкций, 64-битная для стека и инструкций перехода / вызова. В источнике сборки нет значения по умолчанию, это должно подразумеваться регистром или указываться явно. (За исключением некоторых ассемблеров, имеющих значение по умолчанию для push / pop.) Обратите внимание, что 16-битный AX соответствует 16-битному R8W, а RAX и R8 — это пара, различающаяся префиксом REX. — person user1752563 schedule 07.07.2021
@ecm В частности, префикс REX кодирует четыре бита состояния (один бит для размера операнда, три бита для старших регистров). Простое присутствие префикса REX дополнительно кодирует то, что вы хотите sil
, dil
, spl
и bpl
вместо ah
, ch
, dh
и bh
. — person user1752563 schedule 07.07.2021
@Peter Cordes Под «AX ‹—› R8» я имел в виду, что регистры, которые объединены в пары для целей кодирования, — это eax, rax с одной стороны и r8d, r8 с другой. Но Питер, на вопрос, который я связал, почему вы говорите о «версии x86-64 с размером операнда по умолчанию 64-бит»? — person user1752563 schedule 07.07.2021
Версия x86-64 с размером операнда по умолчанию 64-бит является чисто гипотетической. Питер говорит о том, как бы все работало, если бы был процессор, который вел бы такое поведение. Но в реальной жизни это не так. — person user1752563 schedule 08.07.2021
Да, в 64-битном машинном коде размер операнда по умолчанию для большинства инструкций 32-битный, 64-разрядная для стека и инструкций перехода / вызова, а также 64-разрядная для
loop
иjrcxz
. (И размер адреса по умолчанию — 64-битный, поэтомуadd eax, [rdi]
— это 2-байтовая инструкция, без префиксов.) И нет, значения по умолчанию нельзя изменить, у вас не может быть 2-байтовогоadd rax, rdx
.Кодирование размера операнда в 64-битном режиме
0x4?
со старшим битом, установленным в младшем полубайте, 48..4f). Префикс REX с очищенным битом W никогда не может заменить размер операнда на 32-битный для кодов операций, где по умолчанию используется что-то другое. (Какpush
)0x66
, напримерimul ax, [r8], 123
(В других режимах REX отсутствует, и
66
устанавливает значение, отличное от значения по умолчанию.)Интересный факт:
loop
иjrcxz
переопределяются для использования ECX. RCX неявно префиксом размера адреса, а не размером операнда. IIRC, это имеет некоторый смысл, потому что атрибут размера операнда ветви влияет на то, усекает ли он EIP до IP или нет.Например, разборка GNU .intel_syntax тех примеров синтаксиса NASM, приведенных выше.
Обратите внимание, что в примере imul использовался регистр высокого уровня, поэтому для сигнала R8 требовался префикс REX, а не префикс 66 для сигнала 16-битного размера операнда. Бит .W не установлен в префиксе rex, это
0x41
не0x49
.Не имеет смысла иметь одновременно REX.W и префикс
0x66
. Похоже, в этом случае префикс REX.W выигрывает. Одношаговое66 48 05 40 e2 01 00 data16 add rax,0x1e240
в Linux GDB на i7-6700k (Skylake), при пошаговом режиме RIP указывает на конец всей этой инструкции (и добавляет полное немедленное выполнение в RAX), не декодируя его какadd ax, 0xe240
и оставляя RIP, указывающий на середина 4-байтового сразу. (Префикс66
изменяет длину для этого кода операции, как и большинство, у которых есть 32-битный немедленный, который становится 16-битным. См. https://agner.org/optimize/ re: киоски LCP.)Я попросил NASM выдать это из
o16 add rax, 123456
. Префиксы REX в целом нормальны и подходят с префиксом66
, например для кодированияadd r8w, [r15 + r12*4]
, требуя, чтобы все 3 других бита были установлены в младшем полубайте REX.0x67
, напримерadd eax, [edx]
.Конечно, его можно комбинировать с размером операнда, полностью ортогональным.
Обычно 32-битный размер адреса полезен только для Linux x32 ABI (ILP32 в длинном режиме для экономии места в кэше на структуры данных с большим количеством указателей), где вы можете усечь высокий объем мусора из указателя, чтобы убедиться, что математические вычисления адреса правильно переносятся, чтобы оставаться в низких 4 ГиБ, даже с 32-битными отрицательными числами.
В других режимах
67
устанавливает размер адреса не по умолчанию. 16-битный размер адреса также подразумевает 16-битную интерпретацию байта ModRM, поэтому разрешены только[bx|bp + si|di]
, без байта SIB, чтобы обеспечить гибкость 32/64-битной адресации.Режимы и наборы настроек по умолчанию
Нет, настройки по умолчанию нельзя изменить в 64-битном режиме. Различные биты в записи GDT, выбранные CS (или любым другим методом), не имеют значения. AFAIK, таблица в https://en.wikipedia.org/wiki/X86-64#Operating_modes — это полный список возможных комбинаций режимов и размеров операндов / адресов по умолчанию.
Есть только один набор настроек, который вообще разрешает 64-битный размер операнда. Невозможно даже в любом устаревшем режиме иметь такую комбинацию, как 16-битный операнд, 32-битный размер адреса.
В этом есть смысл с точки зрения сложности оборудования. Чем больше различных комбинаций вещей он должен поддерживать, тем больше транзисторов может быть задействовано в и без того сложной и энергоемкой части процессора.
(Хотя размер адреса стека по умолчанию, неявно используемый push / pop, выбирается независимо селектором SS, IIRC. Поэтому я думаю, что у вас может быть обычный 32-битный режим, где
add eax, [edx]
составляет 2 байта, за исключением push / pop / call / ret с использованиемss:sp
вместоss:esp
. Я никогда не пробовал настраивать.Обратите внимание, что 16-битный AX соответствует 16-битному R8W, а RAX и R8 — это пара, различающаяся префиксом REX.
В исходном коде сборки нет значения по умолчанию, это должно подразумеваться регистром или указываться явно.
За исключением некоторых ассемблеров, имеющих значение по умолчанию для push / pop, или нескольких плохих ассемблеров, которые имеют значение по умолчанию для других случаев, включая ассемблер GNU для таких вещей, как
add $1, (%rdi)
по умолчанию dword, с предупреждением только в последних версиях. ГАЗ делает ошибку на неоднозначномmov
, как ни странно. Встроенный ассемблер clang лучше, он ошибается при любом неоднозначном размере операнда.Обратите внимание, что
66
и48
могут появляться одновременно с инструкциями SSE, где66
выбирает другую организацию данных. IIRC, есть также несколько совсем недавних специальных инструкций с такой кодировкой, их нужно будет найти. — person user1752563; 08.07.2021@fuz: о да, правда, когда
66
— это просто обязательный префикс, который по сути является частью кода операции. На этом этапе он на самом деле не действует как префикс размера операнда, хотя он все еще является префиксом и может отображаться в другом порядке, если хотите (хотя Intel рекомендует определенный порядок). — person user1752563; 08.07.2021Однако на самом деле это не часть кода операции; у него просто другая функция (выбор организации данных). — person user1752563; 08.07.2021
@fuz: Вы имеете в виду
ps
противpd
? Да, верно, но он также используется для целочисленных версий SSE2 / SSSE3xmm
и MMX-регистров, таких какpaddb xmm
vs.paddb mm
. И даже внутри ps / pd SSE2cvtps2pd xmm
является (без префикса)NP 0F 5A
в то время как SSE2cvtpd2ps xmm
равно66 0F 5A
. Для других обязательных префиксов, таких какrep
, инструкции, перегруженные им, включают такие разные вещи, какF2 0F 38 F1
crc32 r, r/m32
vs .0F 38 F0
movbe. — person user1752563; 08.07.2021@fuz: Так или иначе,
66
может различать инструкции, которые выполняются на разных портах, если это имеет значение. — person user1752563; 08.07.2021Большое спасибо за такой подробный ответ. Другой вопрос: при работе с 32-битными значениями до сих пор я предпочитал, например, mov eax, r8d, а не mov rax, r8, потому что я ошибочно полагал, что последний принимает префикс, а не первый. Но мне сейчас кажется, что не должно быть разницы, по производительности или как-то иначе? — person user1752563; 08.07.2021
@ user1752563: Правильно, постарайтесь сохранить 32-битные значения (и указатели, используемые в режимах адресации) в устаревших регистрах, чтобы избежать префиксов REX, например
mov eax, esi
составляет 2 байта, по сравнению сmov eax, r8d
иmov rax, r8
, оба имеют 3 байта и одинаковую производительность. Все еще есть случаи, когда 32-битный размер операнда быстрее по причинам, отличным от размера кода, например.div r8d
намного быстрее на Intel до Ice Lake. Иpopcnt eax, r8d
илиimul eax, r8d
быстрее на некоторых AMD. Также xor-zero на SMont. Преимущества использования 32-битных регистров / инструкций в x86-64 — person user1752563; 08.07.2021