Префиксы в системе команд IA-32

Тема в разделе "WASM.ARTICLES", создана пользователем Mikl___, 14 дек 2016.

  1. Mikl___

    Mikl___ Супермодератор Команда форума

    Публикаций:
    14
    Регистрация:
    25 июн 2008
    Сообщения:
    3.714

    Префиксы в системе команд IA-32



    автор Евгений Юлюгин aka yulyugin взято здесь


    Общая структура IA-32 инструкции:
    [​IMG]
    • Префиксы. Могут отсутствовать. Может присутствовать сразу несколько.
    • Опкод. Может состоять из одного, двух или трех байтов.
    • Mod_R/M байт. Используется для адресации операндов. Может отсутствовать в кодировке, если инструкция не имеет явных операндов.
    • SIB (Scale Index Base) байт. Второй байт, использующийся для адресации операндов в памяти. Может отсутствовать.
    • Байт смещения адреса (англ. displacement). 1, 2, 4 или ни одного байта.
    • Константа (англ. immediate). 1, 2, 4 или ни одного байта.
    Более подробное описание структуры инструкции можно найти в Intel 64 and IA-32 Architectures Software Development Manuals. В данной статье будет рассказываться об IA-32 префиксах, особенностях, связанных с их использованием, а также тенденций в их развитии.

    Однобайтные префиксы

    Практически с самых первых процессоров Intel в системе команд IA-32 начали использоваться однобайтные префиксы.
    Префиксы изменяющие сегментный регистр, используемый командой при обращении к ячейке памяти:
    • 0x26 – префикс замены сегмента ES
    • 0x2E – префикс замены сегмента CS
    • 0x36 – префикс замены сегмента SS
    • 0x3E – префикс замены сегмента DS
    • 0x64 – префикс замены сегмента FS
    • 0x65 – префикс замены сегмента GS

    • 0x0F – префикс дополнительных команд (иногда его не считают за настоящий префикс – в этом случае считается, что опкод команды состоит из двух байт, первый из которых 0x0F)

    • 0x66 – префикс переопределения размера операнда (к примеру, вместо регистра eax будет использоваться ax)
    • 0x67 – префикс переопределения размера адреса
    • 0x9B – префикс ожидания (WAIT)
    • 0xF0 – префикс блокировки (LOCK с его помощью реализуется синхронизация многопоточных приложений)
    • 0xF2 – префикс повторения команды REPNZ – работа с последовательностями байт (строками)
    • 0xF3 – префикс повторения команды REP – работа с последовательностями байт (строками)
    Каждый из этих префиксов меняет семантику и (или) структуру машинной инструкции (например, ее длину или выбор мнемоники).

    Обязательные префиксы

    С появлением расширения SSE часть однобайтных префиксов, а именно 0xf2, 0xf3, 0x66 в некоторых случаях стали иметь смысл части опкода. Появились так называемые обязательные префиксы (англ. mandatory prefixes). Примеры таких инструкций приведены ниже.
    КодировкаИнструкцияОбязательный префикс
    0x0f 0x10MOVUPS-
    0xf2 0x0f 0x10MOVSD0xf2
    0xf3 0x0f 0x10MOVSS0xf3
    0x66 0x0f 0x10MOVUPD0x66
    Не сложно заметить, что кодировки этих инструкций отличаются только префиксом. Опкод у них совпадает – 0x0f 0x10. При этом семантика у этих инструкций различна. Например, MOVSD копирует из одного операнда в другой 64 бита, а MOVUPD – 128 бит.

    Префикс REX

    В определенный момент появилась необходимость в поддержке 64-битного адресного пространства и расширения числа адресуемых регистров. С этой задачей успешно справились разработчики AMD, добавив префикс, названный REX. Данный префикс также является однобайтными, и имеет вид 0x4*. Его биты используются для расширения уже существующих полей, кодируемых в Mod_R/M байте, а также ширины операнда. На рисунке приведен пример использования REX префикса для адресации регистров.
    [​IMG]
    Стоит отметить несколько особенностей, связанных с использованием этого префикса. Кодировка 0x4*соответствует префиксу только в 64-битном режиме, во всех остальных режимах она соответствует вариантам инструкций INC/DEC. Интересным свойством данного префикса является то, что он должен быть расположен непосредственно перед байтом опкода, в противном случае он игнорируется. Если REXпрефикс используется вместе с инструкцией требующей присутствия другого обязательного префикса, он должен быть расположен между этим префиксом и байтом кода операции.

    Префикс VEX

    С введением расширения AVX в системе команд IA-32 появился новый префикс, названный VEX. Он уже не однобайтный. Он может состоять либо из двух, либо из трех байт в зависимости от первого байта префикса. 0xc4 и 0xc5 соответственно.
    [​IMG]
    Поля R, X, B, W несут тот же смысл, что и соответствующие поля REX префикса. Поле pp предоставляет функциональность, эквивалентную обязательным SIMD префиксам (например, b01 = 0x66). А поле m-mmmmможет соответствовать целым двум байтам опкода (например, 0b00011 = 0x0f 0x3a). Поле L определяет длину вектора: 0 – 128 бит, 1 – 256 бит.
    Использование VEX префикса предоставляет следующие преимущества:
    • Поддержка до четырех операндов.
    • Поддержка 128-битных XMM регистров и 256-битных YMM регистров.
    • Сжатие кодировки уже введенных инструкций.
    • Удаление необходимости использования REX префикса для адресации регистров общего назначения (R8 – R15), векторных регистров XMM8 – XMM15 (YMM8 – YMM15). VEX позволяет кодировать те же поля, что и REX, и, вдобавок, несколько новых.

    Следуют отметить, что использование VEX префикса вместе с некоторыми однобайтными префиксами (0xf0, 0x66, 0xf2, 0xf3, REX) запрещено и приводит к исключению #UD.

    Префикс EVEX

    Не так давно Intel анонсировал появление нового расширения набора команд с названием AVX3 или AVX512. С появлением этого расширения также появился и новый префикс, получивший название EVEX. Его описание можно найти в Intel Architecture Instruction Set Extensions Programming Reference.
    [​IMG]
    Он представляет собой усовершенствованный вариант VEX префикса, имеет длину уже в 4 байта и начинается с байта 0x62, который во всех режимах, кроме 64-битного соответствует инструкции BOUND, редко используемой в современных программах.

    Приведу некоторые, на мой взгляд, интересные особенности EVEX префикса:

    • Два бита для длины вектора – LL` – необходимые для поддержки векторов размером 128, 256 и 512 бит.
    • Поддержка адресации новых 512-битных регистров ZMM8 — ZMM31.
    • Поддержка регистров маски операндов (англ. opmask registers). Поле EVEX.aaa.
    • EVEX.mm – эквивалент поля VEX.m-mmmm, но занимает два бита вместо пяти.

    Заключение



    В заключение хочется отметить некоторые причины появления столько сложной и, местами, не логичной системы команд. История развития системы команд Intel IA-32 начинается в 70-х годах прошлого столетия, когда ни о каких 64-битных режимах не было и речи. Кроме Intel существенный вклад в эволюцию IA-32 внесла компания AMD. Много усилий было потрачено на поддержание обратной совместимости между различными моделями процессоров. Множество интересных фактов, связанных с развитием архитектуры IA-32 можно найти в статье A. Fog’a.

    Спасибо пользователю Atakua за комментарии к черновикам этой статьи.

    P.S. Все иллюстрации взяты из Intel 64 and IA-32 Architectures Software Development Manuals.
     

    Вложения:

    • 00.png
      00.png
      Размер файла:
      8,3 КБ
      Просмотров:
      1.756
    • 01.png
      01.png
      Размер файла:
      6,6 КБ
      Просмотров:
      1.651
    • 02.png
      02.png
      Размер файла:
      9 КБ
      Просмотров:
      1.633
    • 03.png
      03.png
      Размер файла:
      8 КБ
      Просмотров:
      1.838
    Последнее редактирование: 15 дек 2016
    _edge и xcode нравится это.
  2. Indy_

    Indy_ Well-Known Member

    Публикаций:
    4
    Регистрация:
    29 апр 2011
    Сообщения:
    4.775
    Хороший перевод манов. Но вот реальные вопросы:

    1. Поведение архитектур при раскодировке префиксов отличается, так на ia число префиксов может быть любой, на amd вроде как генерится ловушка при числе больше 4(не помню точно).
    2. Эффективный алгос для определения префиксов(не цикл прохода по строке есно).
    3. Определение дефолтной сегментации vs переопределение сегмента(адресация через стек етц).
    4. Прочие проблемы, но пока достаточно базовых.
     
    Mikl___ нравится это.
  3. Mikl___

    Mikl___ Супермодератор Команда форума

    Публикаций:
    14
    Регистрация:
    25 июн 2008
    Сообщения:
    3.714

    Правильно ли работает ваш дизассемблер?


    автор Евгений Юлюгин aka yulyugin взято здесь

    Все началось с прочтения статьи о языке, предназначенном для создания декодеров – GDSL. В этой статье приводятся некоторые уже известные мне примеры, а также новые особенности, о которых я до этого ничего не слышал. Именно о них я сейчас вам и расскажу.

    Некоторые инструкции, такие как MULSS, MULSD и MULPD (инструкции векторного умножения) одинаковый опкод 0x0f 0x59, но различные обязательные префиксы (0xf2, 0xf3 и 0x66 соответственно). Появляется вопрос, а что же должно происходить, если в коде инструкции присутствуют одновременно несколько таких префиксов? Наверно, логичнее было бы определять, что это за инструкция, по последнему префиксу. Но это не всегда так! Если последним префиксом является 0xf2 или 0xf3, то он считается обязательным, однако 0x66 является обязательным, только если префиксы 0xf2 и 0xf3 отсутствуют в кодировке данной инструкции. Примеры корректного вывода дизассемблера для этих инструкций можно найти в таблице:
    Код инструкцииИнструкцияОбязательный префикс
    66 f3 f2 0f 59 ffMULSD xmm7, xmm7f2
    66 f2 f3 0f 59 ffMULSS xmm7, xmm7f3
    66 0f 59 ffMULPD xmm7, xmm766
    f2 66 0f 59 ffMULSD xmm7, xmm7f2
    0f 59 ffMULPS xmm7, xmm7-
    На какой-то момент я усомнился в правильности этих утверждений, но, к сожалению, документация дает неоднозначное представление о данном вопросе. После чего было решено провести тесты на реальном процессоре, и оказалось, что он работает именно так. Проверка эта осуществлялась с помощью ассемблерных вставок. Пример одного из тестов приведен ниже:
    Код (C):
    1. #include <stdio.h>
    2.  
    3. int main() {
    4.  double a[2] = {2, 2}, b[2] = {0, 0};
    5.  __asm__ __volatile__ (
    6.  // Copy data from a to xmm7 register
    7.  "movupd %1, %%xmm7\n"
    8.  //"mulsd %%xmm7, %%xmm7\n"
    9.  ".byte 0xf2, 0x66, 0x0f, 0x59, 0xff\n"
    10.  // Copy data from xmm7 register to b
    11.  "movupd %%xmm7, %0\n"
    12.  :"=m"(*b)
    13.  :"m"(*a)
    14.  :
    15.  );
    16.  printf("%lf %lf\n", b[0], b[1]);
    17.  return 0;
    18. }
    Скомпилировав и запустив, его вы увидите следующее:
    Код (Text):
    1. $ gcc -O0 -Wall mulsd.c
    2. $ ./a.out
    3. 4.000000 2.000000
    То есть умножился только первый элемент вектора, тогда как второй остался неизменным, что соответствует инструкции MULSD, а не MULPD.
    На данном примере было протестировано несколько дизассемблеров, входящих в состав известных пакетов. Немногие из них справились со своей задачей, о чем было незамедлительно сообщено разработчикам данных продуктов. Сводка результатов приведена ниже:
    Продукт, версияРезультатСообщенная мною ошибка
    Wind River Simics, 4.8успех-
    XEDуспех-
    objdump, 2.23ошибка16083
    GNU GDB, 7.5ошибка16089
    nasm, 2.09ошибка3392269
    ODA, 0.2.0ошибкаПереписка по электронной почте
    objconv, 2.31ошибкаПереписка по электронной почте
    IDA, 6.4 (Evaluation Version)ошибкаПереписка по электронной почте
    llvm-objdump, 3.2ошибка17697
    Следует отметить, что gdb и objdump входят в состав binutils и используют одну и ту же библиотеку для дизассемблирования. Один из разработчиков ODA – Anthony DeRosa – в ответ на мое сообщение об ошибке сказал, что они используют библиотеку libopcodes, входящую в состав binutils. То есть исправление в одном месте должно повлечь за собой корректировку как минимум трех продуктов сразу, но, к сожалению, ни кто из binutils мне пока что не ответил.

    А правильно ли работает дизассемблер, которым пользуетесь вы?
     
    Последнее редактирование: 14 дек 2016