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


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


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


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