Home

Awesome

Hackable Console

Hackable Console is a pretentious project to add debugging capabilities to Libretro cores, and to create a front-end capable of debugging such cores.

Hackable Console screenshot

Why Libretro

The Libretro API provides a common interface that can be used to write multi-media applications in a multi-platform way. Programs implementing the API as a producer of media are called Libretro cores, and programs that can run those cores are called front-ends.

By creating a Libretro front-end, it's possible to load and run most of the 50+ cores available. The API already supports extensions, so the debug API could be added to the cores, and queried by the front-end.

I believe that this will be much easier than trying to integrate debugging capabilities into stand-alone emulators.

Goals

Hackable Console aims to:

  1. Specify an debug API able to
    • Execute of instructions: step-by-step, step into, step out, step over
    • Stop execution at certain addresses or when memory is read or written
    • Visualize and change the internal CPU state: registers, flags, interruptions
    • Disassemble instructions
    • Visualize and change memory
    • Be versioned so it's easy to add new functionalities
  2. Implement a Libretro front-end that can load and run Libretro cores, and that implements the debug API. In addition to the regular debugging capabilities provided by the debug API, the front-end also aims to:
    1. Show symbols instead of addresses where applicable
      • Pre-defined symbols available at startup, i.e. ROM routines
      • User-defined symbols
    2. Automatically show important memory regions, such as those pointed to by selected registers, i.e. BC, DE, HL, IX, and IY in the case of the Z80
    3. Automatically show the system stack, with symbol support
    4. A cheat engine capable of filtering memory and combining filter results
    5. Allow external plugins for specific consoles to be loaded and managed by the front-end
    6. Support scripts
    7. Snapshots
    8. Saving and loading of the entire front-end state to support long running reverse-engineering projects
  3. Add the debug API to existing Libretro cores
    • To help the development of the debug API and the front-end, zx48k-libretro, a core capable of running ZX Spectrum 48K programs and games, was developed using chips

However, as the project is just starting, the current goal is to implement something that works and that can be used to test the code design both for the debug API and the front-end.

The API

The API consists of just one function that cores must support via retro_get_proc_address_interface: hc_set_debugger. This function will receive a pointer to a hc_Debugger structure, which the core must then fill with the information necessary for the front-end to implement the debugging functionality.

typedef void* (*hc_Set)(hc_DebuggerIf* const debugger_if);

I'm a stronger supporter of the "east const" style, for reasons that are better explained elsewhere.

The API is written in C to allow its implementation in as many cores as possible, given the increased inter-operability provided by the language.

hc_DebuggerIf

typedef struct {
    unsigned const frontend_api_version;
    unsigned core_api_version;

    struct {
        /* Informs the front-end that a breakpoint occurred */
        void (* const breakpoint_cb)(unsigned id);

        /* The emulated system */
        hc_System const* system;
    }
    v1;
}
hc_DebuggerIf;

It's likely that hc_DebuggerIf will be extended to allow more than one system per core, as it's common for emulators to support an entire family of consoles or computers.

hc_System

hc_System and all other structures from now on must be filled by the core.

typedef struct {
    struct {
        char const* description;

        /* CPUs */
        hc_Cpu const* const* cpus;
        unsigned num_cpus;

        /* Memory regions that aren't addressable by any of the CPUs on the system */
        hc_Memory const* const* memory_regions;
        unsigned num_memory_regions;

        /* Supported breakpoints not covered by specific functions */
        hc_Breakpoint const* const* break_points;
        unsigned num_break_points;
    }
    v1;
}
hc_System;

memory_regions here are used for blocks of memory that are not directly accessible by any of the CPUs, at list not in their entirety, like memory that is banked or only accessible via I/O.

hc_Cpu

typedef struct {
    struct {
        /* CPU info */
        char const* description;
        unsigned type;
        int is_main;

        /* Memory region that is CPU addressable */
        hc_Memory const* memory_region;

        /* Registers */
        uint64_t (*get_register)(void* ud, unsigned reg);
        void (*set_register)(void* ud, unsigned reg, uint64_t value);
        unsigned (*set_reg_breakpoint)(void* ud, unsigned reg);

        /* Any one of these can be null if the cpu doesn't support the functionality */
        void (*step_into)(void* ud); /* step_into is also used to step a single instruction */
        void (*step_over)(void* ud);
        void (*step_out)(void* ud);

        /* set_break_point can be null when not supported */
        unsigned (*set_exec_breakpoint)(void* ud, uint64_t address);

        /* Breaks on read and writes to the input/output address space */
        unsigned (*set_io_watchpoint)(void* ud, uint64_t address, uint64_t length, int read, int write);

        /* Breaks when an interrupt occurs */
        unsigned (*set_int_breakpoint)(void* ud, unsigned type);

        /* Supported breakpoints not covered by specific functions */
        hc_Breakpoint const* const* break_points;
        unsigned num_break_points;
    }
    v1;
}
hc_Cpu;

memory_region here reflect exact what the CPU sees when it reads bytes. As an example, a banked cartridge won't appear in its entirety here, only the banks that are currently selected to be visible via the CPU address bus.

Many games written in assembly employ tricks to save either memory space, CPU cycles, or both. Sometimes when an address is called, the callee won't return to the instruction following the CALL instruction. The core must be careful to implement step_over and step_out to support the required functionality as best as possible. In any case, the core must never lock into an execution loop that prevents the user from interacting with the front-end.

hc_Memory

typedef struct {
    struct {
        char const* description;
        unsigned alignment; /* in bytes */
        uint64_t base_address;
        uint64_t size;
        uint8_t (*peek)(void* ud, uint64_t address);

        /* poke can be null for read-only memory but all memory should be writeable to allow patching */
        /* poke can be non-null and still don't change the value, i.e. for the main memory region when the address is in rom */
        void (*poke)(void* ud, uint64_t address, uint8_t value);

        /* set_watch_point can be null when not supported */
        unsigned (*set_watchpoint)(void* ud, uint64_t address, uint64_t length, int read, int write);

        /* Supported breakpoints not covered by specific functions */
        hc_Breakpoint const* const* break_points;
        unsigned num_break_points;
    }
    v1;
}
hc_Memory;

A 256 KiB MegaROM MSX cartridge (a cartridge that has banking) can be exposed via two different memory regions, one that begins at 0x4000 and has a size of 32 KiB, and that always reflect what the CPU can see via its address bus and has the HC_CPU_ADDRESSABLE bit set, and another that begins at address 0 and has a size of 256 KiB, which is the entire contents of the cartridge, and that does not have the HC_CPU_ADDRESSABLE bit set as the CPU is not capable of accessing the contents of the entire 256 KiB range via its address bus.

Supported CPUs

During development, support for the Z80 is being coded.

/* Supported CPUs in API version 1 */
#define HC_CPU_Z80 HC_MAKE_CPU_TYPE(0, 1)

/* Z80 registers */
#define HC_Z80_A 0
#define HC_Z80_F 1
#define HC_Z80_BC 2
#define HC_Z80_DE 3
#define HC_Z80_HL 4
#define HC_Z80_IX 5
#define HC_Z80_IY 6
#define HC_Z80_AF2 7
#define HC_Z80_BC2 8
#define HC_Z80_DE2 9
#define HC_Z80_HL2 10
#define HC_Z80_I 11
#define HC_Z80_R 12
#define HC_Z80_SP 13
#define HC_Z80_PC 14
#define HC_Z80_IFF 15
#define HC_Z80_IM 16
#define HC_Z80_WZ 17

#define HC_Z80_NUM_REGISTERS 18

/* Z80 interrupts */
#define HC_Z80_INT 0
#define HC_Z80_NMI 1

The Front-end

The front-end works with the concept of Consoles. Consoles are specific Libretro cores that implement the debug API, and that have an companion Lua script that is responsible for its loading and required configuration and/or automation.

The Lua script is required because some cores have specific needs to be considered ready to be debbuged, i.e.:

Some technology that is used:

The language used for the front-end is C++, and I'm employing multiple-inheritance as a way to allow classes to implement multiple interfaces, where an interface is an abstract C++ class. For all the criticism that multiple-inheritance gets, for this project I believe that it's working quite well. IMO the real issue with OOP is a deep hierarchy tree (not counting issues like cache which I'm not worried about in this project). Everything is declared in the hc namespace.

The Front-end has the concept of a desktop, where multiple views can be added and laid out.

Some notes about the code: