IDA Pro Upgrade. Debugger for Sega Mega Drive (part 1)
- Tutorial
Greetings
Comrades reversers, romhackers: basically this article will be devoted to you. In it, I will tell you how to write your own plugin debugger for IDA Pro
. Yes, it was already the first attempt to start a story , but since then much water has passed under the bridge, many principles have been revised. In general, drove!
Lyrical introduction
Actually, from the previous articles ( one , two , three ), I think it will not be a secret that my favorite processor is this Motorola 68000
. On it, by the way, running is my favorite old Sega Mega Drive
/ Genesis
. And, since it was always interesting to me how Segov’s games were all arranged, I decided from the first months of owning a computer to plunge deeply and permanently into the wilds of disassembling and reversing.
This is how Smd IDA Tools appeared .
The project includes various auxiliary things that make the work on the study of rums for Segou much easier: loader, debugger, helper on commands VDP
. Everything was written for IDA 6.8
, and worked well. But when I decided to tell the world how I did it all the same, it became clear that it would be very difficult to show such code to people, and, moreover, to describe it. Therefore, I could not do it then.
And then came out IDA 7.0
. The desire to port your project under it appeared right away, but the emulator architecture Gens
, on the basis of which I wrote the debugger, turned out to be unsuitable for transfer: assembler inserts under x86
, crutches, difficult to understand code, and much more. And the game Pier Solar and the Great Architects
, which was released on the cartridges in 2010, and which I wanted to explore so much (and there are plenty of anti-emulation tricks), did not start in Gens
'e.
In search of a suitable emulator source, which could be adapted for a debugger, I finally came across a Genesis Plus GX from EkeEke
. So this article appeared.
Part One: Debugger Core
Emulation instructions motorolovskogo processor Genesis Plus GX
engaged in the Musashi . In its original source code there is already a basic debugging functionality (a hook for executing instructions), but EkeEke
decided to remove it as superfluous. Return.
Now the most important thing: you need to decide on the architecture of the debugger. The requirements are as follows:
- Breakpoints (breakpoints) for execution, reading and writing to memory
- Functional
Step Into
,Step Over
- Pause (
Pause
), continue (Resume
) emulation - Read / set registers, read / write memory
If these four points are the work of the debugger from the inside, then you need to think about access to this functionality from the outside. Add one more item:
- Protocol of debugger-server (kernel) communication with debugger-client (GUI, user)
Debugger Kernel: Break List
To implement the list we have the following structure:
typedefstructbreakpoint_s {structbreakpoint_s *next, *prev;int enabled;
int width;
bpt_type_t type;
unsignedint address;
} breakpoint_t;
Fields next
and prev
will store pointers to the next and previous element, respectively.
The field enabled
will be stored 0
if this breakpoint is required to be skipped in trigger checks. width
- the number of bytes starting from the address in the field address
, which covers bryak.
Well, in the field type
we will keep the type of bribe (execution, reading, writing). More details below.
To work with the list of breakpoints, I added the following functions:
staticbreakpoint_t *first_bp = NULL;
static breakpoint_t *add_bpt(bpt_type_t type, unsignedint 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;
}
staticvoiddelete_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(unsignedint 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;
}
return0;
}
staticvoidremove_bpt(unsignedint address, bpt_type_t type){
breakpoint_t *bpt;
if ((bpt = find_breakpoint(address, type)))
delete_breakpoint(bpt);
}
staticintcount_bpt_list(){
breakpoint_t *p;
int i = 0;
for (p = first_bp; p; p = next_breakpoint(p)) {
++i;
}
return i;
}
staticvoidget_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;
}
}
staticvoidclear_bpt_list(){
while (first_bp != NULL) delete_breakpoint(first_bp);
}
staticvoidinit_bpt_list(){
if (first_bp)
clear_bpt_list();
}
voidcheck_breakpoint(bpt_type_t type, int width, unsignedint address, unsignedint 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;
}
}
}
Debugger Core: Main Variables
Actually, I spied this implementation in another PCSXR debugger .
Add variables that will store the emulation state:
staticint dbg_first_paused, dbg_trace, dbg_dont_check_bp;
staticint dbg_step_over;
staticint dbg_last_pc;
staticunsignedint dbg_step_over_addr;
staticint dbg_active, dbg_paused;
The variable dbg_first_paused
we will have is responsible for stopping the emulation at the start of debugging. If 0
it means, you need to pause the emulation and send a message to the client that the emulation has started. After the first pause set in 1
.
dbg_trace
we need to execute one instruction at a time (functional Step Into
). If it’s equal 1
, we execute one instruction, pause, and reset the value to 0
.
dbg_dont_check_bp
I started the variable so that the read / write memory breaks do not work if the debugger does it.
dbg_step_over
we will keep 1
it if we are in the mode Step Over
until the current PC
( Program Counter , aka Instruction Pointer ) becomes equal to the address in dbg_step_over_addr
. After that both variables are reset. dbg_step_over_addr
I will tell you about the calculation of the value later.
I created a variable dbg_last_pc
for one specific case: when we are already on the breakpoint, and the client asks Resume
. To prevent the bryak from working again, I compare the address of the last PC
in this variable with the new one, and, if the values are different, you can check the breakpoint at the current one PC
.
dbg_active
- Actually, it stores the state 1
when debugging is active and you need to check bryaks, process requests from the client.
With a variable dbg_paused
, I think everything is clear: 1
- we are paused (for example, after the breakpoint has triggered) and we expect commands from the client, 0
- we follow the instructions.
We write functions for working with these variables:
staticvoidpause_debugger(){
dbg_trace = 1;
dbg_paused = 1;
}
staticvoidresume_debugger(){
dbg_trace = 0;
dbg_paused = 0;
}
staticvoiddetach_debugger(){
clear_bpt_list();
resume_debugger();
}
staticvoidactivate_debugger(){
dbg_active = 1;
}
staticvoiddeactivate_debugger(){
dbg_active = 0;
}
We see that in the implementation detach_debugger()
I used clearing the list of breakpoints. This is to ensure that after disconnecting the client, the old breakpoints do not continue to operate.
Debugger kernel: implement the hook
Actually, there will be major work with a pause, a continuation of emulation Step Into
, Step Over
.
This is the code for the function process_breakpoints()
:
voidprocess_breakpoints(){
int handled_event = 0;
int is_step_over = 0;
int is_step_in = 0;
if (!dbg_active)
return;
unsignedint 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);
}
}
Let's understand:
- If debugging is not enabled, just exit the hook
- The trick with
setjmp
/longjmp
needed was because the shell windowRetroArch
, for which its own version was writtenGenesis Plus GX
, with which we start the emulation, hangs in anticipation of leaving the frame rendering function, which the emulator implements. I will show the second part of the trick later, because it concerns already the shell over the emulator, rather than the core. - If this is our first triggering of the hook, and, accordingly, the beginning of the emulation, pause and send the event to start the emulation to the client.
- If the client previously sent a command
Step Into
, reset the value of the variabledbg_trace
and pause the emulation. We send to the client the corresponding event. - If we are not on a pause, the mode is
Step Over
on, and the currentPC
is the destination addressdbg_step_over_addr
, reset the necessary variables and pause. - We check breakpoint if we are not on it now, and if the breakpoint has worked, we pause and send the event to the client
Step Over
or breakpoint. - If this is not a bryak, not
Step Into
, and notStep Over
, then the client has asked for a pause. We send event about the pause. - We implement a trick with
longjump
as the implementation of an infinite loop of waiting for actions from the client during a pause.
The code for calculating the address for Step Over
was not as simple as it might be supposed at first. A motorol processor has different length of instructions, so you have to read the address of the next one manually, depending on the opcode. While the need to avoid the type of instruction bra
, jmp
, rts
conditional jumps forward, and execute them as Step Into
. The implementation is as follows:
staticunsignedintcalc_step_over(){
unsignedint pc = m68k_get_reg(M68K_REG_PC);
unsignedint sp = m68k_get_reg(M68K_REG_SP);
unsignedint opc = m68ki_read_imm_16();
unsignedint dest_pc = (unsignedint)(-1);
// jsrif ((opc & 0xFFF8) == 0x4E90) {
m68k_op_jsr_32_ai();
m68k_op_rts_32();
dest_pc = m68k_get_reg(M68K_REG_PC);
}
elseif ((opc & 0xFFF8) == 0x4EA8) {
m68k_op_jsr_32_di();
m68k_op_rts_32();
dest_pc = m68k_get_reg(M68K_REG_PC);
}
elseif ((opc & 0xFFF8) == 0x4EB0) {
m68k_op_jsr_32_ix();
m68k_op_rts_32();
dest_pc = m68k_get_reg(M68K_REG_PC);
}
elseif ((opc & 0xFFFF) == 0x4EB8) {
m68k_op_jsr_32_aw();
m68k_op_rts_32();
dest_pc = m68k_get_reg(M68K_REG_PC);
}
elseif ((opc & 0xFFFF) == 0x4EB9) {
m68k_op_jsr_32_al();
m68k_op_rts_32();
dest_pc = m68k_get_reg(M68K_REG_PC);
}
elseif ((opc & 0xFFFF) == 0x4EBA) {
m68k_op_jsr_32_pcdi();
m68k_op_rts_32();
dest_pc = m68k_get_reg(M68K_REG_PC);
}
elseif ((opc & 0xFFFF) == 0x4EBB) {
m68k_op_jsr_32_pcix();
m68k_op_rts_32();
dest_pc = m68k_get_reg(M68K_REG_PC);
}
// bsrelseif ((opc & 0xFFFF) == 0x6100) {
m68k_op_bsr_16();
m68k_op_rts_32();
dest_pc = m68k_get_reg(M68K_REG_PC);
}
elseif ((opc & 0xFFFF) == 0x61FF) {
m68k_op_bsr_32();
m68k_op_rts_32();
dest_pc = m68k_get_reg(M68K_REG_PC);
}
elseif ((opc & 0xFF00) == 0x6100) {
m68k_op_bsr_8();
m68k_op_rts_32();
dest_pc = m68k_get_reg(M68K_REG_PC);
}
// dbfelseif ((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;
Debugger kernel: initialize and stop debugging
Everything is simple:
voidstop_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;
}
voidstart_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;
}
Debugger kernel: protocol implementation
The communication protocol between the debug server and the client user can be called the second heart of the debugging process, since it implements the functionality of processing requests from the client, and reactions to them.
It was decided to implement based on the Shared the Memory , because it is required to send large blocks of memory: VRAM
, RAM
, ROM
, and the network it will be all the more fun.
The essence is this: the kernel creates shared memory with a predetermined structure, and waits for incoming requests from the client. After processing the request, the answer is stored in the same memory, and the corresponding information is added to the list of events of the debugger in the same memory.
The prototype was chosen as follows:
#ifndef _DEBUG_WRAP_H_#define _DEBUG_WRAP_H_#ifdef __cplusplusextern"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)typedefenum {
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;
typedefenum {
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;
typedefenum {
REG_TYPE_M68K = (1 << 0),
REG_TYPE_S80 = (1 << 1),
REG_TYPE_Z80 = (1 << 2),
REG_TYPE_VDP = (1 << 3),
} register_type_t;
typedefenum {
DBG_EVT_NO_EVENT,
DBG_EVT_STARTED,
DBG_EVT_PAUSED,
DBG_EVT_BREAK,
DBG_EVT_STEP,
DBG_EVT_STOPPED,
} dbg_event_type_t;
typedefstruct {dbg_event_type_t type;
unsignedint pc;
char msg[256];
} debugger_event_t;
typedefstruct {int index;
unsignedint val;
} reg_val_t;
typedefstruct {unsignedint d0, d1, d2, d3, d4, d5, d6, d7;
unsignedint a0, a1, a2, a3, a4, a5, a6, a7;
unsignedint pc, sr, sp, usp, isp, ppc, ir;
} regs_68k_data_t;
typedefenum {
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;
typedefstruct {unsignedint pc, sp, af, bc, de, hl, ix, iy, wz;
unsignedint af2,bc2,de2,hl2;
unsignedchar r, r2, iff1, iff2, halt, im, i;
} regs_z80_data_t;
typedefstruct {unsignedchar regs_vdp[0x20];
unsignedshort dma_len;
unsignedint dma_src, dma_dst;
} vdp_regs_t;
typedefstruct {int type; // register_type_tregs_68k_data_t regs_68k;
reg_val_t any_reg;
vdp_regs_t vdp_regs;
regs_z80_data_t regs_z80;
} register_data_t;
typedefstruct {int size;
unsignedint address;
unsignedchar m68k_rom[MAXROMSIZE];
unsignedchar m68k_ram[0x10000];
unsignedchar z80_ram[0x2000];
} memory_data_t;
typedefstruct {bpt_type_t type;
unsignedint address;
int width;
int enabled;
} bpt_data_t;
typedefstruct {int count;
bpt_data_t breaks[MAX_BREAKPOINTS];
} bpt_list_t;
typedefstruct {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();
voidclose_shared_mem(dbg_request_t **request);
intrecv_dbg_event(dbg_request_t *request, int wait);
voidsend_dbg_request(dbg_request_t *request, request_type_t type);
#ifdef __cplusplus
}
#endif#endif
The first field in the structure will be the query type:
- reading / setting registers
- read / write memory
- work with breakpoints
- pause / continue emulation, detach / stop debugger
Step Into
/Step Over
Further there are registers M68K
, Z80
, VDP
. Next - memories ROM
, RAM
, VRAM
, Z80
.
To add / remove breakpoints, I also started the corresponding structure. Well, their list is also here (for the most part, it is only for display in the GUI, without having to remember all the installed bryaks, as it does IDA
).
Next comes the list of debug events:
- Debug started (required for
IDA Pro
) - Debugging is suspended (saved in the event
PC
, on which the emulation is currently suspended) - Breakpoint triggered (also stores the value
PC
at which the trigger occurred) - It was executed
Step Into
orStep Over
(also, in fact, it is necessary only forIDA
, since it is possible to manage with only one pause event) - The emulation process has been stopped. After pressing the button
Stop
inIDA
without receiving this event, it will wait indefinitely for a stop.
Armed with the idea of a protocol, we implement the processing of client requests, thus obtaining the following debugger kernel code:
#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"staticint dbg_first_paused, dbg_trace, dbg_dont_check_bp;
staticint dbg_step_over;
staticint dbg_last_pc;
staticunsignedint dbg_step_over_addr;
staticdbg_request_t *dbg_req = NULL;
static HANDLE hMapFile = 0;
typedefstructbreakpoint_s {structbreakpoint_s *next, *prev;int enabled;
int width;
bpt_type_t type;
unsignedint address;
} breakpoint_t;
staticbreakpoint_t *first_bp = NULL;
static breakpoint_t *add_bpt(bpt_type_t type, unsignedint 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;
}
staticvoiddelete_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(unsignedint 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;
}
return0;
}
staticvoidremove_bpt(unsignedint address, bpt_type_t type){
breakpoint_t *bpt;
if ((bpt = find_breakpoint(address, type)))
delete_breakpoint(bpt);
}
staticintcount_bpt_list(){
breakpoint_t *p;
int i = 0;
for (p = first_bp; p; p = next_breakpoint(p)) {
++i;
}
return i;
}
staticvoidget_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;
}
}
staticvoidclear_bpt_list(){
while (first_bp != NULL) delete_breakpoint(first_bp);
}
staticvoidinit_bpt_list(){
if (first_bp)
clear_bpt_list();
}
voidcheck_breakpoint(bpt_type_t type, int width, unsignedint address, unsignedint 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;
}
}
}
staticvoidpause_debugger(){
dbg_trace = 1;
dbg_req->dbg_paused = 1;
}
staticvoidresume_debugger(){
dbg_trace = 0;
dbg_req->dbg_paused = 0;
}
staticvoiddetach_debugger(){
clear_bpt_list();
resume_debugger();
}
staticvoidactivate_debugger(){
dbg_req->dbg_active = 1;
}
staticvoiddeactivate_debugger(){
dbg_req->dbg_active = 0;
}
intactivate_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));
return0;
}
voiddeactivate_shared_mem(){
UnmapViewOfFile(dbg_req);
CloseHandle(hMapFile);
hMapFile = NULL;
dbg_req = NULL;
}
staticunsignedintcalc_step_over(){
unsignedint pc = m68k_get_reg(M68K_REG_PC);
unsignedint sp = m68k_get_reg(M68K_REG_SP);
unsignedint opc = m68ki_read_imm_16();
unsignedint dest_pc = (unsignedint)(-1);
// jsrif ((opc & 0xFFF8) == 0x4E90) {
m68k_op_jsr_32_ai();
m68k_op_rts_32();
dest_pc = m68k_get_reg(M68K_REG_PC);
}
elseif ((opc & 0xFFF8) == 0x4EA8) {
m68k_op_jsr_32_di();
m68k_op_rts_32();
dest_pc = m68k_get_reg(M68K_REG_PC);
}
elseif ((opc & 0xFFF8) == 0x4EB0) {
m68k_op_jsr_32_ix();
m68k_op_rts_32();
dest_pc = m68k_get_reg(M68K_REG_PC);
}
elseif ((opc & 0xFFFF) == 0x4EB8) {
m68k_op_jsr_32_aw();
m68k_op_rts_32();
dest_pc = m68k_get_reg(M68K_REG_PC);
}
elseif ((opc & 0xFFFF) == 0x4EB9) {
m68k_op_jsr_32_al();
m68k_op_rts_32();
dest_pc = m68k_get_reg(M68K_REG_PC);
}
elseif ((opc & 0xFFFF) == 0x4EBA) {
m68k_op_jsr_32_pcdi();
m68k_op_rts_32();
dest_pc = m68k_get_reg(M68K_REG_PC);
}
elseif ((opc & 0xFFFF) == 0x4EBB) {
m68k_op_jsr_32_pcix();
m68k_op_rts_32();
dest_pc = m68k_get_reg(M68K_REG_PC);
}
// bsrelseif ((opc & 0xFFFF) == 0x6100) {
m68k_op_bsr_16();
m68k_op_rts_32();
dest_pc = m68k_get_reg(M68K_REG_PC);
}
elseif ((opc & 0xFFFF) == 0x61FF) {
m68k_op_bsr_32();
m68k_op_rts_32();
dest_pc = m68k_get_reg(M68K_REG_PC);
}
elseif ((opc & 0xFF00) == 0x6100) {
m68k_op_bsr_8();
m68k_op_rts_32();
dest_pc = m68k_get_reg(M68K_REG_PC);
}
// dbfelseif ((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;
}
voidprocess_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 = ((unsignedint *)&Z80.pc)[regs_data->any_reg.index];
}
elseif (regs_data->any_reg.index >= 13 && regs_data->any_reg.index <= 19) // R <-> I
{
regs_data->any_reg.val = ((unsignedchar *)&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
{
((unsignedint *)&Z80.pc)[regs_data->any_reg.index] = regs_data->any_reg.val;
}
elseif (regs_data->any_reg.index >= 13 && regs_data->any_reg.index <= 19) // R <-> I
{
((unsignedchar *)&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)
{
unsignedint dest_pc = calc_step_over();
if (dest_pc != (unsignedint)(-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;
}
voidsend_dbg_event(dbg_event_type_t type){
dbg_req->dbg_events[dbg_req->dbg_events_count].type = type;
dbg_req->dbg_events_count += 1;
}
voidstop_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;
}
voidstart_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;
}
intis_debugger_accessible(){
return (dbg_req != NULL);
}
voidprocess_breakpoints(){
int handled_event = 0;
int is_step_over = 0;
int is_step_in = 0;
unsignedint 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);
}
}
intis_debugger_paused(){
return is_debugger_accessible() && dbg_req->dbg_paused && dbg_first_paused && !dbg_trace;
}
#ifndef _DEBUG_H_#define _DEBUG_H_#ifdef __cplusplusextern"C" {
#endif#include<setjmp.h>#include"debug_wrap.h"externvoidstart_debugging();
externvoidstop_debugging();
externintis_debugger_accessible();
externvoidprocess_request();
externintis_debugger_paused();
externintactivate_shared_mem();
externvoiddeactivate_shared_mem();
voidcheck_breakpoint(bpt_type_t type, int width, unsignedint address, unsignedint value);
extern jmp_buf jmp_env;
#ifdef __cplusplus
}
#endif#endif
Now in the function of reading and writing in the memory of the emulator, we need to add the breakpoint check.
Places where to insert a call check_breakpoint
for VDP
easy to determine by logging lines under #ifdef LOGVDP
. As a result, we insert the following calls into 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);
For RAM
it will look like this (file 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);
For client access to shared memory, as well as for sending them requests, and waiting for debug events, we will make a wrapper.
#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)
{
returnNULL;
}
dbg_request_t *request = (dbg_request_t *)MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(dbg_request_t));
if (request == NULL)
{
CloseHandle(hMapFile);
returnNULL;
}
return request;
}
voidclose_shared_mem(dbg_request_t **request){
UnmapViewOfFile(*request);
CloseHandle(hMapFile);
hMapFile = NULL;
*request = NULL;
}
intrecv_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;
}
voidsend_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);
}
}
Immediately ask me to forgive the quality of the code. Chukchi more reverser than a programmer. Perhaps, it was worth choosing more adequate methods for synchronization and waiting, but at the time of writing the code they were working.
Debugger kernel: launch
To enable debugging, I added the corresponding item in the option 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" },
A bit about architecture RetroArch
:
For frame rendering, the emulator pulls a function each time retro_run()
. It is here that the instructions of the processor are executed (and that’s where our hook is triggered), a buffer with a picture is formed. And, until the kernel completes the function retro_run()
, the window RetroArch
will hang. I corrected this trick with setjmp()
/ longjmp()
. So, the first part of the trick I put in the beginning retro_run()
:
if (is_debugger_paused())
{
longjmp(jmp_env, 1);
}
int is_paused = setjmp(jmp_env);
if (is_paused)
{
process_request();
return;
}
Well, at the end of the function, retro_run()
I also stuck a call process_request()
so that when debugging is not paused, to be able to accept requests.
PS Primer for the second part
Update :
In the second part of the article I will talk about writing the plugin debugger itself IDA Pro
, and give links to all sources.