Как написать дизассемблер

Дизассемблер своими руками

Время на прочтение
4 мин

Количество просмотров 47K

     Знание структуры машинных команд уже много лет не является обязательным, для того, чтобы человек мог назвать себя программистом. Естественно так было не всегда. До появления первых ассемблеров программирование осуществлялось непосредственно в машинном коде. Каторжная работа, сопряженная с большим количеством ошибок. Современные ассемблеры позволяют (в разумной степени) абстрагироваться от железа, метода кодирования команд. Что уж говорить о компиляторах высокоуровневых языков. Они поражают сложностью своей реализации и той простотой, с которой программисту позволяется преобразовывать исходный код в последовательность машинных команд (причем преобразовывать, в достаточной степени, оптимально). От программиста требуется лишь знание любимого языка/ IDE. Знание того, во что преобразует компилятор исходный листинг вовсе не обязательно.
Тем же, кому интересно взглянуть на краткое описание структуры кодирования машинных команд, пример реализации и исходный код дизассемблера для x86 архитектуры, добро пожаловать.

     Создание дизассемблера для x86 архитектуры является, хотя задачей и не особо сложной, но все, же довольно специфичной. От программиста требуются определенного рода знания – знания того, как микропроцессор распознает последовательность “байтиков” в машинном коде. Далеко не в каждом вузе можно получить такие знания в объеме достаточном для написания полнофункционального современного дизассемблера – приходится искать самому (как правило, на английском языке). Данный пост не претендует на полноту освещение проблемы создания дизассемблера, в нем лишь кратко рассказывается то, как был написан дизассемблер для x86 архитектуры, 32-разрядного режима исполнения команд. Так же хотелось бы отметить вероятность возможных неточностей при переводе некоторых понятий из официальной спецификации.

     Структура команд для intel x86

Структура команды следующая:
• Опциональные префиксы (каждый префикс имеет размер 1 байт)
• Обязательный опкод команды (1 или 2 байта)
• Mod_R/M – байтик, определяющий структуру операндов команды — опциональный.
• Опциональные байты, занимаемые операндами команды (иногда разделено как один байт поля SIB[Scale, Index, Base], смещения и непосредственного значения).

Префиксы

     Существуют следующие префиксы:
Первые шесть изменяют сегментный регистр, используемый командой при обращении к ячейке памяти.
• 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 – работа с последовательностями байт (строками)

Каждый из этих префиксов меняет семантику и (или) структуру машинной инструкции (например, ее длину или выбор мнемоники).

     Опкоды команд.
Опкод команды иногда один, иногда вмести с префиксом (ами) однозначно определяет мнемонику (название) команды. Команд много. И при усложнении современных микропроцессоров их количество не уменьшается – новые команды появляются, а устаревшие не исчезают (обратная совместимость). Список опкодов и команд ассоциированных с ними, как правило, можно скачать на официальных сайтах производителей микропроцессоров.

Байт Mod_R/M состоит из следующих полей:

• Mod – первые два бита (значение от 0 до 3)
• R/M – следующие три бита (значение от 0 до 7)
• Value of ModR/M – следующие три бита (значение от 0 до 7)

     Реализация:

Для написания дизассемблера мы будем использовать следующую страничку: http://ref.x86asm.net/geek32.html.

     Мы видим несколько таблиц. В сущности, только эти таблицы и описание их полей нам и понадобятся, для написания дизассемблера. Конечно, дополнительно требуется способность к логическому рассуждению и свободное время.
     В первой таблице представлен список машинных команд, не содержащих префикс 0x0F. Во второй список команд содержащих этот префикс (большинство этих команд появились в микропроцессорах семейства “Pentium with MMX” или более поздних).
     Следующие три таблицы позволяют преобразовать байт Mod_R/M в последовательность операндов команды для 32-битного режима кодировки команд. Причем каждая последующая из этих трех таблиц уточняет разбор Mod_R/M байта частных случаев предыдущей таблицы.
     Последняя таблица позволяет преобразовать байт Mod_R/M в последовательность операндов команды для 16-битного режима кодировки команд. По умолчанию считается, что команда кодируется в 32-битном режиме. Для смены режима кодировки используется префикс переопределения размера адреса (0x67).

     Первое, что необходимо сделать, это перенести первые две таблицы в удобные для работы структуры данных. На том же сайте можно скачать xml-версии данных таблиц, и уже их преобразовать в красивые сишные структуры. Я же поступил иначе – загрузил html таблицы в Excel, и уже там, написав несложный скриптик на VBA, получил исходный сишный код, который, уже после ручных исправлений представлял собой требуемые структуры данных.

Сам алгоритм дизассемблирования достаточно прост:

• Собирается список префиксов, используемых в текущей машинной инструкции
• Ищется в одной из двух таблиц соответствующее поле в зависимости от опкода, префиксов и поколения (модели) целевого (искомого) микропроцессора.
• Найденная нами запись характеризуется списком полей такими как поколение (модель) микропроцессора, с которого появилась поддержка данной команды или, например, список флагов, которые данная команда может изменить. Нас же, в основном, интересуют лишь мнемоника (название) команды и список операндов. Проанализировав все операнды найденной и поля байта Mod_R/M, мы сможем узнать текстовое представление и длину команды.

     Количество операндов может колебаться от нуля до трех. Исходные таблицы содержат более сотни типов операндов. Некоторые операнды дублируются – у них различные названия, но последовательность действий обработки Mod_R/M байта (и возможно последующих байтов) у них одинакова.

     Для просмотра примера обработки различных операндов и примера дизассемблирования простейшей функции “Hello world” можно скачать исходный код дизассемблера для компилятора C++ Builder 6.

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

Автор: Сергей Чубченко. Дата публикации: 29.08.2005

Дизассемблер своими руками

Как часто Вы слышали про дизассемблеры? Думаю не стоит объяснять что это такое и для чего это нужно. Вы наверняка не раз отлаживали свой проект в Olly Debugger’е или искали ошибку в ассемблерном коде штатными средствами. Во всех подобных продуктах есть дизассемблер, который довольно быстро разбирает скомпилированный машинный код из EXE файла на ассемблерный код который можно изучать и изменять. В этой статье я опишу как можно самостоятельно написать простенький дизассемблер под свои нужды.

Введение

Для начала думаю нелишне напомнить, для чего все же может пригодиться самодельный дизассемблер. Особенно это актуально, если Ваша работа связана с анализом вирусов в антивирусной лаборатории. В этом случае вы постоянно сталкиваетесь с EXE упаковщиками. Такими программами, которые способны сжимать бинарный код в 2 или более раза, при этом оставив его работоспособным. Из бесплатных представителей таких программ Вы наверняка знаете тот же UPX. А задумывались ли Вы как пишут распаковщики для таких продуктов? Вряд ли. А зря! Ядро распаковщиков, особенно статических (которые распаковывают программу без запуска) основано именно на дизассемблере, который в совокупности с анализатором кода позволяет понять код распаковываемой программы и распаковать ее используя нужный алгоритм упаковщика. Если Вы когда-нибудь решите написать такой распаковщик, то без самодельного дизассемблера не обойтись.

Дизассемблирование

Вы наверняка задаетесь вопросом: «А как это вообще работает и почему бы не написать дизассемблер с нуля?». Как бы это не было тривиально — ответ на него можно получить только осознав, насколько именно Вам сложно написать дизассемблер используя только Intel’овские мануалы на архитектуру процессора. Я лишь кратко рассмотрю принципы кодирования ассемблерных команд, чтобы принцип дизассемблирования был более прозрачным. А дальше решать Вам!

Каждый байт секции кода программы участвует в формировании той или иной машинной инструкции. Чтобы правильно определить начало следующей команды нужно правильно (как бы это сделал процессор) дизассемблировать предыдущую. Для этого необходимо четко представлять себе формат команд ассемблера. Команды процессора Intel кодируются следующим образом:

Структура команд ассемблера

При этом единственный обязательный параметр это «код операции», остальные используются в зависимости от сложности и навороченности той или иной команды. Например «преффикс» используется довольно редко, зато префиксы могут поистине творить чудеса над командами, к примеру префикс 66h меняет размерности регистров и адресов при этом в 16 битной программе этот префикс позволяет юзать 32 битные регистры, а в 32 битной — 16 битные. Поля modR/M позволяют определить формат данных, с которыми оперирует программа, будь то регистры, адреса и прочее. Поле SIB расширяет возможности адресации 32 битного режима. Процессор узнает о присутствии этого поля по битам 100b в поле R/M. Далее идут непосредственно смешения и операнды, описанные в структуре modR/M+SIB.
Расшифровкой именно этих команд и занимается дизассемблер. Чтобы его написать самому с нуля потребуется море сил и времени. Хотя есть два пути. Наиболее простой из них — составить таблицу опкодов и используя нее дизассемблировать команды (именно этот принцип используется в большинстве дизассемблеров длин и в дельфевом декомпилере DeDe). Второй путь — самый сложный. Он подразумевает полное отсутствие таблиц и использование для дизассемблирования только тех данных что описывают мануалы от авторов процессора.
Какой путь выберете Вы — решать только Вам. Я предлагаю на начальном этапе использовать уже готовые решения. Об одном из таких решений читайте ниже.

Выбираем компонент

Как ни странно, если очень сильно постараться, можно найти целых два бесплатных дизассемблера, которые можно внедрить в свою программу на Delphi. Первый можно взять из свободно распространяющегося исходника декомпилятора Delphi — DeDe. При желании этот исходник Вы можете взять на wasm.ru и самостоятельно его изучить. Мы же рассмотрим второй дизассемблер, поставляющийся в виде компонента для Delphi и бесплатный для некоммерческого использования. Называется данный компонент madDisAsm и входит в состав большой библиотеки компонентов называющейся madCollection. Взять эту коллекцию можно отсюда: http://madshi.bei.t-online.de/madCollection.exe. Ссылка актуальна на момент выхода статьи. Теперь когда Вы скачал все что нужно, давайте разберемся, что мы имеем. А имеем мы 8 мощных компонентов, среди которых даже есть madBasic (бешеный барсик :)). Как Вы понимаете, из данного мощного пакета нам потребуется только madDisAsm. Его и рассмотрим.

madDisAsm

Данный компонент практически недокументирован. Документированы только прототипы функций и структур. Примеров же использования нет ни одного, отсюда следует что разбираться придется самим. Чтобы использовать данный компонент в нашем проекте подключим его в разделе Uses модуля так:

uses madDisAsm;

Теперь давайте рассмотрим главные функции данного компонента, которых две. Первая позволяет дизассемблировать код по команде. При этом все надстройки над кодом Вы можете реализовывать самостоятельно в ходе декомпилирования. Прототип функции имеет вид:

function ParseCode (code: pointer; var disAsm: string) : TCodeInfo; overload;
code — pointer на бинарный код программы, который мы хотим дизассемблировать;
disAsm — переменная в которую будет занесена первая дизассемблированная строчка.
TCodeInfo — структура дизассемблированной строчки. одним из элементов этой структуры является ссылка на следующую строчку, которую мы можем использовать для цикличного вызова функции. Вот полный прототип данной структуры:

TCodeInfo = record
IsValid : boolean; // определяет валидность pointer’а на код
Opcode : word; // Опкод, один ($00xx) или два ($0fxx) байта
ModRm : byte; // ModRm байт (о нем я уже писал), если присутствует, иначе 0
Call : boolean; // эта инструкция call?
Jmp : boolean; // эта инструкция jmp?
RelTarget : boolean; // адрес относительный (или абсолютный) ?
Target : pointer; // абсолютный адрес
PTarget : pointer; // pointer на информацию в коде
PPTarget : TPPointer; // pointer на pointer с информацией
TargetSize : integer; // размер информации в байтах (1/2/4)
Enlargeable : boolean; // может ли размер опкода быть расширенным?
This : pointer; // адрес начала инструкции
Next : pointer; // адрес следующей инструкции
end;

Также для нас будет интересная еще одна функция. Ее особенностью является то, что она способна дизассемблировать всю функцию целиком, автоматически находя конец функции по команде retn. Вот ее прототип:

function ParseFunction (func: pointer; var disAsm: string) : TFunctionInfo; overload;

Тут все аналогично предыдущей функции, только в данном случае код будет дизассемблироваться не покомандно а целиком, при этом компонент попытается сам определить ссылки на API и прочие данные, что есть огромный плюс. Структура, возвращаемая данной функцией покруче:

TFunctionInfo = record
IsValid : boolean;
EntryPoint : pointer;
CodeBegin : pointer;
CodeLen : integer;
LastErrorAddr : pointer;
LastErrorNo : cardinal;
LastErrorStr : string;
CodeAreas : array of record
AreaBegin : pointer;
AreaEnd : pointer;
CaseBlock : boolean;
OnExceptBlock : boolean;
CalledFrom : pointer;
Registers : array [0..7] of pointer;
end;
FarCalls : array of record
Call : boolean; // это CALL или JMP?
CodeAddr1 : pointer; // начало инструкции call
CodeAddr2 : pointer; // начало следующей инструкции
Target : pointer;
RelTarget : boolean;
PTarget : pointer;
PPTarget : TPPointer;
end;
UnknownTargets : array of record
Call : boolean;
CodeAddr1 : pointer;
CodeAddr2 : pointer;
end;
Interceptable : boolean;
Copy : record
IsValid : boolean;
BufferLen : integer;
LastErrorAddr : pointer;
LastErrorNo : cardinal;
LastErrorStr : string;
end;
end;

Есть и еще одна третья функция, которая вообще недокументированна:

function ParseFunctionEx (func: pointer; var disAsm: string, exceptAddr: Pointer; maxLines: Integer; autoDelimiters: Boolean);

Насколько я понял эта функция не возвращает структуры, зато дизассемблирует весь код нужной нам функции и кладет его в переменную disAsm. exceptAddr насколько я понял — это адрес конца дизассемблируемой функции (указывать необязательно), maxLines — число дизассемблируемых строк (если 0, то все), autoDelimiters — точно не могу сказать, но ориентировочно это — завершать ли функцию первым ret’ом или нет.

Кодим

Теперь, когда мы разобрались с работой компонента давайте писать дизассемблер! Открываем Delphi и создадим новый проект, затем поместим на форму пару Edit’ов, Memo и два CommandButton’а. В результате этих несложных манипуляций мы получим что-то похожее на интерфейс программы.

Интерфейс программы

Теперь самое время поподробнее рассказать для чего будут использоваться два текстовых поля. В первое мы будем вводить имя открываемого для дизассемблирования файла, а во второе — адрес начала дизассемблируемого кода. Так как дизассемблер понимает только pointer’ы на код — напишем функцию которая будет открывать EXE, считывать с указанного смещения код в переменную и возвращать на нее pointer. Собственно функция будет иметь вид:

function TfrmMain.GetCode(strFileName: string; strOffset: string): pointer;
var
hFile: integer;
read_bytes: cardinal;
EP_code: array[1..64000] of byte;
begin
//открываем файл
hFile:=CreateFileA(pchar(strFileName), GENERIC_READ, FILE_SHARE_READ + FILE_SHARE_WRITE, NIL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
//если файл открыт успешно
if hFile<>-1 then begin
//устанавливаем файловый указатель на начало дизассемблируемого кода
SetFilePointer(hFile,StrToInt(strOffset),NIL,FILE_BEGIN);
//считываем 64000 байт кода
ReadFile(hFile,EP_Code,64000,read_bytes,NIL);
//закрываем файл
CloseHandle(hFile);
//возвращаем pointer на считанный код
result:=@EP_Code;
end else begin
//если не смогли открыть файл — выходим
exit;
end;
end;

В данной функции я использовал исключительно Win32 API. Это позволяет добиться максимальной скорости кода и простоты переносимости на другие языки программирования. Теперь напишем функцию, которая будет покомандно дизассемблировать код, pointer на который будет в нее передаваться:

function TfrmMain.Disasm(strAsm: pointer): string;
var
strDisAsm, strdasm: string;
retval: TCodeInfo;
begin
//получим в strDisAsm первую строчку кода, а в retval — структуру, в которой имеется pointer на следующую ассемблерную команду
retval:=madDisAsm.ParseCode(strAsm,strDisAsm);
//в переменной strdasm мы будем хранить весь дизассемблированный листинг
strdasm:=strDisAsm;
//перебераем циклом команды до тех пор пока не встретим ret
while strpos(pchar(strDisAsm),’ret’)= nil do begin
//дизассемблируем очередную команду
retval:=madDisAsm.ParseCode(retval.Next,strDisAsm);
//добавляем ее в конец дизассемблированного листинга
strdasm:=strdasm + #13#10 + strDisAsm;
end;
//возвращаем дизассемблированный код
result:=strdasm;
end;

Код перестанет дизассемблироваться, когда программа наткнется на первый ret. Если программа не найдет ret, то она продолжит декомпилировать память до тех пор пока не вызовет ошибку доступа. Не забудьте сделать проверку этого. Теперь, когда основные функции готовы — нам осталось написать только обработчики для кнопок на форме. Код кнопки Dasasm будет выглядеть так:

procedure TfrmMain.cmdDisasmClick(Sender: TObject);
begin
txtDisasm.Text:=Disasm(GetCode(txtFileName.Text, txtOffset.Text));
end;

Код кнопка Dasasm function будет выглядеть так:

procedure TfrmMain.cmdDisAsmFunctionClick(Sender: TObject);
var
strDisAsm: string;
begin
madDisasm.ParseFunctionEx(GetCode(txtFileName.Text, txtOffset.Text),strDisAsm,nil,0,true);
txtDisasm.Text:=strDisAsm;
end;

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

Пишем тестовый проект
Раз мы написали дизассемблер, то самое логичное писать тестовый проект на ассемблере. Самым удобным на мой взгляд редактором и компилятором ассемблера является Fasm, который плюс к своему удобству и простоте очень часто обновляется и версия к версии становится все стабильнее и лучше. Взять его можно на wasm.ru. Уже скачали? Тогда запускаем и вводим следующий код:

include ’win32ax.inc’

.data
;создаем переменные с данными
Serial db ’Some program’,0
_MsgCaption db ’Disasm this’,0

.code
start:
;вывод сообщения на экран, что может быть проще
invoke MessageBox,0,Serial,_MsgCaption,MB_OK
;выход из программы
invoke ExitProcess,0
;установим конец процедуры, чтобы наш дизассемблер не завис
retn
.end start

Компилируем. Получаем EXE файл размером 2 килобайта. Да, дельфям до ассемблера далеко.

Ассемблер Fasm (Flat assembler)

Тестируем

Запускайте скорее только что написанный дизассемблер. Вводите в одно текстовое поле путь к тестовому проекту, а во второе адрес точки входа: 1024 (400h). Для простеньких ассемблерных программ адрес точки входа часто равен смещению секции кода, которое часто равно именно 400h. В любом случае эти данные всегда можно взять открыва программу в любом бесплатном PE Editor’е. Жмем теперь любую из кнопок декомпилера и видими в Memo код, напоминающий только что написанный нами, но в более строгом виде:

0011fb5c push 0
0011fb5e push $40100d
0011fb63 push $401000
0011fb68 push 0
0011fb6a call dword ptr [$40307a]
0011fb70 push 0
0011fb72 call dword ptr [$40305c]
0011fb78 ret

Как видите, все прекрасно работает!

Дизассемблер в работе

Заключение

Ну вот в общем и все что хотелось рассказать. Надеюсь, Вы без труда найдете применение написанному нами дизассемблеру.

Одним из приоритетных направлений в разработке любого продукта является его отладка. Ведь мало написать удовлетворяющий требованиям исправно работающий код, так нужно ещё и протестить его в «токсических условиях», на предмет выявления всевозможных ошибок. Для этих целей, инженеры Microsoft включили в состав ОС полноценный механизм дебага в виде двух библиотек пользовательского режима: это Dbgeng.dll – основной движок отладки (Debug Engine), и Dbghelp.dll – вспомогательный процессор отладочных символов PDB (Program Database).

На моей Win-7, библиотека символов имеет размер 0.8 Мб и выдаёт на экспорт аж 205 стандартных API-функций, а вот вторая Dbgeng.dll в три раза тяжелее своего (со)брата с размером ~2.5 Мб, зато экспортирует с выхлопной трубы всего 3 функции. От сюда следует, что эта либа от нас явно что-то скрывает, поскольку жалкие три процедуры никак не могут весить более двух мегабайт. В данной статье мы попытаемся заглянуть внутрь отладочного движка Engine, и в качестве примера вытащим из него полноценный дизассемблер инструкций процессоров х86.

Оглавление:

1. Знакомство с системным механизмом отладки;

2. Component-Object-Model в ассемблере;

3. Структура СОМ-библиотеки Dbgeng.dll;

4. Практика – пишем дизассемблер;

5. Заключение.

—————————————————————

1. Знакомство с механизмом отладки

Первые библиотеки отладки, так-же известные как файлы «Symbolic Debugger Engine», были созданы компанией Microsoft в 2001-году для операционной системы Windows-XP. Большая часть либы Dbghelp.dll содержит в себе функции с префиксом(Sym), что говорит об их принадлежности к символьному процессору. Они позволяют по указанному адресу вычислять имена функций, определять типы данных, а также номер строки и название файла, в котором эта строка находится. Поддерживаются и обратные операции, например поиск адреса функции по её имени. Это достаточно творческая единица, если знать как ею пользоваться (

Ссылка скрыта от гостей

на сайте мелкософт).

В состав этой библиотеки входят и привычные нам функции, без каких-либо префиксов. Например потянув за всего одну EnumerateLoadedModules() можно получить список всех модулей DLL (вместе с виртуальной базой и размером), которые загружены в интересующее нас приложение. Поскольку вся черновая работа происходит в фоне, то в большинстве случаях это удобно. На входе, функция требует лишь дескриптор процесса (в примере ниже я передаю -1, т.е. текущий процесс), и адрес callback-процедуры, куда она в цикле будет сбрасывать информацию о модулях. Функция связана со-своей «обратной процедурой» невидимой нитью, так-что программный цикл выстраивать не нужно – обход на автомате прекращается, как только коллбэк возвращает родителю ошибку:

C-подобный:

format   pe console
entry    start
include 'win32ax.inc'
;//----------
.code
start:  cinvoke  printf,<10,'    Base       Size        Name',
                         10,' ----------|-----------|-----------------',0>
         invoke  EnumerateLoadedModules,-1,Modules,0
        cinvoke  getch
        cinvoke  exit,0
;//----------
proc     Modules  mName,mBase,mSize,mUser   ;//<-------- Callback-процедура
        cinvoke  printf,<10,' 0x%08X  0x%08X  %s',0>,[mBase],[mSize],[mName]
         ret
endp
;//----- ИМПОРТ ----------
section '.idata' import data readable
library  msvcrt,'msvcrt.dll',dbghelp,'dbghelp.dll'
import   msvcrt, printf,'printf',getch,'_getch',exit,'exit'
import   dbghelp, EnumerateLoadedModules,'EnumerateLoadedModules'

Enum.png

Всё идёт прекрасно до тех пор, пока мы не сталкиваемся с вызовом функций из основного движка-отладки Dbgeng.dll – здесь и начинается самое интересное. Эта библиотека построена по модели СОМ (Component-Object-Model), а значит и вызывать из неё функции нужно соответствующим образом. Но проблема в том, что в отличии от крестов С++ и прочих высокоуровневых языков, ни один из ассемблеров не поддерживает на данный момент технологию COM/ActiveX, и врядли уже будет поддерживать в будущем. Ассемблер – это язык низкого уровня, а прослойка СОМ находится в иерархии намного выше.

Как уже упоминалось, библиотека Dbgeng.dll выдаёт на экспорт всего 3-функции (см.в тотале по ctrl+q) – это DebugCreate(), DebugConnect() и DebugConnectWide(). Но под капотом у неё припрятаны ещё порядка 300 внутренних, неэкспортируемых обычным способом функций. Чтобы подобраться к ним, для начала нужно разобраться, что вообще такое COM-интерфейс и как его реализуют современные компиляторы – вот об этом и поговорим..

2. Component-Object-Model в ассемблере

COM – многокомпонентная, клиент-серверная модель объектов Microsoft, которая является продолжением OLE и фундаментальной основой многих других технологий, в том числе ActiveX и DCOM (Distributed COM, работа с сетью). Ключевым аспектом COM является то, что эта технология обеспечивает связь между клиентом (нашим приложением) и сервером (операционной системой) посредством «интерфейсов». Именно интерфейс предоставляет клиенту способ узнать у сервера, какие конкретно возможности он поддерживает на текущий момент.

В терминологии языка С++ интерфейс – это абстрактный базовый класс, все методы которого являются виртуальными. То-есть вызов этих методов осуществляется через специальную таблицу-указателей, известную как vTable. Например, вызов метода QueryInterface из интерфейса IUnknown будет выглядеть так: IUnknown::QueryInterface(). На сайте rsdn имеется

Ссылка скрыта от гостей

, выделенный специально под описание всех нюансов СОМ-технологии.

Если-же посмотреть на СОМ глазами ассемблера, то интерфейс представляет собой ничто-иное, как обычную структуру в памяти. Чтобы придерживаться общих правил, мы будем называть её так-же, т.е. «vTable». В свою очередь методы – это лишь иное название уже привычных нам API-функций, а указатели на эти функции хранятся внутри интерфейса. Таким образом, интерфейс можно рассматривать как массив указателей на СОМ-функции. Влиться в эту тему поможет ветка на

Ссылка скрыта от гостей

, где представлены материалы по СОМ с реальными примерами на ассемблере – «допризывникам» настоятельно рекомендуется к прочтению.

В операционной системе Win имеется огромное количество СОМ-интерфейсов и это не удивительно, ведь Microsoft строит системы используя объектно-ориентированный подход программирования, а частью ООП является как-раз-таки OLE/ActiveX/СОМ. Чтобы из этого общего пула у нас была возможность выбрать и использовать в своих программах конкретный интерфейс, система назначает ему уникальный идентификатор GUID. Собрав в единую базу, Win хранит все эти идентификаторы в своём кусте реестра под названием «HKEY_CLASSES_ROOTInterface». Если выбрать любой GUID в левом окне, то в правом получим отождествлённое с этим идентификатором, название интерфейса:

Reg_GUID.png

СОМ-сервер операционной системы имеет базовый интерфейс под названием «IUnknown». Он глобален и все остальные наследуются именно от него. GUID этого интерфейса имеет значение {00000000-0000-0000-C000-000000000046}. В своей тушке интерфейс хранит указатели на три метода (функции) и на ассемблере будет выглядеть так:

C-подобный:

struct IUnknown
  QueryInterface   dd  0  ;// метод позволяет найти адрес интерфейса в памяти, по его GUID.
  AddRef           dd  0  ;// метод счётчика-ссылок на интерфейс
  Release          dd  0  ;// метод уменьшает счётчик (при достижении нуля интерфейс освобождается)
ends

Как видим, при помощи IUnknown и его метода QueryInterface() можно найти адрес любого СОМ-интерфейса в системе, но только при условии, что мы знаем GUID искомого (нужно будет передать его в качестве аргумента этому методу). Важно запомнить, что базовый интерфейс IUnknown входит в состав буквально всех СОМ-интерфейсов, занимая первые три указателя в нём. СОМ-сервер инкапсулирует его во-все интерфейсы, чтобы вести над ними учёт.

Например, когда мы получаем от сервера ссылку (указатель) на какой-нибудь интерфейс, его метод AddRef() на автомате увеличивает внутренний счётчик-обращений к данному интерфейсу. Если-же интерфейс нам больше не нужен, мы должны вызвать его метод Release(), который соответственно уменьшит этот счётчик на 1. Сервер периодически парсит счётчики активных интерфейсов и если обнаруживает в нём нуль, то из-за ненадобности сразу выгружает его из памяти. Так реализуется «время жизни» СОМ-интерфейсов, и это стандартная схема учёта системных структур, в памяти Win.

3. Структура СОМ-библиотеки Dbgeng.dll

Будем считать, что прошлись по макушкам СОМ-технологии, и теперь рассмотрим её реализацию внутри главного героя этой статьи – библиотеки Dbgeng.dll. В каком-то смысле, эта библиотека сама является полноценным СОМ-сервером, поскольку GUID’ы её интерфейсов не прописаны в системном реестре Win, хотя библиотека и является детищем самой Microsoft. Из этических соображений, все разработчики СОМ-интерфейсов обязаны сопровождать свой продукт полной документацией, чтобы армия прикладных программистов могла использовать незнакомые интерфейсы в своих программах. Связано это с тем, что не зная GUID мы просто не сможем найти ни один интерфейс в системе, и соответственно лишимся возможности вызывать из него методы.

Движок-отладки Dbgeng.dll отлично документирован в репозитории мягких – общие сведения о нём можно почерпнуть

Ссылка скрыта от гостей

. Что касается описания непосредственно имеющихся в наличии методов и GUID всех интерфейсов, то они находятся в заголовочном файле Dbgeng.h, электронная версия которого

Ссылка скрыта от гостей

. Судя по этому хидеру, в данную библиотеку включён не один, а целая дюжина связанных с отладкой различных интерфейсов, и в каждом из них имеются свои функции (методы). Исторически, 16-байтные GUID интерфейсов принято обозначать как IID, что подразумевает «Interface-Identifier».

C-подобный:

;// Названия и GUID'ы интерфейсов Dbgeng.dll
;//****************************************************
  IID_IUnknown               dd  0x00000000, 0x00000000, 0x000000c0, 0x46000000
  IID_IDebugAdvanced         dd  0xf2df5f53, 0x47bd071f, 0x3457e69d, 0x89d6fec3
  IID_IDebugBreakpoint       dd  0x5bd9d474, 0x423a5975, 0xa8648bb8, 0x650e11e7
  IID_IDebugClient           dd  0x27fe5639, 0x4f478407, 0x11ee6483, 0xc88ab08f
  IID_IDebugControl          dd  0x5182e668, 0x416e105e, 0xef2492ad, 0xba240480
  IID_IDebugDataSpaces       dd  0x88f7dfab, 0x4c3a3ea7, 0xe8c4fbae, 0xaa736110
  IID_IDebugEventCallbacks   dd  0x337be28b, 0x4d725036, 0x5fc4bfb6, 0xaa2e9fbb
  IID_IDebugInputCallbacks   dd  0x9f50e42c, 0x499ef136, 0x0373979a, 0x2ded946c
  IID_IDebugOutputCallbacks  dd  0x4bf58045, 0x4c40d654, 0x3068afb0, 0xdc56f390
  IID_IDebugRegisters        dd  0xce289126, 0x45a79e84, 0xbb677e93, 0x93146918
  IID_IDebugSymbolGroup      dd  0xf2528316, 0x44310f1a, 0xd011edae, 0xabe2e196
  IID_IDebugSymbols          dd  0x8c31e98c, 0x48a5983a, 0xe56f1690, 0x50a967d6
  IID_IDebugSystemObjects    dd  0x6b86fe2c, 0x4f0c2c4f, 0x4317a29d, 0x27c3ac11


Одной из примечательных особенностей СОМ-интерфейсов является их масштабируемость. Так, если мы захотим изменить уже существующий интерфейс, то достаточно написать недостающие методы, и добавить указатели на них в конец прежнего интерфейса. К примеру, каждый из представленных выше 13-ти фейсов имеет дополнительные экземпляры, к именам которых добавляется порядковый номер по типу: IDebugClient (основной интерфейс), и дальше IDebugClient2 (3,4,5,6,7). Каждый последующий экземпляр включает в себя какие-то свежие методы и ему назначается новый GUID, в результате чего интерфейс шагает в ногу со-временем.

Если учитывать все интерфейсы вместе с расширенными, то в библиотеке Dbgeng.dll операционной системы Win7 зарегистрировано всего 35 СОМ-интерфейсов, а общее число методов в них приближается к отметке 300. Забегая вперёд скажу, что не все они реализованы на должном уровне, в чём мы убедимся позже. Здесь нужно отметить, что движок-отладки включённый в состав ОС отличается от движка ядерного отладчика «WinDbg» – у системного версия [6.1.7601], а у того, что использует отладчик [6.12.2]. Системный файл плохо зарекомендовал себя тем, что в нём вырезана поддержка удалённой отладки, что является козырем отладочного ядра WinDbg. Поэтому и размеры библиотек у них разные, о чём свидетельствует скрин ниже:

DbgengFile.png


Посмотрим на рисунок ниже, где представлена обобщённая структура библиотеки Dbgeng.dll.
Чтобы воспользоваться услугами сервера-отладки, мы должны сначала активировать его функцией CoInitialize() из библиотеки подсистемы исполнения OLE32.dll. Теперь нужно создать «клиента отладки» функцией DebugCreate() из либы Dbgeng.dll, передав ей в виде аргумента GUID интерфейса «IDebugClient::». Это основной интерфейс клиента, где собраны часто используемые им (т.е. нашим приложением) методы.

Если зайти отладчиком OllyDbg в функцию DebugCreate() по [F7], то можно обнаружить, что она проделывает массу полезной работы – например копирует из тушки движка в пространство пользователя различные структуры, находит через GetProcAddress() и подключает вспомогательные функции отладки из библиотеки Ntdll.dll типа: DbgEvent(), DbgBreakPoint() и многое другое. Именно эта функция создаёт полный контекст отладки в памяти ОЗУ, и нам остаётся лишь вызывать методы из требуемых СОМ-интерфейсов:

Dbgeng.png


В качестве демонстрационного примера для вводной части, предлагаю код ниже, который в цикле будет запрашивать у базового интерфейса «IUnknown::» все имеющиеся в наличии интерфейсы отладочного движка. Как уже упоминалось, всего в библиотеке Dbgeng.dll их зарегистрировано 35-штук (вместе с расширенными), а GUID’ы этих интерфейсов я вынес во-внешний инклуд (см.скрепку в конце статьи). По сути здесь нет ничего особенного, однако следующий нюанс требует некоторого пояснения..

Значит передаём функции DebugCreate() GUID интерфейса «IDebugClient::», на что функция возвращает нам адрес этого интерфейса в памяти. Если вернуться к рис.выше, то можно обнаружить, что первые три метода в любом интерфейсе, есть копия базового интерфейса «IUnknown::», а первый метод – как-раз нужный нам QueryInterface(). Он ожидает на входе два аргумента – это GUID искомого интерфейса, и указатель на переменную, куда метод сохранит его адрес.

Особое внимание нужно обратить на способ вызова СОМ-методов в ассемблере. Дело в том, что помимо обозначенных прототипом аргументов, мы всегда должны добавлять ещё один лишний аргумент – в спецификации его назвали «This» и представляет он собой адрес интерфейса. Другими словами, перед вызовом любого метода из какого-либо интерфейса, мы должны явно указать серверу, из какого именно осуществляем вызов. Этот аргумент(This) всегда является первым аргументом метода – вот пример:

C-подобный:

format   pe console
include 'win32ax.inc'
entry    start
;//----------
.data
InterfaceName:
            dd  i01,i02,i03,i04,i05,i06,i07,i08,i09,i10  ;// таблица указателей на имена интерфейсов
            dd  i11,i12,i13,i14,i15,i16,i17,i18,i19,i20
            dd  i21,i22,i23,i24,i25,i26,i27,i28,i29,i30
            dd  i31,i32,i33,i34,i35

i01         db     10,' IUnknown..............: ',0
i02         db  10,10,' IDebugAdvanced........: ',0
i03         db     10,' IDebugAdvanced2.......: ',0
i04         db     10,' IDebugAdvanced3.......: ',0
i05         db  10,10,' IDebugBreakpoint......: ',0
i06         db     10,' IDebugBreakpoint2.....: ',0
i07         db     10,' IDebugBreakpoint3.....: ',0
i08         db  10,10,' IDebugClient..........: ',0
i09         db     10,' IDebugClient2.........: ',0
i10         db     10,' IDebugClient3.........: ',0
i11         db     10,' IDebugClient4.........: ',0
i12         db     10,' IDebugClient5.........: ',0
i13         db     10,' IDebugClient6.........: ',0
i14         db     10,' IDebugClient7.........: ',0
i15         db  10,10,' IDebugControl.........: ',0
i16         db     10,' IDebugControl2........: ',0
i17         db     10,' IDebugControl3........: ',0
i18         db     10,' IDebugControl4........: ',0
i19         db     10,' IDebugControl5........: ',0
i20         db     10,' IDebugControl6........: ',0
i21         db     10,' IDebugControl7........: ',0
i22         db  10,10,' IDebugDataSpaces......: ',0
i23         db     10,' IDebugDataSpaces2.....: ',0
i24         db     10,' IDebugDataSpaces3.....: ',0
i25         db  10,10,' IDebugEventCallbacks..: ',0
i26         db     10,' IDebugInputCallbacks..: ',0
i27         db     10,' IDebugOutputCallbacks.: ',0
i28         db     10,' IDebugOutputCallbacks2: ',0
i29         db  10,10,' IDebugRegisters.......: ',0
i30         db     10,' IDebugSymbolGroup.....: ',0
i31         db     10,' IDebugSymbols.........: ',0
i32         db     10,' IDebugSymbols2........: ',0
i33         db  10,10,' IDebugSystemObjects...: ',0
i34         db     10,' IDebugSystemObjects2..: ',0
i35         db     10,' IDebugSystemObjects3..: ',0

Client      dd  0  ;// переменная под адрес интерфейса "IDebugClient"
iOffset     dd  0  ;// переменная под адрес остальных интерфейсов (обновляется в цикле)
buff        db  0
;//----------
.code
start:   invoke  SetConsoleTitle,<'*** Debug Engine QueryInterface v0.1 ***',0>
         invoke  CoInitialize,0       ;// активируем СОМ-сервер

;// Создаём клиента отладки (в переменную Client получим указатель на интерфейс)
         invoke  DebugCreate,IID_IDebugClient,Client
         xchg    ebx,eax
        cinvoke  printf,<10,' Client Interface......: %08X',0>,[Client]
         cmp     ebx,S_OK
         jnz     @error               ;// если ошибка..

;// Перебрать имеющиеся в движке интерфейсы
         mov     ecx,35               ;// всего зарегистрировано (длина цикла для LOOP)
         mov     esi,GuidTable        ;// адрес таблицы-гуидов в инклуде "Dbgeng.inc"
         mov     ebx,InterfaceName    ;// адрес таблицы с именами интерфейсов
@@:      push    ecx esi ebx ebx      ;// запомнить для организации цикла!

         mov     eax,[Client]     ;// адрес интерфейса "IDebugClient"
         mov     edx,[eax]        ;// берём из него сразу-же первый указатель на метод QueryInterface()

         push    iOffset          ;// куда сохранять указатель на интерфейс
         push    esi              ;// GUID очередного интерфейса
         push    [Client]         ;// аргумент "This" (от куда вызываем метод)
         call    dword[edx]       ;// QueryInterface()!

         pop     ebx                  ;//
         mov     edx,[ebx]            ;// указатель на имя из таблицы
        cinvoke  printf,<'%s%08X',0>,edx,[iOffset]  ;// вывести на консоль имя и адрес интерфейса

         pop     ebx esi ecx          ;// восстановить данные цикла
         add     esi,16               ;// следующий GUID в таблице
         add     ebx,4                ;// следующий указатель на имя
         loop    @b                   ;// промотать цикл ECX-раз..

@exit:  invoke  CoUninitialize    ;// освобождаем СОМ-сервер
       cinvoke  getch             ;//
       cinvoke  exit,0            ;// GAME OVER!

;//===== ОБРАБОТКА ОШИБКИ ======================

@error: cinvoke  printf,<10,' Operation ERROR!!!',0>
         jmp     @exit

;//----- ИМПОРТ ----------
section '.idata' import data readable
library  msvcrt,'msvcrt.dll',dbgeng,'dbgeng.dll',
         ole32, 'ole32.dll', kernel32,'kernel32.dll'

import   msvcrt, printf,'printf',getch,'_getch',exit,'exit'
import   dbgeng, DebugCreate, 'DebugCreate'
import   ole32,  CoInitialize,'CoInitialize',CoUninitialize,'CoUninitialize'
include 'apikernel32.inc'
include 'equatesdbgeng.inc'   ;//<-------- подключаем свой инклуд!!!

Query.png


Посмотрим на результат работы программы..
Интерфейсы, у которых адресом является нуль, не реализованы в движке-отладки Dbgeng.dll и вызывать из них методы нельзя (получим исключение Access-Violation с кодом 0xC0000005, т.к. будет попытка чтения адреса нуль). Ну с интерфейсами Client::[6,7] и Control::[5,6,7] всё понятно – как видим, это обновы предыдущих и добавлены они только начиная с Win-8. Однако мне так и не удалось найти ответа, почему отсутствуют интерфейсы Breakpoint:: и Callbacks::. Ради эксперимента я даже пробовал подключать не системную библиотеку Dbgeng.dll, а переименовав подсовывал программе либу ядерного отладчика WinDbg, и всё-равно получал аналогичную картину. После нескольких попыток было решено оставить этот вопрос открытым, до лучших времён.

Из остальных интерфейсов можно смело вызывать их методы. Например, лист методов интерфейса IDebugClient::[2,3,4] выглядит так.. а остальные – перечислены в созданном мной инклуде Dbgeng.inc (см.скрепку). Обратите внимание, как добавляются расширенные интерфейсы к предыдущим. Каждый из них включает в себя полный список всех/своих предков, и только в конце добавляются новые.

C-подобный:

;// Методы интерфейсов (в комментах указаны аргументы)
;//***********************************************************
struct IUnknown
  QueryInterface            dd  0  ;// InterfaceId, pInterface
  AddRef                    dd  0  ;//
  Release                   dd  0  ;//
ends

struct IDebugClient
  Header                    IUnknown   ;//<----------- всякий интерфейс начинается с IUnknown
  AttachKrnl                dd  0  ;// Flags, ConnectOptions
  GetKrnlConnectionOptions  dd  0  ;// Buffer, BufferSize, OptionsSize
  SetKrnlConnectionOptions  dd  0  ;// Options
  StartProcessServer        dd  0  ;// Flags, Options, Reserved
  ConnectProcessServer      dd  0  ;// RemoteOptions, Server64
  DisconnectProcessServer   dd  0  ;// Server64
  GetRunProcessSysIds       dd  0  ;// Server64, Ids, Count, ActualCount
  GetRunProcessSysIdExName  dd  0  ;// Server, ExeName, Flags, Id
  GetRunProcessDescription  dd  0  ;// 9 argumets
  AttachProcess             dd  0  ;// Server64, PId, AttachFlags
  CreateProcess             dd  0  ;// Server64, CommandLine, CreateFlags
  CreateProcessAndAttach    dd  0  ;// Server, CommandLine, CreateFlags, PId, AttachFlags
  GetProcessOptions         dd  0  ;// Options
  AddProcessOptions         dd  0  ;// Options
  RemoveProcessOptions      dd  0  ;// Options
  SetProcessOptions         dd  0  ;// Options
  OpenDumpFile              dd  0  ;// DumpFile
  WriteDumpFile             dd  0  ;// DumpFile, Qualifier
  ConnectSession            dd  0  ;// Flags, HistoryLimit
  StartServer               dd  0  ;// Options
  OutputServers             dd  0  ;// OutputControl, Machine, Flags
  TerminateProcesses        dd  0  ;//
  DetachProcesses           dd  0  ;//
  EndSession                dd  0  ;// Flags
  GetExitCode               dd  0  ;// Code
  DispatchCallbacks         dd  0  ;// Timeout
  ExitDispatch              dd  0  ;// Client
  CreateClient              dd  0  ;// Client
  GetInputCallbacks         dd  0  ;// Callbacks
  SetInputCallbacks         dd  0  ;// Callbacks
  GetOutputCallbacks        dd  0  ;// Callbacks
  SetOutputCallbacks        dd  0  ;// Callbacks
  GetOutputMask             dd  0  ;// Mask
  SetOutputMask             dd  0  ;// Mask
  GetOtherOutputMask        dd  0  ;// Client, Mask
  SetOtherOutputMask        dd  0  ;// Client, Mask
  GetOutputWidth            dd  0  ;// Columns
  SetOutputWidth            dd  0  ;// Columns
  GetOutputLinePrefix       dd  0  ;// Buffer, BufferSize, PrefixSize
  SetOutputLinePrefix       dd  0  ;// Prefix
  GetIdentity               dd  0  ;// Buffer, BufferSize, IdentitySize
  OutputIdentity            dd  0  ;// OutputControl, Flags, Format
  GetEventCallbacks         dd  0  ;// Callbacks
  SetEventCallbacks         dd  0  ;// Callbacks
  FlushCallbacks            dd  0  ;//
ends

struct IDebugClient2
  Previous                  IDebugClient   ;//<------ IDebugClient2 включает в себя весь предыдущий интерфейс!
  WriteDumpFile2            dd  0  ;// DumpFile,Qualifier,FormatFlags,Comment
  AddDumpInformationFile    dd  0  ;// InfoFile,Type
  EndProcessServer          dd  0  ;// Server
  WaitForProcessServerEnd   dd  0  ;// Timeout
  IsKrnlDebuggerEnabled     dd  0  ;//
  TerminateCurrentProcess   dd  0  ;//
  DetachCurrentProcess      dd  0  ;//
  AbandonCurrentProcess     dd  0  ;//
ends

struct IDebugClient3
  Previous                       IDebugClient2
  GetRunProcessSysIdExNameWide   dd  0  ;// Server,PCWSTR ExeName,Flags,PId
  GetRunProcessDescriptionWide   dd  0  ;// 9 arguments (see Dbgeng.h)
  CreateProcessWide              dd  0  ;// Server,CommandLine,CreateFlags
  CreateProcessAndAttachWide     dd  0  ;// Server,CmdLine,CreateFlags,ProcessId,AttachFlags
ends

struct IDebugClient4
  Previous                  IDebugClient3
  OpenDumpFileWide          dd  0  ;// FileName,FileHandle
  WriteDumpFileWide         dd  0  ;// FileName,FileHandle,Qualifier,FormatFlags,Comment
  AddDumpInfoFileWide       dd  0  ;// FileName,FileHandle,Type
  GetNumberDumpFiles        dd  0  ;// PNumber
  GetDumpFile               dd  0  ;// Index,Buffer,BufferSize,PNameSize,Handle,PType
  GetDumpFileWide           dd  0  ;// Index,Buffer,BufferSize,PNameSize,Handle,PType
ends

4. Практика – пишем дизассемблер

Теперь, на финишной прямой, собрав воедино всё/вышеизложенное напишем дизассемблер, одноимённый метод которого лежит в интерфейсе IDebugControl::. Чтобы на поверхность всплыла исключительно полезная составляющая кода, я ограничился дизаcсемблированием лишь текущей программы. В идеале, нужно было дать возможность юзеру выбирать исполняемый файл, но в этом случае «пайлоад» утонул-бы в массе дополнительных функций. Здесь главное понять суть, а окружение – это уже второстепенная задача и дело вкуса. Значит алго будет такой:

1. При помощи метода QueryInterface() найти адреса интерфейсов IDebugClient:: и IDebugControl::;

2. Внутри интерфейса IDebugClient:: найти указатель на метод AttachProcess(), чтобы прицепить отладчик к текущему (или любому другому) процессу;

3. Внутри интерфейса IDebugControl:: найти указатели на методы WaitForEvent() и Disassemble() – первый ожидает события отладки, а второй генерит событие Disasm;

4. Вызвать все эти методы, обязательно в указанном выше порядке;

5. Последний метод Disassemble() сбросит в буфер дизассемблированную строку – вывести её на консоль!

6. Прокрутить цикл[5] столько раз, сколько хотим «переварить» инструкций.

7. Выход из программы.


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

C-подобный:

format   pe console
include 'win32ax.inc'
entry    start
;//----------
.data
title       db  '*** Disassembler v0.1 ***',0
clientFace  db  10,' Client Interface............: %08X',0
ctrlFace    db  10,' Control Interface...........: %08X',0
Client      dd  0   ;// адреса СОМ-интерфейсов
Control     dd  0   ;// ...^^^

Attach      dd  0   ;// указатели на методы из интерфейсов
WaitEvent   dd  0   ;//    ...^^^
Disasm      dd  0   ;//       ...^^^

pNextOffs   dd  0,0
DisasmSize  dd  0
buff        db  0
;//----------
.code
start:   invoke  SetConsoleTitle,title
         invoke  CoInitialize,0

;// Запрашиваем интерфейс клиента
         invoke  DebugCreate,IID_IDebugClient,Client
         xchg    ebx,eax
        cinvoke  printf,clientFace,[Client]
         cmp     ebx,S_OK
         jnz     @error

;// Проверить наличие интерфейса 'IDebugControl'
         mov     eax,[Client]        ;// таблица клиента
         mov     edx,[eax]           ;// адрес метода 'QueryInterface' в ней

         push    Control             ;// сюда получим указатель
         push    IID_IDebugControl   ;// GUID запрашиваемого интерфейса
         push    [Client]            ;// This = в каком интерфейсе искать
         call    dword[edx]          ;// QueryInterface!!!
;// Проверить на ошибку
         xchg    ebx,eax
        cinvoke  printf,ctrlFace,[Control]
         cmp     ebx,S_OK
         jnz     @error

;// Получаем адреса нужных нам методов из интерфейсов ==================
;// IDebugClient::AttachProcess
         mov     esi,[Client]
         mov     esi,[esi]
         mov     eax,[esi+IDebugClient.AttachProcess]
         mov     [Attach],eax
        cinvoke  printf,<10,' Client AttachProcess  method: %08X',0>,eax

;// IDebugControl::WaitForEvent
         mov     esi,[Control]
         mov     esi,[esi]
         mov     eax,[esi+IDebugControl.WaitForEvent]
         mov     [WaitEvent],eax
        cinvoke  printf,<10,' Control WaitForEvent  method: %08X',0>,eax

;// IDebugControl::Disassemble
         mov     esi,[Control]
         mov     esi,[esi]
         mov     eax,[esi+IDebugControl.Disassemble]
         mov     [Disasm],eax
        cinvoke  printf,<10,' Control Disassemble   method: %08X',0>,eax

;//***********************************************************
@process:
;// Подцепить отладчик к текущему процессу ()
         invoke  GetCurrentProcessId
         mov     ebx,DEBUG_ATTACH_NONINVASIVE + DEBUG_ATTACH_NONINVASIVE_NO_SUSPEND

         push    ebx            ;// флаги
         push    eax            ;// Pid процесса
         push    0 0            ;// Ulong64 Server
         push    [Client]       ;// This
         call    dword[Attach]
        cinvoke  printf,<10,'           Attach  ReturnCode: %08x',0>,eax

;// Ожидать событие отладки..
         push   -1              ;// INFINITY (ждать бесконечно)
         push    0              ;// DEBUG_WAIT_DEFAULT
         push    [Control]      ;// This
         call    dword[WaitEvent]
        cinvoke  printf,<10,'           DbgWait ReturnCode: %08x',10,10,0>,eax

;//===== Вызвать дизассемблер!!! ============================
         mov     ebx,start      ;// адрес первой инструкции
         mov     ecx,25         ;// всего дизассемблировать инструкций
@@:      push    ecx ebx        ;// запомнить для цикла..

         push    pNextOffs      ;// получим адрес сл.инструкции
         push    DisasmSize     ;// получим размер данных в буфере
         push    256            ;// размер буфера
         push    buff           ;// адрес приёмного буфа
         push    DEBUG_DISASM_EFFECTIVE_ADDRESS  ;// флаг дизасма
         push    0              ;// см.ниже vvv
         push    ebx            ;// qword-адрес в памяти для дизасма
         push    [Control]      ;// This
         call    dword[Disasm]
        cinvoke  printf,<' %s',0>,buff  ;// распечатать дизасм-листинг!

         pop     ebx ecx           ;// восстановить данные цикла
         mov     ebx,[pNextOffs]   ;// адрес сл.инструкции из переменной
         loop    @b                ;// промотать ECX-раз..

@exit:  cinvoke  getch             ;// GAME OVER!
         invoke  CoUninitialize    ;// освободить СОМ-сервер
        cinvoke  exit,0            ;//

;//===== ПРОЦЕДУРЫ ===============================
@error: cinvoke  printf,<10,' Operation ERROR!!!',0>
        jmp      @exit

;//----- ИМПОРТ ----------
section '.idata' import data readable
library  msvcrt,'msvcrt.dll',dbgeng,'dbgeng.dll',
         ole32, 'ole32.dll', kernel32,'kernel32.dll'
import   msvcrt, printf,'printf',getch,'_getch',exit,'exit'
import   dbgeng, DebugCreate,'DebugCreate'
import   ole32,  CoInitialize,'CoInitialize',CoUninitialize,'CoUninitialize'
include 'apikernel32.inc'
include 'equatesdbgeng.inc'

;//----- РЕСУРСЫ ---------
section '.rsrc' resource data readable
directory    RT_VERSION,ver

resource     ver, 1, LANG_NEUTRAL, vInfo
versioninfo  vInfo,
             VOS__WINDOWS32, VFT_APP, VFT2_UNKNOWN,
             LANG_ENGLISH + SUBLANG_DEFAULT, 1252,
            'CompanyName'     , 'https://codeby.net',
            'LegalCopyright'  , 'Copyright 2020-2021 (c)Marylin',
            'ProductName'     , 'Windows 7',
            'ProductVersion'  , '6.1.7601.3821',
            'FileDescription' , 'DbgEngine Disassembler',
            'FileVersion'     , '0.0.1',
            'OriginalFilename', 'DbgDisasm.exe'

Dbgeng_Disassm.png


Здесь я добавил некоторую вспомогательную информацию в шапке, чтобы продемонстрировать расположение интерфейсов и их методов. Так, первые две строчки указывают на наше пользовательское пространство памяти, куда функция DebugCreate() любезно сбросила указатели на интерфейсы. А вот сами методы находятся уже внутри библиотеки Dbgeng.dll, о чём свидетельствует их адреса с базой 0x5D0D0000. Если вызов метода возвращает в EAX=0, значит он прошёл успешно (константа S_OK), иначе в EAX получим следующие коды ошибок:

C-подобный:

;// Перечень ошибок - WinError.h.
;//---------------------------------------------
S_OK            = 0           ;// операция выполнена успешно!
S_FALSE         = 1           ;// без ошибок, но получена только часть результата (см.буфер)
E_NOINTERFACE   = 80004002h   ;// интерфейс не найден
E_POINTER       = 80004003h   ;// неверный указатель
E_ABORT         = 80004004h   ;// операция отвергнута
E_FAIL          = 80004005h   ;// операция не может быть выполнена
E_ACCESSDENIED  = 80070005h   ;// доступ запрещён (отладчик находится в безопасном режиме)
E_HANDLE        = 80070006h   ;// проблема с дескриптором
E_OUTOFMEMORY   = 8007000Eh   ;// ошибка выделения памяти
E_INVALIDARG    = 80070057h   ;// неверный аргумент метода
E_UNEXPECTED    = 8000FFFFh   ;// отладчик в неправильном состоянии (см.WaitForEvent)


5. Заключение.

Программирование СОМ-интерфейсов открывает перед нами огромные возможности, поскольку в своих/больших штанинах они прячут достаточно интересные методы, подобраться к которым можно только через указатель на интерфейс, аля GUID. По модели СОМ построена добрая половина системных библиотек – объектная модель позволяет нам работать с такими механизмами как WMI (инструментарий Windows), технологией DirectX, с библиотекой Shell32.dll и многое другое. Как упоминалось выше, любой СОМ-интерфейс обязан быть документированным, поэтому проблем не возникает – главное уловить логическую нить, а дальше уже дело техники.

По уже отработанной схеме, в скрепку ложу два исполняемых файла, а так-же инклуд с описанием GUID’ов всех интерфейсов движка Dbgeng.dll, с полным описанием входящих в их состав методов. Всем удачи, пока!

I’m interested in writing an x86 dissembler as an educational project.

The only real resource I have found is Spiral Space’s, «How to write a disassembler». While this gives a nice high level description of the various components of a disassembler, I’m interested in some more detailed resources. I’ve also taken a quick look at NASM’s source code but this is somewhat of a heavyweight to learn from.

I realize one of the major challenges of this project is the rather large x86 instruction set I’m going to have to handle. I’m also interested in basic structure, basic disassembler links, etc.

Can anyone point me to any detailed resources on writing a x86 disassembler?

Jay Bosamiya's user avatar

Jay Bosamiya

2,9512 gold badges13 silver badges33 bronze badges

asked May 29, 2009 at 3:50

mmcdole's user avatar

1

Take a look at section 17.2 of the 80386 Programmer’s Reference Manual. A disassembler is really just a glorified finite-state machine. The steps in disassembly are:

  1. Check if the current byte is an instruction prefix byte (F3, F2, or F0); if so, then you’ve got a REP/REPE/REPNE/LOCK prefix. Advance to the next byte.
  2. Check to see if the current byte is an address size byte (67). If so, decode addresses in the rest of the instruction in 16-bit mode if currently in 32-bit mode, or decode addresses in 32-bit mode if currently in 16-bit mode
  3. Check to see if the current byte is an operand size byte (66). If so, decode immediate operands in 16-bit mode if currently in 32-bit mode, or decode immediate operands in 32-bit mode if currently in 16-bit mode
  4. Check to see if the current byte is a segment override byte (2E, 36, 3E, 26, 64, or 65). If so, use the corresponding segment register for decoding addresses instead of the default segment register.
  5. The next byte is the opcode. If the opcode is 0F, then it is an extended opcode, and read the next byte as the extended opcode.
  6. Depending on the particular opcode, read in and decode a Mod R/M byte, a Scale Index Base (SIB) byte, a displacement (0, 1, 2, or 4 bytes), and/or an immediate value (0, 1, 2, or 4 bytes). The sizes of these fields depend on the opcode , address size override, and operand size overrides previously decoded.

The opcode tells you the operation being performed. The arguments of the opcode can be decoded form the values of the Mod R/M, SIB, displacement, and immediate value. There are a lot of possibilities and a lot of special cases, due to the complex nature of x86. See the links above for a more thorough explanation.

answered May 29, 2009 at 4:57

Adam Rosenfield's user avatar

Adam RosenfieldAdam Rosenfield

385k96 gold badges510 silver badges586 bronze badges

2

I would recommend checking out some open source disassemblers, preferably distorm and especially «disOps (Instructions Sets DataBase)» (ctrl+find it on the page).

The documentation itself is full of juicy information about opcodes and instructions.

Quote from https://code.google.com/p/distorm/wiki/x86_x64_Machine_Code

80×86 Instruction:

A 80×86 instruction is divided to a
number of elements:

  1. Instruction prefixes, affects the behaviour of the instruction’s
    operation.
  2. Mandatory prefix used as an opcode byte for SSE instructions.
  3. Opcode bytes, could be one or more bytes (up to 3 whole bytes).
  4. ModR/M byte is optional and sometimes could contain a part of the
    opcode itself.
  5. SIB byte is optional and represents complex memory indirection
    forms.
  6. Displacement is optional and it is a value of a varying size of
    bytes(byte, word, long) and used as an
    offset.
  7. Immediate is optional and it is used as a general number value built
    from a varying size of bytes(byte,
    word, long).

The format looks as follows:

/-------------------------------------------------------------------------------------------------------------------------------------------
|*Prefixes | *Mandatory Prefix | *REX Prefix | Opcode Bytes | *ModR/M | *SIB | *Displacement (1,2 or 4 bytes) | *Immediate (1,2 or 4 bytes) |
-------------------------------------------------------------------------------------------------------------------------------------------/
* means the element is optional.

The data structures and decoding phases are explained in https://code.google.com/p/distorm/wiki/diStorm_Internals

Quote:

Decoding Phases

  1. [Prefixes]
  2. [Fetch Opcode]
  3. [Filter Opcode]
  4. [Extract Operand(s)]
  5. [Text Formatting]
  6. [Hex Dump]
  7. [Decoded Instruction]

Each step is explained also.


The original links are kept for historical reasons:

http://code.google.com/p/distorm/wiki/x86_x64_Machine_Code and http://code.google.com/p/distorm/wiki/diStorm_Internals

viv's user avatar

viv

1032 silver badges3 bronze badges

answered May 29, 2009 at 4:41

hannson's user avatar

hannsonhannson

4,4578 gold badges37 silver badges46 bronze badges

3

Start with some small program that has been assembled, and which gives you both the generated code and the instructions. Get yourself a reference with the instruction architecture, and work through some of the generated code with the architecture reference, by hand. You’ll find that the instructions have a very stereotypical structure of inst op op op with varying number of operands. All you need to do is translate the hex or octal representation of the code to match the instructions; a little playing around will reveal it.

That process, automated, is the core of a disassembler. Ideally, you’re probably going to want to construct a n array of instruction structures internally (or externally, if the program is really large). You can then translate that array into the instructions in assembler format.

Kyle Trauberman's user avatar

answered May 29, 2009 at 4:00

Charlie Martin's user avatar

Charlie MartinCharlie Martin

109k24 gold badges194 silver badges260 bronze badges

You need a table of opcodes to load from.

The fundamental lookup datastructure is a trie, however a table will do well enough if you don’t care much about speed.

To get the base opcode type, beginswith match on the table.

There are a few stock ways of decoding register arguments; however, there are enough special cases to require implementing most of them individually.

Since this is educational, have a look at ndisasm.

answered May 29, 2009 at 4:02

Joshua's user avatar

JoshuaJoshua

39.8k8 gold badges72 silver badges128 bronze badges

Checkout objdump sources — it’s a great tool, it contains many opcode tables and it’s sources can provide a nice base for making your own disassembler.

answered Aug 7, 2011 at 23:27

Grzegorz Wierzowiecki's user avatar

Всем привет. Тут такое дело: ещё одна моя реверсерская мечта сбылась — я написал процессорный модуль для IDA Pro с нуля, за два дня! Если вы когда-то тоже хотели написать свой модуль, но боялись начать — думаю, моя статья сможет помочь.

В качестве кода, который требуется дизасемблировать, будет выступать код виртуальной машины из очень крутого хоррора, который выходил сначала на SNES, потом на PS1, PC и Wonderswan — «Clock Tower — The First Fear«. В игре имеется 9 концовок (sic!), атмосфера гнетущая, а в качестве главного злодея выступает «Scissorman» (человек с руками-ножницами). Заинтересовал? Тогда добро пожаловать…

Пишем дизассемблер виртуальной машины из игры Clock Tower (PS1-PC) для IDA Pro - 1

О самой виртуальной машине

Собственно, сам код, выполняющий опкоды виртуальной машины, не будет являться предметом данной статьи, но, вот краткая информация о нём:

  • 115 опкодов на все случаи жизни

  • для анализа VM я анализировал исполняемые файлы для PS1 и PC, на SNES виртуальной машины нет

  • уже написан дизассемблер-декомпилятор для Ghidra (ссылки в конце статьи)

Пишем дизассемблер виртуальной машины из игры Clock Tower (PS1-PC) для IDA Pro - 2

Подготовка

Итак, приступим к написанию кода. Я писал в Visual Studio на C/C++, но вы можете писать и на Python.

Структура проекта

Согласно устоявшимся принципам разработки процессорных модулей для IDA Pro (которые можно найти в IDA SDK), нам понадобится создать следующие пока ещё пустые файлы:

Инклуды:

  • cpu_name.hpp (где cpu_name — краткое имя вашего процессорного модуля, в моём случае это adc)

  • ins.hpp (будет содержать enum со списком всех опкодов)

Файлы с кодом:

  • ana.cpp (здесь будет находиться код анализатора (хорошо, что не как у radare2 — anal)

  • emu.cpp (сюда мы будем писать код эмулятора)

  • ins.cpp (имена опкодов и их feature-флаги — здесь)

  • out.cpp (код, который отвечает за сам вывод мнемоник/операндов и прочей атрибутики, типа запятых, скобок и т.д.)

  • reg.cpp (можно предположить, что это про регистры, но нет — это про регистрацию модуля, его флаги и прочую конфигурацию. Именно здесь начинается модуль)

Пишем дизассемблер виртуальной машины из игры Clock Tower (PS1-PC) для IDA Pro - 3

Об анализаторе, эмуляторе и выводе

Чтобы проще было изучать готовые процессорные модули и, соответственно, писать свои, стоит сначала понять, что же такое анализатор (ana), эмулятор (emu) и вывод (out). Начнём с ana.

Анализатор (ana)

Задачи анализатора: чтение входных данных, формирование опкодов и операндов для них. Обычно, это выглядит так: читаем байт/ворд/дворд, находим соответствие одному из наших опкодов, затем читаем данные до тех пор, пока не заполним данные для каждого операнда (индекс регистра/адрес в памяти/смещение для прыжка и т.п.). Пока читаем, формируется итоговая длина инструкции.

Эмулятор (emu)

После того, как анализатор закончил свою работу, в дело вступает эмулятор. Его задача: формирование ссылок и пост-анализ кода. Ссылки могут быть:

  • на следующую инструкцию, если это не return, jump и другие подобные инструкции

  • на адреса в памяти: чтение/запись/обычное смещение

  • на стек и локальные переменные

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

Вывод (out)

И, наконец, финальный этап для процессорного модуля — код для вывода накопленной информации об инструкциях (out). Данная часть вызывается Идой только тогда, когда инструкция попадает в область видимости листинга. И именно здесь кроется причина, почему процессорный модуль для Гидры умеет в декомпиляцию из коробки, а модуль из Иды — нет. Выводилка последней — это, фактически, простая printf того, что вы ей укажете, с возможностями вставлять отступы, делать вывод цветным и т.д. Никакого тебе IR (Intermediate Representation). Поэтому, если хочешь декомпилятор для экзотической платформы, у тебя два варианта:

  1. Использовать Ghidra

  2. Не использовать Ghidra. Вместо этого изощряться через Вывод. Годится лишь для простых виртуальных машин

В случае простой виртуальной машины. каковой является VM из Clock Tower, можно извратиться и выводить сразу псевдо-сишный код (ничего страшного в этом нет). Я так и сделал.

Пишем процессорный модуль

Хватит разглагольствовать, давайте уже писать код. Открываем Visual Studio, создаём пустой проект DLL, прописываем пути к инклудам и библиотекам IDA SDK. Указываем следующие процессорные директивы (все три являются обязательными):

__NT__
__IDP__
__X64__

Если необходима поддержка вашего процессорного модуля Идой с 64-битным адресным пространством, также добавляем флаг:

__EA64__

Теперь открываем reg.cpp и вставляем следующий шаблон:

#include "adc.hpp" // your main VM include

processor_t LPH = {
  IDP_INTERFACE_VERSION,
  0x8000 + 666, // proc ID
  PR_USE32 | PR_DEFSEG32 | PRN_HEX | PR_WORD_INS | PR_BINMEM | PR_NO_SEGMOVE | PR_CNDINSNS, // flags
  0, // flags2
  8, 8, // bits in a byte (code/data)
  shnames, // short processor names
  lnames, // long processor names
  asms, // assembler definitions

  notify, // callback to create our proc module instance

  regnames, // register names
  qnumber(regnames), // registers count

  rVcs, rVds, // number of first/last segment register
  0, // segment register size
  rVcs, rVds, // virtual code/data segment register

  NULL, // typical code start sequences
  retcodes, // return opcodes bytes

  0, ADCVM_last, // indices of first/last opcodes (in enum)
  Instructions, // array of instructions
};

В файл adc.hpp необходимо прописать некоторые важные инклуды:

#pragma once

#include "../idaidp.hpp" // this file is located in IDA_SDK/module/ dir
#include "ins.hpp"

Ключевых моментов здесь 3:

  1. proc ID — идентификатор вашего процессорного модуля. Все пользовательские модули должны иметь номер >= 0x8000

  2. Флаги — их много, и позволяют они достаточно тонко настроить работу процессорного модуля. Список флагов и их описания здесь

  3. rVcs/rVds — в моей виртуальной машине регистры не используются, но Иде всё равно требуется указать хотя бы виртуальные регистры. Ими мы займёмся позднее

Первым идёт shnames:

static const char* const shnames[] = { "ADCVM", NULL };

Здесь мы перечисляем все процессоры, которые поддерживает наш модуль (конечно, их может быть несколько. В конце списка обязательно должен быть NULL.

Далее создаём lnames:

static const char* const lnames[] = { "Clock Tower: Clock Tower ADC VM", NULL };

То же самое, только можно указать более длинное имя. Обратите внимание на саму строку — всё, что в ней расположено до двоеточия, является именем семейства процессоров. Итого, вся эта информация в Иде будет отображаться следующим образом:

Пишем дизассемблер виртуальной машины из игры Clock Tower (PS1-PC) для IDA Pro - 4

Следующий кусок кода, на котором многие останавливаются (в том числе и я когда-то) — это описание ассемблера. Что он из себя представляет в понимании Иды? Фактически, по большей части, это — солянка из никем не используемых и никому не нужных элементов синтаксиса ассемблера, которые выводятся в листинг самой Идой (далеко не все) без нашего участия. В основном, это касается нераспознанных данных, массивов и других директив (типа: org, end, equ, public, xref).

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

static const asm_t adcasm = {
  AS_COLON | ASH_HEXF3,
  0,
  "Clock Tower Virtual Machine Bytecode",
  0,
  NULL,         // header lines
  "org",        // org
  "end",        // end

  ";",          // comment string
  '"',          // string delimiter
  ''',         // char delimiter
  ""'",        // special symbols in char and string constants

  "dc",         // ascii string directive
  "dcb",        // byte directive
  "dc",         // word directive
  NULL,         // double words
  NULL,         // qwords
  NULL,         // oword  (16 bytes)
  NULL,         // float  (4 bytes)
  NULL,         // double (8 bytes)
  NULL,         // tbyte  (10/12 bytes)
  NULL,         // packed decimal real
  "bs#s(c,) #d, #v", // arrays (#h,#d,#v,#s(...)
  "ds %s",      // uninited arrays
  "equ",        // equ
  NULL,         // 'seg' prefix (example: push seg seg001)
  "*",          // current IP (instruction pointer)
  NULL,         // func_header
  NULL,         // func_footer
  "global",     // "public" name keyword
  NULL,         // "weak"   name keyword
  "xref",       // "extrn"  name keyword
                // .extern directive requires an explicit object size
  NULL,         // "comm" (communal variable)
  NULL,         // get_type_name
  NULL,         // "align" keyword
  '(', ')',     // lbrace, rbrace
  "%",          // mod
  "&",          // and
  "|",          // or
  "^",          // xor
  "!",          // not
  "<<",         // shl
  ">>",         // shr
  "sizeof",         // sizeof
  AS2_BYTE1CHAR,// One symbol per processor byte
};

Из всего, что здесь написано, нам нужны только флаги (они отвечают за то, как будут отображаться числа, строки, смещения), и имя для нашего ассемблера. Подробнее про структуру asm_t можно почитать здесь, ну а мы двигаем дальше.

Как уже было сказано, notify-колбэк отвечает за создание объекта класса процессорного модуля в момент, когда это нужно Иде. Смысла менять его код нет, поэтому берём как есть:

static int data_id;

ssize_t idaapi notify(void* user_data, int notification_code, va_list va) {
  if (notification_code == processor_t::ev_get_procmod) {
    data_id = 0;
    return size_t(SET_MODULE_DATA(adcvm_t));
  }

  return 0;
}

Заменяем только adcvm_t на имя класса вашего будущего модуля. Также, обращаем внимание на макрос SET_MODULE_DATA. Он отвечает за вызов функции set_module_data и создание экземпляра нашего процессора. Требует объявленного выше int data_id;. Тушкой модуля займёмся чуточку позже, а пока перейдём к регистрам.

В случае виртуальной машины из Clock Tower, вместо регистров используются адреса в рабочей (work) памяти. Но Ида, к сожалению, не может избавиться от своего сегментного прошлого и хочет в любом случае указанных cs и ds регистров, даже если они нигде не используются. Поэтому, regnames будет выглядеть следующим образом:

static const char* const regnames[] = {
  "cs", "ds"
};

Здесь вместо NULL в конце списка, Ида принимает количество регистров, которое можно указать через макрос: qnumber(regnames). Далее необходимо создать enum с самими регистрами (которых нет):

static enum adcvm_regs {
  rVcs, rVds
};

Именно на элементы этого enum-а мы и сослались в структуре. С регистрами покончено, и можно переходить к помощи Иде в распознавании типовых участков кода. Прологов в моём случае не имеется, поэтому указываем NULL, а вот коды возврата есть, поэтому пишем:

static const uchar retcode[] = { 0x00, 0xFF };

static const bytes_t retcodes[] = {
  { sizeof(retcode), retcode },
  { 0, NULL }
};

В разбираемой мной VM, все опкоды имеют минимальную длину два байта, а код инструкции return имеет значение { 0x00, 0xFF }. Итак, мы плавно перешли к самим инструкциям.

Инструкции

Как уже было сказано, опкодов в случае Clock Tower VM ровно 115. Их имена были заботливо оставлены разработчиками прямо в исполняемых файлах (для отладочного вывода, который был в итоге выпилен). В общем, берём эти имена, и добавляем к ним префикс в виде имени процессорного модуля: ADCVM, получаем enum, который необходимо расположить в ins.hpp. В итоге имеем вот такую портянку:

Мой ins.hpp

#pragma once

extern const instruc_t Instructions[]; // to reference it from reg.cpp

enum nameNum {
  ADCVM_null = 0,
  ADCVM_ret,
  ADCVM_div,
  ADCVM_mul,
  ADCVM_sub,
  ADCVM_add,
  ADCVM_dec,
  ADCVM_inc,
  ADCVM_mov,
  ADCVM_equ,
  ADCVM_neq,
  ADCVM_gre,
  ADCVM_lwr,
  ADCVM_geq,
  ADCVM_leq,
  ADCVM_cmp_end,
  ADCVM_allend,
  ADCVM_jmp,
  ADCVM_call,
  ADCVM_evdef,
  ADCVM_end,
  ADCVM_if,
  ADCVM_while,
  ADCVM_nop,
  ADCVM_endif,
  ADCVM_endwhile,
  ADCVM_else,
  ADCVM_msginit,
  ADCVM_msgattr,
  ADCVM_msgout,
  ADCVM_setmark,
  ADCVM_msgwait,
  ADCVM_evstart,
  ADCVM_bgload,
  ADCVM_palload,
  ADCVM_bgmreq,
  ADCVM_sprclr,
  ADCVM_absobjanim,
  ADCVM_objanim,
  ADCVM_allsprclr,
  ADCVM_msgclr,
  ADCVM_screenclr,
  ADCVM_screenon,
  ADCVM_screenoff,
  ADCVM_screenin,
  ADCVM_screenout,
  ADCVM_bgdisp,
  ADCVM_bganim,
  ADCVM_bgscroll,
  ADCVM_palset,
  ADCVM_bgwait,
  ADCVM_wait,
  ADCVM_bwait,
  ADCVM_boxfill,
  ADCVM_bgclr,
  ADCVM_setbkcol,
  ADCVM_msgcol,
  ADCVM_msgspd,
  ADCVM_mapinit,
  ADCVM_mapload,
  ADCVM_mapdisp,
  ADCVM_sprent,
  ADCVM_setproc,
  ADCVM_sceinit,
  ADCVM_userctl,
  ADCVM_mapattr,
  ADCVM_mappos,
  ADCVM_sprpos,
  ADCVM_spranim,
  ADCVM_sprdir,
  ADCVM_gameinit,
  ADCVM_continit,
  ADCVM_sceend,
  ADCVM_mapscroll,
  ADCVM_sprlmt,
  ADCVM_sprwalkx,
  ADCVM_allsprdisp,
  ADCVM_mapwrt,
  ADCVM_sprwait,
  ADCVM_sereq,
  ADCVM_sndstop,
  ADCVM_sestop,
  ADCVM_bgmstop,
  ADCVM_doornoset,
  ADCVM_rand,
  ADCVM_btwait,
  ADCVM_fawait,
  ADCVM_sclblock,
  ADCVM_evstop,
  ADCVM_sereqpv,
  ADCVM_sereqspr,
  ADCVM_scereset,
  ADCVM_bgsprent,
  ADCVM_bgsprpos,
  ADCVM_bgsprset,
  ADCVM_slantset,
  ADCVM_slantclr,
  ADCVM_dummy,
  ADCVM_spcfunc,
  ADCVM_sepan,
  ADCVM_sevol,
  ADCVM_bgdisptrn,
  ADCVM_debug,
  ADCVM_trace,
  ADCVM_tmwait,
  ADCVM_bgspranim,
  ADCVM_abssprent,
  ADCVM_nextcom,
  ADCVM_workclr,
  ADCVM_bgbufclr,
  ADCVM_absbgsprent,
  ADCVM_aviplay,
  ADCVM_avistop,
  ADCVM_sprmark,
  ADCVM_bgmattr,
  ADCVM_last, // required item
};

ins.cpp

Теперь нам предстоит наверное самая муторная работёнка, а именно — формирование списка имён опкодов и указание feature-флагов для них (что это — расскажу позднее). Открываем ins.cpp, вставляем в него следующий шаблон, и начинаем заполнять:

#include "adc.hpp" // your main VM include

const instruc_t Instructions[] = {
  { "", 0 }, // dummy empty instruction
};

CASSERT(qnumber(Instructions) == ADCVM_last);

Каждый элемент списка инструкций представляет из себя структуру instruc_t с полями: имя инструкции, feature-флаги инструкции.

Feature-флаги представляют из себя краткое описание того, что представляет из себя конкретная инструкция на совсем примитивном уровне, типа:

  1. CF_STOP — инструкция не передаёт управление на следующую

  2. CF_USE1CF_USE8 — используются операнды 1-8

  3. CF_CHG1CF_CHG8 — изменяются операнды 1-8

  4. CF_CALL — вызов функции

  5. CF_JUMP — прыжок на другую инструкцию

Самое интересное, что эти флаги не нужны Иде (а зачем, спрашивается, мы их указываем?). Они будут использоваться только вами (далее увидите как именно), тем не менее данную формальность всё равно необходимо соблюсти. Поэтому, вот вам парочка «типичных» инструкций и их флагов:

{ "return", CF_STOP },
{ "jmp", CF_USE1 | CF_JUMP | CF_STOP },
{ "call", CF_USE1 | CF_JUMP | CF_CALL },
{ "mul", CF_USE1 | CF_USE2 | CF_CHG1 },

Думаю, флаги в этих примерах говорят сами за себя. В случае, если операндов нет, вместо флагов указываем 0.

Класс procmod_t

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

struct adcvm_t : public procmod_t {
  virtual ssize_t idaapi on_event(ssize_t msgid, va_list va) override;
  int idaapi ana(insn_t* _insn);
  int idaapi emu(const insn_t& insn) const;
  void handle_operand(const insn_t& insn, const op_t& op, bool isload) const;
};

Именно эти функции будут выполнять всё необходимое для того, чтобы превратить набор байтиков входного файла в красивый дизассемблерный (а можно и псевдо-сишный) листинг. Открываем reg.cpp, и дополняем его следующим шаблоном кода:

ssize_t idaapi adcvm_t::on_event(ssize_t msgid, va_list va) {
  int retcode = 1;

  switch (msgid) {
  case processor_t::ev_init: {
    inf_set_be(false); // our vm uses little endian
    inf_set_gen_lzero(true); // we want to align every hex-value with zeroes
  } break;
  case processor_t::ev_term: {
    clr_module_data(data_id);
  } break;
  case processor_t::ev_newfile: {
    auto* fname = va_arg(va, char*); // here we can load additional data from a current dir
  } break;
  case processor_t::ev_is_cond_insn: {
    const auto* insn = va_arg(va, const insn_t*);
    return is_cond_insn(insn->itype);
  } break;
  case processor_t::ev_is_ret_insn: {
    const auto* insn = va_arg(va, const insn_t*);
    return (insn->itype == ADCVM_ret) ? 1 : -1;
  } break;
  case processor_t::ev_is_call_insn: {
    const auto* insn = va_arg(va, const insn_t*);
    return (insn->itype == ADCVM_call) ? 1 : -1;
  } break;
  case processor_t::ev_ana_insn: {
    auto* out = va_arg(va, insn_t*);
    return ana(out);
  } break;
  case processor_t::ev_emu_insn: {
    const auto* insn = va_arg(va, const insn_t*);
    return emu(*insn);
  } break;
  case processor_t::ev_out_insn: {
    auto* ctx = va_arg(va, outctx_t*);
    out_insn(*ctx);
  } break;
  case processor_t::ev_out_operand: {
    auto* ctx = va_arg(va, outctx_t*);
    const auto* op = va_arg(va, const op_t*);
    return out_opnd(*ctx, *op) ? 1 : -1;
  } break;
  default:
    return 0;
  }

  return retcode;
}

Имена ивентов, которые перехватывает наш колбэк достаточно говорящие, поэтому я разберу лишь некоторые из них:

  1. ev_init — инициализация процессорного модуля. Здесь мы задаём основные свойства, которые нельзя задать флагами. Например, порядок байт (endianness)

  2. ev_term — если пришло данное событие, необходимо выполнитьclr_module_dataдля корректной работы в случае, если запущено несколько экземпляров Иды с нашим модулем

Не все указанные в этом колбэке методы у нас пока созданы, поэтому их созданием как раз и займёмся. И первым у нас идёт анализатор (ana).

ana() и ana.cpp

Открываем файл ana.cpp, и вставляем в него ещё один шаблон:

#include "adc.hpp"

int idaapi adcvm_t::ana(insn_t* _insn) {
  if (_insn == NULL) {
    return 0;
  }

  insn_t& insn = *_insn;
  
  uint16 code = insn.get_next_word();
  
  switch (code) {
    case 0: {
      
    } break;
    default: {
      return 0;
    } break;
  }
  
  return insn.size;
}

Началось… Именно здесь мы будем заполнять структуры insn_t (сама инструкция) и op_t (каждый из операндов). Давайте разбираться.

Для начала мы должны прочитать необходимый токен, который сможет сказать нам, что за опкод у нас на очереди. В моём случае каждая инструкция имеет переменную длину, но сам опкод без операндов имеет длину 2 байта, т.е. word. Его и читаем. Чтение будет происходить согласно указанному ранее порядку байт. К тому же, чтение с использованием конструкции insn.get_next_xxxx() увеличит значение длины формируемой инструкции на соответствующее значение.

Например, инструкция возврата из функции в моём случае имеет значение 0xFF00. Добавим её в наш оператор switch(code), и заполним заодно одно из полей структуры инструкции (insn_t):

switch (code) {
  // ...
  case 0xFF00: {
    insn.itype = ADCVM_ret;
  } break;
  // ...
}

Поле itype — содержит номер инструкции (который обычно привязан к enum-у со всеми имеющимися опкодами). Именно это поле можно будет использовать, чтобы выполнять разные действия для разных инструкций.

Теперь давайте разберём опкод div, который имеет два операнда: dest (делимое) и src (делитель) и сохраняет результат в dest. Так будет выглядеть формирование каждого из операндов (для удобства, были созданы отдельные функции, которые можно использовать в дальнейшем):

static void op_var(insn_t& insn, op_t& x) {
  x.offb = (char)insn.size;

  uint16 ref = insn.get_next_word();
  x.addr = x.value = get_var_addr(ref); // convert short value to a mem address
  x.dtype = dt_word;
  x.type = o_mem;
}

static void op_var_or_val(insn_t& insn, op_t& x) {
  x.offb = (char)insn.size;

  uint16 ref = x.value = insn.get_next_word();
  bool isvar = is_var(ref); // mem variable or just a word value

  if (isvar) {
    x.addr = x.value = get_var_addr(ref);
  }
  
  x.dtype = dt_word;
  x.type = isvar ? o_mem : o_imm;
}

// ...
switch (code) {
  // ...
  case 0xFF0A: {
    insn.itype = ADCVM_div;
		op_var(insn, insn.Op1);
    op_var_or_val(insn, insn.Op2);
  } break;
  // ...
}

Пройдёмся по основным полям операндов, которые здесь заполняются:

  1. offb — смещение на операнд относительно начала инструкции. Если операнды кодируются битами, или значение не известно, можно использовать значение 0. Данное число будет использоваться эмулятором (т.е. нами), и встроенным hex-редактором при отображении операнда

  2. addr — целевой адрес, если данный операнд содержит ссылку

  3. value — значение, если данный операнд содержит число. Сюда же можно поместить дополнительное смещение, если, например, требуется дельта для значения в addr

  4. dtype — тип данных по целевому адресу в addr, либо значения в value. Все основные типы описаны здесь

  5. type — тип собственно операнда. Это может быть: ссылка на данные (o_mem), число (o_imm), ссылка на код (o_near). Другие типы описаны здесь

Ещё можно использовать поле reg, если у вас используются регистры. Если их нет, данное поле можно использовать под свои нужды. Ещё под эти самые нужды можно занимать поля: specval, specflag1specflag4.

Отдельная история — когда вам нужно больше чем 8 операндов. У меня было именно так (9). Решил данную проблему как раз использованием полей reg и specval. Ещё у меня прямо в некоторые инструкции могут быть закодированы: строки (не ссылки, а сами символы), массивы. Сохранить их все в указанные поля не выйдет, поэтому заполняем хотя бы основные, а в остальные закидываем всю возможную информацию, которую сможем использовать потом в эмуляторе и выводе: количество элементов массива. длину строки и т.д.

На этом работа анализатора завершена и мы переходим к формированию ссылок на код и данные.

emu() и emu.cpp

Ещё один шаблон, который предстоит заполнить (кидать в одноимённый файл):

#include "adc.hpp"

void adcvm_t::handle_operand(const insn_t& insn, const op_t& op, bool isload) const {
  switch (op.type) {
  case o_imm: { // val
    set_immd(insn.ea);
    op_num(insn.ea, op.n);
  } break;
  case o_mem: { // var
    insn.create_op_data(op.addr, op);
    insn.add_dref(op.addr, op.offb, isload ? dr_R : dr_W);
  } break;
  case o_near: { // code
    switch (insn.itype) {
    case ADCVM_call: {
    	insn.add_cref(op.addr, op.offb, fl_CN);
    } break;
    case ADCVM_jmp: {
      insn.add_cref(op.addr, op.offb, fl_JN);
    } break;
    default: {
      insn.add_dref(op.addr, op.offb, dr_O);
    } break;
    }
  } break;
  }
}

int adcvm_t::emu(const insn_t& insn) const {
  uint32 feature = insn.get_canon_feature(ph);
  bool flow = ((feature & CF_STOP) == 0);

  if (feature & CF_USE1) handle_operand(insn, insn.Op1, 1);
  if (feature & CF_USE2) handle_operand(insn, insn.Op2, 1);
  if (feature & CF_USE3) handle_operand(insn, insn.Op3, 1);
  if (feature & CF_USE4) handle_operand(insn, insn.Op4, 1);
  if (feature & CF_USE5) handle_operand(insn, insn.Op5, 1);
  if (feature & CF_USE6) handle_operand(insn, insn.Op6, 1);
  if (feature & CF_USE7) handle_operand(insn, insn.Op7, 1);
  if (feature & CF_USE8) handle_operand(insn, insn.Op8, 1);

  if (feature & CF_CHG1) handle_operand(insn, insn.Op1, 0);
  if (feature & CF_CHG2) handle_operand(insn, insn.Op2, 0);
  if (feature & CF_CHG3) handle_operand(insn, insn.Op3, 0);
  if (feature & CF_CHG4) handle_operand(insn, insn.Op4, 0);
  if (feature & CF_CHG5) handle_operand(insn, insn.Op5, 0);
  if (feature & CF_CHG6) handle_operand(insn, insn.Op6, 0);
  if (feature & CF_CHG7) handle_operand(insn, insn.Op7, 0);
  if (feature & CF_CHG8) handle_operand(insn, insn.Op8, 0);

  if (flow) {
    add_cref(insn.ea, insn.ea + insn.size, fl_F);
  }

  return 1;
}

Имеем несколько совершенно непонятных вызовов API-функций из IDA SDK, которые стоит пояснить.

  • set_immd — указание Иде, что в данной инструкции содержится число. Зачем это ей нужно, не знаю

  • op_num — указание Иде на то, что в данном операнде имеется число, что повлияет на его вывод. Возможно, вызов данной функции также активирует возможности по изменению представления числа через горячие клавиши

  • create_op_data — классная функция, которая автоматически устанавливает для целевого адреса тип данных из операнда. Например, если опкод mov копирует word в переменную в памяти, то тип данных для неё будет установлен как word

  • insn.add_dref — добавление ссылки на данные для конкретной инструкции и операнда. Также можно пометить, это ссылка на чтение (dr_R) или на запись (dr_W), либо простая ссылка (dr_O)

  • insn.add_cref — добавление ссылки на код. Это применяется для прыжков (jump) — fl_JN, вызовов (call) — fl_CN, либо обычного потока исполнения (codeflow) — fl_F

Как вы могли заметить, именно здесь и используются указанные нами в ins.cpp флаги (больше нигде). Также, инструкции с условными переходами требуют добавления для них второй ссылки (на случай ложности условия, первая — на случай истинности).

out_insn() и out.cpp

Осталось дело за малым — вывести инструкции с операндами на экран. Для этого возьмём ещё один шаблон:

Шаблон out.cpp

#include "adc.hpp"

class out_adcvm_t : public outctx_t {
  out_adcvm_t(void) = delete; // not used
public:
  bool out_operand(const op_t& x);
  void out_insn(void);
};
CASSERT(sizeof(out_adcvm_t) == sizeof(outctx_t));

DECLARE_OUT_FUNCS_WITHOUT_OUTMNEM(out_adcvm_t);

bool out_adcvm_t::out_operand(const op_t& x) {
  switch (x.type) {
  case o_void: {
    return false;
  } break;
  case o_imm: {
    out_value(x, OOFS_IFSIGN | OOFW_IMM);
  } break;
  case o_near:
  case o_mem: {
    if (!out_name_expr(x, x.addr)) {
      out_tagon(COLOR_ERROR);
      out_btoa(x.addr, 16);
      out_tagoff(COLOR_ERROR);
      remember_problem(PR_NONAME, insn.ea);
    }
  } break;
  }

  return true;
}

void out_adcvm_t::out_insn(void) {
  out_mnemonic();

  int n = 0;
  while (n < UA_MAXOP) {
    if (!insn.ops[n].shown()) {
      n++;
      continue;
    }

    if (insn.ops[n].type == o_void) {
      break;
    }

    out_one_operand(n);

    if (n + 1 < UA_MAXOP && insn.ops[n + 1].type != o_void) {
      out_symbol(',');
      out_char(' ');
    }

    n++;
  }

  flush_outbuf();
}

Не всё понятно в данном шаблоне сразу. Например, что это за класс out_adcvm_t, который наследуется от outctx_t, а за ним обращение к какому-то странному макросу DECLARE_OUT_FUNCS_WITHOUT_OUTMNEM. К тому же, здесь объявлен ещё один метод out_insn(void), без аргументов. Последнее может сбить с толку, т.к. в файле reg.cpp мы видели следующее обращение к методу out_insn:

switch (msgid) {
case processor_t::ev_out_insn: {
    auto* ctx = va_arg(va, outctx_t*);
    out_insn(*ctx);
  } break;
}

Видим, что здесь у данного метода есть входной аргумент. В общем, если вкратце, то вся магия как раз в макросе DECLARE_OUT_FUNCS_WITHOUT_OUTMNEM, который создаёт для нас вспомагательные методы out_insn и out_opnd для более удобного вывода данных в листинг, открывая при этом для использования дополнительные функции из класса outctx_t. Более подробно изучить данную логику можно в файле idaidp.hpp вашего IDA SDK.

Финал

Что, уже? Да, действительно, мы только что закончили писать процессорный модуль для IDA Pro для виртуальной машины из игры Clock Tower — The First Fear. На деле, это оказалось не так и сложно.

Конечно, есть нюансы, куда без них (например, мне пришлось повозиться в выводом строк в кодировке SHIFT-JIS), с операндами, которых больше 8, с массивами. Но, всё это уже позади: модуль успешно дизассемблирует весь загруженный бинарь, справляется как VM из версии для PS1, так и для PC.

Да, в моём случае архитектура оказалась относительно простой виртуальной машиной и, в вашем случае, это может быть что-то посложнее, для чего захочется иметь декомпилятор. Тогда, вариант (пока) здесь только один — Ghidra (о том, как я создавал процессорный модуль для неё я расскажу в следующий раз). Я же могу себе позволить выводить около-сишный листинг прямо в окно дизассемблера Иды.

Пишем дизассемблер виртуальной машины из игры Clock Tower (PS1-PC) для IDA Pro - 5

Пишем дизассемблер виртуальной машины из игры Clock Tower (PS1-PC) для IDA Pro - 6

Спасибо за внимание.

Ссылки

  • Основной репозиторий

  • Документация по IDA SDK

P.S.

Прошу прощения за большие куски кода не под спойлером — Хабр при попытке вставить блок кода в спойлер (на новом редакторе) просто виснет.

Пример дизассемблера .COM-файлов для DOS. Написан на Turbo Assembler 4.1. Разрабатывается группой студентов исключительно в учебных целях.

Авторы управляющей части:
Кравцов Дмитрий, idkravitz@gmail.com
Харитонов Роман, refaim.vl@gmail.com

Авторы модулей разбора инструкций:
Гренкин Глеб — логические инструкции (and, or, xor, test, not)
Елизаров Сергей — инструкции сложения (add, adc)
Кравцов Дмитрий — пустая инструкция (nop), арифметические сдвиги (sal, sar)
Пинчук Олег — инструкции деления (div, idiv)
Харитонов Роман — условные переходы (jxx)

Тестирование и code review:
Бураго Игорь
Лукащук Максим
Строкач Александр

Полезные ссылки:
Assembler IBM PC: http://mini-soft.ru/book/assem/index.php
TASM 3.0 Manual: http://www.citforum.ru/programming/tasm3r/index.shtml
TASM Reference Book: http://www.codenet.ru/progr/asm/tasm/index.php
DOS Function codes: http://spike.scu.edu.au/~barry/interrupts.html
MS-DOS 5.0 Programmer's Reference: http://www.ousob.com/ng/dos5/index.php


FAQ для студентов 236 и 238 групп ИМКН ДВГУ:

Q: Какие программы нужны для работы?
A: 1. Turbo Assembler for DOS (версии не ниже 4.1): http://cloud.github.com/downloads/refaim/disasm/TASM_4.1.ZIP
   2. MinGW Make: http://github.com/downloads/refaim/disasm/mingw-make-win32.zip

Q: Что нужно сделать, чтобы начать писать дизассемблер?
A: 0. Скачать последнюю версию исходного кода: http://github.com/refaim/disasm/zipball/master
   1. В файле make.bat указать путь к TASM на вашем компьютере.
        Пример файла make.bat:
        ------------------------------------------------------------------
        @echo off
        SET TASM_PATH=C:/TASM/BIN
        mingw32-make %*
        ------------------------------------------------------------------
        Путь должен быть набран с использованием прямых ('/'), а не обратных ('') слешэй. Без завершающего слэша и без кавычек.
    2. Переименовать файл example.asm в p_<название_вашей_команды>.asm. Например: p_jmp.asm, p_mov.asm, p_xor.asm. Префикс "p_" нужен для того, чтобы ваш файл был автоматически подключен при сборке проекта утилитой make. Будьте осторожны: TASM для DOS умеет понимать только имена файлов стандарта 8.3 (не более 8 символов на имя, не более 3 символов на расширение).
    3. В полученном файле переименовать процедуру parse в parse_<название_вашей_команды>. Например: parse_jmp, parse_add.
    4. Дописать полученное имя процедуры в файл funcs.inc, отделив его запятой от уже имеющихся там имён.
        Пример файла funcs.inc:
        ------------------------------------------------------------------
        FUNCS equ parse_jxx, parse_nop, parse_add
        ------------------------------------------------------------------

Q: Как скомпилировать дизассемблер?
A: С помощью файла "make.bat". Как им пользоваться:
    "make [CFG=<debug|release>] [all|clean|debug|test]"
    all -- собрать дизассемблер, clean -- удалить все полученные в результате сборки файлы, debug -- собрать дизассемблер и открыть его в Turbo Debugger'е, test -- собрать тестовый пример test.asm.
    Все параметры опциональны. Запуск "make" без параметров аналогичен "make clean all CFG=debug".
    "CFG=debug" отличается от "CFG=release", как легко догадаться, наличием отладочной информации.

Q: Как это всё работает?
A: Основная процедура main из файла main.asm читает байты из входного файла и в цикле вызывает пользовательские процедуры, которые осуществляют разбор. Имена процедур она берет из файла funcs.inc.

Q: А что за файл такой -- "main.asm"? Что с ним делать?
A: Ничего. Он уже работает и трогать его не нужно, а то всё сломается.

Q: А "common.asm"?
A: Его тоже трогать не надо. Но пользоваться определенными в нем процедурами можно.

Q: Куда же тогда писать мой код?
A: В файл "p_<название_вашей_команды>.asm". Внутри него -- куда угодно. Главное, чтобы была экспортируемая (директивой public) процедура, имя которой записано в файле "funcs.inc".

Q: А что мне разбирать? Откуда брать байты? Куда класть результат?
A: После передачи управления вашей процедуре в регистре si находится адрес буфера со входными данными (байтами из файла), а в регистре di адрес выходного буфера, куда нужно писать выходную строку. Выходная строка должна заканчиваться символом перехода на новую строку (LF, ASCII 10) и не должна содержать символа "$" (ASCII 36). Перед возвратом из процедуры вам нужно прибавить к регистрам si и di количество прочитанных и записанных байт соответственно. Если не удалось распознать команду, трогать эти регистры и писать что-либо в выходной буфер не нужно.

Q: Как быть с регистрами?
A: Состояние регистров до вызова вашей процедуры должно совпадать с их состоянием после возврата (исключая регистры si и di, о которых написано выше). Сохранение регистров обеспечивается директивой uses в начале процедуры: "uses ax, bx, dx" (сохранять нужно только используемые регистры). Кто будет портить регистры, зачёта не получит, гарантирую.

Q: А что за директива "locals" в начале файла?
A: Она позволяет создавать в процедурах локальные метки. Префикс, обозначающий локальность -- "@@" (например, "@@exit").

Q: Я всё прочитал, но ничего не понял. Что делать?
A: Посмотреть примеры: "p_nop.asm" и "p_jxx.asm".

Q: Я написал свой дизассемблер, что теперь?
A: Если вы уверены в его работоспособности, ЖЕЛАТЕЛЬНО отправить его авторам (см. выше — Авторы). Вам нужно отправить только модуль p_{имя_команды}.asm. После того как мы убедимся, что ваша программа работает корректно, мы включим её в официальную версию.

А.Б.КРУПНИК

В данной статье мне хочется рассказать о дизассемблировании большой
программы (графического редактора). Не будучи знатоком ассемблера, не
зная до сих пор, как использовать большинство возможностей своего дизассемблера(DisDoc 2.3), я все же решился написать эту статью, так как прекрасно помню, в какой кромешной тьме начинал заниматься дизассемблированием.

Тогда, год назад, я попробовал дизассемблировать простенькую программу и был страшно удивлен тем, что дизассемблер делает это неправильно, и
при повторном ассеблировании программа не работала так, как надо. Тогда же
мне удалось поговорить со знающим человеком и, хотя я чувствовал себя наивным дурачком, мне удалось выяснить главное: ПОЛНОЕ,АВТОМАТИЧЕСКОЕ ДИЗАССЕМБЛИРОВАНИЕ НЕВОЗМОЖНО, над тем текстом, который выдает дизассемблер, нужно довольно долго работать, прежде чем ассемблирование этого текста даст работоспособную программу.

В дальнейшем я постараюсь рассказать о тех приемах, которые превращают
«плохой» текст в «хороший» , т.е. в текст, который не только дает корректно
работающую программу при ассемблировании, но и позволяет себя изменить, чтобы усовершенствовать исходную программу.

ПОЧЕМУ DisDoc?

SOURSER — это название знают все, кто хотя бы краем уха слышал о дизассеблировании. Считается, что это дизассеблер замечательный, мощный, не имеющий конкурентов. Я думаю, что слухи об огромных преимуществах SOURSERа силь но преувеличены. У меня сложилось такое впечатление, что при дизассемблирова нии небольших программ (до 7 кб.) SOURSER предпочтительнее. Когда программа велика (в моем случае — 58 кб ), SOURSER работает очень медленно и, на мой взгляд, не дает никаких преимуществ.

Выбор дизассемблера DisDoc 2.3 был для меня во многом случаен. Начиная работу, я получил тексты на ассемблере как с помощью SOURSERa (версия 3.07), так и с помощью дизассемблера DisDoc 2.3. Затем оба текста после устранения очевидных ошибок были ассемблированы. И вот, то, что было выдано SOURSERом, повисло сразу, а то, что выдал DisDoc 2.3, прежде чем повиснуть, вывело на экран несколько линий. Это и определило выбор. В процессе работы я не раз имел возможность оценить основное преимущество дизассемблера DisDoc — интуитивно понятный, неизощренный, удобный и компактный листинг.

Чтобы понять дальнейшее, необходимо познакомиться с отрывком из листинга, который выдает DisDoc 2.3

		mov	cx,WORD PTR ds:d02453	;02430 
b02430:		add	cx,bx			;02434 
		mov	bx,99e7h		;02436 
		mov	dx,WORD PTR ds:d02449	;02439 
		mov	al,BYTE PTR ds:d02446	;0243d 
		call	s383 ;<09060>		;02440 
		push	cs			;02443 
		pop	ds			;02444 
		ret				;02445 
 ;----------------------------------------------------- 
 d02446		db	00			;02446 . 
 d02447		db	00,00			;02447 .. 
 d02449		db	00,00			;02449 .. 

В поле комментариев указано смещение, которое имела данная инструкция в исходной программе. Например, если вы в исходной программе, подвергаемой дизассемблированию, посмотрите отладчиком смещение 02434, то там окажется инструкция add cx,bx — на это можно положиться! Очень хороши названия меток и элементов данных. По ним сразу можно понять, какое смещение они имели в исходной программе. Например, метка b02430 имела смещение 02430, элемент данных d02446 имел смещение 02446 и т.д. То же самое относится и к подпрограммам. После вызова подпрограммы в треугольных скобках указано смещение, которое имела эта подпрограмма в исходной программе. Например, подпрограмма s383 начиналась в исходной программе со смещения 09060. Такая организация листинга позволяет сохранить однозначное соответствие с исходной программой, что дает возможность проверить отладчиком сомнительные куски кода и данных, сравнить текст, выданный дизассемблером с тем, что есть на самом деле. Это поистине драгоценная возможность. Нужно сказать, что DisDoc имеет большие недостатки, о которых речь еще пойдет, и, следовательно, применение того или иного дизассемблера — дело вкуса.

В любом случае обязательно встретятся

Фундаментальные проблемы

1. Проблема OFFSETa

Предположим, что в тексте, который выдал дизаccемблер есть такой фрагмент:

	mov	ax,bx			;1 
	shl	ax,1 ;004bc		;2 
	mov	si,8429h		;3 
	add	si,ax			;4 
	push	WORD PTR [si]		;5 

Что засылается в регистр si в третьей строчке — число 8429h или смещение некой метки? На этот вопрос позволяет ответить пятая строчка, из которой видно, что регистр si используется для косвенной адресации. Значит, исправленный фрагмент должен выглядеть следующим образом:

	mov	ax,bx			;1 
	shl	ax,1 ;004bc		;2 
	mov	si,OFFSET d08429 ;3 
	add	si,ax			;4 
	push	WORD PTR [si]		;5 

	................................ 

d08429  db 0ff,0ff,0f6	 ;8429 
	db	0ff,0d8,0ff,0a6,0ff,60	;0842c .....` 

Возможно, здесь у многих возникнет сомнение — нужно ли заменять число на соответствующий OFFSET — ведь, казалось бы, в заново ассемблированной программе данные будут иметь то же смещение? К сожалению, это не так. Во первых, мы,как правило, не знаем, какой ассемблер применялся при транслировании оригинального текста, а коды, полученные с помощью разных ассемблеров будут иметь разную длину, что приведет к изменению смещений. Например, команда AND CX,0007h транслируется MASMом 5.1 и TASMом 1.01 как 83E107 и занимает 3 байтa. Но эта же команда может быть транслирована как 81E10700 и занимать 4 байта. Во-вторых, даже если смещение сохранится, программа не поддастся модификации, так как при вставке какого-либо фрагмента кода изменятся смещения и все «развалится». Итак, OFFSETы позволяют склеить программу, делают ее пригодной для модификации. Разобранный пример достаточно примитивен. Попробуем рассмотреть более сложные ситуации и первым делом исследуем фрагмент текста, выданный дизассемблером:

	mov	bx,9006h		;08f66 
b08f75:		mov	WORD PTR ds:d087d0,bx	;08f75 
	................................. 
	call	WORD PTR cs:d087d0	;08fc3 
	...................................... 
;----------------------------------------------------- 
	push	dx			;09006 
	call	s419 ;<099a3>		;09007 
	mov	al,BYTE PTR [si]	;0900a 
	mov	BYTE PTR [si],0ffh	;0900c 
	pop	dx			;0900f 
	ret				;09010 
;-----------------------------------------------------

Здесь возникает тот-же вопрос — что такое 9006h в первой строчке фрагмента — смещение или просто число? Ответить на этот вопрос помогает информация, помещенная дизассемблером в поле комментариев. Мы уже говорили о том что числа, помещенные в этом поле, представляют собой смещения, которые имела инструкция в исходной программе, подвергаемой дизассемблированию. Нетрудно догадаться, что в приведенном фрагменте осуществляется косвенный вызов подпрограммы, и, следовательно, 9006h — это смещение, а не число. Фрагмент должен быть исправлен так:

	mov	bx,OFFSET d09006 ;08f66 
	...................................... 
;----------------------------------------------------- 
d09006:	push	dx			;09006 
	...................................... 
	ret				;09010 

Рассмотрим еще один пример косвенного вызова подпрограммы, в котором OFFSET попадает в область данных.

  
s390 	proc near 
.......................................................... 
	mov	ax,WORD PTR [bx+8792h]	;092c7 
	mov	WORD PTR ds:d087d2,ax	;092cb 
........................................................... 
	call	WORD PTR cs:d087d2	;093c8 
	ret				;093d4 
;----------------------------------------------------- 
	ror	ah,1 ;093d5		;LO]-->[HI..LO]-->[HI 
	jb	b093da ;093d7		;Jump if < (no sign) 
	ret				;093d9 
b093da:	inc	si			;093da 
	ret				;093db 
............................................................ 

Чтобы выяснить, что представляет собой 8792h, нужно посмотреть в область со смещениями, близкими к этому числу. Приведем соответствующий фрагмент, выданный дизассемблером:

  
 d08790		db	00,00,0d5,93 	;08790 ...... 
 ............................................................. 

Видно, что смещению 08792 соответствует слово 0d5,93. Теперь остается заметить, что со смещения 093d5 в исходной программе начинается фрагмент повисшего кода

  
	ror	ah,1 ;093d5 !!!!!!	;LO]-->[HI..LO]-->[HI 
	jb	b093da ;093d7		;Jump if < (no sign) 
	ret				;093d9 
b093da:	inc	si			;093da 
	ret				;093db 

Следовательно,весь разобранный пример — это хитроумный косвенный вызов подпрограммы. Исправленный фрагмент должен выглядеть так:

 
s390	proc near 
.......................................................... 
	mov	ax,WORD PTR [bx+OFFSET d08792]	;092c7 
	mov	WORD PTR ds:d087d2,ax	;092cb 
........................................................... 
	call	WORD PTR cs:d087d2	;093c8 
	ret				;093d4 
;----------------------------------------------------- 
d093d5:	ror	ah,1 ;093d5		;LO]-->[HI..LO]-->[HI 
	jb	b093da ;093d7		;Jump if < (no sign) 
	ret				;093d9 
b093da:	inc	si			;093da 
	ret				;093db 
............................................................ 
 
d08790	db	00,00 	;08790 ...... 
d08792  dw OFFSET d093d5 ;08792 

Здесь я предвижу большие возражения. Мне скажут, что все это можно интерпретировать иначе, что мои доказательства неубедительны и т.д. С этим я совершенно согласен. Более того, эти доказательства неубедительны и для меня. Гораздо сильнее убеждает то, что программа после ассемблирования работает! Дизассемблирование, как и отладка программ — процесс интуитивный. Опытный человек испытывает особое удовольствие от того, что его немотивированные догадки впоследствии подтверждаются. Как часто мысль, пришедшая в автобусе, во сне, в компании, в самой неподходящей обстановке — оказывается верной! Завершим этот пункт еще одним достаточно хитрым примером. В тексте, который выдал дизассемблер, встретился такой фрагмент:

	mov	bx,4f71h 		;0522b 
b0522e:	pop	ax			;0522e 
	cmp	ax,bx			;0522f 
	jnz	b0522e ;05231		;Jump not equal(ZF=0) 
	mov	BYTE PTR ds:d041f4,00	;05233 
	push	ax			;05238 
	ret				;05239 
	................................. 
	call	s229 ;<04fc4>		;04f71

Возникает все тот же вопрос — что такое 4f71h — число или смещение? Чтобы ответить на этот вопрос, нужно понять, что делает этот участок программы. Давайте попробуем в этом разобраться. Очевидно, из стека выталкивается число, сравнивается с 4f71h и если нет равенства, выталкивается следующее число. Если число равно 4f71h, то оно снова заталкивается в стек и происходит возврат из подпрограммы. Но куда? Ясно, что в то место, смещение которого было в исходной программе равно 4f71h. Как видно из текста, в этом месте стоял вызов подпрограммы s229. Значит, таким странным образом вызывается подпрограмма и 4f71h — это смещение! Исправленный фрагмент должен выглядеть так:

	mov	bx, OFFSET d04f71 ;0522b 
b0522e:	pop	ax			;0522e 
	cmp	ax,bx			;0522f 
	jnz	b0522e ;05231		;Jump not equal(ZF=0) 
	mov	BYTE PTR ds:d041f4,00	;05233 
	push	ax			;05238 
	ret				;05239 
	................................. 
d04f71:		call	s229 ;<04fc4>		;04f71 

2.Как отличить данные от команд?

Любой дизассемблер путает данные и команды. Особенно это относится к .COM программам, где все перемешано. Рассмотрим простой пример:

	pop	cx			;03e56 
	ret				;03e57 
;----------------------------------------------------- 
	add	BYTE PTR [bx+si],al	;03e58 
	add	BYTE PTR [bx+si],al	;03e5a 
m03e5c:	mov	BYTE PTR ds:d05830,01	;03e5c 

В этом фрагменте встретились две вычурных, повисших инструкции:

	add	BYTE PTR [bx+si],al	;03e58
	add	BYTE PTR [bx+si],al	;03e5a

Сверху они ограничены инструкцией возврата из подпрограммы ret, а снизу — меткой m03e5c. Ясно, что эти инструкции могут быть только данными. После переделки приведенный фрагмент должен выглядеть так:

	pop	cx			;03e56 
	ret				;03e57 
;----------------------------------------------------- 
d03e58	dw 0  	;03e58 
d03e5a 	db 0  	;03e5a 
d03e5b  db 0 
m03e5c:	mov	BYTE PTR ds:d05830,01	;03e5c 

Тут возникает еще один вопрос: почему в одном случае стоит dw, а в другом — db? Ответ содержится в тексте, который выдал дизассемблер. Там можно найти такие инструкции:

 	mov	si,WORD PTR ds:d03e58	;03dd0
	mov	bl,BYTE PTR ds:d03e5a	;03dd4,

Откуда следует, что d03e58 рассматривается как слово, а d03e5a — как байт. Рассмотрим чуть более сложный, но, тем не менее, очень характерный пример.

b03f53:	cmp	al,05			;03f53 
	jnz	b03f6b ;03f55		;Jump not equal(ZF=0) 
..................................................... 
	ret				;03f69 
;----------------------------------------------------- 
	add	BYTE PTR [si],bh	;03f6a 
	push	es			;03f6c 
	jnz	b03f79 ;03f6d		;Jump not equal(ZF=0) 

В приведенном фрагменте текста метка b03f6b отсутствует. Между тем эта метка должна «разрубить» пополам инструкцию add BYTE PTR [si],bh , которая начинается в оригинальной программе, подвергаемой дизассемблированию, со смещения 03f6a. Выход здесь может быть только один — смещению 03f6a соответствует байт данных, а инструкция начинается со смещения 03f6b. Исправленный фрагмент должен выглядеть так:

b03f53:	cmp	al,05			;03f53 
	jnz	b03f6b ;03f55		;Jump not equal(ZF=0) 
...................................................... 
	ret				;03f69 
;----------------------------------------------------- 
d03f6a	db 0  	;03f6a 
b03f6b:	cmp al,06h			;03f6b 
	jnz	b03f79 ;03f6d		;Jump not equal(ZF=0) 

Путаница между данными и инструкциями возникает довольно часто. SOURSER способен выдавать целые метры бессмысленных инструкций. DisDoc 2.3 в этом отношении ведет себя лучше.

3. Зависимость от транслятора

Программисты на ассемблере склонны пренебрегать правилами хорошего тона, нарушать все мыслимые табу, и это создает дополнительные трудности при дизассемблировании. В качестве примера приведем фрагмент кода, выданного дизассемблером

s25	proc near 
	inc	cx			;0086b 
	add	di,bp			;0086c 
	adc	si,00			;0086e 
	add	dx,si			;00871 
	push	di			;00873 
	shl	di,1 ;00874		;Multiply by 2's 
	adc	dx,00			;00876 
	pop	di			;00879 
	ret				;0087a 

Этот фрагмент представляется совершенно невинным, и действительно, он дизассемблирован правильно. Вся беда в том, что программист задумал изменять этот фрагмент, то есть резать по живому. Оказывается, в программе есть еще такой кусок

	mov	di,086bh		;007f8 
	...................................... 
	mov	BYTE PTR [di],4ah	;00800 
	mov	BYTE PTR [di+07],0f1h	;00803 
	mov	BYTE PTR [di+0ch],0d1h	;00807 
	...................................... 
	ret				;00815 

Рис.1

Так как di используется для косвенной адресации, нам прежде всего необходимо заменить 086bh на соответствующий OFFSET d0086b и пометить этой меткой начало подпрограммы s25:

s25 	proc near
d0086b:	inc	cx			;0086b
..............................................

Далее следует понять, что делают инструкции, приведенные на рис.1 с подпрограммой s25. Пусть эта подпрограмма асслемблирована с помощью TASM 1.01. Выданный ассемблером код будет таким, как показано на рисунке 2.

 41      INC CX       41       INC CX 
 03FD    ADD DI,BP    01EF     ADD DI,BP 
 83D600  ADC SI,0000  83D600   ADC SI,0000 
 03D6    ADD DX,SI    01F2     ADD DX,SI 
 57      PUSH DI      57       PUSH DI 
 D1E7    SHL DI,1     D1E7     SHL DI,1 
 83D200  ADC DX,0000  83D2000  ADC DX,0000 
 5F      POP DI       5F       POP DI 
 C3      RET          C3       RET 

Рис.2 Рис.3

Но вся беда в том, что исходная программа была ассемблирована другим ассемблером и имеет вид, показанный на рисунке 3. Как видно из сравнения рисунков 2 и 3, TASM 1.01 и неизвестный ассемблер транслируют инструкции ADD по-разному, и это приводит к катастрофическим последствиям. Действительно, посмотрим, как воздействует участок кода, показанный на Рис.1 (перед этим заменим 086bh на OFFSET d0086b) на подпрограмму s25, транслируемую TASMом (рис.4) и неизвестным ассемблером (рис.5).

 4A      DEC DX          4A      DEC DX 
 03FD    ADD DI,BP       01EF    ADD DI,BP 
 83D600  ADC SI,0000     83D600  ADC SI,0000 
 03F1    ADD SI,CX ;!!!! 01F1    ADD CX,SI ;!!!! 
 57      PUSH DI         57      PUSH DI 
 D1E7    SHL DI,1        D1E7    SHL DI,1 
 83D100  ADC CX,0000     83D100  ADC CX,0000 
 5F      POP DI          5F      POP DI 
 C3      RET             C3      RET 

Рис.4 Рис.5

Сравнение рисунков 4 и 5 показывает, что логика работы программы меняется в зависимости от того, какой ассемблер применялся. Как выкрутиться из этой ситуации, если нужного ассемблера нет под рукой? Самый простой, но не очень красивый путь — поставить «заплатку». Чтобы можно было использовать TASM, подпрогроамма s25 должна выглядеть так:

s25 	proc near 
d0086b:	inc	cx		;0086b 
	add di,bp   		;0086c 
	adc	si,00		;0086e 
	db 01,0f2   		;add	dx,si !!!!!! ;00871 
	push	di		;00873 
	shl	di,1 		;00874	;Multiply by 2's 
	adc	dx,00		;00876 
	pop	di		;00879 
	ret			;0087a 

Особенности и ошибки дизассемблера DisDoc 2.3

К сожалению, DisDoc 2.3 совершает ошибки, иногда регулярные, а иногда редкие, коварные и даже подлые. Самая противная ошибка — случайный пропуск данныхвстречается довольно редко. Начнем с того, что встречается очень часто.

1. EQU — кто тебя выдумал?

В коде, выданном дизассемблером, часто попадаются такие загадочные куски:

;<00465>
s12 	proc near
d0046c	equ	00046ch
	cmp	bx,5ah			;00465

Каков смысл присвоения d0046c equ 00046ch ? Чтобы выяснить это, нужно отыскать d0046c в тексте. В нашем случае элемент данных d0046c встречается очень далеко от своего первого появления — в подпрограмме s321

	mov	ax,0040h		;06257 
;<es = 0040> 
	mov	es,ax			;0625a 
	mov	al,BYTE PTR es:d0046c	;0625c 
	sti	 ;06260			;Turn ON Interrupts 
b06261:	cmp	al,BYTE PTR es:d0046c	;06261 
	jz	b06261 ;06266		;Jump if equal (ZF=1) 
	mov	al,BYTE PTR es:d0046c	;06268 
	dec	cx			;0626c 
	jnz	b06261 ;0626d		;Jump not equal(ZF=0) 
	pop	ax			;0626f 
	out	61h,al ;06270		;060-067:8024 keybrd contrlr 
;<es = 0000> 
	pop	es			;06272 
	ret				;06273 
s321 	endp 

Рис.6

При виде этого текста возникает догадка, что здесь идет зваимодействие с областью данных BIOSa . Действительно, в регистр es засылается число 40, т.е. es будет указывать на адрес 400 — начало этой области. Тогда следующий вопрос — каков смысл адреса 046сh? Легко выяснить, что по этому адресу находится счетчик прерываний от таймера. Если это так, то фрагмент, приведенный на рис.6, обретает смысл — он дает задержку на число прерываний от таймера, заданное в регистре cx. Но если все сказанное верно, то d0046c должно быть равно не 46сh, а просто 6сh! И действительно, если посмотреть подпрограмму s321 отладчиком, то станет ясно, что вместо mov al,BYTE PTR es:d0046c в тексте должно стоять mov al,6ch.

Итак, чтобы исправить эту ошибку, необходимо:

  • Удалить из начала подпрограммы s12 присвоение d0046c equ 00046ch
  • Переписать приведенный на рис.6 фрагмент s321 следующим образом:
	mov	ax,0040h		;06257 
;<es = 0040> 
	mov	es,ax			;0625a 
	mov	al,BYTE PTR es:006ch	;0625c 
	sti	 ;06260			;Turn ON Interrupts 
b06261:	cmp	al,BYTE PTR es:006ch	;06261 
	jz	b06261 ;06266		;Jump if equal (ZF=1) 
	mov	al,BYTE PTR es:006ch	;06268 
	dec	cx			;0626c 
	jnz	b06261 ;0626d		;Jump not equal(ZF=0) 
	pop	ax			;0626f 
	out	61h,al ;06270		;060-067:8024 keybrd contrlr 
;<es = 0000> 
	pop	es			;06272 
	ret				;06273 
s321 	endp 

Рассмотрим второй пример. В коде, выданном дизассемблером, встретился такой кусок:

;<0074e> 
s22 	proc near 
d0076a	equ	00076ah 
d00771	equ	000771h 
	call	s24 ;<00791>		;0074e 
	............... 
b0076a:	push	cx			;0076a 
	call	s25 ;<0086b>		;0076b 
	call	s23 ;<00776>		;0076e 
	pop	cx			;00771 
	dec	bx			;00772 

Поиск элемента данных d0076a окончился неудачей. А d00771 встретился в таком фрагменте:

	..................................... 
	mov	BYTE PTR ds:b0076a,51h	;0080b 
	mov	BYTE PTR ds:d00771,59h	;00810 
	...................................... 

Здесь явно идет модификация кода подпрограммы s22. Значит, необходимо заменить d00771 на b00771, пометить этой меткой соответствующую инструкцию в s22 и удалить присвоения

  d0076a equ	00076ah
  d00771 equ	000771h

Исправленный фрагмент s22 будет выглядеть так:

;<0074e> 
s22 proc near 
	call	s24 ;<00791>		;0074e 
...................................................... 
b0076a:	push	cx			;0076a 
	call	s25 ;<0086b>		;0076b 
	call	s23 ;<00776>		;0076e 
b00771:	pop	cx			;00771 
	dec	bx			;00772 
.............................................. 
	mov	BYTE PTR ds:b0076a,51h	;0080b 
	mov	BYTE PTR ds:b00771,59h	;00810 
................................................ 

Рассмотрим еще один пример. В начале s32 встретились уже знакомые псевдооператоры:

;<00bf7> 
s32 	proc near 
d00c1c	equ	000c1ch 
d00c1e	equ	000c1eh 

Если посмотреть в область со смещениями, близкими к с1с, то там окажется кусок повисшего кода, который может быть только данными:

	....................................... 
	or	al,BYTE PTR [bp+di]	;00c14 
	add	WORD PTR [bx+di],ax	;00c16 
	add	BYTE PTR [bx+si],al	;00c18 
	add	BYTE PTR [bx+si],al	;00c1a 
	mov	di,1306h		;00c1c 
	add	ax,06c0h		;00c1f 
	...................................... 

Рис.7

Теперь нужно поискать идентификаторы d00c1c и d00c1e в тексте, выданном дизассемблером. Очень быстро можно найти фрагменты типа: mov WORD PTR ds:d00c1c,ax, mov WORD PTR ds:d00c1e,ax. Значит, ошибка дизассемблера состоит в том, что он перепутал данные и команды и на этой почве сделал два неправильных присваивания, equ, попавших в начало подпрограммы s32.

Исправления будут заключаться в следующем:

  • Убрать из начала подпрограммы s32 два псевдооператора equ.
  • Переписать коды на рисунке 7 следующим образом:
d00c14	db 0a,03,01,01,00,00,00,00  ;00c14 
d00c1c	db 0bf,06    ;00c1c 
d00c1e  db 13,05,0c0,06   ;00c1e 

В заключение рассмотрим совсем простенький фрагмент кода:

;<01252> 
s39 	proc near 
d0125d	equ	00125dh 
d0125f	equ	00125fh 
	dec	bh			;01252 
	jz	b0124f ;01254		;Jump if equal (ZF=1) 
	xor	ah,ah			;01256 
	shl	al,1 ;01258		;Multiply by 2's 
	rcl	ah,1 ;0125a		;CF<--[HI .. LO]<--CF 
	ret				;0125c 
;----------------------------------------------------- 
	add	BYTE PTR [bx+si],al	;0125d 
	add	BYTE PTR [bx+si],al	;0125f 
s39 endp 

Укажем без комментариев, что подпрогромма s39 должна выглядеть так:

;<01252> 
s39 	proc near 
	dec	bh			;01252 
	jz	b0124f ;01254		;Jump if equal (ZF=1) 
	xor	ah,ah			;01256 
	shl	al,1 ;01258		;Multiply by 2's 
	rcl	ah,1 ;0125a		;CF<--[HI .. LO]<--CF 
	ret				;0125c 
;----------------------------------------------------- 
d0125d	db 00,00  	;0125d 
d0125f	db 00,00  	;0125f 
s39 	endp 

В заключение этого пункта подведем итоги. Значки equ называют всевдооператорами. Если говорить о дизассемблере DisDoc 2.3, то это название удивительно точное. Если в тексте встретится equ — то ошибка рядом. Между тем, иногда DisDoc 2.3 употребляет equ вполне корректно. Так что будьте бдительны и не дайте себя обмануть.

2. Дурные ошибки.

Иногда поведение дизассемблера трудно объяснить. Например, он выдает

	add	WORD PTR ds:d96be3,07	;038b6 
	shr	WORD PTR ds:d96be3,cl	;038bb ;Divide by 2's вместо 
	add	WORD PTR ds:d06bf3,07	;038b6 
	shr	WORD PTR ds:d06bf3,cl	;038bb ;Divide by 2's , 

теряет или искажает куски данных. К счастью, это происходит очень редко.

Like this post? Please share to your friends:
  • Как написать дизайн исследования медицина
  • Как написать диджей на английском
  • Как написать диван или деван
  • Как написать диарея правильно
  • Как написать диане арбениной