GDB's tty command does work, but it doesn't work well with interactive programs, like if you wanted to debug bash. Even for non-interactive programs, you get the following:
warning: GDB: Failed to set controlling terminal: Operation not permitted
I wrote a small program to fix both of these issues:
// Open a pty and let it idle, so that a process spawned in a different window
// can attach to it, start a new session, and set it as the controlling
// terminal. Useful for gdb debugging with gdb's `tty` command.
#include <inttypes.h>
typedef uint8_t u8; typedef uint16_t u16; typedef uint32_t u32; typedef uint64_t u64;
typedef int8_t i8; typedef int16_t i16; typedef int32_t i32; typedef int64_t i64;
#include <stdlib.h>
#include <assert.h>
#include <errno.h>
#include <stdio.h>
#include <unistd.h>
#include <termios.h>
#include <pty.h>
#include <liburing.h>
#define BSIZE 4096
void raw_terminal(void)
{
if (!isatty(0))
return;
struct termios t;
tcgetattr(0, &t);
t.c_lflag &= ~(ISIG | ICANON | ECHO);
tcsetattr(0, TCSANOW, &t);
}
// Refers to the state of a Joint /while it's waiting in io_uring_enter/.
enum State {
READ,
WRITE
};
// Joins two fds together, like splice, but not a syscall and works on any two
// fds.
struct Joint {
u8 buf[BSIZE];
i32 ifd;
i32 ofd;
enum State state;
u32 nread;
};
void roll_joint(struct Joint *j, struct io_uring *ur, i32 ifd, i32 ofd)
{
j->ifd = ifd;
j->ofd = ofd;
j->state = READ;
struct io_uring_sqe *sqe = io_uring_get_sqe(ur);
io_uring_prep_read(sqe, j->ifd, j->buf, BSIZE, 0);
io_uring_sqe_set_data(sqe, j);
io_uring_submit(ur);
}
i32 main(i32 argc, char **argv)
{
raw_terminal();
struct io_uring ur;
assert(io_uring_queue_init(256, &ur, 0) == 0);
i32 ptm, pts;
assert(openpty(&ptm, &pts, NULL, NULL, NULL) == 0);
dprintf(2, "pid = %u tty = %s\n", getpid(), ttyname(pts));
struct Joint jkbd;
roll_joint(&jkbd, &ur, 0, ptm);
struct Joint jscreen;
roll_joint(&jscreen, &ur, ptm, 1);
for (;;) {
struct io_uring_cqe *cqe;
for (;;) {
// Actions like suspend to RAM can interrupt the io_uring_enter
// syscall. If we get interrupted, try again. For all other errors,
// bail. Also, wait_cqe negates the error for no reason. It never
// returns positive numbers. Very silly.
u32 res = -io_uring_wait_cqe(&ur, &cqe);
if (res == 0)
break;
else if (res != EINTR) {
dprintf(2, "io_uring_enter returns errno %d\n", res);
exit(res);
}
}
struct Joint *j = io_uring_cqe_get_data(cqe);
if (j->state == READ) {
// Exiting READ state. Finish with the read...
j->nread = cqe->res;
assert(j->nread > 0);
// Now, start the write.
j->state = WRITE;
struct io_uring_sqe *sqe = io_uring_get_sqe(&ur);
io_uring_prep_write(sqe, j->ofd, j->buf, j->nread, 0);
io_uring_sqe_set_data(sqe, j);
io_uring_submit(&ur);
}
else if (j->state == WRITE) {
// Exiting WRITE state. Finish with the write...
i64 nwritten = cqe->res;
assert(nwritten == j->nread);
// Now, start the read.
j->state = READ;
struct io_uring_sqe *sqe = io_uring_get_sqe(&ur);
io_uring_prep_read(sqe, j->ifd, j->buf, BSIZE, 0);
io_uring_sqe_set_data(sqe, j);
io_uring_submit(&ur);
}
io_uring_cqe_seen(&ur, cqe);
}
io_uring_queue_exit(&ur);
return 0;
}
Suppose you save the program to idleterm.c. To compile it:
> gcc -o idleterm idleterm.c -luring
To use it, start one terminal window, and on that window, run idleterm. It will print the name of the tty to attach to:
> ./idleterm
pid = 3405922 tty = /dev/pts/0
█
Copy that tty path and paste it into a gdb session in a second window:
> gdb bash
Reading symbols from bash...
(No debugging symbols found in bash)
(gdb) tty /dev/pts/0
(gdb) r
Starting program: /usr/bin/bash
…
A bash prompt will appear in the first window. All of the interactions with bash requiring special TTY behaviour will work normally, including ^C, ^Z, etc.
idleterm passes all keyboard input through to the child process being debugged, including ^C and ^Z. So in order to stop idleterm, you'll have to kill it from a separate window. This is why idleterm prints its pid. Copy the pid, then paste it into the kill command:
> kill 3405922
If there's only one instance of idleterm running on the system, you can of course just use killall.