Русский
Русский
English
Статистика
Реклама

Snes

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

16.04.2021 04:04:08 | Автор: admin


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


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


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


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




Подробнее..

Энтузиаст добавил в ретроконсоль трассировку лучей

16.12.2020 16:22:14 | Автор: admin

Разработчик и энтузиаст Бен Картер добавил в Super NES аппаратный трассировщик лучей. Напомню что самой консоли уже 30 лет.

Для реализации этой идеи его надоумил друг и желание получше изучить Verilog и FPGA. Таким образом появился проект SuperRT. Блогер хотел сделать аналог чипа SuperFX - даже названия похожи. Чип SuperFX располагался в картридже и обрабатывал визуальные эффекты недоступные самой консоли из-за ограничения "железа".

В итоге, Картер собрал кастомную плату картриджа с тремя дополнительными вычислительными ядрами с частотой 50 МГц - они обеспечивают поддержку трассировку лучей. На КПДВ можно можно видеть их работу. Автор извиняется за качество видео - виновата плата видеозахвата.

На фото можно видеть снятый корпус для монтажа проводов, по словам автора, никаких изменений в железо не вносились. Для создания модифицированной платы, автор купил древний картридж с "ужасной игрой Pachinko", удалил ПЗУ с игрой и вставил туда кабели. Еще он использовал несколько логических преобразователей (level shifters) и программируемой логической схемы (FPGA) DE10-Nano. Большинство проводов на снимке выше обеспечивают коммутацию логических преобразователей, необходимых для конвертации напряжения с 5В, передаваемых SNES, в 3,3В допустимых для современных чипов.

Получившийся в итоге франкенштейн способен просчитывать и отображать геометрию, цвета, тени и отражения для примерно 50 миллионов базовых объектов: плоскостей, сфер и трёхмерных объёмов (AABB). Отрендеренное изображение конвертируется в пригодный для SNES формат и передаётся в видеопамять консоли, откуда выводится на экран.

Чип вычисляет и записывает в буфер RAM результаты своей работы. Дизассемблированный командный буфер выглядит так:

0000 Start0001 Plane 0, -1, 0, Dist=-20002 SphereSub OH 2, 1, 5, Rad=50003 SphereSub OH 4, 1, 4, Rad=40004 SphereSub OH 5, 1, 9, Rad=90005 SphereSub OH 2, 1, 2, Rad=20006 SphereSub OH -0.5, 1, 2, Rad=20007 RegisterHitNoReset 0, 248, 0, Reflectiveness=00008 Checkerboard ORH 48, 152, 48, Reflectiveness=00009 ResetHitState0010 Plane 0, -1, 0, Dist=-2.1501460011 RegisterHit 0, 0, 248, Reflectiveness=1530012 AABB 4, -2.5, 11,    8, 3.5, 130013 ResetHitStateAndJump NH 440014 Origin 6, 2, 120015 Plane -0.2929688, 0, -0.9570313, Dist=0.24975590016 PlaneAnd OH 0.2919922, 0, 0.9560547, Dist=0.250017 PlaneAnd OH 0, 1, 0, Dist=10018 PlaneAnd OH 0, -1, 0, Dist=40019 PlaneAnd OH -0.9570313, 0, 0.2919922, Dist=-10020 PlaneAnd OH 0.9560547, 0, -0.2929688, Dist=1.4997560021 RegisterHit 248, 0, 0, Reflectiveness=0

Максимально допустимая частота кадров - 30 FPS. Это связано с ограничением пропускной способности интерфейсов самой консоли. Каждый кадр с трассировкой "весит" 32 килобайта при ограничения интерфейса вывода в 16 килобайт, поэтому полное обновление картинки возможно только каждые два кадра. Автор планирует и дальше развивать свое детище, подробнее можно узнать в его личном блоге.

Подробнее..

Перевод История Streets of Rage

08.11.2020 00:20:08 | Автор: admin

Сегодня в это трудно поверить, но в 80-е и 90-е двухмерные битемапы безраздельно властвовали, особенно в аркадных залах. С тех пор, как Ёсихиса Кишимото усовершенствовал формулу с помощью Renegade и Double Dragon, все разработчики стремились повторить успех, и многим это даже удалось.


image


Хотя к этому времени компания Sega уже представила несколько популярных проектов вроде Altered Beast, Golden Axe и Alien Storm, именно Capcom оседлала этот жанр спасибо таким хитам, как Dynasty Wars, Captain Commando и Final Fight. Подвиг Хаггара, Коди и Гая, расчищавших улицы Метро Сити от всякой шпаны, обернулся для Capcom большим успехом, и вскоре Final Fight была лицензирована для множества домашних компьютеров: от ZX Spectrum до 16-битной Амиги. Компания Nintendo быстро осознала популярность творения Capcom и сделала его своим консольным эксклюзивом для SNES.


Вырезанные кооператив и целый уровень (промышленная зона, если вам интересно) не помешали игре стать успешной, причём настолько, что были выпущены два эксклюзивных сиквела только для консоли Nintendo. Тем временем Sega была вынуждена просто наблюдать, как один из самых популярных аркадных проектов бьёт рекорды продаж на приставке её конкурента. Как показывает история, лучший порт Final Fight вышел именно на Sega Mega-CD, но до этого оставались ещё годы, а Sega не умела предсказывать будущее. Компании нужен был хит, который побьёт популярный эксклюзив Nintendo, и она нуждалась в нём прямо сейчас.


image


Поэтому перед Sega встала задача создать свою собственную игру, которая будет не хуже Final Fight и даже превзойдёт её. Этим проектом стала Streets of Rage, которая дебютировала на консоли Mega Drive в августе 1991 года, через девять месяцев после успешного выпуска Final Fight для SNES.


Мы взяли интервью у Ацуши Сеймии, расспросив его о работе над культовым битемапом. Во время создания Streets of Rage (или Bare Knuckle, как игру называли в Японии) он работал художником. Первый вопрос заключался в следующем: всегда ли Streets of Rage планировалась как ответ Sega на популярный эксклюзив для SNES?


Не могу этого отрицать, честно отвечает Ацуши. На самом деле мы купили аркадный автомат с оригинальной игрой и долго изучали её всей командой.


Внимательное изучение игры от Capcom окупилось. Streets of Rage предлагает множество преимуществ по сравнению с Final Fight на SNES. В первую очередь, можно играть с другом, а это куда веселее. Streets of Rage позволяет выбирать из трёх бойцов, у которых есть счёты с мистером Икс, контролирующим город и полицию. Как и в Final Fight, каждый герой уникален, у каждого есть свои сильные и слабые стороны. Адам Хантер основан на Хаггаре он более медленный, но при этом сильный герой, умеющий справляться с группами противников. Блейз Филдинг больше похожа на Гая она куда быстрее, но не способна выдерживать сильные удары. Последний герой Аксель Блейз, он обладает всеми способностями Коди, но не такой прыгучий, как его товарищи. Все трое являются бывшими полицейскими и поклялись передать мистера Икс в руки правосудия.


image


Нам было интересно узнать, за что отвечал Сеймия во время создания игры. Как художник, я работал над широким спектром задач: от проработки персонажей до ландшафта, объясняет он. Я впервые занимался главными героями, поэтому пришлось многократно переделывать свою работу. Возможно, для молодого художника это было боевым крещением, но переделки в итоге окупились. Многие уровни очень похожи на проект от Capcom например, городские улицы и промышленные районы. Но хватает и разнообразия за счёт прекрасных пляжных участков, увлекательной поездки на лифте и заключительного тяжёлого прохода по коридору, ведущему к пентхаусу мистера Икс, перед битвой с которым игроку предстоит сразиться с боссами из всех предыдущих уровней. Неудивительно, что внешне Streets of Rage был крайне не оригинальным проектом: Сеймия признал, что Sega просто давала игрокам то, чего они хотели: В Final Fight, Double Dragon и других аркадных играх того времени использовался реалистичный визуальный стиль, и я думаю, что это было просто в тренде, признаёт Сеймия.


Возможно, игра и выглядит очень похожей на некоторые из популярных битемапов того времени, но ей удаётся выделяться благодаря разнообразию локаций, интересному дизайну уровней, а также куче интересных врагов и боссов самых разных форм и размеров. В дополнение к простым уличным головорезам имеются ниндзя (как в Teenage Mutant Ninja Turtles), госпожи с хлыстами, сумасшедшие, жонглирующие топорами и факелами, а также мастера боевых искусств.


image


image


Боссы тоже очень разнообразны: от маньяков с когтями до борцов, похожих на Последнего воина (американский рестлер, известный под псевдонимом The Ultimate Warrior). Другие запоминающиеся главари это здоровый огнедышащий толстяк, а также Мона и Лиза две девушки, которых фактически сделали из Блейз путём смены палитры. В итоге Сеймия поработал над всеми персонажами в игре: Команда дизайнеров поручала мне создавать модели на основе историй персонажей, их размера в пикселях и других справочных материалов, вспоминает он. Я так и поступал, хотя и было довольно сложно анимировать персонажа с ограниченным количеством пикселей.


Когда мы обсуждали боссов в игре, всплыла интересная информация. Хоть Сеймия и не сообщил, кого считает своим любимым персонажем, но рассказал, что на пятом уровне игры вместо Моны и Лизы планировался другой босс. Может, однажды мы узнаем, как выглядел этот тип, но теперь понятно, почему вместо оригинального злодея мы получили две копии Блейз.


image


Несмотря на силы и размеры главарей в Streets of Rage, главные герои способны с ними справиться. Все трое имеют доступ к сериям ударов ногами и кулаками, а также умеют наносить удар в прыжке и применять захваты. В этом нет ничего революционного, но управление очень отзывчиво, поэтому игроки чувствуют полный контроль над своими героями в бою. Противники тоже не согласны работать грушами для битья: они могут схватить игрока и бросить его на землю. Но вы можете приземлиться на ноги своевременным нажатием кнопки прыжка и даже атаковать, когда вас берут в захват. Можно даже объединиться со своим товарищем, чтобы выполнять совместные приёмы, а при нажатии на кнопку A приезжает полицейская машина, и бывший коллега героев уничтожает всех врагов с помощью ракетницы. К сожалению, Сеймия не отвечал за боевую механику, но вот что думает по этому поводу: Мне очень нравится, как в игре реализовали совместные приёмы для двух игроков.


Еще одним аспектом игры, фанатом которого является Ацуши, стал прекрасный саундтрек. Музыка очень важна, поэтому мы позвали Юдзо Косиро, вспоминает он. До этого Косиро написал отличную музыку к игре The Revenge of Shinobi. А поскольку глава разработки Streets of Rage, Нориёси Охба, также руководил серией Shinobi, договориться о совместной работе с композитором было нетрудно.


image


image


Сеймия считает, что именно режиссёрский стиль Охбы привёл к необычному для того времени множеству концовок в игре. Например, одна из них даёт игроку возможность объединиться с мистером Икс. Как мне кажется, это черта его стиля: добавить неожиданный поворот в сценарий, утверждает Ацуши. К слову, разработка Streets of Rage была завершена лишь с точки зрения игрового процесса. Релизная версия это то, что решила выпустить Sega, а сами авторы хотели добавить в игру куда больше контента. Много чего не попало в финальную версию, но геймплейные механики мы успели реализовать все до единой, говорит Сеймия.


Streets of Rage получила признание критиков и показала отличные продажи. Её успех на консоли Mega Drive привёл к тому, что в 1992 году появился урезанный порт для Game Gear, в котором нет Адама. А в 1993 году для PAL-региона вышла версия для Master System без режима кооперативной игры. Мы спросили, причастен ли Сеймия к этим портам низкого качества, и он ответил: Нет, я не работал над этими проектами.


image


image


Хотя оригинальная Streets of Rage снискала огромный успех, именно продолжение, выпущенное в 1992 году, заставило геймеров обратить внимание на серию. В сиквеле всё лучше: от музыки (написанной тем же Юдзо Косиро) до более детальных спрайтов и большего количества боевых приемов. Многие игроки до сих пор считают Streets of Rage 2 вершиной жанра. Сеймия работал над сиквелом в качестве художника фонов. Он также участвовал в создании Streets of Rage 3 1994 года, печально известной своим боксёрским кенгуру и большим количеством различий между западной и восточной версиями игры. Есть даже серия комиксов, но Сеймия тут уже точно ни при чём.


Несмотря на спорный триквел, серия Streets of Rage остаётся примером отличных битемапов и важным релизом в 16-битной битве между Sega и Nintendo. Недавно выпущенная четвёртая часть понравилась фанатам серии и поклонникам жанра: да, там нет запоминающегося саундтрека, но в остальном у Lizardcube и Guard Crush Games получилось создать достойное продолжение.


image




Это перевод статьи из 133-го номера журнала Retro Gamer. Ранее выкладывал материал на ресурсе Идеальный Пиксель сайт понравится всем фанатам ретро-игр и старых компьютеров.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru