/* * Copyright (C) 2008 Stefan Hajnoczi . * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of the * License, or any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ /** * @file * * GDB stub for remote debugging * */ #include #include #include "serial.h" typedef uint32_t gdbreg_t; enum { POSIX_EINVAL = 0x1c, /* used to report bad arguments to GDB */ SIZEOF_PAYLOAD = 256, /* buffer size of GDB payload data */ DR7_CLEAR = 0x00000400, /* disable hardware breakpoints */ DR6_CLEAR = 0xffff0ff0, /* clear breakpoint status */ }; /* The register snapshot, this must be in sync with interrupt handler and the * GDB protocol. */ enum { GDBMACH_EAX, GDBMACH_ECX, GDBMACH_EDX, GDBMACH_EBX, GDBMACH_ESP, GDBMACH_EBP, GDBMACH_ESI, GDBMACH_EDI, GDBMACH_EIP, GDBMACH_EFLAGS, GDBMACH_CS, GDBMACH_SS, GDBMACH_DS, GDBMACH_ES, GDBMACH_FS, GDBMACH_GS, GDBMACH_NREGS, GDBMACH_SIZEOF_REGS = GDBMACH_NREGS * sizeof(gdbreg_t) }; /* Breakpoint types */ enum { GDBMACH_BPMEM, GDBMACH_BPHW, GDBMACH_WATCH, GDBMACH_RWATCH, GDBMACH_AWATCH, }; struct gdbstub { int exit_handler; /* leave interrupt handler */ int signo; gdbreg_t *regs; void (*parse) (struct gdbstub * stub, char ch); uint8_t cksum1; /* Buffer for payload data when parsing a packet. Once the * packet has been received, this buffer is used to hold * the reply payload. */ char buf[SIZEOF_PAYLOAD + 4]; /* $...PAYLOAD...#XX */ char *payload; /* start of payload */ int len; /* length of payload */ }; /** Hardware breakpoint, fields stored in x86 bit pattern form */ struct hwbp { int type; /* type (1=write watchpoint, 3=access watchpoint) */ unsigned long addr; /* linear address */ size_t len; /* length (0=1-byte, 1=2-byte, 3=4-byte) */ int enabled; }; static struct hwbp hwbps[4]; static gdbreg_t dr7 = DR7_CLEAR; static inline void gdbmach_set_pc(gdbreg_t * regs, gdbreg_t pc) { regs[GDBMACH_EIP] = pc; } static inline void gdbmach_set_single_step(gdbreg_t * regs, int step) { regs[GDBMACH_EFLAGS] &= ~(1 << 8); /* Trace Flag (TF) */ regs[GDBMACH_EFLAGS] |= (step << 8); } static inline void gdbmach_breakpoint(void) { __asm__ __volatile__("int $3\n"); } static struct hwbp *gdbmach_find_hwbp(int type, unsigned long addr, size_t len) { struct hwbp *available = NULL; unsigned int i; for (i = 0; i < sizeof hwbps / sizeof hwbps[0]; i++) { if (hwbps[i].type == type && hwbps[i].addr == addr && hwbps[i].len == len) { return &hwbps[i]; } if (!hwbps[i].enabled) { available = &hwbps[i]; } } return available; } static void gdbmach_commit_hwbp(struct hwbp *bp) { int regnum = bp - hwbps; /* Set breakpoint address */ switch (regnum) { case 0: __asm__ __volatile__("movl %0, %%dr0\n": :"r"(bp->addr)); break; case 1: __asm__ __volatile__("movl %0, %%dr1\n": :"r"(bp->addr)); break; case 2: __asm__ __volatile__("movl %0, %%dr2\n": :"r"(bp->addr)); break; case 3: __asm__ __volatile__("movl %0, %%dr3\n": :"r"(bp->addr)); break; } /* Set type */ dr7 &= ~(0x3 << (16 + 4 * regnum)); dr7 |= bp->type << (16 + 4 * regnum); /* Set length */ dr7 &= ~(0x3 << (18 + 4 * regnum)); dr7 |= bp->len << (18 + 4 * regnum); /* Set/clear local enable bit */ dr7 &= ~(0x3 << 2 * regnum); dr7 |= bp->enabled << 2 * regnum; } int gdbmach_set_breakpoint(int type, unsigned long addr, size_t len, int enable) { struct hwbp *bp; /* Check and convert breakpoint type to x86 type */ switch (type) { case GDBMACH_WATCH: type = 0x1; break; case GDBMACH_AWATCH: type = 0x3; break; default: return 0; /* unsupported breakpoint type */ } /* Only lengths 1, 2, and 4 are supported */ if (len != 2 && len != 4) { len = 1; } len--; /* convert to x86 breakpoint length bit pattern */ /* Set up the breakpoint */ bp = gdbmach_find_hwbp(type, addr, len); if (!bp) { return 0; /* ran out of hardware breakpoints */ } bp->type = type; bp->addr = addr; bp->len = len; bp->enabled = enable; gdbmach_commit_hwbp(bp); return 1; } static void gdbmach_disable_hwbps(void) { /* Store and clear hardware breakpoints */ __asm__ __volatile__("movl %0, %%dr7\n"::"r"(DR7_CLEAR)); } static void gdbmach_enable_hwbps(void) { /* Clear breakpoint status register */ __asm__ __volatile__("movl %0, %%dr6\n"::"r"(DR6_CLEAR)); /* Restore hardware breakpoints */ __asm__ __volatile__("movl %0, %%dr7\n"::"r"(dr7)); } /* Packet parser states */ static void gdbstub_state_new(struct gdbstub *stub, char ch); static void gdbstub_state_data(struct gdbstub *stub, char ch); static void gdbstub_state_cksum1(struct gdbstub *stub, char ch); static void gdbstub_state_cksum2(struct gdbstub *stub, char ch); static void gdbstub_state_wait_ack(struct gdbstub *stub, char ch); static void serial_write(void *buf, size_t len) { char *p = buf; while (len-- > 0) serial_putc(*p++); } static uint8_t gdbstub_from_hex_digit(char ch) { if (ch >= '0' && ch <= '9') return ch - '0'; else if (ch >= 'A' && ch <= 'F') return ch - 'A' + 0xa; else return (ch - 'a' + 0xa) & 0xf; } static uint8_t gdbstub_to_hex_digit(uint8_t b) { b &= 0xf; return (b < 0xa ? '0' : 'a' - 0xa) + b; } /* * To make reading/writing device memory atomic, we check for * 2- or 4-byte aligned operations and handle them specially. */ static void gdbstub_from_hex_buf(char *dst, char *src, int lenbytes) { if (lenbytes == 2 && ((unsigned long)dst & 0x1) == 0) { uint16_t i = gdbstub_from_hex_digit(src[2]) << 12 | gdbstub_from_hex_digit(src[3]) << 8 | gdbstub_from_hex_digit(src[0]) << 4 | gdbstub_from_hex_digit(src[1]); *(uint16_t *) dst = i; } else if (lenbytes == 4 && ((unsigned long)dst & 0x3) == 0) { uint32_t i = gdbstub_from_hex_digit(src[6]) << 28 | gdbstub_from_hex_digit(src[7]) << 24 | gdbstub_from_hex_digit(src[4]) << 20 | gdbstub_from_hex_digit(src[5]) << 16 | gdbstub_from_hex_digit(src[2]) << 12 | gdbstub_from_hex_digit(src[3]) << 8 | gdbstub_from_hex_digit(src[0]) << 4 | gdbstub_from_hex_digit(src[1]); *(uint32_t *) dst = i; } else { while (lenbytes-- > 0) { *dst++ = gdbstub_from_hex_digit(src[0]) << 4 | gdbstub_from_hex_digit(src[1]); src += 2; } } } static void gdbstub_to_hex_buf(char *dst, char *src, int lenbytes) { if (lenbytes == 2 && ((unsigned long)src & 0x1) == 0) { uint16_t i = *(uint16_t *) src; dst[0] = gdbstub_to_hex_digit(i >> 4); dst[1] = gdbstub_to_hex_digit(i); dst[2] = gdbstub_to_hex_digit(i >> 12); dst[3] = gdbstub_to_hex_digit(i >> 8); } else if (lenbytes == 4 && ((unsigned long)src & 0x3) == 0) { uint32_t i = *(uint32_t *) src; dst[0] = gdbstub_to_hex_digit(i >> 4); dst[1] = gdbstub_to_hex_digit(i); dst[2] = gdbstub_to_hex_digit(i >> 12); dst[3] = gdbstub_to_hex_digit(i >> 8); dst[4] = gdbstub_to_hex_digit(i >> 20); dst[5] = gdbstub_to_hex_digit(i >> 16); dst[6] = gdbstub_to_hex_digit(i >> 28); dst[7] = gdbstub_to_hex_digit(i >> 24); } else { while (lenbytes-- > 0) { *dst++ = gdbstub_to_hex_digit(*src >> 4); *dst++ = gdbstub_to_hex_digit(*src); src++; } } } static uint8_t gdbstub_cksum(char *data, int len) { uint8_t cksum = 0; while (len-- > 0) { cksum += (uint8_t) * data++; } return cksum; } static void gdbstub_tx_packet(struct gdbstub *stub) { uint8_t cksum = gdbstub_cksum(stub->payload, stub->len); stub->buf[0] = '$'; stub->buf[stub->len + 1] = '#'; stub->buf[stub->len + 2] = gdbstub_to_hex_digit(cksum >> 4); stub->buf[stub->len + 3] = gdbstub_to_hex_digit(cksum); serial_write(stub->buf, stub->len + 4); stub->parse = gdbstub_state_wait_ack; } /* GDB commands */ static void gdbstub_send_ok(struct gdbstub *stub) { stub->payload[0] = 'O'; stub->payload[1] = 'K'; stub->len = 2; gdbstub_tx_packet(stub); } static void gdbstub_send_num_packet(struct gdbstub *stub, char reply, int num) { stub->payload[0] = reply; stub->payload[1] = gdbstub_to_hex_digit((char)num >> 4); stub->payload[2] = gdbstub_to_hex_digit((char)num); stub->len = 3; gdbstub_tx_packet(stub); } /* Format is arg1,arg2,...,argn:data where argn are hex integers and data is not an argument */ static int gdbstub_get_packet_args(struct gdbstub *stub, unsigned long *args, int nargs, int *stop_idx) { int i; char ch = 0; int argc = 0; unsigned long val = 0; for (i = 1; i < stub->len && argc < nargs; i++) { ch = stub->payload[i]; if (ch == ':') { break; } else if (ch == ',') { args[argc++] = val; val = 0; } else { val = (val << 4) | gdbstub_from_hex_digit(ch); } } if (stop_idx) { *stop_idx = i; } if (argc < nargs) { args[argc++] = val; } return ((i == stub->len || ch == ':') && argc == nargs); } static void gdbstub_send_errno(struct gdbstub *stub, int errno) { gdbstub_send_num_packet(stub, 'E', errno); } static void gdbstub_report_signal(struct gdbstub *stub) { gdbstub_send_num_packet(stub, 'S', stub->signo); } static void gdbstub_read_regs(struct gdbstub *stub) { gdbstub_to_hex_buf(stub->payload, (char *)stub->regs, GDBMACH_SIZEOF_REGS); stub->len = GDBMACH_SIZEOF_REGS * 2; gdbstub_tx_packet(stub); } static void gdbstub_write_regs(struct gdbstub *stub) { if (stub->len != 1 + GDBMACH_SIZEOF_REGS * 2) { gdbstub_send_errno(stub, POSIX_EINVAL); return; } gdbstub_from_hex_buf((char *)stub->regs, &stub->payload[1], GDBMACH_SIZEOF_REGS); gdbstub_send_ok(stub); } static void gdbstub_read_mem(struct gdbstub *stub) { unsigned long args[2]; if (!gdbstub_get_packet_args (stub, args, sizeof args / sizeof args[0], NULL)) { gdbstub_send_errno(stub, POSIX_EINVAL); return; } args[1] = (args[1] < SIZEOF_PAYLOAD / 2) ? args[1] : SIZEOF_PAYLOAD / 2; gdbstub_to_hex_buf(stub->payload, (char *)args[0], args[1]); stub->len = args[1] * 2; gdbstub_tx_packet(stub); } static void gdbstub_write_mem(struct gdbstub *stub) { unsigned long args[2]; int colon; if (!gdbstub_get_packet_args (stub, args, sizeof args / sizeof args[0], &colon) || colon >= stub->len || stub->payload[colon] != ':' || (stub->len - colon - 1) % 2 != 0) { gdbstub_send_errno(stub, POSIX_EINVAL); return; } gdbstub_from_hex_buf((char *)args[0], &stub->payload[colon + 1], (stub->len - colon - 1) / 2); gdbstub_send_ok(stub); } static void gdbstub_continue(struct gdbstub *stub, int single_step) { gdbreg_t pc; if (stub->len > 1 && gdbstub_get_packet_args(stub, (unsigned long *)&pc, 1, NULL)) { gdbmach_set_pc(stub->regs, pc); } gdbmach_set_single_step(stub->regs, single_step); stub->exit_handler = 1; /* Reply will be sent when we hit the next breakpoint or interrupt */ } static void gdbstub_breakpoint(struct gdbstub *stub) { unsigned long args[3]; int enable = stub->payload[0] == 'Z' ? 1 : 0; if (!gdbstub_get_packet_args (stub, args, sizeof args / sizeof args[0], NULL)) { gdbstub_send_errno(stub, POSIX_EINVAL); return; } if (gdbmach_set_breakpoint(args[0], args[1], args[2], enable)) { gdbstub_send_ok(stub); } else { /* Not supported */ stub->len = 0; gdbstub_tx_packet(stub); } } static void gdbstub_rx_packet(struct gdbstub *stub) { switch (stub->payload[0]) { case '?': gdbstub_report_signal(stub); break; case 'g': gdbstub_read_regs(stub); break; case 'G': gdbstub_write_regs(stub); break; case 'm': gdbstub_read_mem(stub); break; case 'M': gdbstub_write_mem(stub); break; case 'c': /* Continue */ case 'k': /* Kill */ case 's': /* Step */ case 'D': /* Detach */ gdbstub_continue(stub, stub->payload[0] == 's'); if (stub->payload[0] == 'D') { gdbstub_send_ok(stub); } break; case 'Z': /* Insert breakpoint */ case 'z': /* Remove breakpoint */ gdbstub_breakpoint(stub); break; default: stub->len = 0; gdbstub_tx_packet(stub); break; } } /* GDB packet parser */ static void gdbstub_state_new(struct gdbstub *stub, char ch) { if (ch == '$') { stub->len = 0; stub->parse = gdbstub_state_data; } } static void gdbstub_state_data(struct gdbstub *stub, char ch) { if (ch == '#') { stub->parse = gdbstub_state_cksum1; } else if (ch == '$') { stub->len = 0; /* retry new packet */ } else { /* If the length exceeds our buffer, let the checksum fail */ if (stub->len < SIZEOF_PAYLOAD) { stub->payload[stub->len++] = ch; } } } static void gdbstub_state_cksum1(struct gdbstub *stub, char ch) { stub->cksum1 = gdbstub_from_hex_digit(ch) << 4; stub->parse = gdbstub_state_cksum2; } static void gdbstub_state_cksum2(struct gdbstub *stub, char ch) { uint8_t their_cksum; uint8_t our_cksum; stub->parse = gdbstub_state_new; their_cksum = stub->cksum1 + gdbstub_from_hex_digit(ch); our_cksum = gdbstub_cksum(stub->payload, stub->len); if (their_cksum == our_cksum) { serial_write("+", 1); if (stub->len > 0) { gdbstub_rx_packet(stub); } } else { serial_write("-", 1); } } static void gdbstub_state_wait_ack(struct gdbstub *stub, char ch) { if (ch == '+') { stub->parse = gdbstub_state_new; } else { /* This retransmit is very aggressive but necessary to keep * in sync with GDB. */ gdbstub_tx_packet(stub); } } void gdbstub_handler(int signo, gdbreg_t * regs) { struct gdbstub stub; gdbmach_disable_hwbps(); stub.parse = gdbstub_state_new; stub.payload = &stub.buf[1]; stub.signo = signo; stub.regs = regs; stub.exit_handler = 0; gdbstub_report_signal(&stub); while (!stub.exit_handler) stub.parse(&stub, serial_getc()); gdbmach_enable_hwbps(); }