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

    Сегодня я хочу рассказать об одной интересной сложности декодирования/дизассемблирования IA-32 инструкций.

    Перед прочтением этой статьи рекомендую обратиться в статье «Префиксы в системе команд IA-32», описывающей общую структуру IA-32 команды и существующие префиксы. В этой статье я подробнее расскажу про обязательные префиксы (англ. mandatory prefixes) и некоторые нюансы, связанные с ними.

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

    Некоторые инструкции, такие как MULSS, MULSD и MULPD (инструкции векторного умножения) одинаковый опкод 0x0f 0x59, но различные обязательные префиксы (0xf2, 0xf3 и 0x66 соответственно). Появляется вопрос, а что же должно происходить, если в коде инструкции присутствуют одновременно несколько таких префиксов? Наверно, логичнее было бы определять, что это за инструкция, по последнему префиксу. Но это не всегда так! Если последним префиксом является 0xf2 или 0xf3, то он считается обязательным, однако 0x66 является обязательным, только если префиксы 0xf2 и 0xf3 отсутствуют в кодировке данной инструкции. Примеры корректного вывода дизассемблера для этих инструкций можно найти в таблице:

    Код инструкции Инструкция Обязательный префикс
    66 f3 f2 0f 59 ff MULSD xmm7, xmm7 f2
    66 f2 f3 0f 59 ff MULSS xmm7, xmm7 f3
    66 0f 59 ff MULPD xmm7, xmm7 66
    f2 66 0f 59 ff MULSD xmm7, xmm7 f2
    0f 59 ff MULPS xmm7, xmm7 -

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

    #include <stdio.h>
    
    int main() {
        double a[2] = {2, 2}, b[2] = {0, 0};
        __asm__ __volatile__ (
             // Copy data from a to xmm7 register
             "movupd %1, %%xmm7\n"
             //"mulsd %%xmm7, %%xmm7\n"
             ".byte 0xf2, 0x66, 0x0f, 0x59, 0xff\n"
             // Copy data from xmm7 register to b
             "movupd %%xmm7, %0\n"
             :"=m"(*b)
             :"m"(*a)
             :
        ); 
        printf("%lf %lf\n", b[0], b[1]);
        return 0;
    }
    


    Скомпилировав и запустив, его вы увидите следующее:

    $ gcc -O0 -Wall mulsd.c
    $ ./a.out
    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 мне пока что не ответил.

    А правильно ли работает дизассемблер, которым пользуетесь вы?
    Intel
    169,00
    Компания
    Поделиться публикацией

    Комментарии 17

      +6
      но, к сожалению, документация дает неоднозначное представление о данном вопросе

      Может, в первую очередь нужно зарепортить баг всё-таки на документацию?
        +1
        Вы правы, стоит попробовать им написать.

        Но я лично не уверен, что это баг, есть подозрение, что это фича :)
        +1
        На всякий случай проверил, что эти ненормальные инструкции запрещены в NaCl'e. С множественными префиксами в NaCl все строго.
        А вы на AMD процессоре этот странный случай проверяли?
          0
          На AMD не проверял, так как нету таких под рукой.
          Если кто-то может проверить — сообщите, было бы очень интересно.
            0
            По крайней мере приведённая программа выдаёт «4.000000 2.000000» как и на Intel'е.

            Но с префиксом 66 связан другой очень весёлый случай, так что понимаю почему хотелось проверить как себя ведёт AMD:
            sourceware.org/bugzilla/show_bug.cgi?id=13668
              0
              Если можно, готовый код тестирования, я запущу. Продолжим в личке.
            0
            «Наверно, логичнее было бы определять, что это за инструкция, по последнему префиксу»
            Раз это упущено в мануале, то надо бы сначала уточнить у разработчика, повторяется ли это на всех процессорах?
            // Напомнило, как я игрался с turbo debugger на 2 курсе: cs1202.vk.me/u55420/19095012/x_d4218182.jpg
              0
              f2 66 0f 59 ff — это MULSD согласно 2.2.1 REX Prefixes:

              When a REX prefix is used in conjunction with an instruction containing a mandatory prefix, the mandatory prefix must come before the REX so the REX prefix can be immediately preceding the opcode or the escape byte. For example, CVTDQ2PD with REX prefix should have REX placed between F3 and 0F E6. Other placements are ignored.

              66 f3 f2 0f 59 ff и 66 f2 f3 0f 59 ff здесь 66 игнорируется согласно 2.2.1. Использование f3 и f2 unpredictable behavior согласно:

              Repeat prefixes (F2H, F3H) cause an instruction to be repeated for each element of a string. Use these prefixes only with string and I/O instructions (MOVS, CMPS, SCAS,LODS, STOS, INS, and OUTS). Use of repeat prefixes and/or undefined opcodes with other Intel 64 or IA-32 instructions is reserved; such use may cause unpredictable behavior.
                +1
                Причём тут 2.2.1? Ни в одной из этих инструкций REX префикс не используется.
                  0
                  f2 66 0f 59 ff — это MULSD согласно 2.2.1 REX Prefixes:

                  When a REX prefix is used in conjunction with an instruction containing a mandatory prefix, the mandatory prefix must come before the REX so the REX prefix can be immediately preceding the opcode or the escape byte. For example, CVTDQ2PD with REX prefix should have REX placed between F3 and 0F E6. Other placements are ignored.

                  Не понял вашей аргументации, если честно. Тут говорится про REX префикс. В приведенном примере его просто нет.
                    0
                    Да, что-то я напутал.
                    0
                    66 — это не REX префикс. Не понимаю при чем здесь документация о их положении. Следующий пункт тоже не применим. Он говорит о строковых инструкциях, а не о SSE-инструкциях с обязательным префиксом. Как показывает случай f2 66, нельзя рассматривать f2 f3 0f… просто как SSE инструкцию с префиксом f2.
                      +1
                      66 f3 f2 0f 59 ff и 66 f2 f3 0f 59 ff здесь 66 игнорируется согласно 2.2.1. Использование f3 и f2 unpredictable behavior согласно:

                      Repeat prefixes (F2H, F3H) cause an instruction to be repeated for each element of a string. Use these prefixes only with string and I/O instructions (MOVS, CMPS, SCAS,LODS, STOS, INS, and OUTS). Use of repeat prefixes and/or undefined opcodes with other Intel 64 or IA-32 instructions is reserved; such use may cause unpredictable behavior.

                      В данном случае это уже не repeat префиксы. Они несут совсем другую смысловую нагрузку.
                      +2
                      libopcodes вообще плохо себя ведёт на странных комбинациях префиксов/суффиксов, а иногда может вообще запутаться и выдать чушь (вплоть до того, что несуществующую инструкцию запишет в существующую).

                      Несколько примеров:
                         0:	9b                   	rex
                         1:	40                   	rex
                         2:	9b df 28             	fildll (%rax)
                      
                      9b — это, как несложно догадаться, ни разу не rex

                         0:   c4 e3 79 44 c0 02       vpclmullqhqdq %xmm0,%xmm0,%xmm0
                      
                      Это, на самом деле, не vpclmullqhqdq, а совсем даже vpclmullqlqdq.

                         0:	c5 fa 28 d1          	vmovaps %xmm1,%xmm2
                      
                      Это — вообще не поддерживается в железе (похожая инструкция c5 f8 28 d1, разумеется, поддерживается).
                        +9
                        vpclmullqhqdq

                        Ассемблер пострашнел.
                          +1
                          Это не ассемблер пострашнел, это система команд подурнела
                            0
                            Одно без другого никуда.

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.