Пишем плагин отладки для SNES игр в IDA v7

от автора

Приветствую,

Моя очень старая мечта сбылась — я написал модуль-отладчик, с помощью которого можно отлаживать 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. И, я считаю, что теперь то уж настал тот момент, когда я готов рассказать о том, как создать более-менее полноценный отладчик для этого ревёрсерского инструмента.

Что нам потребуется

Для того, чтобы создать свой плагин-отладчик под Иду, нам потребуется:

  1. IDA v7.x
  2. IDA SDK
  3. Эмулятор-отладчик (можно и без отладки, главное с исходниками, которые захочется допилить)
  4. Thrift (да, я выбрал его за сериализацию и RPC прямо "из коробки")
  5. Умение писать на 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 — с ним будет сложнее писать код

С подготовкой вроде бы всё. Теперь начнём писать код.

Плагин

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

ida_plugin.cpp

#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"

Код самого отладчика

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

ida_debug.cpp

#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.

Остальные инклуды:

ida_registers.h

#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, };

ida_debmod.h

#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 и эмулятором будет происходить общение. Для этого нужно держать в голове следующее:

  1. Эмулятор с функцией отладки должен уметь реагировать на запросы Иды "добавить/убрать брейкпоинт", "прочитать/записать память", "получить/изменить регистры"
  2. Эмулятор также должен: уведомлять IDA о том, что: "брейкпоинт сработал", "шаг при пошаговой отладке выполнен", или "процесс отладки начат или завершён"
  3. Ида должна уметь сообщать эмулятору о том, что есть необходимость: "добавить/убрать брейкпоинт", "прочитать/записать память", "получить/изменить регистры"
  4. Ида должна реагировать на сообщения от эмулятора о том, что: "брейкпоинт сработал", "шаг при пошаговой отладке выполнен", или "процесс отладки начат или завершён"

Исходя из перечисленного понимаем, что понадобятся два канала, т.к. каждому может захотеться "пообщаться" в любой момент, асинхронно:

  1. IDA => эмулятор
  2. Эмулятор => 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() — пошаговая отладка.

Итоговый debug_proto.thrift

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;

Реализация серверной части IdaClient

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), и ожидание подключения к эмулятору:

init_ida_server и init_emu_client

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. Вот что получилось:

Полный код ida_debug.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 <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-файла, который я использовал при компиляции эмулятора.

remote_debugger.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) во время отладки нельзя — будет вылетать через раз, и доводить своим непостоянством до сумасшествия. Нужно делать по-умному, например, вот так:

Как правильно менять IDB во время отладки

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 реализации с нуля.

К тому же, получившийся протокол легко масштабируется под другие методы и структуры и легко переносим.

Всем спасибо!


ссылка на оригинал статьи https://habr.com/ru/post/551102/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *