Главная » Хабрахабр » Модернизация IDA Pro. Отладчик для Sega Mega Drive (часть 1)

Модернизация IDA Pro. Отладчик для Sega Mega Drive (часть 1)

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

В ней я расскажу вам, как написать свой плагин-отладчик для IDA Pro. Товарищи реверсеры, ромхакеры: в основном эта статья будет посвящена вам. В общем, погнали! Да, уже была первая попытка начать рассказ, но, с тех пор много воды утекло, многие принципы пересмотрены.

Лирическое вступление

На нём, кстати, работает моя любимая старушка Sega Mega Drive / Genesis. Собственно, из предыдущих статей (раз, два, три), думаю, не будет секретом, что мой любимый процессор — это Motorola 68000. И, так как мне всегда было интересно, как же всё таки устроены сеговские игры, я ещё с первых месяцев владения компьютером решил глубоко и надолго погрязть в дебрях дизассемблирования и реверсинга.

Всё было написано для IDA 6. Так появился Smd IDA Tools.
Проект включает в себя различные вспомогательные штуки, которые делают работу по изучению ромов на Сегу значительно проще: загрузчик, отладчик, хелпер по командам VDP. Но, когда я решил поведать миру о том, как же я всё таки это сделал, стало понятно, что показывать такой код народу, а, тем более, описывать его, будет очень сложно. 8
, и работало хорошо. Поэтому я так и не смог этого сделать тогда.

0. А потом вышла IDA 7. Да и игра Pier Solar and the Great Architects, вышедшая на картриджах в 2010, и которую так хотелось исследовать (а антиэмуляционных трюков там полно), не запускалась в Gens'е. Желание портировать свой проект под неё появилось сразу, но архитектура эмулятора Gens, на основе которого я писал отладчик, оказалась непригодной для переноса: ассемблерные вставки под x86, костыли, сложный в понимании код, и многое другое.

Так и появилась данная статья. В поисках подходящего исходника эмулятора, который можно было бы адаптировать под отладчик, я в итоге наткнулся на Genesis Plus GX от EkeEke.

Часть первая: ядро отладчика

В его оригинальном исходнике уже имеется базовый отладочный функционал (хук на выполнение инструкций), но EkeEke решил убрать его за ненадобностью. Эмуляцией инструкций мотороловского процессора в Genesis Plus GX занимается Musashi. Возвращаем.

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

  • Бряки (точки останова) на исполнение, на чтение и запись в память
  • Функционал Step Into, Step Over
  • Приостановка (Pause), продолжение (Resume) эмуляции
  • Чтение/установка регистров, чтение/запись памяти

Добавляем ещё один пункт: Если эти четыре пункта — это работа отладчика изнутри, то необходимо ещё продумать доступ к данному функционалу извне.

  • Протокол общения отладчика-сервера (ядра) с отладчиком-клиентом (GUI, пользователь)

Ядро отладчика: список бряков

Для реализации списка заводим следующую структуру:

typedef struct breakpoint_s { struct breakpoint_s *next, *prev; int enabled; int width; bpt_type_t type; unsigned int address;
} breakpoint_t;

Подробнее ниже. Поля next и prev будут хранить указатели на следующий и предыдущий элемент соответственно.
Поле enabled будет хранить 0, если данный бряк требуется пропустить в проверках на срабатывание.
width — количество байт начиная от адреса в поле address, которые покрывает бряк.
Ну а в поле type будем хранить тип бряка (исполнение, чтение, запись).

Для работы со списком точек останова я добавил следующие функции:

Функции для работы с точками останова

static breakpoint_t *first_bp = NULL; static breakpoint_t *add_bpt(bpt_type_t type, unsigned int address, int width) else { first_bp = bp; bp->next = bp; bp->prev = bp; } return bp;
} static void delete_breakpoint(breakpoint_t * bp) { if (bp == first_bp) { if (bp->next == bp) { first_bp = NULL; } else { first_bp = bp->next; } } bp->next->prev = bp->prev; bp->prev->next = bp->next; free(bp);
} static breakpoint_t *next_breakpoint(breakpoint_t *bp) { return bp->next != first_bp ? bp->next : 0;
} static breakpoint_t *find_breakpoint(unsigned int address, bpt_type_t type) { breakpoint_t *p; for (p = first_bp; p; p = next_breakpoint(p)) { if ((p->address == address) && ((p->type == BPT_ANY) || (p->type & type))) return p; } return 0;
} static void remove_bpt(unsigned int address, bpt_type_t type)
{ breakpoint_t *bpt; if ((bpt = find_breakpoint(address, type))) delete_breakpoint(bpt);
} static int count_bpt_list()
{ breakpoint_t *p; int i = 0; for (p = first_bp; p; p = next_breakpoint(p)) { ++i; } return i;
} static void get_bpt_data(int index, bpt_data_t *data)
{ breakpoint_t *p; int i = 0; for (p = first_bp; p; p = next_breakpoint(p)) { if (i == index) { data->address = p->address; data->width = p->width; data->type = p->type; data->enabled = p->enabled; break; } ++i; }
} static void clear_bpt_list() { while (first_bp != NULL) delete_breakpoint(first_bp);
} static void init_bpt_list()
{ if (first_bp) clear_bpt_list();
} void check_breakpoint(bpt_type_t type, int width, unsigned int address, unsigned int value)
{ if (!dbg_req || !dbg_req->dbg_active || dbg_dont_check_bp) return; breakpoint_t *bp; for (bp = first_bp; bp; bp = next_breakpoint(bp)) { if (!(bp->type & type) || !bp->enabled) continue; if ((address <= (bp->address + bp->width)) && ((address + width) >= bp->address)) { dbg_req->dbg_paused = 1; break; } }
}

Ядро отладчика: основные переменные

Собственно, данную реализацию я подсмотрел в другом отладчике PCSXR.

Добавляем переменные, которые будут хранить состояние эмуляции:

static int dbg_first_paused, dbg_trace, dbg_dont_check_bp;
static int dbg_step_over;
static int dbg_last_pc;
static unsigned int dbg_step_over_addr;
static int dbg_active, dbg_paused;

Если 0 — значит нужно сделать паузу эмуляции и отправить клиенту сообщение о том, что эмуляция начата. Переменная dbg_first_paused у нас будет отвечать за остановку эмуляции на старте отладки. После первой паузы устанавливаем в 1.

Если равно 1, выполняем одну инструкцию, становимся на паузу, и сбрасываем значение в 0. dbg_trace нам понадобиться для исполнения по одной инструкции (функционал Step Into).

Переменную dbg_dont_check_bp я завёл для того, чтобы бряки на чтение/запись памяти не срабатывали, если это делает отладчик.

После этого обе переменные обнуляем. dbg_step_over у нас будет хранить 1, если мы в режиме Step Over до тех пор, пока текущий PC (Program Counter, он же Instruction Pointer) не станет равным адресу в dbg_step_over_addr. О подсчёте значения dbg_step_over_addr я расскажу позже.

Чтобы бряк не сработал снова, я сравниваю адрес последнего PC в этой переменной с новым, и, если значения разные, можно проверять брейкпоинт на текущем PC. Я завёл переменную dbg_last_pc для одного конкретного случая: когда мы уже стоим на бряке, и клиент просит Resume.

dbg_active — собственно, хранит состояние 1, когда отладка активна и требуется проверять бряки, обрабатывать запросы от клиента.

С переменной dbg_paused, думаю, всё понятно: 1 — мы на паузе (например, после срабатывания бряка) и ожидаем команд от клиента, 0 — выполняем инструкции.

Пишем функции для работы с этими переменными:

static void pause_debugger()
{ dbg_trace = 1; dbg_paused = 1;
} static void resume_debugger()
{ dbg_trace = 0; dbg_paused = 0;
} static void detach_debugger()
{ clear_bpt_list(); resume_debugger();
} static void activate_debugger()
{ dbg_active = 1;
} static void deactivate_debugger()
{ dbg_active = 0;
}

Это нужно для того, чтобы после отсоединения клиента, старые точки останова не продолжали срабатывать. Видим, что в реализации detach_debugger() я использовал очистку списка бряков.

Ядро отладчика: реализуем хук на инструкции

Собственно, здесь и будет происходить основная работа с паузой, продолжением эмуляции, Step Into, Step Over.

Вот такой получился код для функции process_breakpoints():

void process_breakpoints() { int handled_event = 0; int is_step_over = 0; int is_step_in = 0; if (!dbg_active) return; unsigned int pc = m68k_get_reg(M68K_REG_PC); if (dbg_paused && dbg_first_paused && !dbg_trace) longjmp(jmp_env, 1); if (!dbg_first_paused) { dbg_first_paused = 1; dbg_paused = 1; // TODO: Send emulation started event } if (dbg_trace) { is_step_in = 1; dbg_trace = 0; dbg_paused = 1; // TODO: Send event that Step Into has been triggered handled_event = 1; } if (!dbg_paused) { if (dbg_step_over && pc == dbg_step_over_addr) { is_step_over = 1; dbg_step_over = 0; dbg_step_over_addr = 0; dbg_paused = 1; } if (dbg_last_pc != pc) check_breakpoint(BPT_M68K_E, 1, pc, pc); if (dbg_paused) { // TODO: Send event about Step Over or breakpoint has been triggered handled_event = 1; } } if (dbg_first_paused && (!handled_event) && dbg_paused) { // TODO: Send paused event } dbg_last_pc = pc; if (dbg_paused && (!is_step_in || is_step_over)) { longjmp(jmp_env, 1); }
}

Давайте разбираться:

  1. Если отладка не включена, просто выходим из хука
  2. Трюк с setjmp/longjmp нужен был потому, что окно оболочки RetroArch, для которой была написана своя версия Genesis Plus GX, с помощью который мы и запускаем эмуляцию, подвисает в ожидании выхода из функции рендеринга кадра, которую как раз реализует эмулятор. Вторую часть трюка я покажу позже, т.к. она касается уже оболочки над эмулятором, нежели ядра.
  3. Если это наше первое срабатывание хука, а, соответственно, и начало эмуляции, ставим на паузу и отправляем событие о старте эмуляции клиенту.
  4. Если клиент ранее отправил команду Step Into, обнуляем значение переменной dbg_trace и ставим эмуляцию на паузу. Отправляем клиенту соответствующее событие.
  5. Если мы не на паузе, режим Step Over включен, и текущий PC равен адресу назначения dbg_step_over_addr, обнуляем необходимые переменные и ставим на паузу.
  6. Проверяем брейкпоинт, если мы сейчас не на нём, и, если бряк сработал, ставим на паузу и отправляем клиенту событие о Step Over или бряке.
  7. Если это не бряк, не Step Into, и не Step Over, значит клиент попросил паузу. Отправляем событие о сработавшей паузе.
  8. Реализуем трюк с longjump в качестве реализации бесконечного цикла ожидания действий от клиента во время паузы.

У мотороловского процессора бывает разная длина инструкций, поэтому приходится считать адрес следующей вручную, в зависимости от опкода. Код подсчёта адреса для Step Over оказался не таким простым, как можно предположить сначала. Реализация следующая: При том, нужно избегать инструкций типа bra, jmp, rts условных прыжков вперёд, и выполнять их как Step Into.

Код функции calc_step_over()

static unsigned int calc_step_over() { unsigned int pc = m68k_get_reg(M68K_REG_PC); unsigned int sp = m68k_get_reg(M68K_REG_SP); unsigned int opc = m68ki_read_imm_16(); unsigned int dest_pc = (unsigned int)(-1); // jsr if ((opc & 0xFFF8) == 0x4E90) { m68k_op_jsr_32_ai(); m68k_op_rts_32(); dest_pc = m68k_get_reg(M68K_REG_PC); } else if ((opc & 0xFFF8) == 0x4EA8) { m68k_op_jsr_32_di(); m68k_op_rts_32(); dest_pc = m68k_get_reg(M68K_REG_PC); } else if ((opc & 0xFFF8) == 0x4EB0) { m68k_op_jsr_32_ix(); m68k_op_rts_32(); dest_pc = m68k_get_reg(M68K_REG_PC); } else if ((opc & 0xFFFF) == 0x4EB8) { m68k_op_jsr_32_aw(); m68k_op_rts_32(); dest_pc = m68k_get_reg(M68K_REG_PC); } else if ((opc & 0xFFFF) == 0x4EB9) { m68k_op_jsr_32_al(); m68k_op_rts_32(); dest_pc = m68k_get_reg(M68K_REG_PC); } else if ((opc & 0xFFFF) == 0x4EBA) { m68k_op_jsr_32_pcdi(); m68k_op_rts_32(); dest_pc = m68k_get_reg(M68K_REG_PC); } else if ((opc & 0xFFFF) == 0x4EBB) { m68k_op_jsr_32_pcix(); m68k_op_rts_32(); dest_pc = m68k_get_reg(M68K_REG_PC); } // bsr else if ((opc & 0xFFFF) == 0x6100) { m68k_op_bsr_16(); m68k_op_rts_32(); dest_pc = m68k_get_reg(M68K_REG_PC); } else if ((opc & 0xFFFF) == 0x61FF) { m68k_op_bsr_32(); m68k_op_rts_32(); dest_pc = m68k_get_reg(M68K_REG_PC); } else if ((opc & 0xFF00) == 0x6100) { m68k_op_bsr_8(); m68k_op_rts_32(); dest_pc = m68k_get_reg(M68K_REG_PC); } // dbf else if ((opc & 0xfff8) == 0x51C8) { dest_pc = m68k_get_reg(M68K_REG_PC) + 2; } m68k_set_reg(M68K_REG_PC, pc); m68k_set_reg(M68K_REG_SP, sp); return dest_pc;

Ядро отладчика: инициализация и остановка отладки

Тут всё просто:

void stop_debugging()
{ // TODO: Send Stopped event to client detach_debugger(); deactivate_debugger(); dbg_first_paused = dbg_paused = dbg_trace = dbg_dont_check_bp = dbg_step_over = dbg_step_over_addr = dbg_last_pc = 0;
} void start_debugging()
{ if (dbg_active) return; activate_debugger(); init_bpt_list(); dbg_first_paused = dbg_paused = dbg_trace = dbg_dont_check_bp = dbg_step_over = dbg_step_over_addr = dbg_last_pc = 0;
}

Ядро отладчика: реализация протокола

в нём реализован функционал обработки запросов от клиента, и реакции на них.
Реализовывать было решено на основе Shared Memory, потому как требуется пересылать большие блоки памяти: VRAM, RAM, ROM, а по сети это будет тем ещё удовольствием. Протокол общения между сервером-отладчиком и клиентом-пользователем можно смело назвать вторым сердцем процесса отладки, т.к.

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

Прототип был выбран такой:

Исходный код debug_wrap.h

#ifndef _DEBUG_WRAP_H_
#define _DEBUG_WRAP_H_ #ifdef __cplusplus
extern "C" {
#endif #include <Windows.h> #define SHARED_MEM_NAME "GX_PLUS_SHARED_MEM"
#define MAX_BREAKPOINTS 1000
#define MAX_DBG_EVENTS 20 #ifndef MAXROMSIZE
#define MAXROMSIZE ((unsigned int)0xA00000)
#endif #pragma pack(push, 4)
typedef enum { BPT_ANY = (0 << 0), // M68K BPT_M68K_E = (1 << 0), BPT_M68K_R = (1 << 1), BPT_M68K_W = (1 << 2), BPT_M68K_RW = BPT_M68K_R | BPT_M68K_W, // VDP BPT_VRAM_R = (1 << 3), BPT_VRAM_W = (1 << 4), BPT_VRAM_RW = BPT_VRAM_R | BPT_VRAM_W, BPT_CRAM_R = (1 << 5), BPT_CRAM_W = (1 << 6), BPT_CRAM_RW = BPT_CRAM_R | BPT_CRAM_W, BPT_VSRAM_R = (1 << 7), BPT_VSRAM_W = (1 << 8), BPT_VSRAM_RW = BPT_VSRAM_R | BPT_VSRAM_W, // Z80 BPT_Z80_E = (1 << 11), BPT_Z80_R = (1 << 12), BPT_Z80_W = (1 << 13), BPT_Z80_RW = BPT_Z80_R | BPT_Z80_W, // REGS BPT_VDP_REG = (1 << 9), BPT_M68K_REG = (1 << 10),
} bpt_type_t; typedef enum { REQ_NO_REQUEST, REQ_GET_REGS, REQ_SET_REGS, REQ_GET_REG, REQ_SET_REG, REQ_READ_68K_ROM, REQ_READ_68K_RAM, REQ_WRITE_68K_ROM, REQ_WRITE_68K_RAM, REQ_READ_Z80, REQ_WRITE_Z80, REQ_ADD_BREAK, REQ_TOGGLE_BREAK, REQ_DEL_BREAK, REQ_CLEAR_BREAKS, REQ_LIST_BREAKS, REQ_ATTACH, REQ_PAUSE, REQ_RESUME, REQ_STOP, REQ_STEP_INTO, REQ_STEP_OVER,
} request_type_t; typedef enum { REG_TYPE_M68K = (1 << 0), REG_TYPE_S80 = (1 << 1), REG_TYPE_Z80 = (1 << 2), REG_TYPE_VDP = (1 << 3),
} register_type_t; typedef enum { DBG_EVT_NO_EVENT, DBG_EVT_STARTED, DBG_EVT_PAUSED, DBG_EVT_BREAK, DBG_EVT_STEP, DBG_EVT_STOPPED,
} dbg_event_type_t; typedef struct { dbg_event_type_t type; unsigned int pc; char msg[256];
} debugger_event_t; typedef struct { int index; unsigned int val;
} reg_val_t; typedef struct { unsigned int d0, d1, d2, d3, d4, d5, d6, d7; unsigned int a0, a1, a2, a3, a4, a5, a6, a7; unsigned int pc, sr, sp, usp, isp, ppc, ir;
} regs_68k_data_t; typedef enum { REG_68K_D0, REG_68K_D1, REG_68K_D2, REG_68K_D3, REG_68K_D4, REG_68K_D5, REG_68K_D6, REG_68K_D7, REG_68K_A0, REG_68K_A1, REG_68K_A2, REG_68K_A3, REG_68K_A4, REG_68K_A5, REG_68K_A6, REG_68K_A7, REG_68K_PC, REG_68K_SR, REG_68K_SP, REG_68K_USP, REG_68K_ISP, REG_68K_PPC, REG_68K_IR, REG_VDP_00, REG_VDP_01, REG_VDP_02, REG_VDP_03, REG_VDP_04, REG_VDP_05, REG_VDP_06, REG_VDP_07, REG_VDP_08, REG_VDP_09, REG_VDP_0A, REG_VDP_0B, REG_VDP_0C, REG_VDP_0D, REG_VDP_0E, REG_VDP_0F, REG_VDP_10, REG_VDP_11, REG_VDP_12, REG_VDP_13, REG_VDP_14, REG_VDP_15, REG_VDP_16, REG_VDP_17, REG_VDP_18, REG_VDP_19, REG_VDP_1A, REG_VDP_1B, REG_VDP_1C, REG_VDP_1D, REG_VDP_1E, REG_VDP_1F, REG_VDP_DMA_LEN, REG_VDP_DMA_SRC, REG_VDP_DMA_DST, REG_Z80_PC, REG_Z80_SP, REG_Z80_AF, REG_Z80_BC, REG_Z80_DE, REG_Z80_HL, REG_Z80_IX, REG_Z80_IY, REG_Z80_WZ, REG_Z80_AF2, REG_Z80_BC2, REG_Z80_DE2, REG_Z80_HL2, REG_Z80_R, REG_Z80_R2, REG_Z80_IFFI1, REG_Z80_IFFI2, REG_Z80_HALT, REG_Z80_IM, REG_Z80_I,
} regs_all_t; typedef struct { unsigned int pc, sp, af, bc, de, hl, ix, iy, wz; unsigned int af2,bc2,de2,hl2; unsigned char r, r2, iff1, iff2, halt, im, i;
} regs_z80_data_t; typedef struct { unsigned char regs_vdp[0x20]; unsigned short dma_len; unsigned int dma_src, dma_dst;
} vdp_regs_t; typedef struct { int type; // register_type_t regs_68k_data_t regs_68k; reg_val_t any_reg; vdp_regs_t vdp_regs; regs_z80_data_t regs_z80;
} register_data_t; typedef struct { int size; unsigned int address; unsigned char m68k_rom[MAXROMSIZE]; unsigned char m68k_ram[0x10000]; unsigned char z80_ram[0x2000];
} memory_data_t; typedef struct { bpt_type_t type; unsigned int address; int width; int enabled;
} bpt_data_t; typedef struct { int count; bpt_data_t breaks[MAX_BREAKPOINTS];
} bpt_list_t; typedef struct { request_type_t req_type; register_data_t regs_data; memory_data_t mem_data; bpt_data_t bpt_data; int dbg_events_count; debugger_event_t dbg_events[MAX_DBG_EVENTS]; bpt_list_t bpt_list; int dbg_active, dbg_paused; int is_ida;
} dbg_request_t;
#pragma pack(pop) dbg_request_t *open_shared_mem();
void close_shared_mem(dbg_request_t **request);
int recv_dbg_event(dbg_request_t *request, int wait);
void send_dbg_request(dbg_request_t *request, request_type_t type); #ifdef __cplusplus
}
#endif #endif

Первым полем в структуре у нас будет тип запроса:

  • чтение/установка регистров
  • чтение/запись памяти
  • работа с брейкпоинтами
  • приостановка/продолжение эмуляции, отсоединение/остановка отладчика
  • Step Into / Step Over

Следом — блоки памяти ROM, RAM, VRAM, Z80. Далее идут регистры M68K, Z80, VDP.

Ну и их список тоже здесь (по большей части, он лишь для отображения в GUI, без необходимости помнить все установленные бряки, как это делает IDA). Для добавления/удаления бряка я так же завёл соответствующую структуру.

Далее идёт список отладочных событий:

  • Отладка начата (необходим для IDA Pro)
  • Отладка приостановлена (в событии сохраняется PC, на котором в данный момент приостановлена эмуляция)
  • Сработал брейкпоинт (так же хранит значение PC, на котором произошло срабатывание)
  • Был выполнен Step Into или Step Over (тоже, по сути, нужно только для IDA, т.к. можно обойтись и одним лишь событием паузы)
  • Процесс эмуляции был остановлен. После нажатия кнопки Stop в IDA без получения этого события она будет бесконечно ожидать остановки

Вооружившись идеей протокола, реализуем обработку запросов клиента, получая таким образом следующий код ядра отладчика:

Исходный код debug.c

#include "debug.h" #include "shared.h" #define m68ki_cpu m68k
#define MUL (7) #ifndef BUILD_TABLES
#include "m68ki_cycles.h"
#endif #include "m68kconf.h"
#include "m68kcpu.h"
#include "m68kops.h" #include "vdp_ctrl.h"
#include "Z80.h" static int dbg_first_paused, dbg_trace, dbg_dont_check_bp;
static int dbg_step_over;
static int dbg_last_pc;
static unsigned int dbg_step_over_addr; static dbg_request_t *dbg_req = NULL;
static HANDLE hMapFile = 0; typedef struct breakpoint_s { struct breakpoint_s *next, *prev; int enabled; int width; bpt_type_t type; unsigned int address;
} breakpoint_t; static breakpoint_t *first_bp = NULL; static breakpoint_t *add_bpt(bpt_type_t type, unsigned int address, int width) { breakpoint_t *bp = (breakpoint_t *)malloc(sizeof(breakpoint_t)); bp->type = type; bp->address = address; bp->width = width; bp->enabled = 1; if (first_bp) { bp->next = first_bp; bp->prev = first_bp->prev; first_bp->prev = bp; bp->prev->next = bp; } else { first_bp = bp; bp->next = bp; bp->prev = bp; } return bp;
} static void delete_breakpoint(breakpoint_t * bp) { if (bp == first_bp) { if (bp->next == bp) { first_bp = NULL; } else { first_bp = bp->next; } } bp->next->prev = bp->prev; bp->prev->next = bp->next; free(bp);
} static breakpoint_t *next_breakpoint(breakpoint_t *bp) { return bp->next != first_bp ? bp->next : 0;
} static breakpoint_t *find_breakpoint(unsigned int address, bpt_type_t type) { breakpoint_t *p; for (p = first_bp; p; p = next_breakpoint(p)) { if ((p->address == address) && ((p->type == BPT_ANY) || (p->type & type))) return p; } return 0;
} static void remove_bpt(unsigned int address, bpt_type_t type)
{ breakpoint_t *bpt; if ((bpt = find_breakpoint(address, type))) delete_breakpoint(bpt);
} static int count_bpt_list()
{ breakpoint_t *p; int i = 0; for (p = first_bp; p; p = next_breakpoint(p)) { ++i; } return i;
} static void get_bpt_data(int index, bpt_data_t *data)
{ breakpoint_t *p; int i = 0; for (p = first_bp; p; p = next_breakpoint(p)) { if (i == index) { data->address = p->address; data->width = p->width; data->type = p->type; data->enabled = p->enabled; break; } ++i; }
} static void clear_bpt_list() { while (first_bp != NULL) delete_breakpoint(first_bp);
} static void init_bpt_list()
{ if (first_bp) clear_bpt_list();
} void check_breakpoint(bpt_type_t type, int width, unsigned int address, unsigned int value)
{ if (!dbg_req || !dbg_req->dbg_active || dbg_dont_check_bp) return; breakpoint_t *bp; for (bp = first_bp; bp; bp = next_breakpoint(bp)) { if (!(bp->type & type) || !bp->enabled) continue; if ((address <= (bp->address + bp->width)) && ((address + width) >= bp->address)) { dbg_req->dbg_paused = 1; break; } }
} static void pause_debugger()
{ dbg_trace = 1; dbg_req->dbg_paused = 1;
} static void resume_debugger()
{ dbg_trace = 0; dbg_req->dbg_paused = 0;
} static void detach_debugger()
{ clear_bpt_list(); resume_debugger();
} static void activate_debugger()
{ dbg_req->dbg_active = 1;
} static void deactivate_debugger()
{ dbg_req->dbg_active = 0;
} int activate_shared_mem()
{ hMapFile = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, sizeof(dbg_request_t), SHARED_MEM_NAME); if (hMapFile == 0) { return -1; } dbg_req = (dbg_request_t*)MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(dbg_request_t)); if (dbg_req == 0) { CloseHandle(hMapFile); return -1; } memset(dbg_req, 0, sizeof(dbg_request_t)); return 0;
} void deactivate_shared_mem()
{ UnmapViewOfFile(dbg_req); CloseHandle(hMapFile); hMapFile = NULL; dbg_req = NULL;
} static unsigned int calc_step_over() { unsigned int pc = m68k_get_reg(M68K_REG_PC); unsigned int sp = m68k_get_reg(M68K_REG_SP); unsigned int opc = m68ki_read_imm_16(); unsigned int dest_pc = (unsigned int)(-1); // jsr if ((opc & 0xFFF8) == 0x4E90) { m68k_op_jsr_32_ai(); m68k_op_rts_32(); dest_pc = m68k_get_reg(M68K_REG_PC); } else if ((opc & 0xFFF8) == 0x4EA8) { m68k_op_jsr_32_di(); m68k_op_rts_32(); dest_pc = m68k_get_reg(M68K_REG_PC); } else if ((opc & 0xFFF8) == 0x4EB0) { m68k_op_jsr_32_ix(); m68k_op_rts_32(); dest_pc = m68k_get_reg(M68K_REG_PC); } else if ((opc & 0xFFFF) == 0x4EB8) { m68k_op_jsr_32_aw(); m68k_op_rts_32(); dest_pc = m68k_get_reg(M68K_REG_PC); } else if ((opc & 0xFFFF) == 0x4EB9) { m68k_op_jsr_32_al(); m68k_op_rts_32(); dest_pc = m68k_get_reg(M68K_REG_PC); } else if ((opc & 0xFFFF) == 0x4EBA) { m68k_op_jsr_32_pcdi(); m68k_op_rts_32(); dest_pc = m68k_get_reg(M68K_REG_PC); } else if ((opc & 0xFFFF) == 0x4EBB) { m68k_op_jsr_32_pcix(); m68k_op_rts_32(); dest_pc = m68k_get_reg(M68K_REG_PC); } // bsr else if ((opc & 0xFFFF) == 0x6100) { m68k_op_bsr_16(); m68k_op_rts_32(); dest_pc = m68k_get_reg(M68K_REG_PC); } else if ((opc & 0xFFFF) == 0x61FF) { m68k_op_bsr_32(); m68k_op_rts_32(); dest_pc = m68k_get_reg(M68K_REG_PC); } else if ((opc & 0xFF00) == 0x6100) { m68k_op_bsr_8(); m68k_op_rts_32(); dest_pc = m68k_get_reg(M68K_REG_PC); } // dbf else if ((opc & 0xfff8) == 0x51C8) { dest_pc = m68k_get_reg(M68K_REG_PC) + 2; } m68k_set_reg(M68K_REG_PC, pc); m68k_set_reg(M68K_REG_SP, sp); return dest_pc;
} void process_request()
{ if (!dbg_req || !dbg_req->dbg_active) return; if (dbg_req->req_type == REQ_NO_REQUEST) return; switch (dbg_req->req_type) { case REQ_GET_REG: { register_data_t *regs_data = &dbg_req->regs_data; if (regs_data->type & REG_TYPE_M68K) regs_data->any_reg.val = m68k_get_reg(regs_data->any_reg.index); if (regs_data->type & REG_TYPE_VDP) regs_data->any_reg.val = reg[regs_data->any_reg.index]; if (regs_data->type & REG_TYPE_Z80) { if (regs_data->any_reg.index >= 0 && regs_data->any_reg.index <= 12) // PC <-> HL2 { regs_data->any_reg.val = ((unsigned int *)&Z80.pc)[regs_data->any_reg.index]; } else if (regs_data->any_reg.index >= 13 && regs_data->any_reg.index <= 19) // R <-> I { regs_data->any_reg.val = ((unsigned char *)&Z80.r)[regs_data->any_reg.index - 13]; } } } break; case REQ_SET_REG: { register_data_t *regs_data = &dbg_req->regs_data; if (regs_data->type & REG_TYPE_M68K) m68k_set_reg(regs_data->any_reg.index, regs_data->any_reg.val); if (regs_data->type & REG_TYPE_VDP) reg[regs_data->any_reg.index] = regs_data->any_reg.val; if (regs_data->type & REG_TYPE_Z80) { if (regs_data->any_reg.index >= 0 && regs_data->any_reg.index <= 12) // PC <-> HL2 { ((unsigned int *)&Z80.pc)[regs_data->any_reg.index] = regs_data->any_reg.val; } else if (regs_data->any_reg.index >= 13 && regs_data->any_reg.index <= 19) // R <-> I { ((unsigned char *)&Z80.r)[regs_data->any_reg.index - 13] = regs_data->any_reg.val & 0xFF; } } } break; case REQ_GET_REGS: case REQ_SET_REGS: { register_data_t *regs_data = &dbg_req->regs_data; if (regs_data->type & REG_TYPE_M68K) { regs_68k_data_t *m68kr = &regs_data->regs_68k; if (dbg_req->req_type == REQ_GET_REGS) { m68kr->d0 = m68k_get_reg(M68K_REG_D0); m68kr->d1 = m68k_get_reg(M68K_REG_D1); m68kr->d2 = m68k_get_reg(M68K_REG_D2); m68kr->d3 = m68k_get_reg(M68K_REG_D3); m68kr->d4 = m68k_get_reg(M68K_REG_D4); m68kr->d5 = m68k_get_reg(M68K_REG_D5); m68kr->d6 = m68k_get_reg(M68K_REG_D6); m68kr->d7 = m68k_get_reg(M68K_REG_D7); m68kr->a0 = m68k_get_reg(M68K_REG_A0); m68kr->a1 = m68k_get_reg(M68K_REG_A1); m68kr->a2 = m68k_get_reg(M68K_REG_A2); m68kr->a3 = m68k_get_reg(M68K_REG_A3); m68kr->a4 = m68k_get_reg(M68K_REG_A4); m68kr->a5 = m68k_get_reg(M68K_REG_A5); m68kr->a6 = m68k_get_reg(M68K_REG_A6); m68kr->a7 = m68k_get_reg(M68K_REG_A7); m68kr->pc = m68k_get_reg(M68K_REG_PC); m68kr->sr = m68k_get_reg(M68K_REG_SR); m68kr->sp = m68k_get_reg(M68K_REG_SP); m68kr->usp = m68k_get_reg(M68K_REG_USP); m68kr->isp = m68k_get_reg(M68K_REG_ISP); m68kr->ppc = m68k_get_reg(M68K_REG_PPC); m68kr->ir = m68k_get_reg(M68K_REG_IR); } else { m68k_set_reg(M68K_REG_D0, m68kr->d0); m68k_set_reg(M68K_REG_D1, m68kr->d1); m68k_set_reg(M68K_REG_D2, m68kr->d2); m68k_set_reg(M68K_REG_D3, m68kr->d3); m68k_set_reg(M68K_REG_D4, m68kr->d4); m68k_set_reg(M68K_REG_D5, m68kr->d5); m68k_set_reg(M68K_REG_D6, m68kr->d6); m68k_set_reg(M68K_REG_D7, m68kr->d7); m68k_set_reg(M68K_REG_A0, m68kr->a0); m68k_set_reg(M68K_REG_A1, m68kr->a1); m68k_set_reg(M68K_REG_A2, m68kr->a2); m68k_set_reg(M68K_REG_A3, m68kr->a3); m68k_set_reg(M68K_REG_A4, m68kr->a4); m68k_set_reg(M68K_REG_A5, m68kr->a5); m68k_set_reg(M68K_REG_A6, m68kr->a6); m68k_set_reg(M68K_REG_A7, m68kr->a7); m68k_set_reg(M68K_REG_PC, m68kr->pc); m68k_set_reg(M68K_REG_SR, m68kr->sr); m68k_set_reg(M68K_REG_SP, m68kr->sp); m68k_set_reg(M68K_REG_USP, m68kr->usp); m68k_set_reg(M68K_REG_ISP, m68kr->isp); } } if (regs_data->type & REG_TYPE_VDP) { vdp_regs_t *vdp_regs = &regs_data->vdp_regs; for (int i = 0; i < (sizeof(vdp_regs) / sizeof(vdp_regs->regs_vdp[0])); ++i) { if (dbg_req->req_type == REQ_GET_REGS) vdp_regs->regs_vdp[i] = reg[i]; else reg[i] = vdp_regs->regs_vdp[i]; } if (dbg_req->req_type == REQ_GET_REGS) { vdp_regs->dma_len = (reg[20] << 8) | reg[19]; if (!vdp_regs->dma_len) vdp_regs->dma_len = 0x10000; vdp_regs->dma_src = vdp_dma_calc_src(); vdp_regs->dma_dst = vdp_dma_get_dst(); } } if (regs_data->type & REG_TYPE_Z80) { regs_z80_data_t *z80r = &regs_data->regs_z80; if (dbg_req->req_type == REQ_GET_REGS) { z80r->pc = Z80.pc.d; z80r->sp = Z80.sp.d; z80r->af = Z80.af.d; z80r->bc = Z80.bc.d; z80r->de = Z80.de.d; z80r->hl = Z80.hl.d; z80r->ix = Z80.ix.d; z80r->iy = Z80.iy.d; z80r->wz = Z80.wz.d; z80r->af2 = Z80.af2.d; z80r->bc2 = Z80.bc2.d; z80r->de2 = Z80.de2.d; z80r->hl2 = Z80.hl2.d; z80r->r = Z80.r; z80r->r2 = Z80.r2; z80r->iff1 = Z80.iff1; z80r->iff2 = Z80.iff2; z80r->halt = Z80.halt; z80r->im = Z80.im; z80r->i = Z80.i; } else { Z80.pc.d = z80r->pc; Z80.sp.d = z80r->sp; Z80.af.d = z80r->af; Z80.bc.d = z80r->bc; Z80.de.d = z80r->de; Z80.hl.d = z80r->hl; Z80.ix.d = z80r->ix; Z80.iy.d = z80r->iy; Z80.wz.d = z80r->wz; Z80.af2.d = z80r->af2; Z80.bc2.d = z80r->bc2; Z80.de2.d = z80r->de2; Z80.hl2.d = z80r->hl2; Z80.r = z80r->r; Z80.r2 = z80r->r2; Z80.iff1 = z80r->iff1; Z80.iff2 = z80r->iff2; Z80.halt = z80r->halt; Z80.im = z80r->im; Z80.i = z80r->i; } } } break; case REQ_READ_68K_ROM: case REQ_READ_68K_RAM: case REQ_READ_Z80: { dbg_dont_check_bp = 1; memory_data_t *mem_data = &dbg_req->mem_data; for (int i = 0; i < mem_data->size; ++i) { switch (dbg_req->req_type) { case REQ_READ_68K_ROM: mem_data->m68k_rom[mem_data->address + i] = m68ki_read_8(mem_data->address + i); break; case REQ_READ_68K_RAM: mem_data->m68k_ram[(mem_data->address + i) & 0xFFFF] = m68ki_read_8(mem_data->address + i); break; case REQ_READ_Z80: mem_data->z80_ram[(mem_data->address + i) & 0x1FFF] = z80_readmem(mem_data->address + i); break; default: break; } } dbg_dont_check_bp = 0; } break; case REQ_WRITE_68K_ROM: case REQ_WRITE_68K_RAM: case REQ_WRITE_Z80: { dbg_dont_check_bp = 1; memory_data_t *mem_data = &dbg_req->mem_data; for (int i = 0; i < mem_data->size; ++i) { switch (dbg_req->req_type) { case REQ_WRITE_68K_ROM: m68ki_write_8(mem_data->address + i, mem_data->m68k_rom[mem_data->address + i]); break; case REQ_WRITE_68K_RAM: m68ki_write_8(0xFF0000 | ((mem_data->address + i) & 0xFFFF), mem_data->m68k_ram[(mem_data->address + i) & 0xFFFF]); break; case REQ_WRITE_Z80: z80_writemem(mem_data->address + i, mem_data->z80_ram[(mem_data->address + i) & 0x1FFF]); break; default: break; } } dbg_dont_check_bp = 0; } break; case REQ_ADD_BREAK: { bpt_data_t *bpt_data = &dbg_req->bpt_data; if (!find_breakpoint(bpt_data->address, bpt_data->type)) add_bpt(bpt_data->type, bpt_data->address, bpt_data->width); } break; case REQ_TOGGLE_BREAK: { bpt_data_t *bpt_data = &dbg_req->bpt_data; breakpoint_t *bp = find_breakpoint(bpt_data->address, bpt_data->type); if (bp != NULL) bp->enabled = !bp->enabled; } break; case REQ_DEL_BREAK: { bpt_data_t *bpt_data = &dbg_req->bpt_data; remove_bpt(bpt_data->address, bpt_data->type); } break; case REQ_CLEAR_BREAKS: clear_bpt_list(); case REQ_LIST_BREAKS: { bpt_list_t *bpt_list = &dbg_req->bpt_list; bpt_list->count = count_bpt_list(); for (int i = 0; i < bpt_list->count; ++i) get_bpt_data(i, &bpt_list->breaks[i]); } break; case REQ_ATTACH: activate_debugger(); dbg_first_paused = 0; break; case REQ_PAUSE: pause_debugger(); break; case REQ_RESUME: resume_debugger(); break; case REQ_STOP: stop_debugging(); break; case REQ_STEP_INTO: { if (dbg_req->dbg_paused) { dbg_trace = 1; dbg_req->dbg_paused = 0; } } break; case REQ_STEP_OVER: { if (dbg_req->dbg_paused) { unsigned int dest_pc = calc_step_over(); if (dest_pc != (unsigned int)(-1)) { dbg_step_over = 1; dbg_step_over_addr = dest_pc; } else { dbg_step_over = 0; dbg_step_over_addr = 0; dbg_trace = 1; } dbg_req->dbg_paused = 0; } } break; default: break; } dbg_req->req_type = REQ_NO_REQUEST;
} void send_dbg_event(dbg_event_type_t type)
{ dbg_req->dbg_events[dbg_req->dbg_events_count].type = type; dbg_req->dbg_events_count += 1;
} void stop_debugging()
{ send_dbg_event(DBG_EVT_STOPPED); detach_debugger(); deactivate_debugger(); dbg_first_paused = dbg_req->dbg_paused = dbg_trace = dbg_dont_check_bp = dbg_step_over = dbg_step_over_addr = dbg_last_pc = 0;
} void start_debugging()
{ if (dbg_req != NULL && dbg_req->dbg_active) return; activate_debugger(); init_bpt_list(); dbg_first_paused = dbg_req->dbg_paused = dbg_trace = dbg_dont_check_bp = dbg_step_over = dbg_step_over_addr = dbg_last_pc = 0;
} int is_debugger_accessible()
{ return (dbg_req != NULL);
} void process_breakpoints() { int handled_event = 0; int is_step_over = 0; int is_step_in = 0; unsigned int pc = m68k_get_reg(M68K_REG_PC); if (!dbg_req || !dbg_req->dbg_active) return; if (dbg_req->dbg_paused && dbg_first_paused && !dbg_trace) longjmp(jmp_env, 1); if (!dbg_first_paused) { dbg_first_paused = 1; dbg_req->dbg_paused = 1; dbg_req->dbg_events[dbg_req->dbg_events_count].pc = pc; strncpy(dbg_req->dbg_events[dbg_req->dbg_events_count].msg, "gpgx", sizeof(dbg_req->dbg_events[dbg_req->dbg_events_count].msg)); send_dbg_event(DBG_EVT_STARTED); } if (dbg_trace) { is_step_in = 1; dbg_trace = 0; dbg_req->dbg_paused = 1; dbg_req->dbg_events[dbg_req->dbg_events_count].pc = pc; send_dbg_event(DBG_EVT_STEP); handled_event = 1; } if (!dbg_req->dbg_paused) { if (dbg_step_over && pc == dbg_step_over_addr) { is_step_over = 1; dbg_step_over = 0; dbg_step_over_addr = 0; dbg_req->dbg_paused = 1; } if (dbg_last_pc != pc) check_breakpoint(BPT_M68K_E, 1, pc, pc); if (dbg_req->dbg_paused) { dbg_req->dbg_events[dbg_req->dbg_events_count].pc = pc; send_dbg_event(is_step_over ? DBG_EVT_STEP : DBG_EVT_BREAK); handled_event = 1; } } if (dbg_first_paused && (!handled_event) && dbg_req->dbg_paused) { dbg_req->dbg_events[dbg_req->dbg_events_count].pc = pc; send_dbg_event(DBG_EVT_PAUSED); } dbg_last_pc = pc; if (dbg_req->dbg_paused && (!is_step_in || is_step_over)) { longjmp(jmp_env, 1); }
} int is_debugger_paused()
{ return is_debugger_accessible() && dbg_req->dbg_paused && dbg_first_paused && !dbg_trace;
}

Исходный код debug.h

#ifndef _DEBUG_H_
#define _DEBUG_H_ #ifdef __cplusplus
extern "C" {
#endif #include <setjmp.h>
#include "debug_wrap.h" extern void start_debugging();
extern void stop_debugging();
extern int is_debugger_accessible();
extern void process_request();
extern int is_debugger_paused();
extern int activate_shared_mem();
extern void deactivate_shared_mem();
void check_breakpoint(bpt_type_t type, int width, unsigned int address, unsigned int value); extern jmp_buf jmp_env; #ifdef __cplusplus
}
#endif #endif

В итоге вставляем следующие вызовы в vdp_ctrl.c: Теперь в функции чтения и записи в память эмулятора нам необходимо добавить проверку брейкпоинтов.
Места, куда вставлять вызов check_breakpoint для VDP легко определить по строкам логирования под #ifdef LOGVDP.

check_breakpoint(BPT_VRAM_W, 2, addr, data);
...
check_breakpoint(BPT_CRAM_W, 2, addr, data);
...
check_breakpoint(BPT_VSRAM_W, 2, addr, data);
...
check_breakpoint(BPT_VRAM_R, 2, addr, data);
...
check_breakpoint(BPT_CRAM_R, 2, addr, data);
...
check_breakpoint(BPT_VSRAM_R, 2, addr, data);

Для RAM это будет выглядеть так (файл m68kcpu.h):

// m68ki_read_8
check_breakpoint(BPT_M68K_R, 1, address, val);
// m68ki_read_16
check_breakpoint(BPT_M68K_R, 2, address, val);
// m68ki_read_32
check_breakpoint(BPT_M68K_R, 4, address, val);
// m68ki_write_8
check_breakpoint(BPT_M68K_W, 1, address, val);
// m68ki_write_16
check_breakpoint(BPT_M68K_W, 2, address, val);
// m68ki_write_32
check_breakpoint(BPT_M68K_W, 4, address, val);

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

Исходный код debug_wrap.c

#include <Windows.h>
#include <process.h> #include "debug_wrap.h" static HANDLE hMapFile = NULL, hStartFunc = NULL; dbg_request_t *open_shared_mem()
{ hMapFile = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, SHARED_MEM_NAME); if (hMapFile == NULL) { return NULL; } dbg_request_t *request = (dbg_request_t *)MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(dbg_request_t)); if (request == NULL) { CloseHandle(hMapFile); return NULL; } return request;
} void close_shared_mem(dbg_request_t **request)
{ UnmapViewOfFile(*request); CloseHandle(hMapFile); hMapFile = NULL; *request = NULL;
} int recv_dbg_event(dbg_request_t *request, int wait)
{ while (request->dbg_active || request->dbg_events_count) { for (int i = 0; i < MAX_DBG_EVENTS; ++i) { if (request->dbg_events[i].type != DBG_EVT_NO_EVENT) { request->dbg_events_count -= 1; return i; } } if (!wait) return -1; Sleep(10); } return -1;
} void send_dbg_request(dbg_request_t *request, request_type_t type)
{ if (!request) return; request->req_type = type; while (request->dbg_active && request->req_type != REQ_NO_REQUEST) { Sleep(10); }
}

Чукча больше реверсер, чем программист. Сразу прошу меня простить за качество кода. Возможно, для синхронизации и ожидания стоило выбрать более адекватные способы, но, на момент написания кода они работали.

Ядро отладчика: запуск

Для включения отладки я добавил соответствующий пункт в опции Genesis Plus GX:

var.key = "genesis_plus_gx_debugger"; environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var); { if (!var.value || !strcmp(var.value, "disabled")) { if (is_debugger_accessible()) { stop_debugging(); stop_gui(); deactivate_shared_mem(); } } else { activate_shared_mem(); start_debugging(); run_gui(); } } ... { "genesis_plus_gx_debugger", "Debugger; disabled|enabled" },

Именно здесь выполняются инструкции процессора (а там как раз срабатывает наш хук), формируется буфер с картинкой. Немного об архитектуре RetroArch:
Для рендеринга фреймов, эмулятор каждый раз дёргает функцию retro_run(). Я исправил это трюком с setjmp()/longjmp(). И, пока ядро не завершит функцию retro_run(), окно RetroArch будет висеть. Так вот, первую часть трюка я вставил в начало retro_run():

if (is_debugger_paused()) { longjmp(jmp_env, 1); } int is_paused = setjmp(jmp_env); if (is_paused) { process_request(); return; }

Ну и в конце функции retro_run() я так же воткнул вызов process_request(), чтобы когда отладка не на паузе, иметь возможность принимать запросы.

P.S. Затравка для второй части

Update:
Во второй части статьи я расскажу о написании собственно плагина-отладчика для IDA Pro, и дам ссылки на все исходники.


Оставить комментарий

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

*

x

Ещё Hi-Tech Интересное!

Киберпреступники пять месяцев контролировали ASUS Live Update

Злоумышленники разместили на сервере вредоносный файл с бэкдором, подписанный валидным сертификатом ASUS. Как сообщает «Лаборатория Касперского», хакеры из APT-группировки ShadowHammer 5 месяцев контролировали сервис обновлений ASUS Live Update и заразили более полумиллиона компьютеров по всему миру.Исследователи из «Лаборатории Касперского» обнаружили, ...

Kubernetes 1.14: обзор основных новшеств

14. Этой ночью состоится очередной релиз Kubernetes — 1. По сложившейся для нашего блога традиции, рассказываем о ключевых изменениях в новой версии этого замечательного Open Source-продукта. 14 и сооветствующих issues, pull requests, Kubernetes Enhancement Proposals (KEP). Информация, использованная для подготовки ...