From 6e670b5f38903109a0369f81932cca16f8a6eddf Mon Sep 17 00:00:00 2001 From: Stefan Hajnoczi Date: Wed, 11 Jun 2008 12:12:46 +0100 Subject: [PATCH] [GDB] Remote debugging over UDP This commit implements GDB over UDP. Using UDP is more complex than serial and has required some restructuring. The GDB stub is now built using one or both of GDBSERIAL and GDBUDP config.h options. To enter the debugger, execute the gPXE shell command: gdbstub [...] Where is "serial" or "udp". For "udp", the name of a configured network device is required: gdbstub udp net0 The GDB stub listens on UDP port 43770 by default. --- src/config.h | 4 +- src/core/config.c | 9 +- src/core/gdbserial.c | 41 ++++++ src/core/gdbstub.c | 89 +++++++------ src/core/gdbudp.c | 228 +++++++++++++++++++++++++++++++++ src/hci/commands/gdbstub_cmd.c | 105 +++++++++++++++ src/include/gpxe/gdbstub.h | 64 +++++++++ src/tests/gdbstub_test.gdb | 11 +- 8 files changed, 501 insertions(+), 50 deletions(-) create mode 100644 src/core/gdbserial.c create mode 100644 src/core/gdbudp.c create mode 100644 src/hci/commands/gdbstub_cmd.c create mode 100644 src/include/gpxe/gdbstub.h diff --git a/src/config.h b/src/config.h index e695b015..f43da040 100644 --- a/src/config.h +++ b/src/config.h @@ -163,7 +163,9 @@ #undef BUILD_ID /* Include a custom build ID string, * e.g "test-foo" */ #undef NULL_TRAP /* Attempt to catch NULL function calls */ -#undef GDBSTUB /* Remote GDB debugging */ +#undef GDBSERIAL /* Remote GDB debugging over serial */ +#undef GDBUDP /* Remote GDB debugging over UDP + * (both may be set) */ /* @END general.h */ diff --git a/src/core/config.c b/src/core/config.c index 018f084a..42026827 100644 --- a/src/core/config.c +++ b/src/core/config.c @@ -195,6 +195,13 @@ REQUIRE_OBJECT ( sanboot_cmd ); #ifdef NULL_TRAP REQUIRE_OBJECT ( nulltrap ); #endif -#ifdef GDBSTUB +#ifdef GDBSERIAL REQUIRE_OBJECT ( gdbidt ); +REQUIRE_OBJECT ( gdbserial ); +REQUIRE_OBJECT ( gdbstub_cmd ); +#endif +#ifdef GDBUDP +REQUIRE_OBJECT ( gdbidt ); +REQUIRE_OBJECT ( gdbudp ); +REQUIRE_OBJECT ( gdbstub_cmd ); #endif diff --git a/src/core/gdbserial.c b/src/core/gdbserial.c new file mode 100644 index 00000000..2fecd5f6 --- /dev/null +++ b/src/core/gdbserial.c @@ -0,0 +1,41 @@ +/* + * 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. + */ + +#include +#include +#include + +struct gdb_transport serial_gdb_transport __gdb_transport; + +static size_t gdbserial_recv ( char *buf, size_t len ) { + assert ( len > 0 ); + buf [ 0 ] = serial_getc(); + return 1; +} + +static void gdbserial_send ( const char *buf, size_t len ) { + while ( len-- > 0 ) { + serial_putc ( *buf++ ); + } +} + +struct gdb_transport serial_gdb_transport __gdb_transport = { + .name = "serial", + .recv = gdbserial_recv, + .send = gdbserial_send, +}; diff --git a/src/core/gdbstub.c b/src/core/gdbstub.c index df50dc81..c331b85b 100644 --- a/src/core/gdbstub.c +++ b/src/core/gdbstub.c @@ -24,22 +24,24 @@ */ #include -#include #include +#include #include #include -#include -#include +#include #include "gdbmach.h" enum { - POSIX_EINVAL = 0x1c /* used to report bad arguments to GDB */ + POSIX_EINVAL = 0x1c, /* used to report bad arguments to GDB */ + SIZEOF_PAYLOAD = 256, /* buffer size of GDB payload data */ }; struct gdbstub { + struct gdb_transport *trans; + int exit_handler; /* leave interrupt handler */ + int signo; gdbreg_t *regs; - int exit_handler; /* leave interrupt handler */ void ( * parse ) ( struct gdbstub *stub, char ch ); uint8_t cksum1; @@ -47,10 +49,15 @@ struct gdbstub { /* Buffer for payload data when parsing a packet. Once the * packet has been received, this buffer is used to hold * the reply payload. */ - char payload [ 256 ]; - int len; + char buf [ SIZEOF_PAYLOAD + 4 ]; /* $...PAYLOAD...#XX */ + char *payload; /* start of payload */ + int len; /* length of payload */ }; +/* Transports */ +static struct gdb_transport gdb_transport_start[0] __table_start ( struct gdb_transport, gdb_transports ); +static struct gdb_transport gdb_transport_end[0] __table_end ( struct gdb_transport, gdb_transports ); + /* Packet parser states */ static void gdbstub_state_new ( struct gdbstub *stub, char ch ); static void gdbstub_state_data ( struct gdbstub *stub, char ch ); @@ -132,29 +139,13 @@ static uint8_t gdbstub_cksum ( char *data, int len ) { return cksum; } -static int gdbstub_getchar ( struct gdbstub *stub ) { - if ( stub->exit_handler ) { - return -1; - } - return serial_getc(); -} - -static void gdbstub_putchar ( struct gdbstub * stub __unused, char ch ) { - serial_putc ( ch ); -} - static void gdbstub_tx_packet ( struct gdbstub *stub ) { uint8_t cksum = gdbstub_cksum ( stub->payload, stub->len ); - int i; - - gdbstub_putchar ( stub, '$' ); - for ( i = 0; i < stub->len; i++ ) { - gdbstub_putchar ( stub, stub->payload [ i ] ); - } - gdbstub_putchar ( stub, '#' ); - gdbstub_putchar ( stub, gdbstub_to_hex_digit ( cksum >> 4 ) ); - gdbstub_putchar ( stub, gdbstub_to_hex_digit ( cksum ) ); - + 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 ); + stub->trans->send ( stub->buf, stub->len + 4 ); stub->parse = gdbstub_state_wait_ack; } @@ -229,7 +220,7 @@ static void gdbstub_read_mem ( struct gdbstub *stub ) { gdbstub_send_errno ( stub, POSIX_EINVAL ); return; } - args [ 1 ] = ( args [ 1 ] < sizeof stub->payload / 2 ) ? args [ 1 ] : sizeof stub->payload / 2; + 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 ); @@ -306,7 +297,7 @@ static void gdbstub_state_data ( struct gdbstub *stub, char ch ) { stub->len = 0; /* retry new packet */ } else { /* If the length exceeds our buffer, let the checksum fail */ - if ( stub->len < ( int ) sizeof stub->payload ) { + if ( stub->len < SIZEOF_PAYLOAD ) { stub->payload [ stub->len++ ] = ch; } } @@ -325,12 +316,12 @@ static void gdbstub_state_cksum2 ( struct gdbstub *stub, char ch ) { their_cksum = stub->cksum1 + gdbstub_from_hex_digit ( ch ); our_cksum = gdbstub_cksum ( stub->payload, stub->len ); if ( their_cksum == our_cksum ) { - gdbstub_putchar ( stub, '+' ); + stub->trans->send ( "+", 1 ); if ( stub->len > 0 ) { gdbstub_rx_packet ( stub ); } } else { - gdbstub_putchar ( stub, '-' ); + stub->trans->send ( "-", 1 ); } } @@ -351,23 +342,37 @@ static struct gdbstub stub = { }; __cdecl void gdbstub_handler ( int signo, gdbreg_t *regs ) { - int ch; + char packet [ SIZEOF_PAYLOAD + 4 ]; + size_t len, i; + + /* A transport must be set up */ + if ( !stub.trans ) { + return; + } + stub.signo = signo; stub.regs = regs; stub.exit_handler = 0; gdbstub_report_signal ( &stub ); - while ( ( ch = gdbstub_getchar( &stub ) ) != -1 ) { - gdbstub_parse ( &stub, ch ); + while ( !stub.exit_handler && ( len = stub.trans->recv ( packet, sizeof ( packet ) ) ) > 0 ) { + for ( i = 0; i < len; i++ ) { + gdbstub_parse ( &stub, packet [ i ] ); + } } } -/* Activity monitor to detect packets from GDB when we are not active */ -static void gdbstub_activity_step ( struct process *process __unused ) { - if ( serial_ischar() ) { - gdbmach_breakpoint(); +struct gdb_transport *find_gdb_transport ( const char *name ) { + struct gdb_transport *trans; + for ( trans = gdb_transport_start; trans < gdb_transport_end; trans++ ) { + if ( strcmp ( trans->name, name ) == 0 ) { + return trans; + } } + return NULL; } -struct process gdbstub_activity_process __permanent_process = { - .step = gdbstub_activity_step, -}; +void gdbstub_start ( struct gdb_transport *trans ) { + stub.trans = trans; + stub.payload = &stub.buf [ 1 ]; + gdbmach_breakpoint(); +} diff --git a/src/core/gdbudp.c b/src/core/gdbudp.c new file mode 100644 index 00000000..fb381644 --- /dev/null +++ b/src/core/gdbudp.c @@ -0,0 +1,228 @@ +/* + * 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. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct gdb_transport udp_gdb_transport __gdb_transport; + +static struct net_device *netdev; +static uint8_t dest_eth[ETH_ALEN]; +static uint8_t source_eth[ETH_ALEN]; +static struct sockaddr_in dest_addr; +static struct sockaddr_in source_addr; + +static void gdbudp_ensure_netdev_open ( struct net_device *netdev ) { + if ( ( netdev->state & NETDEV_OPEN) == 0 ) { + netdev_open ( netdev ); + } + /* TODO forcing the netdev to be open is useful when + * gPXE closes the netdev between breakpoints. Should + * we restore the state of the netdev, i.e. closed, + * before leaving the interrupt handler? */ +} + +static size_t gdbudp_recv ( char *buf, size_t len ) { + struct io_buffer *iob; + struct ethhdr *ethhdr; + struct arphdr *arphdr; + struct iphdr *iphdr; + struct udp_header *udphdr; + size_t payload_len; + + assert ( netdev ); + gdbudp_ensure_netdev_open ( netdev ); + + for ( ; ; ) { + while ( ( iob = netdev_rx_dequeue ( netdev ) ) != NULL ) { + if ( iob_len ( iob ) > sizeof ( *ethhdr ) + sizeof ( *iphdr ) + sizeof ( *udphdr ) + len ) { + goto bad_packet; + } + + /* Ethernet header */ + ethhdr = iob->data; + iob_pull ( iob, sizeof ( *ethhdr ) ); + if ( ethhdr->h_protocol == htons ( ETH_P_ARP ) ) { + /* Handle ARP requests so the client can connect to us */ + arphdr = iob->data; + if ( arphdr->ar_hrd != htons ( ARPHRD_ETHER ) || + arphdr->ar_pro != htons ( ETH_P_IP ) || + arphdr->ar_hln != ETH_ALEN || + arphdr->ar_pln != sizeof ( struct in_addr ) || + arphdr->ar_op != htons ( ARPOP_REQUEST ) || + memcmp ( arp_target_pa ( arphdr ), &source_addr.sin_addr.s_addr, sizeof ( struct in_addr ) ) ) { + goto bad_packet; + } + + /* Generate an ARP reply */ + arphdr->ar_op = htons ( ARPOP_REPLY ); + memswap ( arp_sender_pa ( arphdr ), arp_target_pa ( arphdr ), sizeof ( struct in_addr ) ); + memcpy ( arp_target_ha ( arphdr ), arp_sender_ha ( arphdr ), ETH_ALEN ); + memcpy ( arp_sender_ha ( arphdr ), source_eth, ETH_ALEN ); + + /* Fix up ethernet header */ + ethhdr = iob_push ( iob, sizeof ( *ethhdr ) ); + memswap ( ethhdr->h_source, ethhdr->h_dest, ETH_ALEN ); + + netdev_tx ( netdev, iob ); + continue; /* no need to free iob */ + } + if ( ethhdr->h_protocol != htons ( ETH_P_IP ) ) { + goto bad_packet; + } + + /* IP header */ + iphdr = iob->data; + iob_pull ( iob, sizeof ( *iphdr ) ); + if ( iphdr->protocol != IP_UDP || iphdr->dest.s_addr != source_addr.sin_addr.s_addr ) { + goto bad_packet; + } + + /* UDP header */ + udphdr = iob->data; + if ( udphdr->dest != source_addr.sin_port ) { + goto bad_packet; + } + + /* Learn the remote connection details */ + memcpy ( dest_eth, ethhdr->h_source, ETH_ALEN ); + dest_addr.sin_addr.s_addr = iphdr->src.s_addr; + dest_addr.sin_port = udphdr->src; + + /* Payload */ + payload_len = ntohs ( udphdr->len ); + if ( payload_len < sizeof ( *udphdr ) || + payload_len > iob_len ( iob ) ) { + goto bad_packet; + } + payload_len -= sizeof ( *udphdr ); + iob_pull ( iob, sizeof ( *udphdr ) ); + memcpy ( buf, iob->data, payload_len ); + + free_iob ( iob ); + return payload_len; + +bad_packet: + free_iob ( iob ); + } + netdev_poll ( netdev ); + } +} + +static void gdbudp_send ( const char *buf, size_t len ) { + struct io_buffer *iob; + struct ethhdr *ethhdr; + struct iphdr *iphdr; + struct udp_header *udphdr; + + /* Check that we are connected */ + if ( dest_addr.sin_port == 0 ) { + return; + } + + assert ( netdev ); + gdbudp_ensure_netdev_open ( netdev ); + + iob = alloc_iob ( sizeof ( *ethhdr ) + sizeof ( *iphdr ) + sizeof ( *udphdr ) + len ); + if ( !iob ) { + return; + } + + /* Payload */ + iob_reserve ( iob, sizeof ( *ethhdr ) + sizeof ( *iphdr ) + sizeof ( *udphdr ) ); + memcpy ( iob_put ( iob, len ), buf, len ); + + /* UDP header */ + udphdr = iob_push ( iob, sizeof ( *udphdr ) ); + udphdr->src = source_addr.sin_port; + udphdr->dest = dest_addr.sin_port; + udphdr->len = htons ( iob_len ( iob ) ); + udphdr->chksum = 0; /* optional and we are not using it */ + + /* IP header */ + iphdr = iob_push ( iob, sizeof ( *iphdr ) ); + memset ( iphdr, 0, sizeof ( *iphdr ) ); + iphdr->verhdrlen = ( IP_VER | ( sizeof ( *iphdr ) / 4 ) ); + iphdr->service = IP_TOS; + iphdr->len = htons ( iob_len ( iob ) ); + iphdr->ttl = IP_TTL; + iphdr->protocol = IP_UDP; + iphdr->dest.s_addr = dest_addr.sin_addr.s_addr; + iphdr->src.s_addr = source_addr.sin_addr.s_addr; + iphdr->chksum = tcpip_chksum ( iphdr, sizeof ( *iphdr ) ); + + /* Ethernet header */ + ethhdr = iob_push ( iob, sizeof ( *ethhdr ) ); + memcpy ( ethhdr->h_dest, dest_eth, ETH_ALEN ); + memcpy ( ethhdr->h_source, source_eth, ETH_ALEN ); + ethhdr->h_protocol = htons ( ETH_P_IP ); + + netdev_tx ( netdev, iob ); +} + +static int gdbudp_init ( int argc, char **argv ) { + struct settings *settings; + + if ( argc != 1 ) { + printf ( "udp: missing argument\n" ); + return 1; + } + + netdev = find_netdev ( argv[0] ); + if ( !netdev ) { + printf ( "%s: no such interface\n", argv[0] ); + return 1; + } + + if ( !netdev_link_ok ( netdev ) ) { + printf ( "%s: link not up\n", argv[0] ); + return 1; + } + + /* Load network settings from device. We keep the MAC address, + * IP address, and UDP port. The MAC and IP could be fetched + * from the network device each time they are used in rx/tx. + * Storing a separate copy makes it possible to use different + * MAC/IP settings than the network stack. */ + memcpy ( source_eth, netdev->ll_addr, ETH_ALEN ); + source_addr.sin_port = htons ( 43770 ); /* TODO default port */ + settings = netdev_settings ( netdev ); + fetch_ipv4_setting ( settings, &ip_setting, &source_addr.sin_addr ); + if ( source_addr.sin_addr.s_addr == 0 ) { + printf ( "%s: no IP address configured\n", argv[0] ); + return 1; + } + + return 0; +} + +struct gdb_transport udp_gdb_transport __gdb_transport = { + .name = "udp", + .init = gdbudp_init, + .send = gdbudp_send, + .recv = gdbudp_recv, +}; diff --git a/src/hci/commands/gdbstub_cmd.c b/src/hci/commands/gdbstub_cmd.c new file mode 100644 index 00000000..74167525 --- /dev/null +++ b/src/hci/commands/gdbstub_cmd.c @@ -0,0 +1,105 @@ +/* + * 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. + */ + +#include +#include +#include +#include + +/** @file + * + * GDB stub command + * + */ + +/** + * "gdbstub" command syntax message + * + * @v argv Argument list + */ +static void gdbstub_syntax ( char **argv ) { + printf ( "Usage:\n" + " %s [...]\n" + "\n" + "Start remote debugging using one of the following transports:\n" + " serial use serial port (if compiled in)\n" + " udp use UDP over network interface (if compiled in)\n", + argv[0] ); +} + +/** + * The "gdbstub" command + * + * @v argc Argument count + * @v argv Argument list + * @ret rc Exit code + */ +static int gdbstub_exec ( int argc, char **argv ) { + static struct option longopts[] = { + { "help", 0, NULL, 'h' }, + { NULL, 0, NULL, 0 }, + }; + const char *trans_name; + struct gdb_transport *trans; + int c; + + /* Parse options */ + while ( ( c = getopt_long ( argc, argv, "h", longopts, NULL ) ) >= 0 ){ + switch ( c ) { + case 'h': + /* Display help text */ + default: + /* Unrecognised/invalid option */ + gdbstub_syntax ( argv ); + return 1; + } + } + + /* At least one argument */ + if ( optind == argc ) { + gdbstub_syntax ( argv ); + return 1; + } + + trans_name = argv[optind++]; + + /* Initialise transport */ + trans = find_gdb_transport ( trans_name ); + if ( !trans ) { + printf ( "%s: no such transport (is it compiled in?)\n", trans_name ); + return 1; + } + + if ( trans->init ) { + if ( trans->init ( argc - optind, &argv[optind] ) != 0 ) { + return 1; + } + } + + /* Enter GDB stub */ + gdbstub_start ( trans ); + return 0; +} + +/** GDB stub commands */ +struct command gdbstub_commands[] __command = { + { + .name = "gdbstub", + .exec = gdbstub_exec, + }, +}; diff --git a/src/include/gpxe/gdbstub.h b/src/include/gpxe/gdbstub.h new file mode 100644 index 00000000..adc7e382 --- /dev/null +++ b/src/include/gpxe/gdbstub.h @@ -0,0 +1,64 @@ +#ifndef _GPXE_GDBSTUB_H +#define _GPXE_GDBSTUB_H + +/** @file + * + * GDB remote debugging + * + */ + +#include +#include + +/** + * A transport mechanism for the GDB protocol + * + */ +struct gdb_transport { + /** Transport name */ + const char *name; + /** + * Set up the transport given a list of arguments + * + * @v argc Number of arguments + * @v argv Argument list + * @ret Return status code + * + * Note that arguments start at argv[0]. + */ + int ( * init ) ( int argc, char **argv ); + /** + * Perform a blocking read + * + * @v buf Buffer + * @v len Size of buffer + * @ret Number of bytes read into buffer + */ + size_t ( * recv ) ( char *buf, size_t len ); + /** + * Write, may block + * + * @v buf Buffer + * @v len Size of buffer + */ + void ( * send ) ( const char *buf, size_t len ); +}; + +#define __gdb_transport __table ( struct gdb_transport, gdb_transports, 01 ) + +/** + * Look up GDB transport by name + * + * @v name Name of transport + * @ret GDB transport or NULL + */ +extern struct gdb_transport *find_gdb_transport ( const char *name ); + +/** + * Break into the debugger using the given transport + * + * @v trans GDB transport + */ +extern void gdbstub_start ( struct gdb_transport *trans ); + +#endif /* _GPXE_GDBSTUB_H */ diff --git a/src/tests/gdbstub_test.gdb b/src/tests/gdbstub_test.gdb index c0c59644..c86d4f2a 100755 --- a/src/tests/gdbstub_test.gdb +++ b/src/tests/gdbstub_test.gdb @@ -3,16 +3,16 @@ # Run: # make bin/gpxe.hd.tmp # make -# tests/gdbstub_test.gdb +# gdb +# (gdb) target remote :TCPPORT +# OR +# (gdb) target remote udp:IP:UDPPORT +# (gdb) source tests/gdbstub_test.gdb define gpxe_load_symbols file bin/gpxe.hd.tmp end -define gpxe_connect - target remote localhost:4444 -end - define gpxe_assert if $arg0 != $arg1 echo FAIL $arg2\n @@ -78,7 +78,6 @@ define gpxe_test_step end gpxe_load_symbols -gpxe_connect gpxe_start_tests gpxe_test_regs_read gpxe_test_regs_write