Пусть есть код x86 процессора. И пусть нам необходимо написать процедуру, дисассемблирующую (разбирающую) команды процессора (инструкции).
Здесь дисассемблировать значит решить (не все но некоторые) такие задачи, как:
Поскольку (для одной инструкции) значения битов, установленных в предыдущих байтах определяют дальнейший ход разбора последующих байтов, и все инструкции переменной длины, то разбирать их придется байт за байтом.
Возможно несколько способов побайтового разбора инструкций.
disasm: ...
lodsb ; взять байт - код инструкции
movzx ebx, al
jmp jmptable[ebx*4] ; вызвать соответствующую процедуру
...
jmptable dd opc_00, opc_01, ...
Такой способ достаточно быстрый, занимает много памяти, весьма удобен для различных эмуляторов, и имеет потенциальную возможность для дисассемблирования всех инструкций. (так как 256 указателей)
Этод способ не сильно отличается от предыдущего, за исключением того, что вместо указателей на процедуры в таблице хранятся флаги, в соответствии с которыми выполняются те или иные действия.
disasm: ...
lodsb ; взять байт - код инструкции
movzx ebx, al
mov ecx, flagtable[ebx*4] ; получить флаги
...
test ecx, flag_prefix ; выполнять действия в соответствии
jnz __prefix ; с флагами
...
test ecx, flag_modrm
jnz __modrm
...
flagtable dd flag_modrm+flag_s+flag_w, ...
Надо сказать, что возможны модификации этих способов, когда вместо таблиц для байтов будут хранится таблицы для слов.
xxxtable dd 65536 dup (?) ; 256k
Лет десять назад за такие вещи сожгли бы на костре ;-), а сегодня уже не каждый скажет, что это заняло бы много памяти. Хотя для некоторых случаев можно было бы увеличить скорость дисассемблирования.
Здесь обсуждается по-сути, преобразование вышеперечисленных таблиц в код.
Но вы ошибаетесь, если думаете, что я предложу сделать так:
disasm: ...
lodsb
...
cmp al, 00h
je opc_00
...
cmp al, 0FFh
je opc_FF
...
Здесь идея заключается в том, чтобы таблицу переходов преобразовать в код процессора следующим образом:
disasm: ...
lodsb
shl al, 1
jnc opc_0xxxxxxx
jc opc_1xxxxxxx
...
opc_0xxxxxxx: shl al, 1
jnc opc_00xxxxxx
jc opc_01xxxxxx
...
Преимущество метода в том, что дерево вовсе не является строго "двоичным", и для битовых масок, которые содержат "любые" значения цепочка команд будет несколько короче.
Например для сегментных префиксов 26,2E,36,3E и соответствующей им битовой маски 001xx110 будут выполнены примерно следующие инструкции:
...
test al, 10000000b
jz opc_0xxxxxxx ; jmp
opc_0xxxxxxx: test al, 01000000b
jz opc_00xxxxxx ; jmp
opc_00xxxxxx: test al, 00100000b
jz opc_000xxxxx ; no jmp
jnz opc_001xxxxx ; jmp
opc_001xxxxx: ; здесь два элемента дерева пропущены - и в этом плюс
test al, 00000100b
jz opc_001xx0xx ; no jmp
jz opc_001xx1xx ; jmp
...
В этом методе последовательно выполняются проверки битовых масок.
Итак, пусть нам нужно выявить все сегментные префиксы, как то - ES,CS,SS,DS,FS и GS.
Очевидно, что опкоды их кодируются так:
ES: 26 00100110 CS: 2E 00110110 SS: 36 00101110 DS: 3E 00111110 FS: 64 01100100 GS: 65 01100101
и тогда мы имеем две битовых маски: 001xx110 и 0110010x
Возникает вопрос, как вместо 6-ти сравнений и переходов сделать что-то более правильное.
Эта проблема решается достаточно просто. Например вот так:
disasm: ...
lodsb
...
push eax
and al, 11111110 ; 64/65
cmp al, 01100100
pop eax
je __prefix_seg
...
push eax
and al, 11100111b ; 26/2E/36/3E
cmp al, 00100110b
pop eax
je __prefix_seg
...
Но вся проблема в том, что редкий маньяк будет в ручную переделывать битовые маски из таблицы опкодов в битовые значения для команд and и cmp, ибо опкодов тьма, а набивать еще в два раза больше - совсем тяжело.
В общем, к чему я это все. Гвоздем этого текста призван стать нижеследующий макрос.
; --- begin CMPJ.MAC -----------------------------------------------------
; programmed/debugged under TASM32 5.0
; by default assigned BX = not AX
cmpj_al equ al ; регистры по умолчанию
cmpj_bl equ bl
cmpj_ax equ ax
cmpj_bx equ bx
; примеры использования макроса:
; cmpj E9,label --> cmp al, E9 / je label
; cmpj 6x,label --> test bl,60 / jnz skip
; / test al,90 / jz label / skip:
; cmpj 0x,label --> test al,F0 / jz label
; cmpj xF,label --> test bl,0F / jz label
; cmpj xx,label --> jmp label
; cmpj 8x,label --> push eax / and al, F0
; / cmp al, 80 / pop eax / je label
; cmpj 100010xx,label
; cmpj 1xx4,label
; cmpj xx000xxx11111111,label
; виды параметров, передаваемых макросу:
; cmpj HH,label hex form, 2 digits (0..9,A..F or "x")
; cmpj HHHH,label hex form, 4 digits (0..9,A..F or "x")
; cmpj BBBBBBBB,label binary form, 8 digits (0..1 or "x")
; cmpj BBBBBBBBBBBBBBBB,label binary form, 16 digits (0..1 or "x")
; lower-case hex-digits "a".."f" are available
cmpj macro mask, label
local count,base,reg0,reg1,max,andmask,cmpmask,i
local skip,mask0,mask1
count = 0
irpc c,<mask>
count = count + 1
endm ;irpc
if count eq 2
base = 16
max = 255
reg1 = cmpj_al
reg0 = cmpj_bl
elseif count eq 4
base = 16
max = 65535
reg1 = cmpj_ax
reg0 = cmpj_bx
elseif count eq 8
base = 2
max = 255
reg1 = cmpj_al
reg0 = cmpj_bl
elseif count eq 16
base = 2
max = 65535
reg1 = cmpj_ax
reg0 = cmpj_bx
else
%out cmpj: invalid bitmask(mask) length
.err
exitm
endif
andmask = 0
cmpmask = 0
irpc c,<mask>
andmask = andmask * base
cmpmask = cmpmask * base
if ("&c" ge "0") and ("&c" le "9")
i = "&c"-"0"
elseif ("&c" ge "a") and ("&c" le "f")
i = "&c"-"a"+10
elseif ("&c" ge "A") and ("&c" le "F")
i = "&c"-"A"+10
elseif ("&c" eq "x") or ("&c" eq "X")
i = -1
else
%out cmpj: invalid digit in bitmask(mask) -- c
.err
exitm
endif
if i ge base
%out cmpj: too big digit in bitmask(mask) -- c
.err
exitm
endif
if i ne -1
andmask = andmask + base-1
cmpmask = cmpmask + i
endif
endm;irpc
mask0 = cmpmask
mask1 = andmask xor cmpmask
if andmask eq max
cmp reg1, cmpmask
je label
else
if cmpmask eq 0
if andmask eq 0
jmp label
else
test reg1, andmask
jz label
endif
else
; push eax
; and reg, andmask
; cmp reg, cmpmask
; pop eax
; je label
if mask1 eq 0
test reg0, mask0
jz label
else
test reg0, mask0
jnz skip
test reg1, mask1
jz label
skip: endif
endif
endif
endm;cmpj
; --- end CMPJ.MAC -------------------------------------------------------
Используется макрос так: в AX загружается значение опкода и следующего за
ним байта (на случай двухбайтовых проверок), в BX загружается not AX.
Первым параметром макросу подается битовая маска, вторым - имя метки,
на которую передавать управление.
disasm: ...
mov eax, [esi]
mov ebx, eax
not ebx
...
cmpj 001xx000,__prefix_seg
cmpj 0110010x,__prefix_seg
...
Вот собственно и все. Возможности макроса понятны из комментариев к нему.