MOVZX отсутствует 32-битный регистр 64-битный регистр

Вот инструкция, которая копирует (конвертирует) неподписанные регистры: http://www.felixcloutier.com/x86/MOVZX.html

В основном инструкция имеет 8->16, 8->32, 8->64, 16->32 и 16->64.

Где преобразование 32->64? Должен ли я использовать подписанную версию для этого?
Если так, как вы используете полные 64 бита для целого числа без знака?

1 ответ

Короткий ответ

использование mov eax, edi обнулить EDI в RAX, если вы уже не можете гарантировать, что старшие биты RDI равны нулю. См.: Почему инструкции x86-64 для 32-битных регистров обнуляют верхнюю часть полного 64-битного регистра?

Предпочитаю использовать разные регистры источника / назначения, потому что mov-elmination mov eax,eax на процессорах Intel. При переходе в другой регистр вы получаете нулевую задержку без необходимости в исполнительном блоке, когда вы можете сэкономить регистр, что приятно. (gcc, очевидно, не знает этого и, как правило, ноль расширяется на месте).


Длинный ответ

Причина машинного кода, почему нет кодировки для movzx с 32-битным источником

итоги: для каждой другой ширины источника для movzx и movsx нужен свой код операции. Ширина назначения контролируется префиксами. поскольку mov может сделать работу, новый код операции для movzx dst, r/m32 будет излишним.

При разработке синтаксиса ассемблера AMD64 AMD предпочла не делать movzx rax, edx работать как псевдоинструкция для mov eax, edx, Вероятно, это хорошо, потому что знание того, что запись 32-битных регистров обнуляет старшие байты, очень важно для написания эффективного кода для x86-64.


AMD64 нужен был новый код операции для расширения знака с 32-битным исходным операндом. Они назвали мнемонику movsxd по какой-то причине вместо того, чтобы сделать его третьим опкодом для movsx Мнемоника. Intel документирует их все вместе в одной ручной записи ISA. Они переделали 1-байтовый код операции, который был ARPL в 32-битном режиме, так movsxd на самом деле на 1 байт короче movsx из 8 или 16-битных источников (при условии, что вам все еще нужен префикс REX для расширения до 64-битных).

Различные размеры назначения используют один и тот же код операции с различным размером операнда 1. (66 или же REX.W префикс для 16-битной или 64-битной версии вместо 32-битной по умолчанию). например movsx eax, bl а также movsx rax, bl отличаются только префиксом REX; тот же код операции. (movsx ax, bl тоже то же самое, но с префиксом 66 для размера операнда 16).

До AMD64 не было необходимости в коде операции, который считывал бы 32-битный источник, поскольку максимальная ширина назначения составляла 32 бита, а расширение знака до того же размера было просто копией. Заметить, что movsxd eax, eax законно, но не рекомендуется Вы даже можете закодировать его с 66 префикс для чтения 32-битного источника и записи 16-битного назначения 2

Использование MOVSXD без REX.W в 64-битном режиме не рекомендуется. Обычный MOV следует использовать вместо MOVSXD без REX.W.

32-> 64-битное расширение знака может быть сделано с cdq расширить подпись EAX в EDX:EAX (например, до 32-разрядного idiv). Это был единственный путь до x86-64 (за исключением того, что, конечно, копирование и использование арифметического сдвига вправо действительно передавали бит знака).


Но AMD64 уже нуля расширяется с 32 до 64 бесплатно с любой инструкцией, которая записывает 32-битный регистр. Это позволяет избежать ложных зависимостей при выполнении не по порядку, поэтому AMD порвала с традицией 8086 / 386, оставляя старшие байты нетронутыми при записи частичного регистра. ( Почему GCC не использует частичные регистры?)

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


Иногда вам нужно потратить инструкцию, чтобы что-то расширить. Это часто встречается в выходных данных компилятора для небольших функций, потому что соглашения о вызовах SysV и Windows x64 в x86-64 допускают большое количество мусора в аргументах и ​​возвращаемых значениях.

Как обычно, спросите у компилятора, хотите ли вы знать, как что-то сделать в asm, особенно если вы не видите инструкции, которые ищете. Я опустил ret в конце каждой функции.

Source + asm из проводника компилятора Godbolt для соглашения о вызовах System V (аргументы в RDI, RSI, RDX,...):

#include <stdint.h>

uint64_t zext(uint32_t a) { return a; }
uint64_t extract_low(uint64_t a) { return a & 0xFFFFFFFF; }
    # both compile to
    mov     eax, edi

int use_as_index(int *p, unsigned a) { return p[a]; }
   # gcc
    mov     esi, esi         # missed optimization: mov same,same can't be eliminated on Intel
    mov     eax, DWORD PTR [rdi+rsi*4]

   # clang
    mov     eax, esi         # with signed int a, we'd get movsxd
    mov     eax, dword ptr [rdi + 4*rax]


uint64_t zext_load(uint32_t *p) { return *p; }
    mov     eax, DWORD PTR [rdi]

uint64_t zext_add_result(unsigned a, unsigned b) { return a+b; }
    lea     eax, [rdi+rsi]

Размер адреса по умолчанию равен 64 в x86-64. Высокий мусор не влияет на младшие биты сложения, так что это экономит байт против lea eax, [edi+esi] который нуждается в префиксе размера адреса 67, но дает идентичные результаты для каждого ввода. Конечно, add edi, esi будет производить расширенный с нуля результат в RDI.

uint64_t zext_mul_result(unsigned a, unsigned b) { return a*b; }
   # gcc8.1
    mov     eax, edi
    imul    eax, esi

   # clang6.0
    imul    edi, esi
    mov     rax, rdi    # silly: mov eax,edi would save a byte here

(Intel рекомендует уничтожить результат mov сразу же, когда у вас есть выбор, освобождая микроархитектурные ресурсы, которые mov устранение требует и увеличивает вероятность успеха mov устранение (что не на 100% в семействе Sandybridge, в отличие от AMD Ryzen). GCC на выбор mov / imul лучший.

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

Конечно, когда эти функции встроены, компилятор обычно будет знать полное состояние регистров, если они не получены из возвращаемых значений функции. И также это не должно производить результат в определенном регистре (возвращаемое значение RAX). Но если ваш источник неаккуратный с микшированием unsigned с size_t или же uint64_t компилятор может быть вынужден выдавать инструкции для усечения 64-битных значений. (Глядя на выходные данные компилятора, хороший способ поймать это и выяснить, как настроить исходный код, чтобы компилятор мог сохранять инструкции.)


Сноска 1: Забавный факт: синтаксис AT&T (который использует различные мнемоники, такие как movswl (знак-расширение слова-> длинный (dword) или movzbl) может определить размер получателя из регистра, как movzb %al, %ecx, но не собираюсь movz %al, %ecx хотя нет никакой двусмысленности. Так что лечит movzb как собственное мнемоническое, с обычным суффиксом размера операнда, который может быть выведен или явен. Это означает, что каждый отдельный код операции имеет свою мнемонику в синтаксисе AT&T.

См. Также сборку cltq и разность movslq для урока истории по избыточности между CDQE для EAX->RAX и MOVSXD для любых регистров. Смотрите, что делает cltq в сборке? или документы GAS для обозначения AT&T и Intel для расширения нуля / знака.

Сноска 2: Глупые компьютерные трюки с movsxd ax, [rsi]:

Ассемблеры отказываются собирать movsxd eax, eax или же movsxd ax, eax, но есть возможность вручную закодировать его. ndisasm даже не разбирать его (просто db 0x63), но GNU objdump делает. Фактические процессоры тоже его декодируют. Я попробовал Skylake просто чтобы убедиться:

 ; NASM source                           ; register value after stepi in GDB
mov     rdx, 0x8081828384858687
movsxd  rax, edx                         ; RAX = 0xffffffff84858687
db 0x63, 0xc2        ;movsxd  eax, edx   ; RAX = 0x0000000084858687
xor     eax,eax                          ; RAX = 0
db 0x66, 0x63, 0xc2  ;movsxd  ax, edx    ; RAX = 0x0000000000008687

Так как же процессор справляется с этим внутри? Действительно ли он читает 32 бита, а затем усекает его до размера операнда? Оказывается, справочное руководство Intel по ISA документирует 16-битную форму как 63 /rMOVSXD r16, r/m16 , так movsxd ax, [unmapped_page - 2] не виноват. (Но это неправильно документирует не-REX формы как действительные в режиме compat / legacy; на самом деле 0x63 там декодирует как ARPL. Это не первая ошибка в руководствах Intel).

Это имеет смысл: аппаратное обеспечение может просто декодировать его до того же уровня, что и mov r16, r/m16 или же mov r32, r/m32 когда нет префикса REX.W Или нет! Skylake - х movsxd eax,edx (но нет movsxd rax, edx ) имеет выходную зависимость от регистра назначения, как будто он сливается с адресатом! Петля с times 4db 0x63, 0xc2 ; movsx eax, edx работает на 4 часах за итерацию (1 на movsxd итак 1 цикл задержки). Меры довольно равномерно распределены по всем 4 целочисленным портам исполнения ALU. Петля с movsxd eax,edx / movsxd ebx,edx / 2 другие места назначения работают на ~1.4 такта на итерацию (чуть хуже, чем 1,25 такта на итерацию, узкое место переднего конца, если вы используете простой 4x mov eax, edx или 4х movsxd rax, edx.) Приурочен с perf на линуксе на i7-6700к.

Мы знаем это movsxd eax, edx обнуляет старшие биты RAX, так что он фактически не использует биты из регистра назначения, которого он ожидает, но, по-видимому, обрабатывает 16- и 32-битные аналогично, что внутренне упрощает декодирование и упрощает обработку этого углового кодирования, которое никто никогда не должен использовать. 16-битная форма всегда должна сливаться с местом назначения, поэтому она действительно зависит от выходного регистра. (Skylake не переименовывает 16-битные регистры отдельно от полных регистров).

GNU binutils неправильно его разбирает: gdb и objdump показывают исходный операнд как 32 бита, например

  4000c8:       66 63 c2                movsxd ax,edx
  4000cb:       66 63 06                movsxd ax,DWORD PTR [rsi]

когда это должно быть

  4000c8:       66 63 c2                movsxd ax,dx
  4000cb:       66 63 06                movsxd ax,WORD PTR [rsi]

В синтаксисе AT&T, objdump забавно все еще использует movslq, Так что я думаю, что это рассматривает как целое мнемонику, а не как movsl инструкция с q размер операнда. Или это просто результат того, что никто не заботится о том особом случае, когда газ все равно не будет собираться (он отвергает movsll и проверяет ширину регистра для movslq).

Перед проверкой руководства я на самом деле проверил Skylake с NASM, чтобы определить, будет ли нагрузка неисправной или нет. Это конечно не

section .bss
    align 4096
    resb 4096
unmapped_page: 
 ; When built into a static executable, this page is followed by an unmapped page on my system,
 ; so I didn't have to do anything more complicated like call mmap

 ...
_start:
    lea     rsi, [unmapped_page-2]
    db 0x66, 0x63, 0x06  ;movsxd  ax, [rsi].  Runs without faulting on Skylake!  Hardware only does a 2-byte load

    o16 movsxd  rax, dword [rsi]  ; REX.W prefix takes precedence over o16 (0x66 prefix); this faults
    mov      eax, [rsi]            ; definitely faults if [rsi+2] isn't readable

Обратите внимание, что movsx al, ax невозможно: размер байтового операнда требует отдельного кода операции. Префиксы выбираются только между 32 (по умолчанию), 16-битным (0x66) и в длинном режиме 64-битным (REX.W). movs/zx ax, word [mem] стало возможным с 386 года, но чтение источника шире, чем место назначения, является угловым случаем, который является новым в x86-64, и только для расширения знака. (И получается, что 16-битная кодировка назначения фактически читает только 16-битный источник).


Другие возможности ISA-дизайна, которые AMD решила не использовать:

Кстати, AMD могла бы (но не имела) проектировать AMD64, чтобы всегда расширять знак вместо всегда нулевого расширения при 32-разрядных регистрах записи. Это было бы менее удобно для программного обеспечения в большинстве случаев, и, вероятно, также потребовало бы нескольких дополнительных транзисторов, но это все же позволило бы избежать ложных зависимостей от старого значения, которое сидело в регистре. Это может добавить дополнительную задержку затвора где-нибудь, потому что старшие биты результата зависят от младших битов, в отличие от нулевого расширения, где они зависят только от того факта, что это 32-битная операция. (Но это, вероятно, неважно.)

Если бы AMD спроектировала это таким образом, им бы понадобился movzxd вместо movsxd, Я думаю, что основным недостатком этого дизайна будут дополнительные инструкции при упаковке битовых полей в более широкий регистр. Бесплатное расширение нуля удобно для shl rax,32 / or rax, rdx после rdtsc что пишет edx а также eax, например. Если бы это было расширение знака, вам нужна инструкция для обнуления старших байтов rdx перед or,


Другие ISA сделали другой выбор: MIPS III (в 1995 г.) расширил архитектуру до 64 бит без введения нового режима. В отличие от x86, в 32-битном формате командных слов фиксированной ширины было достаточно свободного места для кода операции.

MIPS начинался как 32-битная архитектура и никогда не имел устаревших частичных регистров, как 32-битный x86 из 16-битного наследия 8086 и полной поддержки 8086 8-битного размера операндов с AX = AH:AL частичные регистры и так далее для легкого портирования исходного кода 8080.

MIPS 32-битные арифметические инструкции, такие как addu на 64-битных процессорах требуется, чтобы их входы были правильно расширены по знаку, и выдают выходные данные с расширением знака (Все работает только при запуске устаревшего 32-битного кода без учета более широких регистров, потому что сдвиги особенные.)

ADDU rd, rs, rt ( из руководства MIPS III, стр. A-31)

Ограничения:
На 64-битных процессорах, если GPR rt или GPR rs не содержат 32-битные значения с расширенными знаками (равны биты 63..31), результат операции не определен.

Операция:

  if (NotWordValue(GPR[rs]) or NotWordValue(GPR[rt])) then UndefinedResult() endif
  temp ←GPR[rs] + GPR[rt]
  GPR[rd]← sign_extend(temp31..0)

(Обратите внимание, что U для без знака в addu действительно неправильно, как указывает руководство. Вы используете это для подписанной арифметики также, если вы действительно не хотите add отловить подписанное переполнение.)

Есть DADDU инструкция для двойного слова ADDU, которая делает то, что вы ожидаете. Аналогично DDIV/DMULT/DSUBU, а также DSLL и другие смены.

Побитовые операции остаются прежними: существующий код операции И становится 64-битным И; нет необходимости в 64-битном И, но также нет свободного расширения знака 32-битных И.

32-разрядные сдвиги MIPS являются особыми (SLL - это 32-разрядные сдвиги. DSLL - это отдельная инструкция).

SLL Shift Word Left Logical

Операция:

s ← sa
temp ← GPR[rt] (31-s)..0 || 0 s
GPR[rd]← sign_extend(temp)

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

Весь этот раздел о MIPS не имеет ничего общего с x86-64, но это интересное сравнение, чтобы взглянуть на другое (лучшее IMO) решение по дизайну, принятое AMD64.

Я включил вывод компилятора MIPS64 в ссылку Godbolt выше для этих примеров функций. (И несколько других, которые рассказывают нам больше о соглашении о вызовах и о том, какие компиляторы.) dext увеличить ноль с 32 до 64 бит; но эта инструкция не была добавлена ​​до mips64r2. С -march=mips3, return p[a] для неподписанных a должен использовать два сдвига двойного слова (влево, затем вправо на 32 бита) для расширения нуля! Также требуется дополнительная инструкция для добавления результатов с нулевым расширением, т. Е. Для приведения типов из unsigned к uint64_t,

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

Другие вопросы по тегам