/* SPDX-License-Identifier: Unlicense */ #define OPTPARSE_IMPLEMENTATION #define OPTPARSE_API static #ifdef __GNUC__ #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wold-style-cast" #pragma GCC diagnostic ignored "-Wshadow" #endif #include "optparse/optparse.h" #ifdef __GNUC__ #pragma GCC diagnostic pop #endif #include "chardev.hpp" #include "bus.hpp" #include "graphics.hpp" #include "vdp.hpp" #include "io.hpp" #include "m68k_debugging.hpp" #include "gdbremote_parser.hpp" #include "utils.hpp" #include "musashi-m68k/m68k.h" #include "musashi-m68k/m68kcpu.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if !defined(DEBUG_TRACE_INSTRUCTIONS) # define DEBUG_TRACE_INSTRUCTIONS 0 #endif #if !defined(DEBUG_TRACE_GDB_REMOTE) # define DEBUG_TRACE_GDB_REMOTE 0 #endif #define MESSAGE_BUFFER_SIZE 1024 unsigned char g_rom[ROM_SIZE] = {}; unsigned char g_ram[RAM_SIZE] = {}; unsigned char g_sound_ram[SOUND_RAM_SIZE] = {}; IO g_io1(IO1_START); unsigned char g_psg[PSG_SIZE] = {}; VDP g_vdp(VDP_START); std::vector code_bkpts{}, read_bkpts{}, write_bkpts{}, access_bkpts{}; uint64_t g_cycles_counter = 0; bool quit{}; template struct Backtrace { static_assert(S > 0, "Backtrace size cannot be zero"); void Push(T value) { head = (head + 1) % S; buffer[head] = value; } void Normalize(void) { constexpr auto last_offset = S - 1; for (; head < last_offset; head++) { const T tmp = buffer[last_offset]; memmove(&buffer[1], &buffer[0], (S - 1) * sizeof(buffer[0])); buffer[0] = tmp; } } static constexpr size_t Size(void) { return S; } T buffer[S]{}; size_t head{}; }; static constexpr struct timespec kOneMillisecond{0, 1000000}; static constexpr struct timespec kIdleSleepTime{0, 10000000}; static char msg_buf[MESSAGE_BUFFER_SIZE]; static M68KDebuggingControl g_m68k_debug{}; static Backtrace g_pc_backtrace{}; static int set_socket_reuseaddr(const int socket_fd) { const int val = 1; const socklen_t len = sizeof(val); return setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, (void*)&val, len); } static inline int SetNonblock(const int fd) { const int flags = fcntl(fd, F_GETFL); if (flags == -1) { return errno; } const int ret = fcntl(fd, F_SETFL, flags | O_NONBLOCK); if (ret == -1) { return errno; } return 0; } static int setup_socket( const char* host, uint16_t port, char* addrstr, size_t addrstr_size) { struct addrinfo hints{}, * result{}; hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; hints.ai_flags = AI_PASSIVE; char port_str[6]{}; snprintf(port_str, sizeof(port_str), "%u", port); const int gai_ret = getaddrinfo(host, port_str, &hints, &result); if (gai_ret != 0) { fprintf(stderr, "getaddrinfo(%s): %s\n", host, gai_strerror(gai_ret)); return -1; } int socket_fd = -1; for (struct addrinfo* rp = result; rp != nullptr; rp = rp->ai_next) { socket_fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); if (socket_fd == -1) continue; // Set O_NONBLOCK for the socket const int ret = SetNonblock(socket_fd); if (ret) { fprintf(stderr, "fcntl(socket_fd, F_SETFL O_NONBLOCK): %s\n", strerror(ret)); close(socket_fd); socket_fd = -1; break; } // Make TCP port reusable in case of kill or crash. // Deliberate explanation: https://stackoverflow.com/a/3233022 if (set_socket_reuseaddr(socket_fd) == -1) { perror("setsockopt failed"); close(socket_fd); socket_fd = -1; break; } if (0 == bind(socket_fd, rp->ai_addr, rp->ai_addrlen)) { void* addr_ptr; if (rp->ai_addr->sa_family == AF_INET) { addr_ptr = reinterpret_cast( &((struct sockaddr_in*)rp->ai_addr)->sin_addr); } else if (rp->ai_addr->sa_family == AF_INET6) { addr_ptr = reinterpret_cast( &((struct sockaddr_in6*)rp->ai_addr)->sin6_addr); } else { assert(false); } inet_ntop(rp->ai_addr->sa_family, addr_ptr, addrstr, addrstr_size); addrstr[addrstr_size - 1] = '\0'; break; } fprintf(stderr, "Binding to %s:%u failed\n", addrstr, port); close(socket_fd); socket_fd = -1; } freeaddrinfo(result); if (socket_fd == -1) { fprintf(stderr, "Could not bind or create socket: " "getaddrinfo did not give any suitable entry\n"); return -1; } fprintf(stderr, "Binding to %s:%u done\n", addrstr, port); return socket_fd; } static inline bool IsReset(const std::string& command) { if (command == "reset") return true; if (command == "reset halt") return true; if (command == "system_reset") return true; return false; } static std::string CreateResponse( M68KDebuggingControl& m68k_debug, const GDBRemote::Packet& packet) { using namespace GDBRemote; switch (packet.type) { case PacketType::kNone: break; case PacketType::kQuerySupported: return "PacketSize=400000" // PacketSize number is in HEX // These commented-out options are from OpenOCD // TODO ";qXfer:memory-map:read+" // TODO ";qXfer:features:read+" // TODO ";qXfer:threads:read+" ";QStartNoAckMode+" // It makes GDB shit fast! ";vContSupported+" // We support "vCont?" packet ; case PacketType::kQueryHaltReason: return "S05"; // TODO real reason case PacketType::kQueryC: break; case PacketType::kQueryFThreadInfo: break; case PacketType::kQuerySThreadInfo: break; case PacketType::kQueryRTOSThreadInfo: break; case PacketType::kQueryAttached: return "1"; case PacketType::kQueryTraceStatus: break; case PacketType::kQueryRcmd: { const auto * const packet_data = static_cast(packet.data.get()); if (IsReset(packet_data->data)) { m68k_pulse_reset(); // I don't want to skip reset cycles here, because GDB does not // re-read registers every time, and if we skip reset cycles // here, the consequent "stepi" in gdb will end up on second // instruction of reset handler. If we don't skip reset cycles, // the first "stepi" after "monitor reset" will land us on the // first instruction in reset handler - I'd prefer this exact // behavior for now. return "OK"; } } break; case PacketType::kQStartNoAckMode: m68k_debug.SetNoAckMode(true); return "OK"; case PacketType::kSetThreadForCont: return "OK"; case PacketType::kSetThreadForOps: break; case PacketType::kVMustReplyEmpty: break; case PacketType::kEnableExtendedMode: break; case PacketType::kInterrupt: m68k_debug.RaiseBreakpoint(); return "S05"; // TODO real reason case PacketType::kContinue: m68k_debug.SetRunning(true); return "OK"; case PacketType::kContinueAskSupported: return "vCont:c:s"; case PacketType::kReadGeneralRegisters: { const M68KCPUState state = m68k_debug.GetCPUState(); std::string result{}; for (size_t i = 0; i < state.registers_count; i++) { constexpr size_t value_size = std::string_view("12345678").length(); char value[value_size + 1]{}; const int ret = snprintf(value, value_size + 1, "%08x", state.registers[i]); (void) ret; assert(ret == value_size); result += std::string(value, value_size); } return result; } case PacketType::kWriteGeneralRegisters: { const auto * const data = static_cast(packet.data.get()); const auto& registers = data->registers; for (size_t i = 0; i < registers.size(); i++) { m68k_debug.SetRegister(static_cast(i), registers[i]); } return "OK"; } break; case PacketType::kReadRegister: { const auto * const data = static_cast(packet.data.get()); const auto reg_id = static_cast(data->reg_id); if (reg_id >= M68KRegister::kRegistersCount) { return "E00"; } const uint32_t value = m68k_debug.GetRegister(reg_id); constexpr size_t value_size = std::string_view("12345678").length(); char value_str[value_size + 1]{}; const int ret = snprintf(value_str, value_size + 1, "%08x", value); (void) ret; assert(ret == value_size); return std::string(value_str, value_size); } break; case PacketType::kWriteRegister: { const auto * const data = static_cast(packet.data.get()); const auto reg_id = static_cast(data->reg_id); const auto value = data->value; if (reg_id >= M68KRegister::kRegistersCount) { return "E00"; } m68k_debug.SetRegister(reg_id, value); return "OK"; } break; case PacketType::kReadMemory: { const auto * const data = static_cast(packet.data.get()); const uint32_t offset = data->offset; const uint32_t length = data->length; auto ret_data = std::string(length * 2, '\0'); for (uint32_t i = 0; i < length; i++) { const uint8_t byte = m68k_debug.Read8(i + offset); Utils::ConvertByteToHex(byte, &ret_data[i*2]); } return ret_data; } case PacketType::kWriteMemory: { const auto * const packet_data = static_cast(packet.data.get()); const uint32_t offset = packet_data->offset; const uint32_t length = packet_data->length; const auto write_data = packet_data->data; for (uint32_t i = 0; i < length; i++) { m68k_debug.Write8(i + offset, write_data[i]); } return "OK"; } case PacketType::kStep: g_cycles_counter += m68k_execute(1); return "S05"; // TODO real reason case PacketType::kSetBreakpoint: { const auto * const bkpt_data = static_cast(packet.data.get()); m68k_debug.SetBreakpoint(bkpt_data->type, bkpt_data->offset, bkpt_data->length); return "OK"; } case PacketType::kDeleteBreakpoint: { const auto * const bkpt_data = static_cast(packet.data.get()); m68k_debug.RemoveBreakpoint(bkpt_data->type, bkpt_data->offset, bkpt_data->length); return "OK"; } } return ""; } static void exit_error(const char* fmt, ...) { va_list args; va_start(args, fmt); vfprintf(stderr, fmt, args); va_end(args); fprintf(stderr, "\n"); exit(EXIT_FAILURE); } /* Called when the CPU pulses the RESET line */ void m68k_reset_callback(void) { g_vdp.Reset(); } /* Called when the CPU acknowledges an interrupt */ int m68k_irq_ack(int level) { (void) level; CPU_INT_LEVEL = 0; return M68K_INT_ACK_AUTOVECTOR; } static void make_hex(char* buff, unsigned int pc, unsigned int length) { char* ptr = buff; for (;length>0;length -= 2) { sprintf(ptr, "%04x", m68k_read_disassembler_16(pc)); pc += 2; ptr += 4; if (length > 2) *ptr++ = ' '; } } static void PrintInstructionTrace(const uint32_t pc) { char buff[100]; const unsigned int instr_size = m68k_disassemble(buff, pc, M68K_CPU_TYPE_68000); char buff2[100]; make_hex(buff2, pc, instr_size); printf(" %08x: %-20s: %s\n", pc, buff2, buff); fflush(stdout); } // TODO m68k_set_illg_instr_callback for true software breakpoint "4e4f" int m68k_instr_callback(const int pc) { g_pc_backtrace.Push(pc); for (size_t bi = 0; bi < code_bkpts.size(); bi++) { if (code_bkpts[bi].offset == static_cast(pc)) { g_m68k_debug.RaiseBreakpoint(); m68k_end_timeslice(); printf("Breakpoint @ 0x%08x\n", pc); g_pc_backtrace.Normalize(); printf("PC backtrace (size=%zu):\n", g_pc_backtrace.Size()); for (size_t i = 0; i < g_pc_backtrace.Size(); i++) { PrintInstructionTrace(g_pc_backtrace.buffer[i]); } return 1; } } if (DEBUG_TRACE_INSTRUCTIONS) { PrintInstructionTrace(pc); } return 0; } void m68k_breakpoint_callback(void) { g_pc_backtrace.Normalize(); printf("PC backtrace (size=%zu):\n", g_pc_backtrace.Size()); for (size_t i = 0; i < g_pc_backtrace.Size(); i++) { PrintInstructionTrace(g_pc_backtrace.buffer[i]); } g_m68k_debug.RaiseBreakpoint(); m68k_end_timeslice(); } void ParseAndReact( const int conn_fd, GDBRemote::ExchangeContext& exchange_ctx, M68KDebuggingControl& m68k_debug, const char* const msg_data, const size_t msg_data_len) { for (size_t i = 0; i < static_cast(msg_data_len); i++) { const auto res = exchange_ctx.Consume(msg_data[i]); if (res == nullptr) continue; if (DEBUG_TRACE_GDB_REMOTE && res->packet.length() > 0) { printf("<- \"%s\"\n", exchange_ctx.GetLastPacket().c_str()); const auto packet = GDBRemote::Packet::Parse(res->packet); printf( " Packet type: \"%s\"\n", GDBRemote::Packet::PacketTypeToString(packet.type)); } if (res->ack.length() > 0 && !m68k_debug.IsNoAckModeEnabled()) { if (DEBUG_TRACE_GDB_REMOTE) printf("-> \"%s\"\n", res->ack.c_str()); if (send(conn_fd, &res->ack[0], res->ack.length(), 0) == -1) perror("Send failed (ack/nak)"); } if (res->packet.length() > 0) { const auto packet = GDBRemote::Packet::Parse(res->packet); const auto response = exchange_ctx.WrapDataToSend(CreateResponse(m68k_debug, packet)); if (DEBUG_TRACE_GDB_REMOTE) printf("-> \"%s\"\n", response.c_str()); if (send(conn_fd, &response[0], response.length(), 0) == -1) perror("Send failed (response)"); } } } static void RunSingleVideoCycle(M68KDebuggingControl& m68k_debug, Graphics& graphics) { do { g_cycles_counter += m68k_execute(4000); if (m68k_debug.HasBreakpoint()) { return; } } while (!g_vdp.Scanline()); graphics.Render(g_vdp); g_cycles_counter += m68k_execute(400000); } static int emulator(M68KDebuggingControl& m68k_debug, Graphics& graphics, CharDev &gdb_chardev) { if (gdb_chardev.type == CharDevType::kUndefined) { while (!quit) { RunSingleVideoCycle(m68k_debug, graphics); } return EXIT_SUCCESS; } constexpr size_t addrstr_size = 160; char addrstr[addrstr_size]{}; const int port = gdb_chardev.port ? gdb_chardev.port : 3333; int socket_fd; if (gdb_chardev.path_len) { char *host = new (std::nothrow) char[gdb_chardev.path_len + 1]; assert(host); memcpy(host, gdb_chardev.path, gdb_chardev.path_len); host[gdb_chardev.path_len] = '\0'; socket_fd = setup_socket(host, port, addrstr, addrstr_size); delete [] host; } else { socket_fd = setup_socket("0.0.0.0", port, addrstr, addrstr_size); } if (socket_fd == -1) return EXIT_FAILURE; // Mark socket as listener if (listen(socket_fd, 4) == -1 ) { perror("Listen failed"); close(socket_fd); return EXIT_FAILURE; } printf("Listening TCP %s:%u\n", addrstr, port); while (!quit) { struct sockaddr client_address{}; socklen_t address_len{}; const int conn_fd = accept(socket_fd, &client_address, &address_len); if (conn_fd == -1) { gdb_chardev.fd = conn_fd; if (errno == EWOULDBLOCK) { if (m68k_debug.IsRunning()) { RunSingleVideoCycle(m68k_debug, graphics); if (m68k_debug.HasBreakpoint()) { m68k_debug.SetRunning(false); printf("ResetPendingBreakpoint\n"); m68k_debug.ResetPendingBreakpoint(); } } else { struct pollfd fds[1] = { {/*.fd = */socket_fd, /*.events = */POLLIN, /*.revents = */0}}; const int timeout_msecs = 200; if (0 == poll(fds, 1, timeout_msecs)) { graphics.ReRender(); } } continue; } perror("Accept failed"); close(socket_fd); return EXIT_FAILURE; } printf("Connection accepted\n"); // Set O_NONBLOCK for connection const int ret = SetNonblock(conn_fd); if (ret == -1) { perror("fcntl(conn_fd, F_SETFL O_NONBLOCK)"); return -1; } GDBRemote::ExchangeContext exchange_ctx{}; while (!quit) { const ssize_t read_size = recv(conn_fd, msg_buf, MESSAGE_BUFFER_SIZE, 0); const int err = errno; if (read_size > 0) { ParseAndReact(conn_fd, exchange_ctx, m68k_debug, msg_buf, read_size); continue; } else if (read_size == 0) { printf("Client disconnected\n"); break; } else if (read_size == -1 && err != EWOULDBLOCK) { perror("Recv failed"); break; } if (m68k_debug.IsRunning()) { RunSingleVideoCycle(m68k_debug, graphics); if (m68k_debug.HasBreakpoint()) { m68k_debug.SetRunning(false); printf("ResetPendingBreakpoint\n"); m68k_debug.ResetPendingBreakpoint(); const auto response = exchange_ctx.WrapDataToSend("S05"); if (DEBUG_TRACE_GDB_REMOTE) printf("-> \"%s\"\n", response.c_str()); if (send(conn_fd, &response[0], response.length(), 0) == -1) perror("Send failed (response)"); } } else if (err == EWOULDBLOCK) { struct pollfd fds[1] = { {/*.fd = */conn_fd, /*.events = */POLLIN, /*.revents = */0}}; const int timeout_msecs = 200; if (0 == poll(fds, 1, timeout_msecs)) { graphics.ReRender(); } } } close(conn_fd); gdb_chardev.fd = -1; m68k_debug.SetNoAckMode(false); } close(socket_fd); return 0; } static void sigint_handler(int sig) { printf("Signal %d received, exiting\n",sig); quit = true; } static void PrintUsage(FILE *s, const char *argv0) { // Please, keep all lines in 80 columns range when printed. fprintf(s, "Usage: %s [options] \n" "Options:\n" " -h, --help Show this message.\n" " -g, --gdb[=DEV] Accept GDB connection on DEV (default: 'tcp::3333').\n" " -S, --stop Freeze CPU at startup (use 'c' in GDB to start\n" " execution).\n" , argv0); } int main(int, char* argv[]) { struct optparse_long longopts[] = { {"help", 'h', OPTPARSE_NONE}, {"gdb", 'g', OPTPARSE_OPTIONAL}, {"stop", 'S', OPTPARSE_NONE}, {}, }; struct CharDev gdb_chardev{}; const char *program_file_name = nullptr; bool stop = false; struct optparse options; optparse_init(&options, argv); // Parse opts int option; while ((option = optparse_long(&options, longopts, nullptr)) != -1) { switch (option) { case 'h': PrintUsage(stdout, argv[0]); return EXIT_SUCCESS; break; case 'g': { const char *path = options.optarg ? options.optarg : "tcp::3333"; gdb_chardev = CharDev::Parse(path); if (gdb_chardev.type == CharDevType::kPty) { fprintf(stderr, "main: PTY chardev currently is not supported\n"); return EXIT_FAILURE; } else if (gdb_chardev.type == CharDevType::kUndefined) { fprintf(stderr, "invalid chardev specified: \"%s\"\n", path); return EXIT_FAILURE; } } break; case 'S': stop = true; printf("stop=%s\n", stop ? "true" : "false"); break; case '?': fprintf(stderr, "main: optparse_long: Error: \"%s\"\n", options.errmsg); return EXIT_FAILURE; } } // Parse input file name char *arg; while ((arg = optparse_arg(&options))) { if (program_file_name == nullptr) { program_file_name = arg; } else { fprintf(stderr, "error: too many free arguments provided\n"); return EXIT_FAILURE; } } if (program_file_name == nullptr) { fprintf(stderr, "main: Error: no program file name specified, see usage below.\n"); PrintUsage(stderr, argv[0]); return EXIT_FAILURE; } FILE* const fhandle = fopen(program_file_name, "rb"); if (fhandle == nullptr) { exit_error("Unable to open %s", argv[1]); } const size_t fread_ret = fread(g_rom, 1, ROM_SIZE, fhandle); if (fread_ret <= 0) { exit_error("Error reading %s", argv[1]); } printf("Read into ROM %zu bytes\n", fread_ret); struct sigaction sa; sa.sa_handler = sigint_handler; sigaction(SIGINT, &sa, nullptr); Graphics graphics{}; if (!graphics.IsOk()) { return EXIT_FAILURE; } m68k_init(); m68k_set_cpu_type(M68K_CPU_TYPE_68000); m68k_pulse_reset(); g_cycles_counter += m68k_execute(1); // Skip reset cycles g_m68k_debug.SetRunning(!stop); emulator(g_m68k_debug, graphics, gdb_chardev); return EXIT_SUCCESS; }