Приветствую,
Моя очень старая мечта сбылась — я написал модуль-отладчик, с помощью которого можно отлаживать SNES (Super Nintendo) игры прямо в IDA
! Если интересно узнать, как я это сделал, "прошу под кат" (как тут принято говорить).
Введение
Я давно увлекаюсь реверс-инжинирингом. Сначала это было просто хобби, затем стало работой (и при этом хобби никуда не делось). Только на работе "всё серьёзно", а дома — это баловство в виде обратной разработки игр под ретро-приставки: Sega Mega Drive / Genesis
, PS1
, AmigaOS
. Задача обычно стоит следующая: понять как работает игра, если есть сжатие победить его, понять как строится уровень, как размещаются враги на уровне и т.д.
За то время, которое прошло с момента, как я начал этим заниматься, мною было написано несколько удобных и полезных инструментов для тех, кто хотел реверсить игры на Сегу, Соньку, и другие платформы.
Мне удалось разреверсить один очень крутой shoot'em-up: Thunder Force 3 (а именно благодаря этой игре я и познакомился с Идой). Я написал редактор уровней, разреверсил игру до исходников на ассемблере, и всё это попутно создавая и улучшая инструмент, который в последствии и облегчал данную работу — плагин-отладчик сеговских ромов для IDA, который я назвал просто — Gensida (т.к. в основе лежал один очень популярный эмулятор этой платформы GENS, а точнее его модификация).
Без эмулятора с отладкой тоже можно создать такой плагин, но для этого придётся писать отладочный функционал с нуля, что не всегда хочется делать.
Со временем я узнал, что у Thunder Force 3
есть и версия для SNES — Thunder Spirits
, которая имеет несколько новых уровней и некоторые изменения в интерфейсе. Так вот, мне захотелось портировать всё это на Сегу, дополнив игру. Но, знаний как о самой Super Nintendo, так и о том, как её реверсить, у меня не было. Я пошёл гуглить и понял, что… как-то всё плохо с отладкой у "сеги подороже". На данный момент существует всего ДВА (!) эмулятора SNES с отладкой, и у одного нет исходников, а второй… второй имеет настолько убогий исходный код, что я боялся даже с ним работать.
Тем не менее, овладев некоторыми знаниями и умениями, и переборов желание не ввязываться в такой ужасный код (эмулятора), я смог написать и Snesida — отладчик SNES ромов для IDA. И, я считаю, что теперь то уж настал тот момент, когда я готов рассказать о том, как создать более-менее полноценный отладчик для этого ревёрсерского инструмента.
Что нам потребуется
Для того, чтобы создать свой плагин-отладчик под Иду, нам потребуется:
- IDA v7.x
- IDA SDK
- Эмулятор-отладчик (можно и без отладки, главное с исходниками, которые захочется допилить)
- Thrift (да, я выбрал его за сериализацию и RPC прямо "из коробки")
- Умение писать на C++
Думаю, список достаточно простой и понятный. Если чего-то из этого у вас нет, то плагин не получится, увы.
А теперь пишем код
Прежде чем начать, советую ознакомиться со статьёй "Модернизация IDA Pro. Отладчик для Sega Mega Drive (часть 2)", т.к. многие моменты здесь будут повторяться, но будут и некоторые новые (т.к. SDK Иды обновляется, и то, что работало раньше, теперь не применимо).
Собственно, написание любого плагина для IDA всегда начинается с создания кода-шаблона. Я использую для этого Visual Studio (на данный момент самой свежей является версия 2019).
Открываем Студию, создаём новый проект DLL, и прописываем в следующие пути к библиотекам в свойствах Linker для проекта:
- d:\idasdk76\lib\x64_win_vc_32\ — это для плагина, который будет работать с 32-битными приложениями (открываться в
ida.exe
) - d:\idasdk76\lib\x64_win_vc_64\ — это для плагина, который будет работать с 64-битными приложениями (открываться в
ida64.exe
) - Если у вас не Windows и компилятор не Visual Studio, посмотрите другие имеющиеся папки в d:\idasdk76\lib\
В линкуемые библиотеки добавляем ida.lib
. Теперь создаём пустой cpp-файл, чтобы VS показала свойства C/C++ компилятора и указываем:
- d:\idasdk76\include\ — в спискок путей к инклудам
- Меняем
/MDd
и/MD
на/MTd
и/MT
соответственно в свойствахCode Generation
— просто, чтобы не зависеть от лишних библиотек, которые не всегда установлены __NT__;__IDP__;__X64__;
— вPreprocessor Definitions
компилятора__EA64__;
— дополнительно к предыдущим флагам, если плагин будет работать с 64-битными приложениями- Убираем
SDL Checks
— с ним будет сложнее писать код
С подготовкой вроде бы всё. Теперь начнём писать код.
Плагин
Собственно, как вы уже, должно быть, поняли, отладчик для Иды это тоже плагин, а значит он должен ей как-то идентифицироваться. Поэтому пишем следующий код:
#include <ida.hpp>
#include <idp.hpp>
#include <dbg.hpp>
#include <loader.hpp>
#include "ida_plugin.h"
extern debugger_t debugger;
static bool plugin_inited;
static bool init_plugin(void) {
return (ph.id == PLFM_65C816);
}
static void print_version()
{
static const char format[] = NAME " debugger plugin v%s;\nAuthor: DrMefistO [Lab 313] <newinferno@gmail.com>.";
info(format, VERSION);
msg(format, VERSION);
}
static plugmod_t* idaapi init(void) {
if (init_plugin()) {
dbg = &debugger;
plugin_inited = true;
print_version();
return PLUGIN_KEEP;
}
return PLUGIN_SKIP;
}
static void idaapi term(void) {
if (plugin_inited) {
plugin_inited = false;
}
}
static bool idaapi run(size_t arg) {
return false;
}
char comment[] = NAME " debugger plugin by DrMefistO.";
char help[] =
NAME " debugger plugin by DrMefistO.\n"
"\n"
"This module lets you debug SNES roms in IDA.\n";
plugin_t PLUGIN = {
IDP_INTERFACE_VERSION,
PLUGIN_PROC | PLUGIN_DBG,
init,
term,
run,
comment,
help,
NAME " debugger plugin",
""
};
Здесь мы описываем наш плагин, инициализируем структуру dbg
, т.к. мы отладчик, и указываем, что работаем мы только с платформой PLFM_65C816
(в моём случае). Более подробно в статье про отладчик для Сеги.
Следом идёт ida_plugin.h
. Тут всё просто — константы для cpp-файла плагина:
#pragma once
#define NAME "snesida"
#define VERSION "1.0"
Код самого отладчика
Собственно, пока у нас в голове только идея отладчика, и мы ей горим, всё что мы можем пока написать, это базовый код, который будем постепенно дополнять. Начиная с этой части, если сравнивать с предыдущей статьёй, появились значительные изменения в коде и концепции в написании отладчика, поэтому читаем внимательно:
#include <ida.hpp>
#include <dbg.hpp>
#include <auto.hpp>
#include <deque>
#include <mutex>
#include "ida_plugin.h"
#include "ida_debmod.h"
#include "ida_registers.h"
static ::std::mutex list_mutex;
static eventlist_t events;
static const char* const p_reg[] =
{
"CF",
"ZF",
"IF",
"DF",
"XF",
"MF",
"VF",
"NF",
};
static register_info_t registers[] = {
{"A", 0, RC_CPU, dt_word, NULL, 0},
{"X", 0, RC_CPU, dt_word, NULL, 0},
{"Y", 0, RC_CPU, dt_word, NULL, 0},
{"D", 0, RC_CPU, dt_word, NULL, 0},
{"DB", 0, RC_CPU, dt_byte, NULL, 0},
{"PC", REGISTER_IP | REGISTER_ADDRESS, RC_CPU, dt_dword, NULL, 0},
{"S", REGISTER_SP | REGISTER_ADDRESS, RC_CPU, dt_word, NULL, 0},
{"P", REGISTER_READONLY, RC_CPU, dt_byte, p_reg, 0xFF},
{"m", REGISTER_READONLY, RC_CPU, dt_byte, NULL, 0},
{"x", REGISTER_READONLY, RC_CPU, dt_byte, NULL, 0},
{"e", REGISTER_READONLY, RC_CPU, dt_byte, NULL, 0},
};
static const char* register_classes[] = {
"General Registers",
NULL
};
static drc_t idaapi init_debugger(const char* hostname, int portnum, const char* password, qstring* errbuf)
{
return DRC_OK;
}
static drc_t idaapi term_debugger(void)
{
return DRC_OK;
}
static drc_t s_get_processes(procinfo_vec_t* procs, qstring* errbuf) {
process_info_t info;
info.name.sprnt("bsnes");
info.pid = 1;
procs->add(info);
return DRC_OK;
}
static drc_t idaapi s_start_process(const char* path,
const char* args,
const char* startdir,
uint32 dbg_proc_flags,
const char* input_path,
uint32 input_file_crc32,
qstring* errbuf = NULL)
{
::std::lock_guard<::std::mutex> lock(list_mutex);
events.clear();
return DRC_OK;
}
static drc_t idaapi prepare_to_pause_process(qstring* errbuf)
{
return DRC_OK;
}
static drc_t idaapi emul_exit_process(qstring* errbuf)
{
return DRC_OK;
}
static gdecode_t idaapi get_debug_event(debug_event_t* event, int timeout_ms)
{
while (true)
{
::std::lock_guard<::std::mutex> lock(list_mutex);
// are there any pending events?
if (events.retrieve(event))
{
return events.empty() ? GDE_ONE_EVENT : GDE_MANY_EVENTS;
}
if (events.empty())
break;
}
return GDE_NO_EVENT;
}
static drc_t idaapi continue_after_event(const debug_event_t* event)
{
dbg_notification_t req = get_running_notification();
switch (event->eid())
{
case PROCESS_SUSPENDED:
break;
case PROCESS_EXITED:
break;
}
return DRC_OK;
}
static drc_t idaapi s_set_resume_mode(thid_t tid, resume_mode_t resmod) // Run one instruction in the thread
{
switch (resmod)
{
case RESMOD_INTO: ///< step into call (the most typical single stepping)
break;
case RESMOD_OVER: ///< step over call
break;
}
return DRC_OK;
}
static drc_t idaapi read_registers(thid_t tid, int clsmask, regval_t* values, qstring* errbuf)
{
if (clsmask & RC_CPU)
{
}
return DRC_OK;
}
static drc_t idaapi write_register(thid_t tid, int regidx, const regval_t* value, qstring* errbuf)
{
if (regidx >= static_cast<int>(SNES_REGS::SR_PC) && regidx <= static_cast<int>(SNES_REGS::SR_EFLAG)) {
}
return DRC_OK;
}
static drc_t idaapi get_memory_info(meminfo_vec_t& areas, qstring* errbuf)
{
memory_info_t info;
info.start_ea = 0x0000;
info.end_ea = 0x01FFF;
info.sclass = "STACK";
info.bitness = 0;
info.perm = SEGPERM_READ | SEGPERM_WRITE;
areas.push_back(info);
// Don't remove this loop
for (int i = 0; i < get_segm_qty(); ++i)
{
segment_t* segm = getnseg(i);
info.start_ea = segm->start_ea;
info.end_ea = segm->end_ea;
qstring buf;
get_segm_name(&buf, segm);
info.name = buf;
get_segm_class(&buf, segm);
info.sclass = buf;
info.sbase = get_segm_base(segm);
info.perm = segm->perm;
info.bitness = segm->bitness;
areas.push_back(info);
}
// Don't remove this loop
return DRC_OK;
}
static ssize_t idaapi read_memory(ea_t ea, void* buffer, size_t size, qstring* errbuf)
{
return size;
}
static ssize_t idaapi write_memory(ea_t ea, const void* buffer, size_t size, qstring* errbuf)
{
return size;
}
static int idaapi is_ok_bpt(bpttype_t type, ea_t ea, int len)
{
switch (type)
{
case BPT_EXEC:
case BPT_READ:
case BPT_WRITE:
case BPT_RDWR:
return BPT_OK;
}
return BPT_BAD_TYPE;
}
static drc_t idaapi update_bpts(int* nbpts, update_bpt_info_t* bpts, int nadd, int ndel, qstring* errbuf)
{
for (int i = 0; i < nadd; ++i)
{
ea_t start = bpts[i].ea;
ea_t end = bpts[i].ea + bpts[i].size - 1;
bpts[i].code = BPT_OK;
}
for (int i = 0; i < ndel; ++i)
{
ea_t start = bpts[nadd + i].ea;
ea_t end = bpts[nadd + i].ea + bpts[nadd + i].size - 1;
bpts[nadd + i].code = BPT_OK;
}
*nbpts = (ndel + nadd);
return DRC_OK;
}
static ssize_t idaapi idd_notify(void*, int msgid, va_list va) {
drc_t retcode = DRC_NONE;
qstring* errbuf;
switch (msgid)
{
case debugger_t::ev_init_debugger:
{
const char* hostname = va_arg(va, const char*);
int portnum = va_arg(va, int);
const char* password = va_arg(va, const char*);
errbuf = va_arg(va, qstring*);
QASSERT(1522, errbuf != NULL);
retcode = init_debugger(hostname, portnum, password, errbuf);
}
break;
case debugger_t::ev_term_debugger:
retcode = term_debugger();
break;
case debugger_t::ev_get_processes:
{
procinfo_vec_t* procs = va_arg(va, procinfo_vec_t*);
errbuf = va_arg(va, qstring*);
retcode = s_get_processes(procs, errbuf);
}
break;
case debugger_t::ev_start_process:
{
const char* path = va_arg(va, const char*);
const char* args = va_arg(va, const char*);
const char* startdir = va_arg(va, const char*);
uint32 dbg_proc_flags = va_arg(va, uint32);
const char* input_path = va_arg(va, const char*);
uint32 input_file_crc32 = va_arg(va, uint32);
errbuf = va_arg(va, qstring*);
retcode = s_start_process(path,
args,
startdir,
dbg_proc_flags,
input_path,
input_file_crc32,
errbuf);
}
break;
case debugger_t::ev_get_debapp_attrs:
{
debapp_attrs_t* out_pattrs = va_arg(va, debapp_attrs_t*);
out_pattrs->addrsize = 3;
out_pattrs->is_be = false;
out_pattrs->platform = "bsnes";
out_pattrs->cbsize = sizeof(debapp_attrs_t);
retcode = DRC_OK;
}
break;
case debugger_t::ev_rebase_if_required_to:
{
ea_t new_base = va_arg(va, ea_t);
retcode = DRC_OK;
}
break;
case debugger_t::ev_request_pause:
errbuf = va_arg(va, qstring*);
retcode = prepare_to_pause_process(errbuf);
break;
case debugger_t::ev_exit_process:
errbuf = va_arg(va, qstring*);
retcode = emul_exit_process(errbuf);
break;
case debugger_t::ev_get_debug_event:
{
gdecode_t* code = va_arg(va, gdecode_t*);
debug_event_t* event = va_arg(va, debug_event_t*);
int timeout_ms = va_arg(va, int);
*code = get_debug_event(event, timeout_ms);
retcode = DRC_OK;
}
break;
case debugger_t::ev_resume:
{
debug_event_t* event = va_arg(va, debug_event_t*);
retcode = continue_after_event(event);
}
break;
case debugger_t::ev_thread_suspend:
{
thid_t tid = va_argi(va, thid_t);
retcode = DRC_OK;
}
break;
case debugger_t::ev_thread_continue:
{
thid_t tid = va_argi(va, thid_t);
retcode = DRC_OK;
}
break;
case debugger_t::ev_set_resume_mode:
{
thid_t tid = va_argi(va, thid_t);
resume_mode_t resmod = va_argi(va, resume_mode_t);
retcode = s_set_resume_mode(tid, resmod);
}
break;
case debugger_t::ev_read_registers:
{
thid_t tid = va_argi(va, thid_t);
int clsmask = va_arg(va, int);
regval_t* values = va_arg(va, regval_t*);
errbuf = va_arg(va, qstring*);
retcode = read_registers(tid, clsmask, values, errbuf);
}
break;
case debugger_t::ev_write_register:
{
thid_t tid = va_argi(va, thid_t);
int regidx = va_arg(va, int);
const regval_t* value = va_arg(va, const regval_t*);
errbuf = va_arg(va, qstring*);
retcode = write_register(tid, regidx, value, errbuf);
}
break;
case debugger_t::ev_get_memory_info:
{
meminfo_vec_t* ranges = va_arg(va, meminfo_vec_t*);
errbuf = va_arg(va, qstring*);
retcode = get_memory_info(*ranges, errbuf);
}
break;
case debugger_t::ev_read_memory:
{
size_t* nbytes = va_arg(va, size_t*);
ea_t ea = va_arg(va, ea_t);
void* buffer = va_arg(va, void*);
size_t size = va_arg(va, size_t);
errbuf = va_arg(va, qstring*);
ssize_t code = read_memory(ea, buffer, size, errbuf);
*nbytes = code >= 0 ? code : 0;
retcode = code >= 0 ? DRC_OK : DRC_NOPROC;
}
break;
case debugger_t::ev_write_memory:
{
size_t* nbytes = va_arg(va, size_t*);
ea_t ea = va_arg(va, ea_t);
const void* buffer = va_arg(va, void*);
size_t size = va_arg(va, size_t);
errbuf = va_arg(va, qstring*);
ssize_t code = write_memory(ea, buffer, size, errbuf);
*nbytes = code >= 0 ? code : 0;
retcode = code >= 0 ? DRC_OK : DRC_NOPROC;
}
break;
case debugger_t::ev_check_bpt:
{
int* bptvc = va_arg(va, int*);
bpttype_t type = va_argi(va, bpttype_t);
ea_t ea = va_arg(va, ea_t);
int len = va_arg(va, int);
*bptvc = is_ok_bpt(type, ea, len);
retcode = DRC_OK;
}
break;
case debugger_t::ev_update_bpts:
{
int* nbpts = va_arg(va, int*);
update_bpt_info_t* bpts = va_arg(va, update_bpt_info_t*);
int nadd = va_arg(va, int);
int ndel = va_arg(va, int);
errbuf = va_arg(va, qstring*);
retcode = update_bpts(nbpts, bpts, nadd, ndel, errbuf);
}
break;
default:
retcode = DRC_NONE;
}
return retcode;
}
debugger_t debugger{
IDD_INTERFACE_VERSION,
NAME,
0x8000 + 6581, // (6)
"65816",
DBG_FLAG_NOHOST | DBG_FLAG_CAN_CONT_BPT | DBG_FLAG_SAFE | DBG_FLAG_FAKE_ATTACH | DBG_FLAG_NOPASSWORD |
DBG_FLAG_NOSTARTDIR | DBG_FLAG_NOPARAMETERS | DBG_FLAG_ANYSIZE_HWBPT | DBG_FLAG_DEBTHREAD | DBG_FLAG_PREFER_SWBPTS,
DBG_HAS_GET_PROCESSES | DBG_HAS_REQUEST_PAUSE | DBG_HAS_SET_RESUME_MODE | DBG_HAS_THREAD_SUSPEND | DBG_HAS_THREAD_CONTINUE | DBG_HAS_CHECK_BPT,
register_classes,
RC_CPU,
registers,
qnumber(registers),
0x1000,
NULL,
0,
0,
DBG_RESMOD_STEP_INTO | DBG_RESMOD_STEP_OVER,
NULL,
idd_notify
};
Основное изменение, коснувшееся кода плагинов отладчиков по сравнению с тем, что мы писали в статье про отладчик для Сеги, это то, что колбэк теперь всего один — idd_notify
, но он один теперь обрабатывает все те сообщения, которые раньше приходилось обрабатывать по отдельности. Так что, если захотите просто портировать свой старый код плагина-отладчика, возьмите шаблон колбэка из данной статьи, и адаптируйте его под имеющийся код.
Вторым важным изменением стало введением "стандартизированных" кодов возврата у функций отладчика — drc_t
. Тут всё просто: если функция отработала без ошибок, возвращаем DRC_OK
, иначе — DRC_FAILED
.
Остальные инклуды:
#pragma once
#define RC_CPU (1 << 0)
#define RC_PPU (1 << 1)
enum class SNES_REGS : uint8_t
{
SR_A,
SR_X,
SR_Y,
SR_D,
SR_DB,
SR_PC,
SR_S,
SR_P,
SR_MFLAG,
SR_XFLAG,
SR_EFLAG,
};
#pragma once
#include <deque>
#include <ida.hpp>
#include <idd.hpp>
//--------------------------------------------------------------------------
// Very simple class to store pending events
enum queue_pos_t
{
IN_FRONT,
IN_BACK
};
struct eventlist_t : public std::deque<debug_event_t>
{
private:
bool synced;
public:
// save a pending event
void enqueue(const debug_event_t &ev, queue_pos_t pos)
{
if (pos != IN_BACK)
push_front(ev);
else
push_back(ev);
}
// retrieve a pending event
bool retrieve(debug_event_t *event)
{
if (empty())
return false;
// get the first event and return it
*event = front();
pop_front();
return true;
}
};
В ida_registers.h
мы просто перечисляем список регистров для удоства обращений к ним в коде, а в ida_debmod.h
описан формат eventlist_t
, который мы будем использовать для хранения событий, с которыми будет работать IDA.
Подготовка завершена
Теперь, когда код шаблона у нас имеется, стоит понять, что мы будем делать дальше. А дальше нам нужно соорудить модель, по которой между IDA и эмулятором будет происходить общение. Для этого нужно держать в голове следующее:
- Эмулятор с функцией отладки должен уметь реагировать на запросы Иды "добавить/убрать брейкпоинт", "прочитать/записать память", "получить/изменить регистры"
- Эмулятор также должен: уведомлять IDA о том, что: "брейкпоинт сработал", "шаг при пошаговой отладке выполнен", или "процесс отладки начат или завершён"
- Ида должна уметь сообщать эмулятору о том, что есть необходимость: "добавить/убрать брейкпоинт", "прочитать/записать память", "получить/изменить регистры"
- Ида должна реагировать на сообщения от эмулятора о том, что: "брейкпоинт сработал", "шаг при пошаговой отладке выполнен", или "процесс отладки начат или завершён"
Исходя из перечисленного понимаем, что понадобятся два канала, т.к. каждому может захотеться "пообщаться" в любой момент, асинхронно:
- IDA => эмулятор
- Эмулятор => IDA
Учитывая это, можно, опять же, пойти по стопам предыдущей статьи про сеговский отладчик, а можно захотеть использовать "модные и современные" технологии для реализации RPC и сериализации любых данных. Мой выбор пал в сторону Thrift
, т.к. с ним работать гораздо удобнее, и он практически не требует дополнительной подготовки (как, например, доклеивание RPC в protobuf, но тут, скорее, на любителя). Единственная сложность, это компиляция сего зверя, но, я оставлю это за рамками данной статьи.
Thrift — пишем прототип RPC
Давайте ещё раз посмотрим на те 4 пункта, которые я описал выше, и которые мы всё ещё держим в голове, откроем блокнот, и напишем что-то вроде этого:
service IdaClient {
oneway void start_event(),
oneway void add_visited(1:set<i32> visited, 2:bool is_step),
oneway void pause_event(1:i32 address),
oneway void stop_event(),
}
Как видим, в Thrift нету ничего сложного. Здесь мы описали сервис IdaClient
, которым будет пользоваться эмулятор, и обработчик которого будет располагаться в IDA. Все эти методы помечены ключевым словом oneway
, т.к., по сути, нам не нужно дожидаться их выполнения, и в принципе ожидать, что их обработают.
start_event()
будет сообщать Иде о том, что ром выбрал и его эмуляция началась.
add_visited()
— метод, с помощью которого мы будем сообщать в Иду о том коде, который был выполнен эмулятором. Это полезно при отладке как раз таки ретро-платформ, т.к. в ромах для них код часто перемежается с данными. Если таковой функции в выбранном вами эмуляторе нет, её можно также пропустить и в протоколе.
pause_event()
— этим методом мы будем сообщать Иде о том, что произошла пауза эмуляции по какой-либо причине: будь то брейкпоинт, завершился шаг при StepInto или StepOver или какой-то другой причине. В качестве нагрузки данный метод будет также передавать адрес, где именно произошла остановка.
stop_event()
— думаю, тут всё понятно. Эмуляция завершилась, например, по причине завершения процесса эмуляции.
С этим разобрались, теперь часть посложнее — отладочный RPC:
service BsnesDebugger {
i32 get_cpu_reg(1:BsnesRegister reg),
BsnesRegisters get_cpu_regs(),
void set_cpu_reg(1:BsnesRegister reg, 2:i32 value),
binary read_memory(1:DbgMemorySource src, 2:i32 address, 3:i32 size),
void write_memory(1:DbgMemorySource src, 2:i32 address, 3:binary data),
void add_breakpoint(1:DbgBreakpoint bpt),
void del_breakpoint(1:DbgBreakpoint bpt),
void pause(),
void resume(),
void start_emulation(),
void exit_emulation(),
void step_into(),
void step_over(),
}
Здесь у нас описана серверная часть, которая будет крутиться в эмуляторе, и к которой Ида время от времени будет приставать. Давайте разберём её более детально:
i32 get_cpu_reg(1:BsnesRegister reg),
void set_cpu_reg(1:BsnesRegister reg, 2:i32 value),
Эти методы мы будем использовать тогда, когда нам потребуется прочитать или записать один регистр. Использованный enum BsnesRegister
выглядит так:
enum BsnesRegister {
pc,
a,
x,
y,
s,
d,
db,
p,
mflag,
xflag,
eflag,
}
Фактически, это те регистры, значения которых мы хотим видеть во время отладки, у вас они могут быть другими.
Т.к. IDA сама никогда не запрашивает по одному регистру, а требует все сразу, напишем метод, который будет их все сразу и отдавать:
struct BsnesRegisters {
1:i32 pc,
2:i32 a,
3:i32 x,
4:i32 y,
5:i32 s,
6:i32 d,
7:i16 db,
8:i16 p,
9:i8 mflag,
10:i8 xflag,
11:i8 eflag,
}
service BsnesDebugger {
...
BsnesRegisters get_cpu_regs(),
...
}
Здесь я завёл одну общую структуру под регистры, указав их размеры и указал её в качестве возвращаемого значения для метода get_cpu_regs()
.
Теперь работа с памятью:
enum DbgMemorySource {
CPUBus,
APUBus,
APURAM,
DSP,
VRAM,
OAM,
CGRAM,
CartROM,
CartRAM,
SA1Bus,
SFXBus,
SGBBus,
SGBROM,
SGBRAM,
}
service BsnesDebugger {
...
binary read_memory(1:DbgMemorySource src, 2:i32 address, 3:i32 size),
void write_memory(1:DbgMemorySource src, 2:i32 address, 3:binary data),
...
}
Здесь мы использовали встроенный в Thrift тип данных binary
, и указали различные области памяти, которые могут быть прочитаны (взято из эмулятора).
Теперь пришла очередь брейкпоинтов:
enum BpType {
BP_PC = 1,
BP_READ = 2,
BP_WRITE = 4,
}
enum DbgBptSource {
CPUBus,
APURAM,
DSP,
VRAM,
OAM,
CGRAM,
SA1Bus,
SFXBus,
SGBBus,
}
struct DbgBreakpoint {
1:BpType type,
2:i32 bstart,
3:i32 bend,
4:bool enabled,
5:DbgBptSource src,
}
service BsnesDebugger {
...
void add_breakpoint(1:DbgBreakpoint bpt),
void del_breakpoint(1:DbgBreakpoint bpt),
...
}
Т.к. список областей памяти, которые можно читать, и на которые можно ставить брейкпоинты отличаются, заводим отдельный список DbgBptSource
. Также указываем тип брейкпоинта BpType
и адрес его начала/конца bstart
/bend
. Ещё нам может понадобиться включать брейкпоинт не сразу enabled
.
С основными сложными частями протокола закончили, теперь можно описать более простые:
service BsnesDebugger {
...
void pause(),
void resume(),
void start_emulation(),
void exit_emulation(),
void step_into(),
void step_over(),
...
}
Метод pause()
будет приостанавливать процесс отладки по запросу от IDA, resume()
— продолжать.
start_emulation()
— нужен для того, чтобы IDA могла сообщить эмулятору, что она начала процесс отладки, и ожидает от него какие-либо события. Фактически, используется в качестве синхронизации начала эмуляции между плагином-отладчиком и собственно эмулятором.
exit_emulation()
— на случай, если мы захотим остановить отладку из IDA, а не из эмулятора.
step_into()
и step_over()
— пошаговая отладка.
enum BsnesRegister {
pc,
a,
x,
y,
s,
d,
db,
p,
mflag,
xflag,
eflag,
}
struct BsnesRegisters {
1:i32 pc,
2:i32 a,
3:i32 x,
4:i32 y,
5:i32 s,
6:i32 d,
7:i16 db,
8:i16 p,
9:i8 mflag,
10:i8 xflag,
11:i8 eflag,
}
enum BpType {
BP_PC = 1,
BP_READ = 2,
BP_WRITE = 4,
}
enum DbgMemorySource {
CPUBus,
APUBus,
APURAM,
DSP,
VRAM,
OAM,
CGRAM,
CartROM,
CartRAM,
SA1Bus,
SFXBus,
SGBBus,
SGBROM,
SGBRAM,
}
enum DbgBptSource {
CPUBus,
APURAM,
DSP,
VRAM,
OAM,
CGRAM,
SA1Bus,
SFXBus,
SGBBus,
}
struct DbgBreakpoint {
1:BpType type,
2:i32 bstart,
3:i32 bend,
4:bool enabled,
5:DbgBptSource src,
}
service BsnesDebugger {
i32 get_cpu_reg(1:BsnesRegister reg),
BsnesRegisters get_cpu_regs(),
void set_cpu_reg(1:BsnesRegister reg, 2:i32 value),
binary read_memory(1:DbgMemorySource src, 2:i32 address, 3:i32 size),
void write_memory(1:DbgMemorySource src, 2:i32 address, 3:binary data),
void add_breakpoint(1:DbgBreakpoint bpt),
void del_breakpoint(1:DbgBreakpoint bpt),
void pause(),
void resume(),
void start_emulation(),
void exit_emulation(),
void step_into(),
void step_over(),
}
service IdaClient {
oneway void start_event(),
oneway void add_visited(1:set<i32> changed, 2:bool is_step),
oneway void pause_event(1:i32 address),
oneway void stop_event(),
}
От RPC-прототипа к реализации
На этом процесс написания RPC-прототипа завершён. Чтобы сгенерировать из него код для языка C++, качаем Thrift-компилятор, выполняем из командной строки следующее:
thrift --gen cpp debug_proto.thrift
На выходе мы получим каталог gen-cpp
, в котором нас будут ждать не только файлики, которые нужно будет компилировать вместе с проектом, но и шаблон кода каждого из сервисов — IdaClient
и BsnesDebugger
.
Добавляем сгенерированные файлы в студийный проект (кроме файлов *_server.skeleton.cpp
). Также необходимо слинковать наш проект плагина (и эмулятора) со скомпилированными статичными библиотеками thrift
-а и libevent
-а (мы будем использовать "nonblocking" вариант Thrift). У этих библиотек имеется CMake вариант сборки, который значительно упрощает процесс.
Код IdaClient хэндлера
Теперь давайте напишем шаблон кода, реализующий IdaClient
-сервис:
#include "gen-cpp/IdaClient.h"
#include "gen-cpp/BsnesDebugger.h"
#include <thrift/protocol/TBinaryProtocol.h>
#include <thrift/transport/TSocket.h>
#include <thrift/transport/TBufferTransports.h>
#include <thrift/server/TNonblockingServer.h>
#include <thrift/transport/TNonblockingServerSocket.h>
#include <thrift/concurrency/ThreadFactory.h>
using namespace ::apache::thrift;
using namespace ::apache::thrift::protocol;
using namespace ::apache::thrift::transport;
using namespace ::apache::thrift::server;
using namespace ::apache::thrift::concurrency;
::std::shared_ptr<BsnesDebuggerClient> client;
::std::shared_ptr<TNonblockingServer> srv;
::std::shared_ptr<TTransport> cli_transport;
static void pause_execution()
{
try {
if (client) {
client->pause();
}
}
catch (...) {
}
}
static void continue_execution()
{
try {
if (client) {
client->resume();
}
}
catch (...) {
}
}
static void stop_server() {
try {
srv->stop();
}
catch (...) {
}
}
static void finish_execution()
{
try {
if (client) {
client->exit_emulation();
}
}
catch (...) {
}
stop_server();
}
class IdaClientHandler : virtual public IdaClientIf {
public:
void pause_event(const int32_t address) override {
::std::lock_guard<::std::mutex> lock(list_mutex);
debug_event_t ev;
ev.pid = 1;
ev.tid = 1;
ev.ea = address | 0x800000;
ev.handled = true;
ev.set_eid(PROCESS_SUSPENDED);
events.enqueue(ev, IN_BACK);
}
void start_event() override {
::std::lock_guard<::std::mutex> lock(list_mutex);
debug_event_t ev;
ev.pid = 1;
ev.tid = 1;
ev.ea = BADADDR;
ev.handled = true;
ev.set_modinfo(PROCESS_STARTED).name.sprnt("BSNES");
ev.set_modinfo(PROCESS_STARTED).base = 0;
ev.set_modinfo(PROCESS_STARTED).size = 0;
ev.set_modinfo(PROCESS_STARTED).rebase_to = BADADDR;
events.enqueue(ev, IN_BACK);
}
void stop_event() override {
::std::lock_guard<::std::mutex> lock(list_mutex);
debug_event_t ev;
ev.pid = 1;
ev.handled = true;
ev.set_exit_code(PROCESS_EXITED, 0);
events.enqueue(ev, IN_BACK);
}
void add_visited(const std::set<int32_t>& changed, bool is_step) override {
}
};
В этом коде мы реагируем на события эмуляции и сообщаем о них Иде, добавляя эти события в список. Более подробно о них можно прочитать в той же статье про отладчик для Сеги. Код add_visited()
пока оставляем пустым. О нём позже.
Теперь напишем код, который будет отвечать за поднятие сервиса на стороне Иды (будем использовать порт 9091), и ожидание подключения к эмулятору:
static void init_ida_server() {
try {
::std::shared_ptr<IdaClientHandler> handler(new IdaClientHandler());
::std::shared_ptr<TProcessor> processor(new IdaClientProcessor(handler));
::std::shared_ptr<TNonblockingServerTransport> serverTransport(new TNonblockingServerSocket(9091));
::std::shared_ptr<TFramedTransportFactory> transportFactory(new TFramedTransportFactory());
::std::shared_ptr<TProtocolFactory> protocolFactory(new TBinaryProtocolFactory());
srv = ::std::shared_ptr<TNonblockingServer>(new TNonblockingServer(processor, protocolFactory, serverTransport));
::std::shared_ptr<ThreadFactory> tf(new ThreadFactory());
::std::shared_ptr<Thread> thread = tf->newThread(srv);
thread->start();
} catch (...) {
}
}
static void init_emu_client() {
::std::shared_ptr<TTransport> socket(new TSocket("127.0.0.1", 9090));
cli_transport = ::std::shared_ptr<TTransport>(new TFramedTransport(socket));
::std::shared_ptr<TBinaryProtocol> protocol(new TBinaryProtocol(cli_transport));
client = ::std::shared_ptr<BsnesDebuggerClient>(new BsnesDebuggerClient(protocol));
show_wait_box("Waiting for BSNES-PLUS emulation...");
while (true) {
if (user_cancelled()) {
break;
}
try {
cli_transport->open();
break;
}
catch (...) {
}
}
hide_wait_box();
}
Осталось дополнить имеющийся шаблон ida_debug.cpp
кодом для работы со Thrift. Вот что получилось:
#include "gen-cpp/IdaClient.h"
#include "gen-cpp/BsnesDebugger.h"
#include <thrift/protocol/TBinaryProtocol.h>
#include <thrift/transport/TSocket.h>
#include <thrift/transport/TBufferTransports.h>
#include <thrift/server/TNonblockingServer.h>
#include <thrift/transport/TNonblockingServerSocket.h>
#include <thrift/concurrency/ThreadFactory.h>
using namespace ::apache::thrift;
using namespace ::apache::thrift::protocol;
using namespace ::apache::thrift::transport;
using namespace ::apache::thrift::server;
using namespace ::apache::thrift::concurrency;
#include <ida.hpp>
#include <dbg.hpp>
#include <auto.hpp>
#include <deque>
#include <mutex>
#include "ida_plugin.h"
#include "ida_debmod.h"
#include "ida_registers.h"
::std::shared_ptr<BsnesDebuggerClient> client;
::std::shared_ptr<TNonblockingServer> srv;
::std::shared_ptr<TTransport> cli_transport;
static ::std::mutex list_mutex;
static eventlist_t events;
static const char* const p_reg[] =
{
"CF",
"ZF",
"IF",
"DF",
"XF",
"MF",
"VF",
"NF",
};
static register_info_t registers[] = {
{"A", 0, RC_CPU, dt_word, NULL, 0},
{"X", 0, RC_CPU, dt_word, NULL, 0},
{"Y", 0, RC_CPU, dt_word, NULL, 0},
{"D", 0, RC_CPU, dt_word, NULL, 0},
{"DB", 0, RC_CPU, dt_byte, NULL, 0},
{"PC", REGISTER_IP | REGISTER_ADDRESS, RC_CPU, dt_dword, NULL, 0},
{"S", REGISTER_SP | REGISTER_ADDRESS, RC_CPU, dt_word, NULL, 0},
{"P", REGISTER_READONLY, RC_CPU, dt_byte, p_reg, 0xFF},
{"m", REGISTER_READONLY, RC_CPU, dt_byte, NULL, 0},
{"x", REGISTER_READONLY, RC_CPU, dt_byte, NULL, 0},
{"e", REGISTER_READONLY, RC_CPU, dt_byte, NULL, 0},
};
static const char* register_classes[] = {
"General Registers",
NULL
};
static struct apply_codemap_req : public exec_request_t {
private:
const std::set<int32_t>& _changed;
const bool _is_step;
public:
apply_codemap_req(const std::set<int32_t>& changed, bool is_step) : _changed(changed), _is_step(is_step) {};
int idaapi execute(void) override {
auto m = _changed.size();
if (!_is_step) {
show_wait_box("Applying codemap: %d/%d...", 1, m);
}
auto x = 0;
for (auto i = _changed.cbegin(); i != _changed.cend(); ++i) {
if (!_is_step && user_cancelled()) {
break;
}
if (!_is_step) {
replace_wait_box("Applying codemap: %d/%d...", x, m);
}
ea_t addr = (ea_t)(*i | 0x800000);
auto_make_code(addr);
plan_ea(addr);
show_addr(addr);
x++;
}
if (!_is_step) {
hide_wait_box();
}
return 0;
}
};
static void apply_codemap(const std::set<int32_t>& changed, bool is_step)
{
if (changed.empty()) return;
apply_codemap_req req(changed, is_step);
execute_sync(req, MFF_FAST);
}
static void pause_execution()
{
try {
if (client) {
client->pause();
}
}
catch (...) {
}
}
static void continue_execution()
{
try {
if (client) {
client->resume();
}
}
catch (...) {
}
}
static void stop_server() {
try {
srv->stop();
}
catch (...) {
}
}
static void finish_execution()
{
try {
if (client) {
client->exit_emulation();
}
}
catch (...) {
}
stop_server();
}
class IdaClientHandler : virtual public IdaClientIf {
public:
void pause_event(const int32_t address) override {
::std::lock_guard<::std::mutex> lock(list_mutex);
debug_event_t ev;
ev.pid = 1;
ev.tid = 1;
ev.ea = address | 0x800000;
ev.handled = true;
ev.set_eid(PROCESS_SUSPENDED);
events.enqueue(ev, IN_BACK);
}
void start_event() override {
::std::lock_guard<::std::mutex> lock(list_mutex);
debug_event_t ev;
ev.pid = 1;
ev.tid = 1;
ev.ea = BADADDR;
ev.handled = true;
ev.set_modinfo(PROCESS_STARTED).name.sprnt("BSNES");
ev.set_modinfo(PROCESS_STARTED).base = 0;
ev.set_modinfo(PROCESS_STARTED).size = 0;
ev.set_modinfo(PROCESS_STARTED).rebase_to = BADADDR;
events.enqueue(ev, IN_BACK);
}
void stop_event() override {
::std::lock_guard<::std::mutex> lock(list_mutex);
debug_event_t ev;
ev.pid = 1;
ev.handled = true;
ev.set_exit_code(PROCESS_EXITED, 0);
events.enqueue(ev, IN_BACK);
}
void add_visited(const std::set<int32_t>& changed, bool is_step) override {
apply_codemap(changed, is_step);
}
};
static void init_ida_server() {
try {
::std::shared_ptr<IdaClientHandler> handler(new IdaClientHandler());
::std::shared_ptr<TProcessor> processor(new IdaClientProcessor(handler));
::std::shared_ptr<TNonblockingServerTransport> serverTransport(new TNonblockingServerSocket(9091));
::std::shared_ptr<TFramedTransportFactory> transportFactory(new TFramedTransportFactory());
::std::shared_ptr<TProtocolFactory> protocolFactory(new TBinaryProtocolFactory());
srv = ::std::shared_ptr<TNonblockingServer>(new TNonblockingServer(processor, protocolFactory, serverTransport));
::std::shared_ptr<ThreadFactory> tf(new ThreadFactory());
::std::shared_ptr<Thread> thread = tf->newThread(srv);
thread->start();
} catch (...) {
}
}
static void init_emu_client() {
::std::shared_ptr<TTransport> socket(new TSocket("127.0.0.1", 9090));
cli_transport = ::std::shared_ptr<TTransport>(new TFramedTransport(socket));
::std::shared_ptr<TBinaryProtocol> protocol(new TBinaryProtocol(cli_transport));
client = ::std::shared_ptr<BsnesDebuggerClient>(new BsnesDebuggerClient(protocol));
show_wait_box("Waiting for BSNES-PLUS emulation...");
while (true) {
if (user_cancelled()) {
break;
}
try {
cli_transport->open();
break;
}
catch (...) {
}
}
hide_wait_box();
}
static drc_t idaapi init_debugger(const char* hostname, int portnum, const char* password, qstring* errbuf)
{
return DRC_OK;
}
static drc_t idaapi term_debugger(void)
{
finish_execution();
return DRC_OK;
}
static drc_t s_get_processes(procinfo_vec_t* procs, qstring* errbuf) {
process_info_t info;
info.name.sprnt("bsnes");
info.pid = 1;
procs->add(info);
return DRC_OK;
}
static drc_t idaapi s_start_process(const char* path,
const char* args,
const char* startdir,
uint32 dbg_proc_flags,
const char* input_path,
uint32 input_file_crc32,
qstring* errbuf = NULL)
{
::std::lock_guard<::std::mutex> lock(list_mutex);
events.clear();
init_ida_server();
init_emu_client();
try {
if (client) {
client->start_emulation();
}
}
catch (...) {
return DRC_FAILED;
}
return DRC_OK;
}
static drc_t idaapi prepare_to_pause_process(qstring* errbuf)
{
pause_execution();
return DRC_OK;
}
static drc_t idaapi emul_exit_process(qstring* errbuf)
{
finish_execution();
return DRC_OK;
}
static gdecode_t idaapi get_debug_event(debug_event_t* event, int timeout_ms)
{
while (true)
{
::std::lock_guard<::std::mutex> lock(list_mutex);
// are there any pending events?
if (events.retrieve(event))
{
return events.empty() ? GDE_ONE_EVENT : GDE_MANY_EVENTS;
}
if (events.empty())
break;
}
return GDE_NO_EVENT;
}
static drc_t idaapi continue_after_event(const debug_event_t* event)
{
dbg_notification_t req = get_running_notification();
switch (event->eid())
{
case STEP:
case PROCESS_SUSPENDED:
if (req == dbg_null || req == dbg_run_to) {
continue_execution();
}
break;
case PROCESS_EXITED:
stop_server();
break;
}
return DRC_OK;
}
static drc_t idaapi s_set_resume_mode(thid_t tid, resume_mode_t resmod) // Run one instruction in the thread
{
switch (resmod)
{
case RESMOD_INTO: ///< step into call (the most typical single stepping)
try {
if (client) {
client->step_into();
}
}
catch (...) {
return DRC_FAILED;
}
break;
case RESMOD_OVER: ///< step over call
try {
if (client) {
client->step_over();
}
}
catch (...) {
return DRC_FAILED;
}
break;
}
return DRC_OK;
}
static drc_t idaapi read_registers(thid_t tid, int clsmask, regval_t* values, qstring* errbuf)
{
if (clsmask & RC_CPU)
{
BsnesRegisters regs;
try {
if (client) {
client->get_cpu_regs(regs);
values[static_cast<int>(SNES_REGS::SR_PC)].ival = regs.pc | 0x800000;
values[static_cast<int>(SNES_REGS::SR_A)].ival = regs.a;
values[static_cast<int>(SNES_REGS::SR_X)].ival = regs.x;
values[static_cast<int>(SNES_REGS::SR_Y)].ival = regs.y;
values[static_cast<int>(SNES_REGS::SR_S)].ival = regs.s;
values[static_cast<int>(SNES_REGS::SR_D)].ival = regs.d;
values[static_cast<int>(SNES_REGS::SR_DB)].ival = regs.db;
values[static_cast<int>(SNES_REGS::SR_P)].ival = regs.p;
values[static_cast<int>(SNES_REGS::SR_MFLAG)].ival = regs.mflag;
values[static_cast<int>(SNES_REGS::SR_XFLAG)].ival = regs.xflag;
values[static_cast<int>(SNES_REGS::SR_EFLAG)].ival = regs.eflag;
}
}
catch (...) {
return DRC_FAILED;
}
}
return DRC_OK;
}
static drc_t idaapi write_register(thid_t tid, int regidx, const regval_t* value, qstring* errbuf)
{
if (regidx >= static_cast<int>(SNES_REGS::SR_PC) && regidx <= static_cast<int>(SNES_REGS::SR_EFLAG)) {
try {
if (client) {
client->set_cpu_reg(static_cast<BsnesRegister::type>(regidx), value->ival & 0xFFFFFFFF);
}
}
catch (...) {
return DRC_FAILED;
}
}
return DRC_OK;
}
static drc_t idaapi get_memory_info(meminfo_vec_t& areas, qstring* errbuf)
{
memory_info_t info;
info.start_ea = 0x0000;
info.end_ea = 0x01FFF;
info.sclass = "STACK";
info.bitness = 0;
info.perm = SEGPERM_READ | SEGPERM_WRITE;
areas.push_back(info);
// Don't remove this loop
for (int i = 0; i < get_segm_qty(); ++i)
{
segment_t* segm = getnseg(i);
info.start_ea = segm->start_ea;
info.end_ea = segm->end_ea;
qstring buf;
get_segm_name(&buf, segm);
info.name = buf;
get_segm_class(&buf, segm);
info.sclass = buf;
info.sbase = get_segm_base(segm);
info.perm = segm->perm;
info.bitness = segm->bitness;
areas.push_back(info);
}
// Don't remove this loop
return DRC_OK;
}
static ssize_t idaapi read_memory(ea_t ea, void* buffer, size_t size, qstring* errbuf)
{
std::string mem;
try {
if (client) {
client->read_memory(mem, DbgMemorySource::CPUBus, (int32_t)ea, (int32_t)size);
memcpy(&((unsigned char*)buffer)[0], mem.c_str(), size);
}
}
catch (...) {
return DRC_FAILED;
}
return size;
}
static ssize_t idaapi write_memory(ea_t ea, const void* buffer, size_t size, qstring* errbuf)
{
std::string mem((const char*)buffer);
try {
if (client) {
client->write_memory(DbgMemorySource::CPUBus, (int32_t)ea, mem);
}
}
catch (...) {
return 0;
}
return size;
}
static int idaapi is_ok_bpt(bpttype_t type, ea_t ea, int len)
{
DbgMemorySource::type btype = DbgMemorySource::CPUBus;
switch (btype) {
case DbgMemorySource::CPUBus:
case DbgMemorySource::APURAM:
case DbgMemorySource::DSP:
case DbgMemorySource::VRAM:
case DbgMemorySource::OAM:
case DbgMemorySource::CGRAM:
case DbgMemorySource::SA1Bus:
case DbgMemorySource::SFXBus:
break;
default:
return BPT_BAD_TYPE;
}
switch (type)
{
case BPT_EXEC:
case BPT_READ:
case BPT_WRITE:
case BPT_RDWR:
return BPT_OK;
}
return BPT_BAD_TYPE;
}
static drc_t idaapi update_bpts(int* nbpts, update_bpt_info_t* bpts, int nadd, int ndel, qstring* errbuf)
{
for (int i = 0; i < nadd; ++i)
{
ea_t start = bpts[i].ea;
ea_t end = bpts[i].ea + bpts[i].size - 1;
DbgBreakpoint bp;
bp.bstart = start;
bp.bend = end;
bp.enabled = true;
switch (bpts[i].type)
{
case BPT_EXEC:
bp.type = BpType::BP_PC;
break;
case BPT_READ:
bp.type = BpType::BP_READ;
break;
case BPT_WRITE:
bp.type = BpType::BP_WRITE;
break;
case BPT_RDWR:
bp.type = BpType::BP_READ;
break;
}
DbgMemorySource::type type = DbgMemorySource::CPUBus;
switch (type) {
case DbgMemorySource::CPUBus:
bp.src = DbgBptSource::CPUBus;
break;
case DbgMemorySource::APURAM:
bp.src = DbgBptSource::APURAM;
break;
case DbgMemorySource::DSP:
bp.src = DbgBptSource::DSP;
break;
case DbgMemorySource::VRAM:
bp.src = DbgBptSource::VRAM;
break;
case DbgMemorySource::OAM:
bp.src = DbgBptSource::OAM;
break;
case DbgMemorySource::CGRAM:
bp.src = DbgBptSource::CGRAM;
break;
case DbgMemorySource::SA1Bus:
bp.src = DbgBptSource::SA1Bus;
break;
case DbgMemorySource::SFXBus:
bp.src = DbgBptSource::SFXBus;
break;
default:
continue;
}
try {
if (client) {
client->add_breakpoint(bp);
}
}
catch (...) {
return DRC_FAILED;
}
bpts[i].code = BPT_OK;
}
for (int i = 0; i < ndel; ++i)
{
ea_t start = bpts[nadd + i].ea;
ea_t end = bpts[nadd + i].ea + bpts[nadd + i].size - 1;
DbgBreakpoint bp;
bp.bstart = start;
bp.bend = end;
bp.enabled = true;
switch (bpts[i].type)
{
case BPT_EXEC:
bp.type = BpType::BP_PC;
break;
case BPT_READ:
bp.type = BpType::BP_READ;
break;
case BPT_WRITE:
bp.type = BpType::BP_WRITE;
break;
case BPT_RDWR:
bp.type = BpType::BP_READ;
break;
}
DbgMemorySource::type type = DbgMemorySource::CPUBus;
switch (type) {
case DbgMemorySource::CPUBus:
bp.src = DbgBptSource::CPUBus;
break;
case DbgMemorySource::APURAM:
bp.src = DbgBptSource::APURAM;
break;
case DbgMemorySource::DSP:
bp.src = DbgBptSource::DSP;
break;
case DbgMemorySource::VRAM:
bp.src = DbgBptSource::VRAM;
break;
case DbgMemorySource::OAM:
bp.src = DbgBptSource::OAM;
break;
case DbgMemorySource::CGRAM:
bp.src = DbgBptSource::CGRAM;
break;
case DbgMemorySource::SA1Bus:
bp.src = DbgBptSource::SA1Bus;
break;
case DbgMemorySource::SFXBus:
bp.src = DbgBptSource::SFXBus;
break;
default:
continue;
}
try {
if (client) {
client->del_breakpoint(bp);
}
}
catch (...) {
return DRC_FAILED;
}
bpts[nadd + i].code = BPT_OK;
}
*nbpts = (ndel + nadd);
return DRC_OK;
}
static ssize_t idaapi idd_notify(void*, int msgid, va_list va) {
drc_t retcode = DRC_NONE;
qstring* errbuf;
switch (msgid)
{
case debugger_t::ev_init_debugger:
{
const char* hostname = va_arg(va, const char*);
int portnum = va_arg(va, int);
const char* password = va_arg(va, const char*);
errbuf = va_arg(va, qstring*);
QASSERT(1522, errbuf != NULL);
retcode = init_debugger(hostname, portnum, password, errbuf);
}
break;
case debugger_t::ev_term_debugger:
retcode = term_debugger();
break;
case debugger_t::ev_get_processes:
{
procinfo_vec_t* procs = va_arg(va, procinfo_vec_t*);
errbuf = va_arg(va, qstring*);
retcode = s_get_processes(procs, errbuf);
}
break;
case debugger_t::ev_start_process:
{
const char* path = va_arg(va, const char*);
const char* args = va_arg(va, const char*);
const char* startdir = va_arg(va, const char*);
uint32 dbg_proc_flags = va_arg(va, uint32);
const char* input_path = va_arg(va, const char*);
uint32 input_file_crc32 = va_arg(va, uint32);
errbuf = va_arg(va, qstring*);
retcode = s_start_process(path,
args,
startdir,
dbg_proc_flags,
input_path,
input_file_crc32,
errbuf);
}
break;
case debugger_t::ev_get_debapp_attrs:
{
debapp_attrs_t* out_pattrs = va_arg(va, debapp_attrs_t*);
out_pattrs->addrsize = 3;
out_pattrs->is_be = false;
out_pattrs->platform = "snes";
out_pattrs->cbsize = sizeof(debapp_attrs_t);
retcode = DRC_OK;
}
break;
case debugger_t::ev_rebase_if_required_to:
{
ea_t new_base = va_arg(va, ea_t);
retcode = DRC_OK;
}
break;
case debugger_t::ev_request_pause:
errbuf = va_arg(va, qstring*);
retcode = prepare_to_pause_process(errbuf);
break;
case debugger_t::ev_exit_process:
errbuf = va_arg(va, qstring*);
retcode = emul_exit_process(errbuf);
break;
case debugger_t::ev_get_debug_event:
{
gdecode_t* code = va_arg(va, gdecode_t*);
debug_event_t* event = va_arg(va, debug_event_t*);
int timeout_ms = va_arg(va, int);
*code = get_debug_event(event, timeout_ms);
retcode = DRC_OK;
}
break;
case debugger_t::ev_resume:
{
debug_event_t* event = va_arg(va, debug_event_t*);
retcode = continue_after_event(event);
}
break;
case debugger_t::ev_thread_suspend:
{
thid_t tid = va_argi(va, thid_t);
pause_execution();
retcode = DRC_OK;
}
break;
case debugger_t::ev_thread_continue:
{
thid_t tid = va_argi(va, thid_t);
continue_execution();
retcode = DRC_OK;
}
break;
case debugger_t::ev_set_resume_mode:
{
thid_t tid = va_argi(va, thid_t);
resume_mode_t resmod = va_argi(va, resume_mode_t);
retcode = s_set_resume_mode(tid, resmod);
}
break;
case debugger_t::ev_read_registers:
{
thid_t tid = va_argi(va, thid_t);
int clsmask = va_arg(va, int);
regval_t* values = va_arg(va, regval_t*);
errbuf = va_arg(va, qstring*);
retcode = read_registers(tid, clsmask, values, errbuf);
}
break;
case debugger_t::ev_write_register:
{
thid_t tid = va_argi(va, thid_t);
int regidx = va_arg(va, int);
const regval_t* value = va_arg(va, const regval_t*);
errbuf = va_arg(va, qstring*);
retcode = write_register(tid, regidx, value, errbuf);
}
break;
case debugger_t::ev_get_memory_info:
{
meminfo_vec_t* ranges = va_arg(va, meminfo_vec_t*);
errbuf = va_arg(va, qstring*);
retcode = get_memory_info(*ranges, errbuf);
}
break;
case debugger_t::ev_read_memory:
{
size_t* nbytes = va_arg(va, size_t*);
ea_t ea = va_arg(va, ea_t);
void* buffer = va_arg(va, void*);
size_t size = va_arg(va, size_t);
errbuf = va_arg(va, qstring*);
ssize_t code = read_memory(ea, buffer, size, errbuf);
*nbytes = code >= 0 ? code : 0;
retcode = code >= 0 ? DRC_OK : DRC_NOPROC;
}
break;
case debugger_t::ev_write_memory:
{
size_t* nbytes = va_arg(va, size_t*);
ea_t ea = va_arg(va, ea_t);
const void* buffer = va_arg(va, void*);
size_t size = va_arg(va, size_t);
errbuf = va_arg(va, qstring*);
ssize_t code = write_memory(ea, buffer, size, errbuf);
*nbytes = code >= 0 ? code : 0;
retcode = code >= 0 ? DRC_OK : DRC_NOPROC;
}
break;
case debugger_t::ev_check_bpt:
{
int* bptvc = va_arg(va, int*);
bpttype_t type = va_argi(va, bpttype_t);
ea_t ea = va_arg(va, ea_t);
int len = va_arg(va, int);
*bptvc = is_ok_bpt(type, ea, len);
retcode = DRC_OK;
}
break;
case debugger_t::ev_update_bpts:
{
int* nbpts = va_arg(va, int*);
update_bpt_info_t* bpts = va_arg(va, update_bpt_info_t*);
int nadd = va_arg(va, int);
int ndel = va_arg(va, int);
errbuf = va_arg(va, qstring*);
retcode = update_bpts(nbpts, bpts, nadd, ndel, errbuf);
}
break;
default:
retcode = DRC_NONE;
}
return retcode;
}
debugger_t debugger{
IDD_INTERFACE_VERSION,
NAME,
0x8000 + 6581, // (6)
"65816",
DBG_FLAG_NOHOST | DBG_FLAG_CAN_CONT_BPT | DBG_FLAG_SAFE | DBG_FLAG_FAKE_ATTACH | DBG_FLAG_NOPASSWORD |
DBG_FLAG_NOSTARTDIR | DBG_FLAG_NOPARAMETERS | DBG_FLAG_ANYSIZE_HWBPT | DBG_FLAG_DEBTHREAD | DBG_FLAG_PREFER_SWBPTS,
DBG_HAS_GET_PROCESSES | DBG_HAS_REQUEST_PAUSE | DBG_HAS_SET_RESUME_MODE | DBG_HAS_THREAD_SUSPEND | DBG_HAS_THREAD_CONTINUE | DBG_HAS_CHECK_BPT,
register_classes,
RC_CPU,
registers,
qnumber(registers),
0x1000,
NULL,
0,
0,
DBG_RESMOD_STEP_INTO | DBG_RESMOD_STEP_OVER,
NULL,
idd_notify
};
Дабы не описывать весь этот код, здесь я опишу лишь типичный код для работы со Thrift со стороны IDA:
try {
if (client) {
client->step_over();
}
}
catch (...) {
return DRC_FAILED;
}
return DRC_OK;
Т.е. мы просто оборачиваем код для работы с клиентом BsnesDebugger
(серверную часть которого сейчас также напишем) в обработчик исключения и возвращаем либо ошибку, либо ОК.
Код BsnesDebugger хэндлера
Теперь мы дошли до модификации непосредственно эмулятора. Как ни странно, изменений потребуется не так много. Для того, чтобы не вдаваться в подробности реализации конкретного эмулятора, и чтобы не бомбить о том, какая же здесь ужасная структура кода, я просто приведу шаблон cpp-файла, который я использовал при компиляции эмулятора.
#include "gen-cpp/IdaClient.h"
#include "gen-cpp/BsnesDebugger.h"
#include <thrift/protocol/TBinaryProtocol.h>
#include <thrift/transport/TSocket.h>
#include <thrift/transport/TBufferTransports.h>
#include <thrift/server/TNonblockingServer.h>
#include <thrift/transport/TNonblockingServerSocket.h>
#include <thrift/concurrency/ThreadFactory.h>
using namespace ::apache::thrift;
using namespace ::apache::thrift::protocol;
using namespace ::apache::thrift::transport;
using namespace ::apache::thrift::server;
using namespace ::apache::thrift::concurrency;
#include "../ui-base.hpp"
static ::std::shared_ptr<IdaClientClient> client;
static ::std::shared_ptr<TNonblockingServer> srv;
static ::std::shared_ptr<TTransport> cli_transport;
static ::std::mutex list_mutex;
::std::set<int32_t> visited;
static void send_visited(bool is_step) {
const auto part = visited.size();
::std::lock_guard<::std::mutex> lock(list_mutex);
try {
if (client) {
client->add_visited(visited, is_step);
}
}
catch (...) {
}
visited.clear();
}
static void stop_client() {
try {
if (client) {
send_visited(false);
client->stop_event();
}
cli_transport->close();
}
catch (...) {
}
}
static void init_ida_client() {
::std::shared_ptr<TTransport> socket(new TSocket("127.0.0.1", 9091));
cli_transport = ::std::shared_ptr<TTransport>(new TFramedTransport(socket));
::std::shared_ptr<TBinaryProtocol> protocol(new TBinaryProtocol(cli_transport));
client = ::std::shared_ptr<IdaClientClient>(new IdaClientClient(protocol));
while (true) {
try {
cli_transport->open();
break;
}
catch (...) {
Sleep(10);
}
}
atexit(stop_client);
}
static void toggle_pause(bool enable) {
application.debug = enable;
application.debugrun = enable;
if (enable) {
audio.clear();
}
}
class BsnesDebuggerHandler : virtual public BsnesDebuggerIf {
public:
int32_t get_cpu_reg(const BsnesRegister::type reg) override {
switch (reg) {
case BsnesRegister::pc:
case BsnesRegister::a:
case BsnesRegister::x:
case BsnesRegister::y:
case BsnesRegister::s:
case BsnesRegister::d:
case BsnesRegister::db:
case BsnesRegister::p:
return SNES::cpu.getRegister((SNES::CPUDebugger::Register)reg);
case BsnesRegister::mflag:
return (SNES::cpu.usage[SNES::cpu.regs.pc] & SNES::CPUDebugger::UsageFlagM) ? 1 : 0;
case BsnesRegister::xflag:
return (SNES::cpu.usage[SNES::cpu.regs.pc] & SNES::CPUDebugger::UsageFlagX) ? 1 : 0;
case BsnesRegister::eflag:
return (SNES::cpu.usage[SNES::cpu.regs.pc] & SNES::CPUDebugger::UsageFlagE) ? 1 : 0;
}
}
void get_cpu_regs(BsnesRegisters& _return) override {
_return.pc = SNES::cpu.getRegister(SNES::CPUDebugger::Register::RegisterPC);
_return.a = SNES::cpu.getRegister(SNES::CPUDebugger::Register::RegisterA);
_return.x = SNES::cpu.getRegister(SNES::CPUDebugger::Register::RegisterX);
_return.y = SNES::cpu.getRegister(SNES::CPUDebugger::Register::RegisterY);
_return.s = SNES::cpu.getRegister(SNES::CPUDebugger::Register::RegisterS);
_return.d = SNES::cpu.getRegister(SNES::CPUDebugger::Register::RegisterD);
_return.db = SNES::cpu.getRegister(SNES::CPUDebugger::Register::RegisterDB);
_return.p = SNES::cpu.getRegister(SNES::CPUDebugger::Register::RegisterP);
_return.mflag = (SNES::cpu.usage[SNES::cpu.regs.pc] & SNES::CPUDebugger::UsageFlagM) ? 1 : 0;
_return.xflag = (SNES::cpu.usage[SNES::cpu.regs.pc] & SNES::CPUDebugger::UsageFlagX) ? 1 : 0;
_return.eflag = (SNES::cpu.usage[SNES::cpu.regs.pc] & SNES::CPUDebugger::UsageFlagE) ? 1 : 0;
}
void set_cpu_reg(const BsnesRegister::type reg, const int32_t value) override {
switch (reg) {
case BsnesRegister::pc:
case BsnesRegister::a:
case BsnesRegister::x:
case BsnesRegister::y:
case BsnesRegister::s:
case BsnesRegister::d:
case BsnesRegister::db:
case BsnesRegister::p:
SNES::cpu.setRegister((SNES::CPUDebugger::Register)reg, value);
}
}
void add_breakpoint(const DbgBreakpoint& bpt) override {
SNES::Debugger::Breakpoint add;
add.addr = bpt.bstart;
add.addr_end = bpt.bend;
add.mode = bpt.type;
add.source = (SNES::Debugger::Breakpoint::Source)bpt.src;
SNES::debugger.breakpoint.append(add);
}
void del_breakpoint(const DbgBreakpoint& bpt) override {
for (auto i = 0; i < SNES::debugger.breakpoint.size(); ++i) {
auto b = SNES::debugger.breakpoint[i];
if (b.source == (SNES::Debugger::Breakpoint::Source)bpt.src && b.addr == bpt.bstart && b.addr_end == bpt.bend && b.mode == bpt.type) {
SNES::debugger.breakpoint.remove(i);
break;
}
}
}
void read_memory(std::string& _return, const DbgMemorySource::type src, const int32_t address, const int32_t size) override {
_return.clear();
SNES::debugger.bus_access = true;
for (auto i = 0; i < size; ++i) {
_return += SNES::debugger.read((SNES::Debugger::MemorySource)src, address + i);
}
SNES::debugger.bus_access = false;
}
void write_memory(const DbgMemorySource::type src, const int32_t address, const std::string& data) override {
SNES::debugger.bus_access = true;
for (auto i = 0; i < data.size(); ++i) {
SNES::debugger.write((SNES::Debugger::MemorySource)src, address, data[i]);
}
SNES::debugger.bus_access = false;
}
void exit_emulation() override {
try {
if (client) {
send_visited(false);
client->stop_event();
}
}
catch (...) {
}
application.app->exit();
}
void pause() override {
step_into();
}
void resume() override {
toggle_pause(false);
}
void start_emulation() override {
init_ida_client();
try {
if (client) {
client->start_event();
visited.clear();
client->pause_event(SNES::cpu.getRegister(SNES::CPUDebugger::RegisterPC));
}
}
catch (...) {
}
}
void step_into() override {
SNES::debugger.step_type = SNES::Debugger::StepType::StepInto;
application.debugrun = true;
SNES::debugger.step_cpu = true;
}
void step_over() override {
SNES::debugger.step_type = SNES::Debugger::StepType::StepOver;
SNES::debugger.step_over_new = true;
SNES::debugger.call_count = 0;
application.debugrun = true;
SNES::debugger.step_cpu = true;
}
};
static void stop_server() {
srv->stop();
}
void init_dbg_server() {
::std::shared_ptr<BsnesDebuggerHandler> handler(new BsnesDebuggerHandler());
::std::shared_ptr<TProcessor> processor(new BsnesDebuggerProcessor(handler));
::std::shared_ptr<TNonblockingServerTransport> serverTransport(new TNonblockingServerSocket(9090));
::std::shared_ptr<TFramedTransportFactory> transportFactory(new TFramedTransportFactory());
::std::shared_ptr<TProtocolFactory> protocolFactory(new TBinaryProtocolFactory());
srv = ::std::shared_ptr<TNonblockingServer>(new TNonblockingServer(processor, protocolFactory, serverTransport));
::std::shared_ptr<ThreadFactory> tf(new ThreadFactory());
::std::shared_ptr<Thread> thread = tf->newThread(srv);
thread->start();
atexit(stop_server);
SNES::debugger.breakpoint.reset();
SNES::debugger.step_type = SNES::Debugger::StepType::StepInto;
application.debugrun = true;
SNES::debugger.step_cpu = true;
}
void send_pause_event(bool is_step) {
try {
if (client) {
client->pause_event(SNES::cpu.getRegister(SNES::CPUDebugger::RegisterPC));
send_visited(is_step);
}
}
catch (...) {
}
}
Фактически, я взял код, который уже был во встроенном отладчике, и скопировал его в реализацию каждого из требуемых методов интерфейса BsnesDebugger
.
Часть объектов и методов я не делал статичными, т.к. к ним нам нужно будет обращаться из других участков кода эмулятора. Эти методы и объекты представлены в следующем списке:
::std::set<int32_t> visited;
— сюда мы будем добавлять код, который выполнялся во время эмуляции, и который мы будем отправлять в Идуvoid init_dbg_server()
— будем запускать RPC-сервер не при запуске эмулятора, а при запуске эмуляции выбранного ромаvoid send_pause_event(bool is_step)
— данный метод я использую не только для уведомления Иды о том, что эмуляция приостановлена, но и для отправки перед этим карты кода (codemap). Подробнее про параметрbool is_step
иcodemap
я расскажу чуть позже
Теперь остаётся найти, где же эмулятору стоит сообщать о паузе, где начинается эмуляция, и где заполняется карта кода. Вот эти места:
Выполнение одной инструкции:
alwaysinline uint8_t CPUDebugger::op_readpc() {
extern std::set<int32_t> visited; // я решил не использовать отдельный header
visited.insert(regs.pc); // вставляем в карту кода текущее значение регистра PC
usage[regs.pc] |= UsageExec;
int offset = cartridge.rom_offset(regs.pc);
if (offset >= 0) cart_usage[offset] |= UsageExec;
// execute code without setting read flag
return CPU::op_read((regs.pc.b << 16) + regs.pc.w++);
}
Открытие SNES рома:
Пошаговое исполнение:
Реакция на срабатывание брейкпоинта:
Хитрости применения codemap в Иде
Осталось рассказать о хитростях работы с функциями анализатора в IDA, и затем со спокойной (но переживающей "сомпилируется ли") душой нажать на Build Solution
.
Оказалось, что просто так взять и в цикле выполнять функции, которые меняют IDB (файлы проектов в IDA) во время отладки нельзя — будет вылетать через раз, и доводить своим непостоянством до сумасшествия. Нужно делать по-умному, например, вот так:
static struct apply_codemap_req : public exec_request_t {
private:
const std::set<int32_t>& _changed;
const bool _is_step;
public:
apply_codemap_req(const std::set<int32_t>& changed, bool is_step) : _changed(changed), _is_step(is_step) {};
int idaapi execute(void) override {
auto m = _changed.size();
if (!_is_step) {
show_wait_box("Applying codemap: %d/%d...", 1, m);
}
auto x = 0;
for (auto i = _changed.cbegin(); i != _changed.cend(); ++i) {
if (!_is_step && user_cancelled()) {
break;
}
if (!_is_step) {
replace_wait_box("Applying codemap: %d/%d...", x, m);
}
ea_t addr = (ea_t)(*i | 0x800000);
auto_make_code(addr);
plan_ea(addr);
show_addr(addr);
x++;
}
if (!_is_step) {
hide_wait_box();
}
return 0;
}
};
static void apply_codemap(const std::set<int32_t>& changed, bool is_step)
{
if (changed.empty()) return;
apply_codemap_req req(changed, is_step);
execute_sync(req, MFF_FAST);
}
Если вкратце, то суть в использовании метода execute_sync()
и реализации своего варианта структуры exec_request_t
и её колбэка int idaapi execute(void)
. Это рекомендованный разработчиками способ.
Выводы и компиляция
Фактически, мы закончили писать свой собственный плагин-отладчик для IDA. Мне показалось, что как раз для реализации общения между Идой и эмулятором и создания отладчика Thrift подошёл как нельзя кстати. С минимальными усилиями мне удалось написать и серверную и клиентскую часть для обеих сущностей, не городя велосипеды в виде открытия сокетов по разному для разных платформ, и изобретения RPC реализации с нуля.
К тому же, получившийся протокол легко масштабируется под другие методы и структуры и легко переносим.
Всем спасибо!
tvoybro
Mesen-S.
github.com/SourMesen/Mesen-S
DrMefistO Автор
Да, я совсем о нём забыл, хотя пытался с ним работать тоже! Тут скорее должна быть фраза "На тот момент", спасибо.
DrMefistO Автор
Кстати, проект оригинальным автором успешно заброшен.
tvoybro
Да, недавно обнаружил с огорчением, что на гитхабе заморозили проект. Но всё-таки в плане отладки это лучшее что есть для SNES. Говорю уверенно, поскольку уже успел обнаружить, что для SNES хоумбрю инструментов практически нет никаких, и потому что Mesen как дебаггер себя зарекомендовал в «обычной» версии, для Денди.
Вообще я первым комментом намекал, что было бы круто поддержать именно Mesen для IDA, всё-таки он более-менее актуальный на текущий момент, и даже наверное самый функциональный.
А если получится это сделать для SNES, то 146% получится и для NES. А при реверсе NES игр этого как раз таки очень не хватает — моментального результата в эмуляторе. Я бы может сам и заморочился, но 99% моих скилов уж очень «ретро».
DrMefistO Автор
Я пытался Месен-С вклинить в качестве отладчика для иды, но из-за того, что у него гуй фактически выполняет функции ядра отладки (берёт на себя эту работу вместо выполнения её ядром эмулятора), нормальное встраивание не представляется возможным, увы.
К тому же у Месен-С функционал ограниченнее чем у эмулятора для NES.
Сам Месен для нес, думается, устроен так же. Но у меня в планах юзать не его, а фсеух.