Приветствую!
Товарищи реверсеры, ромхакеры: в основном эта статья будет посвящена вам. В ней я расскажу вам, как написать свой плагин-отладчик для IDA Pro
. Да, уже была первая попытка начать рассказ, но, с тех пор много воды утекло, многие принципы пересмотрены. В общем, погнали!
Лирическое вступление
Собственно, из предыдущих статей (раз, два, три), думаю, не будет секретом, что мой любимый процессор — это Motorola 68000
. На нём, кстати, работает моя любимая старушка Sega Mega Drive
/ Genesis
. И, так как мне всегда было интересно, как же всё таки устроены сеговские игры, я ещё с первых месяцев владения компьютером решил глубоко и надолго погрязть в дебрях дизассемблирования и реверсинга.
Так появился Smd IDA Tools.
Проект включает в себя различные вспомогательные штуки, которые делают работу по изучению ромов на Сегу значительно проще: загрузчик, отладчик, хелпер по командам VDP
. Всё было написано для IDA 6.8
, и работало хорошо. Но, когда я решил поведать миру о том, как же я всё таки это сделал, стало понятно, что показывать такой код народу, а, тем более, описывать его, будет очень сложно. Поэтому я так и не смог этого сделать тогда.
А потом вышла IDA 7.0
. Желание портировать свой проект под неё появилось сразу, но архитектура эмулятора Gens
, на основе которого я писал отладчик, оказалась непригодной для переноса: ассемблерные вставки под x86
, костыли, сложный в понимании код, и многое другое. Да и игра Pier Solar and the Great Architects
, вышедшая на картриджах в 2010, и которую так хотелось исследовать (а антиэмуляционных трюков там полно), не запускалась в Gens
‘е.
В поисках подходящего исходника эмулятора, который можно было бы адаптировать под отладчик, я в итоге наткнулся на Genesis Plus GX от EkeEke
. Так и появилась данная статья.
Часть первая: ядро отладчика
Эмуляцией инструкций мотороловского процессора в Genesis Plus GX
занимается Musashi. В его оригинальном исходнике уже имеется базовый отладочный функционал (хук на выполнение инструкций), но EkeEke
решил убрать его за ненадобностью. Возвращаем.
Теперь самое главное: необходимо определиться с архитектурой отладчика. Требования следующие:
- Бряки (точки останова) на исполнение, на чтение и запись в память
- Функционал
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) { 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; } } }
Ядро отладчика: основные переменные
Собственно, данную реализацию я подсмотрел в другом отладчике 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;
Переменная dbg_first_paused
у нас будет отвечать за остановку эмуляции на старте отладки. Если 0
— значит нужно сделать паузу эмуляции и отправить клиенту сообщение о том, что эмуляция начата. После первой паузы устанавливаем в 1
.
dbg_trace
нам понадобиться для исполнения по одной инструкции (функционал Step Into
). Если равно 1
, выполняем одну инструкцию, становимся на паузу, и сбрасываем значение в 0
.
Переменную dbg_dont_check_bp
я завёл для того, чтобы бряки на чтение/запись памяти не срабатывали, если это делает отладчик.
dbg_step_over
у нас будет хранить 1
, если мы в режиме Step Over
до тех пор, пока текущий PC
(Program Counter, он же Instruction Pointer) не станет равным адресу в dbg_step_over_addr
. После этого обе переменные обнуляем. О подсчёте значения dbg_step_over_addr
я расскажу позже.
Я завёл переменную dbg_last_pc
для одного конкретного случая: когда мы уже стоим на бряке, и клиент просит Resume
. Чтобы бряк не сработал снова, я сравниваю адрес последнего PC
в этой переменной с новым, и, если значения разные, можно проверять брейкпоинт на текущем PC
.
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); } }
Давайте разбираться:
- Если отладка не включена, просто выходим из хука
- Трюк с
setjmp
/longjmp
нужен был, потому, что окно оболочкиRetroArch
, для которой и был написанGenesis Plus GX
, подвисает в ожидании выхода из функции рендеринга кадра, которую как раз реализует эмулятор. Вторую часть трюка я покажу позже, т.к. она касается уже оболочки над эмулятором, нежели ядра. - Если это наше первое срабатывание хука, а, соответственно, и начало эмуляции, ставим на паузу и отправляем событие о старте эмуляции клиенту.
- Если клиент ранее отправил команду
Step Into
, обнуляем значение переменнойdbg_trace
и ставим эмуляцию на паузу. Отправляем клиенту соответствующее событие. - Если мы не на паузе, режим
Step Over
включен, и текущийPC
равен адресу назначенияdbg_step_over_addr
, обнуляем необходимые переменные и ставим на паузу. - Проверяем брейкпоинт, если мы сейчас не на нём, и, если бряк сработал, ставим на паузу и отправляем клиенту событие о
Step Over
или бряке. - Если это не бряк, не
Step Into
, и неStep Over
, значит клиент попросил паузу. Отправляем событие о сработавшей паузе. - Реализуем трюк с
longjump
в качестве реализации бесконечного цикла ожидания действий от клиента во время паузы.
Код подсчёта адреса для Step Over
оказался не таким простым, как можно предположить сначала. У мотороловского процессора бывает разная длина инструкций, поэтому приходится считать адрес следующей вручную, в зависимости от опкода. При том, нужно избегать инструкций типа bra
, jmp
, rts
условных прыжков вперёд, и выполнять их как Step Into
. Реализация следующая:
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
, а по сети это будет тем ещё удовольствием.
Суть такова: ядро создаёт расшареную память с заранее определённой структурой, и ожидает входящих запросов от клиента. После обработки запроса в ту же память сохраняется ответ, и в список событий отладчика в той же памяти добавляется соответствующая информация.
Прототип был выбран такой:
#ifndef _DEBUG_WRAP_H_ #define _DEBUG_WRAP_H_ #ifdef __cplusplus extern "C" { #endif #include #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
Далее идут регистры M68K
, Z80
, VDP
. Следом — блоки памяти ROM
, RAM
, VRAM
, Z80
.
Для добавления/удаления бряка я так же завёл соответствующую структуру. Ну и их список тоже здесь (по большей части, он лишь для отображения в GUI, без необходимости помнить все установленные бряки, как это делает IDA
).
Далее идёт список отладочных событий:
- Отладка начата (необходим для
IDA Pro
) - Отладка приостановлена (в событии сохраняется
PC
, на котором в данный момент приостановлена эмуляция) - Сработал брейкпоинт (так же хранит значение
PC
, на котором произошло срабатывание) - Был выполнен
Step Into
илиStep Over
(тоже, по сути, нужно только дляIDA
, т.к. можно обойтись и одним лишь событием паузы) - Процесс эмуляции был остановлен. После нажатия кнопки
Stop
вIDA
без получения этого события она будет бесконечно ожидать остановки
Вооружившись идеей протокола, реализуем обработку запросов клиента, получая таким образом следующий код ядра отладчика:
#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 = ®s_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 = ®s_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 = ®s_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; }
#ifndef _DEBUG_H_ #define _DEBUG_H_ #ifdef __cplusplus extern "C" { #endif #include #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
Теперь в функции чтения и записи в память эмулятора нам необходимо добавить проверку брейкпоинтов.
Места, куда вставлять вызов check_breakpoint
для VDP
легко определить по строкам логирования под #ifdef LOGVDP
. В итоге вставляем следующие вызовы в vdp_ctrl.c
:
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);
Для доступа клиента к расшареной памяти, а также для отправки им запросов, и ожидания отладочных событий сделаем обёртку.
#include #include #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()
. Именно здесь выполняются инструкции процессора (а там как раз срабатывает наш хук), формируется буфер с картинкой. И, пока ядро не завершит функцию retro_run()
, окно RetroArch
будет висеть. Я исправил это трюком с setjmp()
/longjmp()
. Так вот, первую часть трюка я вставил в начало 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
, и дам ссылки на все исходники.
Источник