LWN:如何修复GDB中一个古老的问题!
关注了就能看到更多这么棒的文章哦~
How to fix an ancient GDB problem
By Jonathan Corbet
September 29, 2022
Cauldron
DeepL assisted translation
https://lwn.net/Articles/909496/
GDB 调试器历史相当长,早在 1986 年就创建了。因此,经常看到一些 GDB 的开发发生在挺长的时间段内,但是,哪怕是考虑上这个因素,在看到 2007 年首次报告的一个公开 bug 仍然存在的时候,读者可能仍然会觉得有些吃惊。在 2022 年的 GNU Tools Cauldron 会议上,GDB 维护者 Pedro Alves 谈到了为什么这个问题一直难以解决,以及最终的解决方案是什么。
Alves 说,这个问题与键盘中断的处理有关,这种 keyboard interrupt 通常是由用户点击 control-C 造成的。用户通常期望,在 target 程序运行时,在 GDB 内部收到中断,就会停止程序并返回 GDB 提示。然而,如果该程序屏蔽掉了 SIGINT 信号的话,中断将永远不会被传递回来。比较好的情况是看到 GDB 没有停下来;在最坏的情况下,整个 debug session 会话可能被卡住,需要另开一个终端来 kill 它。GDB 用户应该是不喜欢这种行为的。
这个问题源于 GDB 对终端(terminal)和中断信号(interrupt signals)的处理方式。在 Unix 中一个 "会话(session)" 代表着若干个共享着同一个控制终端的进程组。通常来说,被调试的进程与 GDB 是在同一个会话中运行的,并且与 GDB 共享同一个终端,但是 GDB 会将该进程放入另一个进程组。多个进程组可以共享一个终端,但其中只有一个进程组–前台组(foreground group)–会接收到用户在该终端生成的信号。GDB 通常是作为前台组运行,但是当它在运行目标程序时,会指定该程序的组为前台组。
一般情况下,如果 target 进程收到一个 SIGINT 信号,此信号会被 GDB 截获;这是用 ptrace() 进行跟踪分析的具体实现之一。GDB 会把目标程序停下来,并且发出提示;这个信号实际上不会被传递给该程序。然而,如果目标程序阻止了 SIGINT,那么此信号保持 pending 状态;因为它从未被发送给接收者,所以 ptrace() 不知道需要拦截它。这可能会导致大家一起被卡住了。还有其他途径可以导致同样的情况;例如,sigwait() 调用可以采用一种让这些 pending 信号不会再发送出去的方式来把它处理掉。
Alves 说,解决方案与计算机科学中的其他问题都是一样的:添加另一个抽象层(add another layer of indirection)。在这个场景中,就需要使用伪终端(PTY, pseudo-terminal)这个抽象层,它会被赋予 target 进程而不是真正的终端。然后 GDB 会成为两个终端之间的中间人。任何由目标程序写入 PTY 的输出内容都会被简单地复制到真正的终端上。输入就比较麻烦了,因为目标程序可能已经改变了终端的模式;GDB 必须把真实的终端配置成 raw mode,然后把真实终端的所有输入都复制到 PTY 中。当目标程序不再运行时,终端被改回 "readline mode",从而方便跟 GDB 交互。
现在,target 就可以对 SIGINT 做任何事情都不会影响 GDB 了,GDB 作为真实终端的前台进程,可以直接处理这些事件。由于该终端处于 raw 模式,这意味着其可以识别出 interrupt 字符并做出相应的反应。还有其他的好处;由于 GDB 仍然可以控制什么时候把输出内容送到(真实)终端,那么它可以避免将自己的输出与 target 进程的输出信息混在一起。还有另一个优点,GDB 现在能够在 interrupt 之后继续保留用户所选择的线程(就是 debug 过程中所关注的哪一个特定线程);这在以前是不可能的。
他说,还实现了一个 "逃生舱把手(escape hatch)",如果有人希望回到以前的行为的话,就可以利用这个开关;无论如何,都需要这个功能来确保其他 Unix 系统可以使用这个 GDB。
他还介绍了一些其他的问题。前台进程组中的第一个进程被内核认为是 "session leader";如果该进程 exit 的话,那么它的子进程将收到一个 SIGHUP 信号。大多数应用程序都没有对此做好准备,从而会被 kill 掉。现在,target 程序有了自己的终端,在启动之后它就成为了 session leader。如果这个进程进行了 fork 并退出,它的子进程很可能会意外结束掉,这可不是用户在调试过程中想看到的行为。
这种情况下的解决方案是 double-fork 技巧的一个变种:在启动目标程序之前,GDB 将 fork 两次,第一个进程除了等待之外,什么都不做。它将成为 session leader;由于它没有退出,所以如果 target 程序做了什么意外的动作,也不会产生 SIGHUP 信号。
GDB 仍然有必要去把那些拦截了 SIGINT 的程序给停下来。出于一些明显的原因,它不能使用 SIGINT 来达到这个目的。他说,这里的解决方案就是使用无法被拦截的 SIGSTOP 信号来代替它。
Emacs 用户总是能提出他们自己特有的一些特殊情况。Emacs 将 control-C 改作它用了,并将 SIGINT 重映射到 control-G。这种情况下,用户肯定希望能把 control-C 传递给 target。解决方案就是新增一个 GDB 命令,允许用户指定哪个键应该中断进程并返回到 GDB 提示符。
这个 patch 的原型在 2019 年首次出现,但直到 2021 年才来到 GDB 邮件列表。当时出现了一些问题,包括 session-leader 相关的问题。这些都已经解决了,Alvis 打算近期再次发布这组 patch。他总结说,他的目标是每年至少发出来一次,直到问题最终得到解决为止。
[感谢 LWN 订户对我参加这次活动的支持]。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~