Приветствую,
Моя очень старая мечта сбылась — я написал модуль-отладчик, с помощью которого можно отлаживать 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 реализации с нуля.
К тому же, получившийся протокол легко масштабируется под другие методы и структуры и легко переносим.
Всем спасибо!
Комментариев нет:
Отправить комментарий