Сегодня я хочу рассказать об одной интересной сложности декодирования/дизассемблирования IA-32 инструкций.
Перед прочтением этой статьи рекомендую обратиться в статье «Префиксы в системе команд IA-32», описывающей общую структуру IA-32 команды и существующие префиксы. В этой статье я подробнее расскажу про обязательные префиксы (англ. mandatory prefixes) и некоторые нюансы, связанные с ними.
Все началось с прочтения статьи о языке, предназначенном для создания декодеров – GDSL. В этой статье приводятся некоторые уже известные мне примеры, а также новые особенности, о которых я до этого ничего не слышал. Именно о них я сейчас вам и расскажу.
Некоторые инструкции, такие как
На какой-то момент я усомнился в правильности этих утверждений, но, к сожалению, документация дает неоднозначное представление о данном вопросе. После чего было решено провести тесты на реальном процессоре, и оказалось, что он работает именно так. Проверка эта осуществлялась с помощью ассемблерных вставок. Пример одного из тестов приведен ниже:
Скомпилировав и запустив его вы увидите следующее:
То есть умножился только первый элемент вектора, тогда как второй остался неизменным, что соответствует инструкции
На данном примере было протестировано несколько дизассемблеров, входящих в состав известных пакетов. Немногие из них справились со своей задачей, о чем было незамедлительно сообщено разработчикам данных продуктов. Сводка результатов приведена ниже:
Следует отметить, что gdb и objdump входят в состав binutils и используют одну и ту же библиотеку для дизассемблирования. Один из разработчиков ODA – Anthony DeRosa – в ответ на мое сообщение об ошибке сказал, что они используют библиотеку libopcodes, входящую в состав binutils. То есть исправление в одном месте должно повлечь за собой корректировку как минимум трех продуктов сразу, но, к сожалению, никто из binutils мне пока что не ответил.
А правильно ли работает дизассемблер, которым пользуетесь вы?
Перед прочтением этой статьи рекомендую обратиться в статье «Префиксы в системе команд 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 мне пока что не ответил.
А правильно ли работает дизассемблер, которым пользуетесь вы?