Merge remote-tracking branch 'redoste/gdb' into dev (#27)

This commit is contained in:
Lephe 2024-03-25 08:05:48 +01:00
commit faead4bc1d
No known key found for this signature in database
GPG key ID: 1BBA026E13FC0495
14 changed files with 1208 additions and 10 deletions

View file

@ -70,6 +70,8 @@ set(SOURCES
src/fs/fugue/fugue_rmdir.c
src/fs/fugue/fugue_unlink.c
src/fs/fugue/util.c
# GDB remote serial protocol
src/gdb/gdb.c
# Gray engine
src/gray/engine.c
src/gray/gclear.c
@ -227,6 +229,9 @@ set(SOURCES
src/tmu/inth-tmu.s
src/tmu/sleep.c
src/tmu/tmu.c
# UBC driver
src/ubc/ubc.c
src/ubc/ubc.S
# USB driver
src/usb/asyncio.c
src/usb/classes/ff-bulk.c

View file

@ -144,6 +144,19 @@ typedef struct {
/* GINT_CALL_NULL: Empty function call */
#define GINT_CALL_NULL ((gint_call_t){ .function = NULL, .args = {} })
/* GINT_CALL_FLAG(): Build an indirect call to an odd address
This is used as a workaround to have an extra flag in gint_call_t structures
without altering their size and exploits the fact that SuperH code has to be
aligned on even addresses.
It is up to the caller to notice the presence of the flag and realign the
address properly.
This flag is currently only checked by gint_inth_callback to specify that the
called function will take the interrupt context as its first argument. */
#define GINT_CALL_FLAG(func, ...) \
GINT_CALL((void*)((uint32_t)(func) | 1), __VA_ARGS__)
/* gint_call(): Perform an indirect call */
static GINLINE int gint_call(gint_call_t cb)
{

79
include/gint/gdb.h Normal file
View file

@ -0,0 +1,79 @@
//---
// gint:gdb - GDB remote serial protocol
//---
#ifndef GINT_GDB
#define GINT_GDB
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
/* Error codes for GDB functions */
enum {
GDB_NO_INTERFACE = -1,
GDB_ALREADY_STARTED = -2,
GDB_USB_ERROR = -3,
};
/* gdb_cpu_state_t: State of the CPU when breaking
This struct keep the same register indices as those declared by GDB to allow
easy R/W without needing a "translation" table.
See : https://sourceware.org/git/?p=binutils-gdb.git;a=blob;f=gdb/sh-tdep.c;
h=c402961b80a0b4589243023ea5362d43f644a9ec;hb=4f3e26ac6ee31f7bc4b04abd
8bdb944e7f1fc5d2#l327
*/
// TODO : Should we expose r*b*, ssr, spc ? are they double-saved when breaking
// inside an interrupt handler ?
typedef struct {
union {
struct {
uint32_t r0;
uint32_t r1;
uint32_t r2;
uint32_t r3;
uint32_t r4;
uint32_t r5;
uint32_t r6;
uint32_t r7;
uint32_t r8;
uint32_t r9;
uint32_t r10;
uint32_t r11;
uint32_t r12;
uint32_t r13;
uint32_t r14;
uint32_t r15;
uint32_t pc;
uint32_t pr;
uint32_t gbr;
uint32_t vbr;
uint32_t mach;
uint32_t macl;
uint32_t sr;
} reg;
uint32_t regs[23];
};
} gdb_cpu_state_t;
/* gdb_start(): Start the GDB remote serial protocol server
This function will start the GDB remote serial protocol implementation and
block until the program is resumed from the connected debugger.
It currently only supports USB communication and will fail if USB is already
in use.*/
int gdb_start(void);
/* gdb_main(): Main GDB loop
This function handles GDB messages sent over USB and will mutate the cpu_state
struct, memory and the UBC configuration accordingly. */
void gdb_main(gdb_cpu_state_t* cpu_state);
#ifdef __cplusplus
}
#endif
#endif /* GINT_GDB */

View file

@ -155,10 +155,29 @@ static GINLINE void *gint_inthandler(int code, void const *h, size_t size) {
.callback:
.long _gint_inth_callback
The gint_call_t object can be built with GINT_CALL_FLAG to specify that the
called function should get a pointer to a gint_inth_callback_context_t
strcture as its first argument.
@call Address of a gint_call_t object
Returns the return value of the callback. */
extern int (*gint_inth_callback)(gint_call_t const *call);
/* gint_inth_callback_context_t: Context of the interrupted function when an
interrupt is handled by gint_inth_callback. */
typedef struct {
uint32_t ssr;
uint32_t spc;
uint32_t r7;
uint32_t r6;
uint32_t r5;
uint32_t r4;
uint32_t r3;
uint32_t r2;
uint32_t r1;
uint32_t r0;
} gint_inth_callback_context_t;
/* gint_set_quit_handler(): Setup a call to be invoked when leaving the add-in
This function sets up the provided GINT_CALL() to be invoked when the

View file

@ -130,6 +130,11 @@ void *intc_handler(int event_code, void const *handler, size_t size);
the numerous constraints of intc_handler(), at the cost of always going back
to userspace and a small time overhead.
Since gint_inth_callback is used to do the heavy lifting of setting up a sane
context for C code, the gint_call_t object can be built with GINT_CALL_FLAG
if the called function expects a gint_inth_callback_context_t structure as its
first argument.
@event_code Identifier of the interrupt block
@function Function to use as a handler
Returns true on success, false if the event code is invalid. */

87
include/gint/mpu/ubc.h Normal file
View file

@ -0,0 +1,87 @@
//---
// gint:mpu:ubc - User Break Controller
//---
#ifndef GINT_MPU_UBC
#define GINT_MPU_UBC
#ifdef __cplusplus
extern "C" {
#endif
#include <gint/defs/types.h>
typedef volatile struct
{
lword_union(CBR0, /* Match condition setting 0 */
uint32_t MFE :1; /* Match Flag Enable */
uint32_t AIE :1; /* ASID Enable */
uint32_t MFI :6; /* Match Flag Specify */
uint32_t AIV :8; /* ASID Specify */
uint32_t :1;
uint32_t SZ :3; /* Operand Size Select */
uint32_t :4;
uint32_t CD :2; /* Bus Select */
uint32_t ID :2; /* Instruction Fetch / Operand Access Select */
uint32_t :1;
uint32_t RW :2; /* Bus Command Select */
uint32_t CE :1; /* Channel Enable */
);
lword_union(CRR0, /* Match operation setting 0 */
uint32_t :30;
uint32_t PCB :1; /* PC Break Select */
uint32_t BIE :1; /* Break Enable */
);
uint32_t CAR0; /* Match address setting 0 */
uint32_t CAMR0; /* Match address mask setting 0 */
pad(0x10);
lword_union(CBR1, /* Match condition setting 1 */
uint32_t MFE :1; /* Match Flag Enable */
uint32_t AIE :1; /* ASID Enable */
uint32_t MFI :6; /* Match Flag Specify */
uint32_t AIV :8; /* ASID Specify */
uint32_t DBE :1; /* Data Value Enable */
uint32_t SZ :3; /* Operand Size Select */
uint32_t ETBE :1; /* Execution Count Value Enable */
uint32_t :3;
uint32_t CD :2; /* Bus Select */
uint32_t ID :2; /* Instruction Fetch / Operand Access Select */
uint32_t :1;
uint32_t RW :2; /* Bus Command Select */
uint32_t CE :1; /* Channel Enable */
);
lword_union(CRR1, /* Match operation setting 1 */
uint32_t :30;
uint32_t PCB :1; /* PC Break Select */
uint32_t BIE :1; /* Break Enable */
);
uint32_t CAR1; /* Match address setting 1 */
uint32_t CAMR1; /* Match address mask setting 1 */
uint32_t CDR1; /* Match data setting 1 */
uint32_t CDMR1; /* Match data mask setting 1 */
lword_union(CETR1, /* Execution count break 1 */
uint32_t :20;
uint32_t CET :12; /* Execution Count */
);
pad(0x5c4);
lword_union(CCMFR, /* Channel match flag */
uint32_t :30;
uint32_t MF1 :1; /* Channel 1 Condition Match Flag */
uint32_t MF0 :1; /* Channel 0 Condition Match Flag */
);
pad(0x1c);
lword_union(CBCR, /* Break control */
uint32_t :31;
uint32_t UBDE :1; /* User Break Debugging Support Function Enable */
);
} GPACKED(4) sh7305_ubc_t;
#define SH7305_UBC (*(sh7305_ubc_t *)0xff200000)
#ifdef __cplusplus
}
#endif
#endif /* GINT_MPU_UBC */

58
include/gint/ubc.h Normal file
View file

@ -0,0 +1,58 @@
//---
// gint:ubc - User Break Controller driver
//---
#ifndef GINT_UBC
#define GINT_UBC
#ifdef __cplusplus
extern "C" {
#endif
#include <gint/gdb.h>
/* Read and write the DBR register */
void ubc_setDBR(void* DBR);
void* ubc_getDBR(void);
/* ubc_dbh(): Low level UBC debug handler
The handler will backup the current CPU state and call ubc_debug_handler(). */
void ubc_dbh(void);
/* ubc_debug_handler(): C level UBC debug handler
The execution will be redirected to the handler set by the user or the break
will be ignored in case no handler has been set. */
void ubc_debug_handler(gdb_cpu_state_t* cpu_state);
/* ubc_set_debug_handler(): Set user debug handler
Set a custom debug handler that will be called when a break condition from
the UBC is reached. */
void ubc_set_debug_handler(void (*h)(gdb_cpu_state_t*));
/* ubc_dbh_lock: Lock set by ubc_dbh() when a UBC break is currently being
handled. */
extern uint8_t ubc_dbh_lock;
/* UBC Breakpoint types */
typedef enum {
UBC_BREAK_BEFORE, /* Break before the instruction is executed */
UBC_BREAK_AFTER, /* Break after the instruction is executed :
at this point PC will point to the next instruction. */
} ubc_break_mode_t;
/* ubc_set_breakpoint(): Set a breakpoint in a UBC channel and enable it
Return false when an invalid channel number is provided, true if the
breakpoint was correctly set up. */
bool ubc_set_breakpoint(int channel, void* break_address, ubc_break_mode_t break_mode);
/* ubc_get_break_address(): Get a breakpoint address if it's enabled
If the channel is disabled the function will return false and *break_address
will not be updated. */
bool ubc_get_break_address(int channel, void** break_address);
/* ubc_disable_channel(): Disable a UBC channel
Return true on success. If an invalid channel number is provided, it will
return false. */
bool ubc_disable_channel(int channel);
#ifdef __cplusplus
}
#endif
#endif /* GINT_UBC */

View file

@ -517,6 +517,14 @@ uint16_t usb_dc_string(uint16_t const *literal, size_t len);
This is mostly used by the driver to answer GET_DESCRIPTOR requests. */
usb_dc_string_t *usb_dc_string_get(uint16_t id);
//---
// USB interrupts
//---
/* usb_interrupt_context: Context of the function interrupted by a USB interrupt
The pointer is set back to NULL when the interrupt is finished being handled. */
extern gint_inth_callback_context_t* usb_interrupt_context;
#ifdef __cplusplus
}
#endif

644
src/gdb/gdb.c Normal file
View file

@ -0,0 +1,644 @@
#include <gint/cpu.h>
#include <gint/exc.h>
#include <gint/gdb.h>
#include <gint/ubc.h>
#include <gint/usb-ff-bulk.h>
#include <gint/usb.h>
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static void gdb_hexlify(char* output_string, const uint8_t* input_buffer, size_t input_size)
{
const char* hex = "0123456789ABCDEF";
for (size_t i = 0; i < input_size; i++) {
uint8_t byte = input_buffer[i];
output_string[i*2 + 0] = hex[(byte & 0xF0) >> 4];
output_string[i*2 + 1] = hex[byte & 0x0F];
}
}
// TODO : bug in fxlibc ? strtoul doesn't support uppercase
static uint32_t gdb_unhexlify_sized(const char* input_string, size_t input_length)
{
uint32_t ret = 0;
for (size_t i = 0; i < input_length; i++) {
uint8_t nibble_hex = tolower(input_string[i]);
uint8_t nibble = nibble_hex >= 'a' && nibble_hex <= 'f' ? nibble_hex - 'a' + 10 :
nibble_hex >= '0' && nibble_hex <= '9' ? nibble_hex - '0' : 0;
ret = (ret << 4) | nibble;
}
return ret;
}
static uint32_t gdb_unhexlify(const char* input_string)
{
return gdb_unhexlify_sized(input_string, strlen(input_string));
}
static enum {
GDB_STATE_STOPPED,
GDB_STATE_FIRST_BREAK,
GDB_STATE_STARTED,
} GPACKEDENUM gdb_state = GDB_STATE_STOPPED;
static void gdb_send(const char *data, size_t size)
{
usb_fxlink_header_t header;
usb_fxlink_fill_header(&header, "gdb", "remote", size);
int pipe = usb_ff_bulk_output();
usb_write_sync(pipe, &header, sizeof(header), false);
usb_write_sync(pipe, data, size, false);
usb_commit_sync(pipe);
}
static char *gdb_recv_buffer = NULL;
static const size_t gdb_recv_buffer_capacity = 256;
static size_t gdb_recv_buffer_size = 0;
static ssize_t gdb_recv(char *buffer, size_t buffer_size)
{
if (gdb_recv_buffer_size >= buffer_size) {
memcpy(buffer, gdb_recv_buffer, buffer_size);
memmove(gdb_recv_buffer, &gdb_recv_buffer[buffer_size], gdb_recv_buffer_size - buffer_size);
gdb_recv_buffer_size -= buffer_size;
return buffer_size;
}
usb_fxlink_header_t header;
while (!usb_fxlink_handle_messages(&header)) {
sleep();
}
// TODO : should we abort or find a way to gracefully shutdown the debugger ?
if (strncmp(header.application, "gdb", 16) == 0
&& strncmp(header.type, "remote", 16) == 0) {
if (header.size > gdb_recv_buffer_capacity - gdb_recv_buffer_size) {
abort();
}
usb_read_sync(usb_ff_bulk_input(), &gdb_recv_buffer[gdb_recv_buffer_size], header.size, false);
gdb_recv_buffer_size += header.size;
return gdb_recv(buffer, buffer_size);
} else {
abort();
}
}
static ssize_t gdb_recv_packet(char* buffer, size_t buffer_size)
{
char read_char;
// Waiting for packet start '$'
do {
if (gdb_recv(&read_char, 1) != 1) {
return -1;
}
} while (read_char != '$');
uint8_t checksum = 0;
size_t packet_len = 0;
while (true) {
if (gdb_recv(&read_char, 1) != 1) {
return -1;
}
if (read_char != '#') {
// -1 to ensure space for a NULL terminator
if (packet_len >= (buffer_size - 1)) {
return -1;
}
buffer[packet_len++] = read_char;
checksum += read_char;
} else {
break;
}
}
buffer[packet_len] = '\0';
char read_checksum_hex[3];
if (gdb_recv(read_checksum_hex, 2) != 2) {
return -1;
}
read_checksum_hex[2] = '\0';
uint8_t read_checksum = gdb_unhexlify(read_checksum_hex);
if (read_checksum != checksum) {
read_char = '-';
gdb_send(&read_char, 1);
return -1;
} else {
read_char = '+';
gdb_send(&read_char, 1);
return packet_len;
}
}
static ssize_t gdb_send_packet(const char* packet, size_t packet_length)
{
if (packet == NULL || packet_length == 0) {
// Empty packet
gdb_send("$#00", 4);
return 4;
}
size_t buffer_length = packet_length + 1 + 4;
// TODO : find if it's more efficient to malloc+copy on each packet or send 3 small fxlink messages
char* buffer = malloc(buffer_length);
uint8_t checksum = 0;
for (size_t i = 0; i < packet_length; i++) {
checksum += packet[i];
}
buffer[0] = '$';
memcpy(&buffer[1], packet, packet_length);
snprintf(&buffer[buffer_length - 4], 4, "#%02X", checksum);
// -1 to not send the NULL terminator of snprintf
gdb_send(buffer, buffer_length - 1);
free(buffer);
return buffer_length;
}
static void gdb_send_stop_reply(void)
{
gdb_send_packet("S05", 3); // SIGTRAP
}
static void gdb_handle_qXfer_packet(const char* packet, const char* data, size_t data_size)
{
char offset_hex[16] = {0}, length_hex[16] = {0};
for (size_t i = 0; i < sizeof(offset_hex); i++) {
offset_hex[i] = *(packet++); // consume offset
if (*packet == ',') break;
}
packet++; // consume ','
for (size_t i = 0; i < sizeof(length_hex); i++) {
length_hex[i] = *(packet++); // consume length
if (*packet == '\0') break;
}
size_t offset = (size_t)gdb_unhexlify(offset_hex);
size_t length = (size_t)gdb_unhexlify(length_hex);
if (offset >= data_size) {
gdb_send_packet("l", 1);
} else if (offset + length >= data_size) {
char *reply_buffer = malloc(data_size - offset + 1);
reply_buffer[0] = 'l';
memcpy(&reply_buffer[1], &data[offset], data_size - offset);
gdb_send_packet(reply_buffer, data_size - offset + 1);
free(reply_buffer);
} else {
char *reply_buffer = malloc(length + 1);
reply_buffer[0] = 'm';
memcpy(&reply_buffer[1], &data[offset], length);
gdb_send_packet(reply_buffer, length + 1);
free(reply_buffer);
}
}
/* We implement the memory-map qXfer extension to mark add-in memory as read-only
* and enforce hardware breakpoints.
* See : https://sourceware.org/gdb/onlinedocs/gdb/Memory-Map-Format.html
* https://sourceware.org/gdb/onlinedocs/gdb/Set-Breaks.html
*/
// TODO : Should we mark other regions as ROM ?
static const char gdb_memory_map_xml[] = "<?xml version=\"1.0\"?>"
"<!DOCTYPE memory-map PUBLIC \"+//IDN gnu.org//DTD GDB Memory Map V1.0//EN\" \"http://sourceware.org/gdb/gdb-memory-map.dtd\">"
"<memory-map>"
"<memory type=\"rom\" start=\"0x00300000\" length=\"0x00200000\"/>" // add-in rom
"<memory type=\"ram\" start=\"0x08100000\" length=\"0x00080000\"/>" // add-in ram
"<memory type=\"ram\" start=\"0x8c000000\" length=\"0x01000000\"/>" // fx-CG50 stack
"<memory type=\"ram\" start=\"0x88000000\" length=\"0x01000000\"/>" // Prizm stack
"</memory-map>";
static void gdb_handle_query_packet(const char* packet)
{
if (strncmp("qSupported", packet, 10) == 0) {
const char* qsupported_ans = "PacketSize=255;qXfer:memory-map:read+";
gdb_send_packet(qsupported_ans, strlen(qsupported_ans));
} else if (strncmp("qXfer:memory-map:read::", packet, 23) == 0) {
// -1 to not send the NULL terminator
gdb_handle_qXfer_packet(&packet[23], gdb_memory_map_xml, sizeof(gdb_memory_map_xml) - 1);
} else {
gdb_send_packet(NULL, 0);
}
}
static void gdb_handle_read_general_registers(gdb_cpu_state_t* cpu_state)
{
char reply_buffer[23*8];
if (!cpu_state) {
memset(reply_buffer, 'x', sizeof(reply_buffer));
memcpy(&reply_buffer[offsetof(gdb_cpu_state_t, reg.pc)*2],
"A0000000", 8); // pc needs to be set to make GDB happy
} else {
gdb_hexlify(reply_buffer, (uint8_t*)cpu_state->regs,
sizeof(cpu_state->regs));
}
gdb_send_packet(reply_buffer, sizeof(reply_buffer));
}
static void gdb_handle_read_register(gdb_cpu_state_t* cpu_state, const char* packet)
{
uint8_t register_id = gdb_unhexlify(&packet[1]);
char reply_buffer[8];
if (!cpu_state || register_id >= sizeof(cpu_state->regs)/sizeof(uint32_t)) {
memset(reply_buffer, 'x', sizeof(reply_buffer));
} else {
gdb_hexlify(reply_buffer, (uint8_t*)&cpu_state->regs[register_id],
sizeof(cpu_state->regs[register_id]));
}
gdb_send_packet(reply_buffer, sizeof(reply_buffer));
}
static void gdb_handle_write_general_registers(gdb_cpu_state_t* cpu_state, const char* packet)
{
if (!cpu_state) {
gdb_send_packet(NULL, 0);
return;
}
packet++; // consume 'G'
// Let's not handle incomplete 'G' packets as they're rarely used anyway
if (strlen(packet) != sizeof(cpu_state->regs)*2) {
gdb_send_packet(NULL, 0);
return;
}
for (size_t i = 0; i < sizeof(cpu_state->regs)/sizeof(uint32_t); i++) {
cpu_state->regs[i] = gdb_unhexlify_sized(&packet[i * sizeof(uint32_t) * 2],
sizeof(uint32_t) * 2);
}
gdb_send_packet("OK", 2);
}
static void gdb_handle_write_register(gdb_cpu_state_t* cpu_state, const char* packet)
{
if (!cpu_state) {
gdb_send_packet(NULL, 0);
return;
}
char register_id_hex[16] = {0}, value_hex[16] = {0};
packet++; // consume 'P'
for (size_t i = 0; i < sizeof(register_id_hex); i++) {
register_id_hex[i] = *(packet++); // consume register id
if (*packet == '=') break;
}
packet++; // consume '='
for (size_t i = 0; i < sizeof(value_hex); i++) {
value_hex[i] = *(packet++); // consume register value
if (*packet == '\0') break;
}
uint32_t register_id = gdb_unhexlify(register_id_hex);
uint32_t value = gdb_unhexlify(value_hex);
if (register_id >= sizeof(cpu_state->regs)/sizeof(uint32_t)) {
gdb_send_packet(NULL, 0);
} else {
cpu_state->regs[register_id] = value;
gdb_send_packet("OK", 2);
}
}
static volatile bool gdb_tlbh_enable = false;
static volatile bool gdb_tlbh_caught = false;
static void gdb_handle_read_memory(const char* packet)
{
char address_hex[16] = {0}, size_hex[16] = {0};
uint8_t* read_address;
size_t read_size;
packet++; // consume 'm'
for (size_t i = 0; i < sizeof(address_hex); i++) {
address_hex[i] = *(packet++); // consume address
if (*packet == ',') break;
}
packet++; // consume ','
for (size_t i = 0; i < sizeof(size_hex); i++) {
size_hex[i] = *(packet++); // consume size
if (*packet == '\0') break;
}
read_address = (uint8_t*) gdb_unhexlify(address_hex);
read_size = (size_t) gdb_unhexlify(size_hex);
char *reply_buffer = malloc(read_size * 2);
gdb_tlbh_enable = true;
gdb_tlbh_caught = false;
for (size_t i = 0; i < read_size && !gdb_tlbh_caught; i++) {
gdb_hexlify(&reply_buffer[i * 2], &read_address[i], 1);
}
gdb_tlbh_enable = false;
if (gdb_tlbh_caught) {
gdb_send_packet("E22", 3); // EINVAL
gdb_tlbh_caught = false;
} else {
gdb_send_packet(reply_buffer, read_size * 2);
}
free(reply_buffer);
}
static void gdb_handle_write_memory(const char* packet)
{
char address_hex[16] = {0}, size_hex[16] = {0};
uint8_t* read_address;
size_t read_size;
packet++; // consume 'M'
for (size_t i = 0; i < sizeof(address_hex); i++) {
address_hex[i] = *(packet++); // consume address
if (*packet == ',') break;
}
packet++; // consume ','
for (size_t i = 0; i < sizeof(size_hex); i++) {
size_hex[i] = *(packet++); // consume size
if (*packet == ':') break;
}
packet++; // consume ':'
read_address = (uint8_t*) gdb_unhexlify(address_hex);
read_size = (size_t) gdb_unhexlify(size_hex);
gdb_tlbh_enable = true;
gdb_tlbh_caught = false;
for (size_t i = 0; i < read_size && !gdb_tlbh_caught; i++) {
read_address[i] = (uint8_t)gdb_unhexlify_sized(&packet[i * 2], 2);
}
gdb_tlbh_enable = false;
if (gdb_tlbh_caught) {
gdb_send_packet("E22", 3); // EINVAL
gdb_tlbh_caught = false;
} else {
gdb_send_packet("OK", 2);
}
}
static bool gdb_parse_hardware_breakpoint_packet(const char* packet, void** read_address)
{
packet++; // consume 'z' or 'Z'
if (*packet != '1') { // hardware breakpoint
return false;
}
packet++; // consume '1'
packet++; // consume ','
char address_hex[16] = {0}, kind_hex[16] = {0};
for (size_t i = 0; i < sizeof(address_hex); i++) {
address_hex[i] = *(packet++); // consume address
if (*packet == ',') break;
}
packet++; // consume ','
for (size_t i = 0; i < sizeof(kind_hex); i++) {
kind_hex[i] = *(packet++); // consume kind
if (*packet == '\0' || *packet == ';') break;
}
*read_address = (void*) gdb_unhexlify(address_hex);
uint32_t read_kind = gdb_unhexlify(kind_hex);
if (read_kind != 2) { // SuperH instructions are 2 bytes long
return false;
}
return true;
}
static void gdb_handle_insert_hardware_breakpoint(const char* packet)
{
void* read_address;
if (!gdb_parse_hardware_breakpoint_packet(packet, &read_address)) {
gdb_send_packet(NULL, 0);
return;
}
void *channel0_addr, *channel1_addr;
bool channel0_used = ubc_get_break_address(0, &channel0_addr);
bool channel1_used = ubc_get_break_address(1, &channel1_addr);
/* As stated by GDB doc : "the operations should be implemented in an idempotent way."
* Thus we first check if the breakpoint is already placed in one of the UBC channel.
*/
if ((channel0_used && channel0_addr == read_address) ||
(channel1_used && channel1_addr == read_address)) {
gdb_send_packet("OK", 2);
} else if (!channel0_used) {
ubc_set_breakpoint(0, read_address, UBC_BREAK_BEFORE);
gdb_send_packet("OK", 2);
} else if (!channel1_used) {
ubc_set_breakpoint(1, read_address, UBC_BREAK_BEFORE);
gdb_send_packet("OK", 2);
} else {
/* TODO : We should find a proper way to inform GDB that we are
* limited by the number of UBC channels.
*/
gdb_send_packet(NULL, 0);
}
}
static void gdb_handle_remove_hardware_breakpoint(const char* packet)
{
void* read_address;
if (!gdb_parse_hardware_breakpoint_packet(packet, &read_address)) {
gdb_send_packet(NULL, 0);
return;
}
void *channel0_addr, *channel1_addr;
bool channel0_used = ubc_get_break_address(0, &channel0_addr);
bool channel1_used = ubc_get_break_address(1, &channel1_addr);
if (channel0_used && channel0_addr == read_address) {
ubc_disable_channel(0);
}
if (channel1_used && channel1_addr == read_address) {
ubc_disable_channel(1);
}
gdb_send_packet("OK", 2);
}
static struct {
bool single_stepped;
bool channel0_used;
bool channel1_used;
void* channel0_addr;
void* channel1_addr;
} gdb_single_step_backup = { false };
static void gdb_handle_single_step(uint32_t pc, ubc_break_mode_t break_mode)
{
gdb_single_step_backup.channel0_used = ubc_get_break_address(0, &gdb_single_step_backup.channel0_addr);
gdb_single_step_backup.channel1_used = ubc_get_break_address(1, &gdb_single_step_backup.channel1_addr);
ubc_disable_channel(0);
ubc_set_breakpoint(1, (void*)pc, break_mode);
gdb_single_step_backup.single_stepped = true;
}
void gdb_main(gdb_cpu_state_t* cpu_state)
{
if (gdb_single_step_backup.single_stepped) {
if (gdb_single_step_backup.channel0_used) {
ubc_set_breakpoint(0, gdb_single_step_backup.channel0_addr, UBC_BREAK_BEFORE);
} else {
ubc_disable_channel(0);
}
if (gdb_single_step_backup.channel1_used) {
ubc_set_breakpoint(1, gdb_single_step_backup.channel1_addr, UBC_BREAK_BEFORE);
} else {
ubc_disable_channel(1);
}
gdb_single_step_backup.single_stepped = false;
}
if (cpu_state != NULL) {
gdb_send_stop_reply();
}
while (1) {
char packet_buffer[256];
ssize_t packet_size = gdb_recv_packet(packet_buffer, sizeof(packet_buffer));
if (packet_size <= 0) {
// TODO : Should we break or log on recv error ?
continue;
}
switch (packet_buffer[0]) {
case '?': // Halt reason
gdb_send_stop_reply();
break;
case 'q':
gdb_handle_query_packet(packet_buffer);
break;
case 'g':
gdb_handle_read_general_registers(cpu_state);
break;
case 'p':
gdb_handle_read_register(cpu_state, packet_buffer);
break;
case 'm':
gdb_handle_read_memory(packet_buffer);
break;
case 'G':
gdb_handle_write_general_registers(cpu_state, packet_buffer);
break;
case 'P':
gdb_handle_write_register(cpu_state, packet_buffer);
break;
case 'M':
gdb_handle_write_memory(packet_buffer);
break;
case 'k': // Kill request
abort();
return;
case 'Z':
gdb_handle_insert_hardware_breakpoint(packet_buffer);
break;
case 'z':
gdb_handle_remove_hardware_breakpoint(packet_buffer);
break;
case 's':
gdb_handle_single_step(cpu_state->reg.pc, UBC_BREAK_AFTER);
return;
case 'c': // Continue
return;
default: // Unsupported packet
gdb_send_packet(NULL, 0);
break;
}
}
}
static void gdb_notifier_function(void)
{
// We ignore fxlink notifications when we're already inside GDB code.
if (ubc_dbh_lock || gdb_state != GDB_STATE_STARTED)
return;
// We make sure we are called during a USB interrupt.
if (usb_interrupt_context == NULL)
return;
// And we make sure an other step break is not already set up.
if (gdb_single_step_backup.single_stepped)
return;
gdb_handle_single_step(usb_interrupt_context->spc, UBC_BREAK_AFTER);
}
static int gdb_panic_handler(uint32_t code)
{
// If we are currently expecting to handle TLB misses
if (gdb_tlbh_enable) {
// We only handle TLB miss reads (0x040) and writes (0x060)
if (code != 0x040 && code != 0x060)
return 1;
gdb_tlbh_caught = true;
// We skip the offending instruction and continue
gint_exc_skip(1);
return 0;
}
// If we are in user code, let's break
else if (!ubc_dbh_lock && gdb_state == GDB_STATE_STARTED) {
// We make sure an other step break is not already set up
if (gdb_single_step_backup.single_stepped)
return 1;
uint32_t spc;
__asm__("stc spc, %0" : "=r"(spc));
gdb_handle_single_step(spc, UBC_BREAK_BEFORE);
return 0;
}
return 1;
}
int gdb_start(void)
{
if (gdb_state != GDB_STATE_STOPPED) {
return GDB_ALREADY_STARTED;
}
if (usb_is_open()) {
return GDB_NO_INTERFACE;
}
if (!gdb_recv_buffer) {
gdb_recv_buffer = malloc(gdb_recv_buffer_capacity);
}
usb_interface_t const *interfaces[] = { &usb_ff_bulk, NULL };
if (usb_open(interfaces, GINT_CALL_NULL) < 0) {
return GDB_USB_ERROR;
}
usb_open_wait();
usb_fxlink_set_notifier(gdb_notifier_function);
// TODO : Should we detect if other panic or debug handlers are setup ?
gint_exc_catch(gdb_panic_handler);
ubc_set_debug_handler(gdb_main);
gdb_state = GDB_STATE_FIRST_BREAK;
gdb_main(NULL);
gdb_state = GDB_STATE_STARTED;
return 0;
}

View file

@ -79,6 +79,7 @@ GNORETURN static void gint_default_panic(GUNUSED uint32_t code)
if(code == 0x1040) name = "Add-in too large";
if(code == 0x1060) name = "Memory init failed";
if(code == 0x1080) name = "Stack overflow";
if(code == 0x10a0) name = "UBC in bank 1 code";
if(name[0]) dtext(1, 9, name);
else dprint(1, 9, "%03x", code);
@ -119,6 +120,7 @@ GNORETURN static void gint_default_panic(GUNUSED uint32_t code)
if(code == 0x1040) name = "Add-in not fully mapped (too large)";
if(code == 0x1060) name = "Memory initialization failed (heap)";
if(code == 0x1080) name = "Stack overflow during world switch";
if(code == 0x10a0) name = "UBC break in register bank 1 code";
dprint(6, 25, "%03x %s", code, name);

View file

@ -210,11 +210,15 @@ _gint_inth_callback_reloc:
stc.l r7_bank, @-r15
stc.l spc, @-r15
stc.l ssr, @-r15
/* We backup the address of the gint_inth_callback_context_t built on
the stack to the user bank. */
ldc r15, r4_bank
stc.l sr, @-r15
/* Save some values to user bank; once we enable interrupts, the kernel
bank might be overwritten at any moment. */
ldc r4, r0_bank
ldc r4, r3_bank
/* Enable interrupts and go back to user bank. On SH4, SR.IMASK is set
to the level of the current interrupt, which makes sure we can only
@ -244,15 +248,25 @@ _gint_inth_callback_reloc:
.load_sr:
ldc r1, sr
/* We are now in the user bank with r0 set. Perform the call. We want
/* We are now in the user bank with r3 set. Perform the call. We want
to forward the return value to kernel bank, but this bank can be
changed at any moment since interrupts are enabled. */
sts.l pr, @-r15
mov.l @(4, r0), r4
mov.l @(8, r0), r5
mov.l @(12, r0), r6
mov.l @(16, r0), r7
mov.l @r0, r0
/* We only set r4 to the value of the first argument in the gint_call_t
if the address is even (i.e. GINT_CALL_FLAG() was not used). */
mov.l @r3, r0
tst #1, r0
bf .do_not_set_r4
mov.l @(4, r3), r4
.do_not_set_r4:
/* And we make sure to realign the address in case it was odd. */
mov #0xfe, r2
and r2, r0
mov.l @(8, r3), r5
mov.l @(12, r3), r6
mov.l @(16, r3), r7
jsr @r0
nop
lds.l @r15+, pr

127
src/ubc/ubc.S Normal file
View file

@ -0,0 +1,127 @@
.global _ubc_setDBR
.global _ubc_getDBR
.text
_ubc_setDBR:
ldc r4, dbr
rts
nop
_ubc_getDBR:
stc dbr, r0
rts
nop
.global _ubc_dbh
_ubc_dbh:
/* We don't support breaking in a context where register bank 1 is used */
stc ssr, r0
mov.l .sr_rb1_mask, r1
tst r0, r1
bf .dbh_panic
/* We backup registers in the correct order to build gdb_cpu_state_t */
stc.l ssr, @-r15
sts.l macl, @-r15
sts.l mach, @-r15
stc.l vbr, @-r15
stc.l gbr, @-r15
sts.l pr, @-r15
stc.l spc, @-r15
stc.l sgr, @-r15
mov.l r14, @-r15
mov.l r13, @-r15
mov.l r12, @-r15
mov.l r11, @-r15
mov.l r10, @-r15
mov.l r9, @-r15
mov.l r8, @-r15
stc.l R7_BANK, @-r15
stc.l R6_BANK, @-r15
stc.l R5_BANK, @-r15
stc.l R4_BANK, @-r15
stc.l R3_BANK, @-r15
stc.l R2_BANK, @-r15
stc.l R1_BANK, @-r15
stc.l R0_BANK, @-r15
/* We set the ubc_dbh_lock before enabling interrupts */
mov.l .ubc_dbh_lock, r0
mov #1, r1
mov.b r1, @r0
/* Enable interrupts and switch register bank
Original SR is kept in r8 */
stc sr, r8
mov r8, r1
mov.l .sr_mask, r0
and r0, r1
ldc r1, sr
mov r15, r4
mov.l .handler, r0
jsr @r0
nop
/* Restore original SR to access the correct register bank */
ldc r8, sr
/* We can release the ubc_dbh_lock now that interrupts have been
disabled */
mov.l .ubc_dbh_lock, r0
mov #0, r1
mov.b r1, @r0
ldc.l @r15+, R0_BANK
ldc.l @r15+, R1_BANK
ldc.l @r15+, R2_BANK
ldc.l @r15+, R3_BANK
ldc.l @r15+, R4_BANK
ldc.l @r15+, R5_BANK
ldc.l @r15+, R6_BANK
ldc.l @r15+, R7_BANK
mov.l @r15+, r8
mov.l @r15+, r9
mov.l @r15+, r10
mov.l @r15+, r11
mov.l @r15+, r12
mov.l @r15+, r13
mov.l @r15+, r14
ldc.l @r15+, sgr
ldc.l @r15+, spc
lds.l @r15+, pr
ldc.l @r15+, gbr
ldc.l @r15+, vbr
lds.l @r15+, mach
lds.l @r15+, macl
ldc.l @r15+, ssr
rte
nop
.dbh_panic:
stc sr, r1
mov.l .sr_mask, r0
and r0, r1
ldc r1, sr
mov.l .panic_code, r4
mov.l .panic, r0
mov.l @r0, r0
jmp @r0
nop
.align 4
.handler: .long _ubc_debug_handler
.ubc_dbh_lock: .long _ubc_dbh_lock
.panic_code: .long 0x10a0
.panic: .long _gint_exc_panic
.sr_rb1_mask: .long (1 << 29)
.sr_mask: .long ~0x300000f0 /* IMASK = 0 : mask no interrupts
BL = 0 : do not block interrupts
RB = 0 : use register BANK0
*/
.data
.global _ubc_dbh_lock
_ubc_dbh_lock: .byte 0

131
src/ubc/ubc.c Normal file
View file

@ -0,0 +1,131 @@
#include <gint/drivers.h>
#include <gint/exc.h>
#include <gint/gdb.h>
#include <gint/mpu/power.h>
#include <gint/mpu/ubc.h>
#include <gint/ubc.h>
#define UBC SH7305_UBC
#define POWER SH7305_POWER
static bool hpowered(void)
{
return POWER.MSTPCR0.UDB == 0;
}
static void hpoweron(void)
{
// Power on the UBC via MSTPCR0
POWER.MSTPCR0.UDB = 0;
// Set the DBR register
ubc_setDBR(ubc_dbh);
// Disable break channel 0 and 1
UBC.CBR0.CE = 0;
UBC.CBR1.CE = 0;
// Enable user break debugging support (i.e. usage of the DBR register)
UBC.CBCR.UBDE = 1;
}
static void hpoweroff(void)
{
POWER.MSTPCR0.UDB = 1;
UBC.CBR0.CE = 0;
UBC.CBR1.CE = 0;
}
#define UBC_BREAK_CHANNEL(CBR, CRR, CAR, CAMR) do { \
UBC.CBR.MFE = 0; /* Don't include Match Flag in match condition */ \
UBC.CBR.AIE = 0; /* Don't include ASID check in match condition */ \
UBC.CBR.MFI = 0; /* Default value of MFI is reserved, make it legal */ \
UBC.CBR.SZ = 0; /* Match on any operand size */ \
UBC.CBR.CD = 0; /* Match on operand bus access */ \
UBC.CBR.ID = 1; /* Match on instruction fetch */ \
UBC.CBR.RW = 1; /* Match on read cycle */ \
\
UBC.CRR.PCB = pcb; /* Set PC break {before,after} instruction execution */ \
UBC.CRR.BIE = 1; /* Break when channel matches */ \
\
UBC.CAR = (uint32_t)break_address; /* Match address */ \
UBC.CAMR = 0; /* Match mask (0 bits are included) */ \
UBC.CBR.CE = 1; /* Enable channel */ \
} while (0)
bool ubc_set_breakpoint(int channel, void* break_address, ubc_break_mode_t break_mode)
{
if (!hpowered())
hpoweron();
uint32_t pcb = break_mode == UBC_BREAK_AFTER ? 1 : 0;
if (channel == 0) {
UBC_BREAK_CHANNEL(CBR0, CRR0, CAR0, CAMR0);
return true;
} else if (channel == 1) {
UBC_BREAK_CHANNEL(CBR1, CRR1, CAR1, CAMR1);
return true;
} else {
return false;
}
}
#undef UBC_BREAK_CHANNEL
bool ubc_get_break_address(int channel, void** break_address)
{
if (!hpowered())
hpoweron();
if (channel == 0 && UBC.CBR0.CE) {
*break_address = (void*) UBC.CAR0;
return true;
} else if (channel == 1 && UBC.CBR1.CE) {
*break_address = (void*) UBC.CAR1;
return true;
} else {
return false;
}
}
bool ubc_disable_channel(int channel)
{
if (!hpowered())
hpoweron();
if (channel == 0) {
UBC.CBR0.CE = 0;
return true;
} else if (channel == 1) {
UBC.CBR1.CE = 0;
return true;
} else {
return false;
}
}
static void (*ubc_application_debug_handler)(gdb_cpu_state_t*) = NULL;
void ubc_debug_handler(gdb_cpu_state_t* cpu_state)
{
// Clear match flags
UBC.CCMFR.lword = 0;
if (ubc_application_debug_handler != NULL) {
ubc_application_debug_handler(cpu_state);
}
// For now we will ignore breaks when no debug handler is set
// TODO : Should we log or panic when no debug handler is set ?
}
// TODO : Should we use the struct designed for GDB or make it more generic ?
void ubc_set_debug_handler(void (*h)(gdb_cpu_state_t*)) {
ubc_application_debug_handler = h;
}
gint_driver_t drv_ubc = {
.name = "UBC",
.hpowered = hpowered,
.hpoweron = hpoweron,
.hpoweroff = hpoweroff,
.flags = GINT_DRV_SHARED,
};
GINT_DECLARE_DRIVER(30, drv_ubc);

View file

@ -12,7 +12,7 @@
#define USB SH7305_USB
static void usb_interrupt_handler(void);
static void usb_interrupt_handler(gint_inth_callback_context_t* interrupt_context);
/* Shorthand to clear a bit in INTSTS0 */
#define INTSTS0_clear(field_name) { \
@ -213,7 +213,7 @@ int usb_open(usb_interface_t const **interfaces, gint_call_t callback)
USB.NRDYSTS = 0x0000;
USB.BEMPSTS = 0x0000;
intc_handler_function(0xa20, GINT_CALL(usb_interrupt_handler));
intc_handler_function(0xa20, GINT_CALL_FLAG(usb_interrupt_handler));
intc_priority(INTC_USB, 8);
/* Pull D+ up to 3.3V, notifying connection when possible. Read
@ -251,8 +251,12 @@ void usb_close(void)
// Userspace interrupt handler
//---
static void usb_interrupt_handler(void)
gint_inth_callback_context_t* usb_interrupt_context;
static void usb_interrupt_handler(gint_inth_callback_context_t* interrupt_context)
{
usb_interrupt_context = interrupt_context;
GUNUSED static char const * const device_st[] = {
"powered", "default", "address", "configured",
"suspended-powered", "suspended-default", "suspended-address",
@ -315,6 +319,8 @@ static void usb_interrupt_handler(void)
/* Restore PIPESEL which can have been used for transfers */
USB.PIPESEL.word = pipesel;
usb_interrupt_context = NULL;
}
//---