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


Товарищи реверсеры, ромхакеры: в основном эта статья будет посвящена вам. В ней я расскажу вам, как написать свой плагин-отладчик для 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);
    }
}

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


  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

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


Для добавления/удаления бряка я так же завёл соответствующую структуру. Ну и их список тоже здесь (по большей части, он лишь для отображения в 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

Теперь в функции чтения и записи в память эмулятора нам необходимо добавить проверку брейкпоинтов.
Места, куда вставлять вызов 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);

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


Исходный код 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(). Именно здесь выполняются инструкции процессора (а там как раз срабатывает наш хук), формируется буфер с картинкой. И, пока ядро не завершит функцию 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, и дам ссылки на все исходники.

Комментарии (21)


  1. DCNick3
    02.01.2019 23:18

    Возможно, помещение списка точек останова в более эффективную для проверки наличия в ней определённого адреса позволит ускорить обработку инструкций (O(n) -> O(logn)). Например, для этого можно использовать set или unordered_set из стандартной библиотеки c++.


    1. DrMefistO Автор
      02.01.2019 23:23

      Я делал именно C-реализацию, чтобы код был более менее универсальным для вставки в другие проекты. Как раз *Genesis Plus GX* ядро выполнено на C.


    1. DrMefistO Автор
      02.01.2019 23:25

      Предоставленный эффективный вариант для C всячески приветствуется!


      1. DCNick3
        03.01.2019 01:03
        +1

        В стандартной библиотеке C, опять же, по причине её универсальности и простоты, нет реализаций таких нетривиальных структур данных. Тут придётся либо положиться на внешние библиотеки (или мини-библиотеки из двух файлов: исходного и заголовочного), либо предоставлять свою реализацию.
        Например, есть uthash.


        1. DrMefistO Автор
          03.01.2019 01:10

          Я попробую этот вариант для реализации бряков по значению, т.к. именно они будут являться бутылочным горлышком у точек останова в принципе.


          1. DCNick3
            03.01.2019 01:24

            #include <Windows.h>

            А вы точно уверены, что ваш код всё еще портативен и универсален?)


            1. DrMefistO Автор
              03.01.2019 01:34

              Это исправляется:)


        1. horror_x
          03.01.2019 01:51

          В данном случае нет никакой потребности во вспомогательных структурах, там хватит обыкновенной битовой маски, по биту на адрес.


  1. nzeemin
    03.01.2019 03:37

    Насколько я вижу, лицензия на IDA это как минимум несколько сот долларов, т.е. даже для фрилансера будет напряжно, не говоря уже о программировании как хобби (в случае с ромхакингом итп.). Может таки разумнее взглянуть на то что есть в опен-сорсе? Radare2 например.


    1. mistergrim
      03.01.2019 07:27

      Лицензия IDA для частного лица вещь вообще неподъёмная, не из-за цены даже. Вы просто попробуйте ссылку на BUY найти (нет, в итоге вы её найдёте).


    1. DrMefistO Автор
      03.01.2019 11:35

      Забавно, но под каждой статьёй про иду будет комент про радар2. Я же только за. Просто портировать всё это богатство по него, да и ждать, пока в гуи мой проц добавят (да, гуи) вся жизнь пройдёт...


      Поработайте-ка вы несколько суток с консольной прогой над одним проектом — я же простой человек, а не Столлман.


      1. spiiin
        04.01.2019 15:03

        А ещё кроме иды варианты есть какие-то для исследователя. кстати?


        1. DrMefistO Автор
          04.01.2019 15:16

          Radare2, binary ninja. Но под всё из них придётся пилить лоадеры и дебагеры, или того хуже — поддержку проца.


  1. mistergrim
    03.01.2019 07:21
    -1

    Да и игра Pier Solar and the Great Architects, вышедшая на картриджах в 2010, и которую так хотелось исследовать (а антиэмуляционных трюков там полно),
    Риторический вопрос — зачем это вообще.


    1. DrMefistO Автор
      03.01.2019 11:31

      Хобби, научный или спортивный интерес, челлендж. Ну, или, просто потому что могу.:)


      1. mistergrim
        03.01.2019 12:06

        Да, пожалуй, меня не поняли.
        Я хотел сказать — зачем защищать игру в 2010 году?
        Ну разве что по той же причине.


        1. DrMefistO Автор
          03.01.2019 12:21

          А, ну, причина такова: что за картридж в 2010 году они просили 55$, и игру сделали шикарную, в которую вложили силы многие хоумбрюшники. И, просто так сразу отдаваться дамперам картирджей (которые обязательно захотят сдампить игру, ведь надежды, что играть в неё на картриджах будут не только лишь все, очень мало) будет означать, что все труды были слиты в унитаз.


          К слову, только два эмулятора поддерживают Pier Solar, да и то, один не до конца корректно.


          1. mistergrim
            03.01.2019 12:25

            Интересно, насколько такой трюк поднял продажи. Экземпляра на два, наверное.


            1. DrMefistO Автор
              03.01.2019 12:28

              На самом деле куда больше, они потом ещё репринты сделал, а спустя пару лет — ещё.


              Я был первым, кто сдампил репринт (ревизия 2) в 2012-м, кажется, до меня ещё дампили бету, и ревизию 1.


              1. mistergrim
                04.01.2019 14:31

                Я немного о другом — много ли тех, кого отсутствие цифровой версии сподвигло на поиск и покупку б/у консоли?


                1. DrMefistO Автор
                  04.01.2019 14:37

                  Оно обычно не рассчитывается на покупку консоли, и целевая аудитория — ретро-ностальгирующие гики. А их, на самом деле, очень даже много. Это я Вам, как один из них говорю.:)