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