收到不少同学发邮件询问gdb的 Ctrl+c 怎么让内核给停下来,这里就写写关于这方面的内容。
目前常用的gdb与内核的通信是基于串口的kgdboc模块来连接的,所以本文就以串口的kgdboc来讲解。
文章分两个部分来解释这个问题,一部分是gdb端对ctrl+c操作做的一些处理,
另外一部分则是内核和KGDB对停止指令处理。
1: gdb端:
在Linux console程序中,ctrl+c是用来终止当前在终端窗口中运行的命令或脚本。
ctrl+c == kill -2 (SIGINT)
当gdb捕获Ctrl+c发出的信号(SIGINT)后,会做一些相应的处理,然后会给远程终端发送一个停止运行的特殊序列指令.
相关代码:
A: gdb 中断指令设置申明代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | gdb-7.2/gdb/remote.c : 702 /* Allow the user to specify what sequence to send to the remote when he requests a program interruption: Although ^C is usually what remote systems expect (this is the default, here), it is sometimes preferable to send a break. On other systems such as the Linux kernel, a break followed by g, which is Magic SysRq g is required in order to interrupt the execution. */ const char interrupt_sequence_control_c[] = "Ctrl-C"; const char interrupt_sequence_break[] = "BREAK"; const char interrupt_sequence_break_g[] = "BREAK-g"; static const char *interrupt_sequence_modes[] = { interrupt_sequence_control_c, interrupt_sequence_break, interrupt_sequence_break_g, NULL }; static const char *interrupt_sequence_mode = interrupt_sequence_control_c; |
B: gdb 发送中断指令代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | gdb-7.2/gdb/remote.c : 3112 /* Send interrupt_sequence to remote target. */ static void send_interrupt_sequence () { if (interrupt_sequence_mode == interrupt_sequence_control_c) serial_write (remote_desc, "\x03", 1); else if (interrupt_sequence_mode == interrupt_sequence_break) serial_send_break (remote_desc); else if (interrupt_sequence_mode == interrupt_sequence_break_g) { serial_send_break (remote_desc); serial_write (remote_desc, "g", 1); } else internal_error (__FILE__, __LINE__, _("Invalid value for interrupt_sequence_mode: %s."), interrupt_sequence_mode); } |
C: gdb 注册SIGINT信号来捕获Ctrl+c,并发送出“中断指令“
随着逻辑和需求的不同,在gdb中对SIGINT处理也不同,这里只贴出部分gdb注册SIGINT信号代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | gdb-7.2/gdb/remote.c : 5439 if (!target_is_async_p ()) { ofunc = signal (SIGINT, remote_interrupt); /* If the user hit C-c before this packet, or between packets, pretend that it was hit right here. */ if (quit_flag) { quit_flag = 0; remote_interrupt (SIGINT); } } ret = getpkt_sane (&rs->buf, &rs->buf_size, wait_forever_enabled_p); if (!target_is_async_p ()) signal (SIGINT, ofunc); } |
Ctrl+c --> trigger SIGINT --> run to remote_interrupt():
1 2 3 4 5 6 7 8 9 10 | remote_interrupt() --> sigint_remote_token() --> async_remote_interrupt() --> target_stop() { (*current_target.to_stop)() } --> remote_ops.to_stop = remote_stop; --> remote_stop_as() --> send_interrupt_sequence() |
2: 由上面的代码可了解,随着各种gdbserver的实现方式不一样,gdb的interrupt-sequence也不同。
而在调试内核时,我们需要发送“BREAK-g”序列给内核,
所以如果你的gdb的 ctrl+c不能停止内核,可以检测下是否发送了正确的interrupt-sequence给内核.
1 2 3 4 5 6 7 8 9 10 11 12 | (gdb) show remote interrupt-sequence Send the ASCII ETX character (Ctrl-c) to the remote target to interrupt the execution of the program. (gdb) set remote interrupt-sequence Requires an argument. Valid arguments are Ctrl-C, BREAK, BREAK-g. (gdb) set remote interrupt-sequence BREAK-g (gdb) show remote interrupt-sequence Send a break signal and 'g' a.k.a. Magic SysRq g to the remote target to interrupt the execution of Linux kernel. |
2: kernel:
为什么gdb 发送了 “BREAK-g”给内核,内核就能停下来呢?
这里我们先看下gdb是怎么发送“BREAK-g”的,还是用代码来解释吧.
1: 先看下send_interrupt_sequence() 中serial_send_break() 的实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | A: gdb-7.2/gdb/serial.c : 459 int serial_send_break (struct serial *scb) { return (scb->ops->send_break (scb)); } B: gdb-7.2/gdb/ser-unix.c : 388 void _initialize_ser_hardwire (void) { ... ops->send_break = hardwire_send_break; ... } static int hardwire_send_break (struct serial *scb) { #ifdef HAVE_TERMIOS return tcsendbreak (scb->fd, 0); #endif #ifdef HAVE_TERMIO return ioctl (scb->fd, TCSBRK, 0); #endif } |
从上面的代码可以看出, BREAK其实就是给串口写以特殊位。
当串口硬件收到这个特殊位后,则会置Line Status Register位为UART_LSR_BI,
#define UART_LSR_BI 0x00 /* Break interrupt indicator */
然后在串口接收函数里根据上面描述的寄存器状态来做一些逻辑处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | linux-2.6/drivers/tty/serial/8250.c : 1458 irqreturn_t serial8250_interrupt()-->serial8250_handle_port() -->receive_chars() void receive_chars() { do { if (unlikely(lsr & UART_LSR_BRK_ERROR_BITS)) { if (lsr & UART_LSR_BI) { ... /* 处理收到特殊的BreakIndicator */ if (uart_handle_break(&up->port)) goto ignore_char; } } /* 判断接收到的字符是否为magic key */ if (uart_handle_sysrq_char(&up->port, ch)) goto ignore_char; /* 将接收到的字符放入缓冲区 */ uart_insert_char(&up->port, lsr, UART_LSR_OE, ch, flag); ignore_char: /* 获取下一个接收的字符 */ lsr = serial_inp(up, UART_LSR); } while ((lsr & (UART_LSR_DR | UART_LSR_BI)) && (max_count-- > 0)); } |
其中uart_handle_break()函数的实现很简单,就是设置下port->sysrq.
1 2 3 4 5 6 7 | linux-2.6/include/linux/serial_core.h : 508 static inline int uart_handle_break(struct uart_port *port) { ... port->sysrq = jiffies + HZ*5; ... } |
在发送Break特殊位后,紧接着是发送一个“g“字符过来,而uart_handle_sysrq_char则是比较关键的函数,
它将处理"g"这个字符,即最终调用sysrq的函数handle_sysrq(‘g’)来查找是否由注册来magic g的函数。
1 2 3 4 5 6 | linux-2.6/include/linux/serial_core.h :487 uart_handle_sysrq_char(struct uart_port *port, unsigned int ch) { if (port->sysrq) handle_sysrq(ch); } |
magic key "g":
目前,内核中,magic 'g'是被kgdb注册了, 一旦sysrq g被触发,则将进入kgdb模式。
PS:如果你的内核编译了kgdb,并使能,你可以使用如下命令来强制进入kgdb的调试模式
#echo "g" > /proc/sysrq-trigger
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | linux-2.6/kernel/debug/debug_core.c: 797 static void sysrq_handle_dbg(int key) { kgdb_breakpoint(); --> 触发一个断点,从而进入kgdb模式 } static struct sysrq_key_op sysrq_dbg_op = { .handler = sysrq_handle_dbg, .help_msg = "debug(G)", .action_msg = "DEBUG", }; ifdef CONFIG_MAGIC_SYSRQ register_sysrq_key('g', &sysrq_dbg_op); #endif |