Это не полноценный дизассмеблер. Это разбор инструкций и подсчет их размера. То есть, не учитываются данные: смещения, значения, регистры. Учитывается только название инструкции - мнемоника.
У кого нет времени на описание, переходим сразу к онлайн версии.
Или скачать: x64disasm.php, instructions.php
Или посмотреть: x64disasm.php, instructions.php
Внимание. Мало слов, много примеров.
Если нет понимания про префиксы, ModR/M и SIB, то отличное описание на OSDev Wiki.
Здесь только то, что нужно в данном проекте.
За основу возьмем базу инструкций с опкодами и битностью из iced
1. Префиксы наследия (legacy)
1.1. 0x66
1.2. 0x67
1.3. 0x66, 0xF2, 0xF3
2. Новые префиксы
2.1. REX
2.2. VEX
2.3. EVEX
2.4. MVEX
2.5. XOP
4. База инструкций
4.1. Префикс внутри
4.2. Третий опкод внутри
4.3. Байт константа после ModR/M
4.4. Пересечения опкодов
4.5. Исключение
6. Заключение
А теперь подробнее. Погнали...
0x26, 0x2E, 0x36, 0x3E, 0x64, 0x65, 0x66, 0x67, 0xF0, 0xF2, 0xF3.
Нас интересуют 0x66, 0x67, 0xF2, 0xF3
, только они меняют размер инструкции.
Они могут идти друг за другом, бывает встречается и такое 66 67 66 66 66 2E 0F 1F 84 00 00 00 00 00
1.1. 0x66 - указывает на использование 16 битной инструкции, если такая имеется:
ADD EAX,[RAX] ; 03 00 < o32 03 /r\nADD AX,[RAX] ; 66 03 00 < o16 03 /r
Но в ней не указано количество байтов. А вот здесь указано:
ADD EAX,11111111 ; 05 11 11 11 11 < o32 05 id\nADD EAX,1111 ; 66 05 11 11 < o16 05 iw
В iced существует такая же проблема с Jxx как и в IDA. Учитывается префикс 0x66.
Для примера JZ в iced.
JE rel16\no16 0F 84 cw\n16/32/64-bit
Подразумевается, что 66 0F 84 00 00
интерпретируется как JZ word
.
Но на самом деле это не так, 0x66 префикс никак не влияет на эту инструкцию:
JZ dword ; 66 0F 84 00 00 00 00 < o32 0F 84 cd\nJZ dword ; 0F 84 00 00 00 00 < o32 0F 84 cd
Это выяснилось при тестировании дизассемблера, он жестко спотыкался об эти байты.
Кстати, так же происходит и с JMP инструкций, но IDA справляется с этим.
JMP rel16\no16 E9 cw\n16/32/64-bit
Итого:
JMP dword ; 66 E9 00 00 00 00 < o32 E9 cd\nJMP dword ; E9 00 00 00 00 < o32 E9 cd
1.2. 0x67 - указывает на использование 16 битной адресации, если такая имеется:
MOV AL,[1111111111111111] ; A0 11 11 11 11 11 11 11 11\nMOV AL,[11111111] ; 67 A0 11 11 11 11
1.3. 0x66, 0xF2, 0xF3 - и префикс, и опкод в одном лице.
Если у инструкции нет такого префикса (опкода), он все равно может встречаться в коде:
SLDT [RAX] ; F2 0F 00 00\nSLDT [RAX] ; 0F 00 00
А есть инструкции которые не будут идентифицированы без этого префикса:
POPCNT EAX,[RAX] ; F3 0F B8 00\n#UD ; 0F B8 00
А вот где могут быть все четыре (с и без префикса) варианта:
PSHUFHW xmm1,[RAX],0 ; F3 0F 70 00 00\nPSHUFLW xmm1,[RAX],0 ; F2 0F 70 00 00 \nPSHUFD xmm1,[RAX],0 ; 66 0F 70 00 00\nPSHUFW mm1,[RAX],0 ; 0F 70 00 00
Здесь уже поменьше неразберихи и более структурировано.
$instructions['EVEX']['7D.66.0F38']['W0']['512']['/r xxx'] = 'VPERMT2B zmm1 {k1}{z}, zmm2, zmm3/m512';
7D
- опкод, идет сразу после префиксаЭто реальный байт, а вот далее уже обрабатываются биты, у каждого префикса свои, но суть одна:
66
- еще один опкод/префикс (66, F3, F2)0F38
- map (0F, 0F38, 0F3A, MAP5, MAP6, X8, X9, XA)W0
- prefix.W (W0, W1, WIG)512
- длина битности (128, 256, 512, L0, L1, LZ, LIG)Его значение может быть 0x40-0x4F
(в x86 это INC/DEC). Нагляднее побитно:
+---+---+---+---+---+---+---+---+\n| 0 1 0 0 | W | R | X | B |\n+---+---+---+---+---+---+---+---+
Этот префикс сигнализирует как использовать инструкцию (32 или 64 бит) и расширение для ModR/M и SIB.
Для примера возьмем из предыдущего раздела. Для данных инструкций меняется только регистр:
PSHUFHW xmm1,[R8],0 ; F3 49 0F 70 00 00\nPSHUFLW xmm1,[R8],0 ; F2 41 0F 70 00 00\nPSHUFD xmm1,[R8],0 ; 66 4F 0F 70 00 00\nPSHUFW mm1,[R8],0 ; 43 0F 70 00 00
А теперь о инструкции где есть размеры.
Если REX.W == 1 (>=0x48)
то используется 64 битная инструкция и значения уже берутся исходя из этого, иначе по умолчанию 32 бита.
MOV EAX,11111111 ; B8 11 11 11 11\nMOV EAX,11111111 ; 40 B8 11 11 11 11 (REX = 0x40, W == 0)\nMOV RAX,1111111111111111 ; 4E B8 11 11 11 11 11 11 11 11 (REX = 0x4E, REX.W == 1)
REX префиксы могут идти друг за другом, ничего при этом не меняя.
Новый вид префикса для 64 битов.
Одну инструкцию можно представить в двух вариантах VEX:
; VEX.LIG.F3.0F.WIG 5F /r\nVMAXSS xmm0, xmm15, [RAX] ; C5 82 5F 00\nVMAXSS xmm0, xmm15, [RAX] ; C4 A1 82 5F 00
Но, это возможно, если так называемая карта (map) у инструкции равна 0F. Это значение единственный в двухбайтовом префиксе.
В трехбайтовом префиксе есть еще 0F38 и 0F3A.
Тот же VEX, только четыре байта начинайющихся с 0x63: 63 XX YY XX
Для орпеделения префикса нужно из YY байта проверить [2] бит:
Это подарок от AMD.
Байт идентификатор 0x8F, но необходимо проверять следующий байт и биты, чтобы [5:0] >= 01000
Иначе это может быть POP:
POP [RDI] ; 8F 07\nXOP ; 8F 08
А если рассматривать через призму ModR/M, то это /1 (0b01), /2 (0b10), /3 (0b11).
Все остальное то же самое.
Новая вводная от Интел это следующий байт (ModR/M) от опкода: проверка полей mod, reg, r/m.
11:rrr:000
- [7:6] - обязательно равно 11, [5:3] - любое, [2:0] - обязательно равно 000!(11):000:bbb
- [7:6] - не равно 11, [5:3] - обязательно равно 000, [2:0] - любое!(11):rrr:bbb
- [7:6] - не равно 11, [5:3] - любое, [2:0] - любоеЕсли условия не выполнены, то это не наша инструкция.
Что за великолепие в этой прекрасной базе инструкций? Спросите вы.
Почему максимум два опкода на инструкцию, хотя их может быть три.
Почему префиксы (и не только) внутри?
Да, это база выстрадана на практике. И на примерах это видно.
4.1. Префикс внутри
$instructions['0F.01']['^F3 /5 xxx'] = 'RSTORSSP m64';\n$instructions['0F.01']['/7 xxx'] = 'INVLPG m';\n
То есть, в следующем байте после F3.0F.01 ModR/M
, reg должен быть равен 101 (/5). Если это не так, а допустим 111 (/7), то мы теряемся, ибо других инструкций в нашем массиве нет.
Если брать без префикса, то там уже как раз есть /7 (111) и мы получаем мнемонику нашей инструкции.
Допустим:
F3 0F 01 28 (reg=101)
- мы получаем RSTORSSP m64 (RSTORSSP qword ptr ds:[rax])F3 0F 01 38 (reg=111)
- мы ничего не получаем, потому что есть префикс F3, но по документации он не нужен, а может быть. (INVLPG byte ptr ds:[rax])4.2. Третий опкод внутри
Пример как байт за опкодом (ModR/M) может все менять:
$instructions['0F.1E']['^F3 |FB . xxx'] = 'ENDBR32';\n$instructions['0F.1E']['^F3 |FA . xxx'] = 'ENDBR64';\n$instructions['0F.1E']['^F3 /1 xxx'] = 'RDSSPD r32';\n$instructions['0F.1E']['/r x32'] = 'RESERVEDNOP r/m32, r32';\n
F3 0F 1E FA (reg=111)
- ENDBR64F3 0F 1E CA (reg=001)
- RDSSPD EDXF3 0F 1E D2 (reg=010)
- RESERVEDNOP EDX/EDXF3 0F 1E DA (reg=011)
- RESERVEDNOP EDX/EBX4.3. Байт константа после ModR/M
$instructions['0F.0F']['/r $B6 xxx'] = 'PFRCPIT2 mm, mm/m64';\n$instructions['0F.0F']['/r $B4 xxx'] = 'PFMUL mm, mm/m64';\n
0F 0F 00 B6
- pfrcpit2 mm0, qword ptr ds:[rax]0F 0F 00 B4
- pfmul mm0, qword ptr ds:[rax]0F 0F 00 B5
- invalid4.4. Пересечения опкодов
$instructions['9B']['|D9 /6 x32'] = 'FSTENV m28byte';\n$instructions['9B']['|D9 /7 xxx'] = 'FSTCW m2byte';
9B D9 30
- FSTENV [rax]9B D9 38
- FSTCW [rax] Для 9B D9
только два ModR/M.reg /6 и /7, и если будет иначе, то все строится иначе.
То есть, 9B D9 00
будет:
WAIT ; 9B\nFLD d,[RAX] ; D9 00
Из этого
$instructions['9B']['. xxx'] = 'WAIT';\n$instructions['D9']['/0 xxx'] = 'FLD m32fp';
Такие пересечения есть у опкодов начинающихся на: 9B, C6, C7, D8, D9, DA, DB, DC, DD, DE, DF
Все они скомпонованы и отсортированы по аналогии. Кстати, /[0-7] приоритетнее /r.
4.5. Исключение
Есть такие инструкции:
0F 20 /r
- MOV r64, cr0F 21 /r
- MOV r64, dr0F 22 /r
- MOV cr, r640F 23 /r
- MOV dr, r64 Все понятно, ModR/M... Но, оказывается, в данной инструкции игнорируются ModR/M.mod ([7:6] биты).
То есть, вообще не учитываются, надо внедрять исключение, ибо от этих бит зависит длинна инструкции. Получается, что это не совсем /r.
Короче, было принято волевое решение изобрести велосипед и насильно присваивать ModR/M.mod=11, дабы избежать изменения количество байт в инструкции.
По аналогии с 11:rrr:bbb получилось вот такая конструкция ~11:rrr:bbb.
$instructions['0F.20']['~11:rrr:bbb xxx'] = 'MOV r64, cr';\n$instructions['0F.21']['~11:rrr:bbb xxx'] = 'MOV r64, dr';\n$instructions['0F.22']['~11:rrr:bbb xxx'] = 'MOV cr, r64';\n$instructions['0F.23']['~11:rrr:bbb xxx'] = 'MOV dr, r64';
5. Как пользовать
<?php\n\ninclude 'x64disasm.php';\n\n$data = "\x00\x01\x02\x03";\n$data = x64disasm($data);\n\nprint_r($data);\n
Получаем
Array\n(\n\t[0] => ADD r/m8, r8\n\t[2] => ADD r8, r/m8\n)
6. Остальное
Почему PHP? Нам никто не мешает махать красными флагами.
И этот код будет работать на любой версии PHP.
Точки в опкодах? Для удобства восприятия.
Почему строковый тип? Для удобства восприятия.
Зачем? Расширить знания (потом расскажу).
Github? Да, но потом. Или может быть лучше поддержать отечество и использовать gitverse.ru...
...