How to fix an ancient GDB problem
The problem in question, Alves said, has to do with the handling of keyboard interrupts, which normally result from the user hitting control-C. The user's normal expectation is that an interrupt within GDB while the target program is running will stop the program and return the GDB prompt. If, however, that program has blocked the SIGINT signal, the interrupt will never be delivered. At best, GDB will not stop; at worst, the entire debugging session can become stuck and need to be killed from another terminal. GDB users, it seems, tend not to like that behavior.
This problem results from how GDB handles both terminals and interrupt signals. A "session", in the Unix sense, is a set of process groups, all of which share a single controlling terminal. Normally, the debugged process runs in the same session as — and shares the terminal with — GDB, but GDB puts that process into a different process group. Multiple process groups can share a terminal, but only one of those — the foreground group — will receive signals generated by the user at that terminal. GDB normally runs as the foreground group but, when it runs the target program, it designates that program's group as the foreground group instead.
Normally, if the target process receives a SIGINT signal, it will
be intercepted by GDB; that happens as part of how tracing with ptrace()
works. GDB will respond by stopping the target program and putting out a
prompt; the signal is never actually delivered to that program. If,
however, the program has blocked SIGINT then the signal remains
pending; since it is never delivered, ptrace() has nothing to
intercept. That can result in everything getting stuck. There are other
paths to the same situation; sigwait()
calls, for example, can consume pending signals in a way that causes them
to never actually be delivered.
The solution, Alves said, is the same as for any other problem in computer science: add another layer of indirection. In this case, that layer takes the form of a pseudo-terminal (PTY) that is given to the target process rather than the real controlling terminal. GDB then acts as an intermediary between the two terminals. Any output written by the target program to the PTY is simply copied to the real terminal. Input is a bit trickier, since the target can have changed the terminal's modes; GDB has to put the real terminal into raw mode, then copy all of the input from the real terminal into the PTY. When the target is not running, the terminal is put back into "readline mode" for interaction with GDB.
Now, the target can do anything it wants with SIGINT without affecting GDB, which, as the foreground process on the real terminal, can handle events directly. Since that terminal is in raw mode, that means recognizing the interrupt character and responding accordingly. There are other advantages as well; since GDB remains in control of when output goes to the (real) terminal, it can avoid intermixing its own output with that from the target. Another advantage is that GDB is now able to preserve the user's thread selection (the specific thread that debugging activity is focused on) after an interrupt; this wasn't possible before.
There is, he said, an "escape hatch" for anybody wanting the previous behavior; it needs to be there to support other Unix systems in any case.
There were a few other remaining problems, he said. The first process in the foreground process group is considered the "session leader" by the kernel; if that process exits, then its children will be sent a SIGHUP signal. Most applications are not prepared for that and will be killed as a result. Now that the target has its own terminal, it becomes the session leader once it starts. If that process forks and exits, its child processes are likely to meet an untimely end — not the debugging experience that the user is likely to have had in mind.
The solution in this case is a variation on the double-fork technique; before launching the target, GDB will fork twice, with the first process doing nothing but waiting. It will become the session leader; since it doesn't exit, no SIGHUP signals will be generated if the target does surprising things.
GDB still has to be able to stop programs that block SIGINT; for obvious reasons, it cannot use SIGINT for that purpose. The solution here, he said, is to use SIGSTOP, which cannot be blocked, instead.
As is often the case, Emacs users present their own special challenges. Emacs uses control-C for its own purposes, and remaps SIGINT to control-G instead. In cases like this, the user almost certainly wants control-C to be passed through to the target. The answer is a GDB command that allows the user to specify which key should interrupt the process and return to the GDB prompt.
This patch was first prototyped in 2019, but didn't make it to the GDB list until 2021. There were a few problems that turned up at that point, including the session-leader difficulty. Those have all been resolved, and Alves intends to post the patch set again sometime soon. His objective, he concluded, is to post it at least once per year until the problem is finally solved.
[Thanks to LWN subscribers for supporting my travel to this event.]
| Index entries for this article | |
|---|---|
| Conference | GNU Tools Cauldron/2022 |
The LWN site is currently under high scraper load, so comment display has been suppressed for anonymous users. If you are a human, you may read the comments by clicking the button below:
Note: you can avoid this step in the future by logging into your LWN account.
