xref: /third_party/toybox/toys/pending/strace.c (revision 0f66f451)
1/* strace.c - Trace system calls.
2 *
3 * Copyright 2020 The Android Open Source Project
4 *
5 * See https://man7.org/linux/man-pages/man2/syscall.2.html
6
7USE_STRACE(NEWTOY(strace, "^p#s#v", TOYFLAG_USR|TOYFLAG_SBIN))
8
9config STRACE
10  bool "strace"
11  default n
12  help
13    usage: strace [-fv] [-p PID] [-s NUM] COMMAND [ARGS...]
14
15    Trace systems calls made by a process.
16
17    -s	String length limit.
18    -v	Dump all of large structs/arrays.
19*/
20
21#include <sys/ptrace.h>
22#include <sys/syscall.h>
23#include <sys/user.h>
24
25#define FOR_strace
26#include "toys.h"
27
28GLOBALS(
29  long s, p;
30
31  char ioctl[32], *fmt;
32  long regs[256/sizeof(long)], syscall;
33  pid_t pid;
34  int arg;
35)
36
37  struct user_regs_struct regs;
38
39
40// Syscall args from https://man7.org/linux/man-pages/man2/syscall.2.html
41// REG_ORDER is args 0-6, SYSCALL, RESULT
42#if defined(__ARM_EABI__)
43static const char REG_ORDER[] = {0,1,2,3,4,5,7,0};
44#elif defined(__ARM_ARCH) && __ARM_ARCH == 8
45static const char REG_ORDER[] = {0,1,2,3,4,5,8,0};
46#elif defined(__i386__)
47// ebx,ecx,edx,esi,edi,ebp,orig_eax,eax
48static const char REG_ORDER[] = {0,1,2,3,4,5,11,6};
49#elif defined(__m68k__)
50// d1,d2,d3,d4,d5,a0,orig_d0,d0
51static const char REG_ORDER[] = {0,1,2,3,4,7,16,14);
52#elif defined(__PPC__) || defined(__PPC64__)
53static const char REG_ORDER[] = {3,4,5,6,7,8,0,3};
54#elif defined(__s390__) // also covers s390x
55// r2,r3,r4,r5,r6,r7,r1,r2 but mask+addr before r0 so +2
56static const char REG_ORDER[] = {4,5,6,7,8,9,3,4};
57#elif defined(__sh__)
58static const char REG_ORDER[] = {4,5,6,7,0,1,3,0};
59#elif defined(__x86_64__)
60// rdi,rsi,rdx,r10,r8,r9,orig_rax,rax
61static const char REG_ORDER[] = {14,13,12,7,9,8,15,10};
62#else
63#error unsupported architecture
64#endif
65
66#define C(x) case x: return #x
67
68#define FS_IOC_FSGETXATTR 0x801c581f
69#define FS_IOC_FSSETXATTR 0x401c5820
70#define FS_IOC_GETFLAGS 0x80086601
71#define FS_IOC_SETFLAGS 0x40086602
72#define FS_IOC_GETVERSION 0x80087601
73#define FS_IOC_SETVERSION 0x40047602
74struct fsxattr {
75  unsigned fsx_xflags;
76  unsigned fsx_extsize;
77  unsigned fsx_nextents;
78  unsigned fsx_projid;
79  unsigned fsx_cowextsize;
80  char fsx_pad[8];
81};
82
83static char *strioctl(int i)
84{
85  switch (i) {
86    C(FS_IOC_FSGETXATTR);
87    C(FS_IOC_FSSETXATTR);
88    C(FS_IOC_GETFLAGS);
89    C(FS_IOC_GETVERSION);
90    C(FS_IOC_SETFLAGS);
91    C(FS_IOC_SETVERSION);
92    C(SIOCGIFADDR);
93    C(SIOCGIFBRDADDR);
94    C(SIOCGIFCONF);
95    C(SIOCGIFDSTADDR);
96    C(SIOCGIFFLAGS);
97    C(SIOCGIFHWADDR);
98    C(SIOCGIFMAP);
99    C(SIOCGIFMTU);
100    C(SIOCGIFNETMASK);
101    C(SIOCGIFTXQLEN);
102    C(TCGETS);
103    C(TCSETS);
104    C(TIOCGWINSZ);
105    C(TIOCSWINSZ);
106  }
107  sprintf(toybuf, "%#x", i);
108  return toybuf;
109}
110
111// TODO: move to lib, implement errno(1)?
112static char *strerrno(int e)
113{
114  switch (e) {
115    // uapi errno-base.h
116    C(EPERM);
117    C(ENOENT);
118    C(ESRCH);
119    C(EINTR);
120    C(EIO);
121    C(ENXIO);
122    C(E2BIG);
123    C(ENOEXEC);
124    C(EBADF);
125    C(ECHILD);
126    C(EAGAIN);
127    C(ENOMEM);
128    C(EACCES);
129    C(EFAULT);
130    C(ENOTBLK);
131    C(EBUSY);
132    C(EEXIST);
133    C(EXDEV);
134    C(ENODEV);
135    C(ENOTDIR);
136    C(EISDIR);
137    C(EINVAL);
138    C(ENFILE);
139    C(EMFILE);
140    C(ENOTTY);
141    C(ETXTBSY);
142    C(EFBIG);
143    C(ENOSPC);
144    C(ESPIPE);
145    C(EROFS);
146    C(EMLINK);
147    C(EPIPE);
148    C(EDOM);
149    C(ERANGE);
150    // uapi errno.h
151    C(EDEADLK);
152    C(ENAMETOOLONG);
153    C(ENOLCK);
154    C(ENOSYS);
155    C(ENOTEMPTY);
156    C(ELOOP);
157    C(ENOMSG);
158    // ...etc; fill in as we see them in practice?
159  }
160  sprintf(toybuf, "%d", e);
161  return toybuf;
162}
163
164#undef C
165
166static void xptrace(int req, pid_t pid, void *addr, void *data)
167{
168  if (ptrace(req, pid, addr, data)) perror_exit("ptrace pid %d", pid);
169}
170
171static void get_regs()
172{
173  xptrace(PTRACE_GETREGS, TT.pid, 0, TT.regs);
174}
175
176static void ptrace_struct(long addr, void *dst, size_t bytes)
177{
178  int offset = 0, i;
179  long v;
180
181  for (i=0; i<bytes; i+=sizeof(long)) {
182    errno = 0;
183    v = ptrace(PTRACE_PEEKDATA, TT.pid, addr + offset);
184    if (errno) perror_exit("PTRACE_PEEKDATA failed");
185    memcpy(dst + offset, &v, sizeof(v));
186    offset += sizeof(long);
187  }
188}
189
190// TODO: this all relies on having the libc structs match the kernel structs,
191// which isn't always true for glibc...
192static void print_struct(long addr)
193{
194  if (!addr) { // All NULLs look the same...
195    fprintf(stderr, "NULL");
196    while (*TT.fmt != '}') ++TT.fmt;
197    ++TT.fmt;
198  } else if (strstart(&TT.fmt, "ifreq}")) {
199    struct ifreq ir;
200
201    ptrace_struct(addr, &ir, sizeof(ir));
202    // TODO: is this always an ioctl? use TT.regs[REG_ORDER[1]] to work out what to show.
203    fprintf(stderr, "{...}");
204  } else if (strstart(&TT.fmt, "fsxattr}")) {
205    struct fsxattr fx;
206
207    ptrace_struct(addr, &fx, sizeof(fx));
208    fprintf(stderr, "{fsx_xflags=%#x, fsx_extsize=%d, fsx_nextents=%d, "
209        "fsx_projid=%d, fsx_cowextsize=%d}", fx.fsx_xflags, fx.fsx_extsize,
210        fx.fsx_nextents, fx.fsx_projid, fx.fsx_cowextsize);
211  } else if (strstart(&TT.fmt, "long}")) {
212    long l;
213
214    ptrace_struct(addr, &l, sizeof(l));
215    fprintf(stderr, "%ld", l);
216  } else if (strstart(&TT.fmt, "longx}")) {
217    long l;
218
219    ptrace_struct(addr, &l, sizeof(l));
220    fprintf(stderr, "%#lx", l);
221  } else if (strstart(&TT.fmt, "rlimit}")) {
222    struct rlimit rl;
223
224    ptrace_struct(addr, &rl, sizeof(rl));
225    fprintf(stderr, "{rlim_cur=%lld, rlim_max=%lld}",
226        (long long)rl.rlim_cur, (long long)rl.rlim_max);
227  } else if (strstart(&TT.fmt, "sigset}")) {
228    long long ss;
229    int i;
230
231    ptrace_struct(addr, &ss, sizeof(ss));
232    fprintf(stderr, "[");
233    for (i=0; i<64;++i) {
234      // TODO: use signal names, fix spacing
235      if (ss & (1ULL<<i)) fprintf(stderr, "%d ", i);
236    }
237    fprintf(stderr, "]");
238  } else if (strstart(&TT.fmt, "stat}")) {
239    struct stat sb;
240
241    ptrace_struct(addr, &sb, sizeof(sb));
242    // TODO: decode IFMT bits in st_mode
243    if (FLAG(v)) {
244      // TODO: full atime/mtime/ctime dump.
245      fprintf(stderr, "{st_dev=makedev(%#x, %#x), st_ino=%ld, st_mode=%o, "
246          "st_nlink=%ld, st_uid=%d, st_gid=%d, st_blksize=%ld, st_blocks=%ld, "
247          "st_size=%lld, st_atime=%ld, st_mtime=%ld, st_ctime=%ld}",
248          dev_major(sb.st_dev), dev_minor(sb.st_dev), sb.st_ino, sb.st_mode,
249          sb.st_nlink, sb.st_uid, sb.st_gid, sb.st_blksize, sb.st_blocks,
250          (long long)sb.st_size, sb.st_atime, sb.st_mtime, sb.st_ctime);
251    } else {
252      fprintf(stderr, "{st_mode=%o, st_size=%lld, ...}", sb.st_mode,
253        (long long)sb.st_size);
254    }
255  } else if (strstart(&TT.fmt, "termios}")) {
256    struct termios to;
257
258    ptrace_struct(addr, &to, sizeof(to));
259    fprintf(stderr, "{c_iflag=%#lx, c_oflag=%#lx, c_cflag=%#lx, c_lflag=%#lx}",
260        (long)to.c_iflag, (long)to.c_oflag, (long)to.c_cflag, (long)to.c_lflag);
261  } else if (strstart(&TT.fmt, "timespec}")) {
262    struct timespec ts;
263
264    ptrace_struct(addr, &ts, sizeof(ts));
265    fprintf(stderr, "{tv_sec=%lld, tv_nsec=%lld}",
266        (long long)ts.tv_sec, (long long)ts.tv_nsec);
267  } else if (strstart(&TT.fmt, "winsize}")) {
268    struct winsize ws;
269
270    ptrace_struct(addr, &ws, sizeof(ws));
271    fprintf(stderr, "{ws_row=%hu, ws_col=%hu, ws_xpixel=%hu, ws_ypixel=%hu}",
272        ws.ws_row, ws.ws_col, ws.ws_xpixel, ws.ws_ypixel);
273  } else abort();
274}
275
276static void print_ptr(long addr)
277{
278  if (!addr) fprintf(stderr, "NULL");
279  else fprintf(stderr, "0x%lx", addr);
280}
281
282static void print_string(long addr)
283{
284  long offset = 0, total = 0;
285  int done = 0, i;
286
287  fputc('"', stderr);
288  while (!done) {
289    errno = 0;
290    long v = ptrace(PTRACE_PEEKDATA, TT.pid, addr + offset);
291    if (errno) return;
292    memcpy(toybuf, &v, sizeof(v));
293    for (i=0; i<sizeof(v); ++i) {
294      if (!toybuf[i]) {
295        // TODO: handle the case of dumping n bytes (e.g. read()/write()), not
296        // just NUL-terminated strings.
297        done = 1;
298        break;
299      }
300      if (isprint(toybuf[i])) fputc(toybuf[i], stderr);
301      else {
302        // TODO: reuse an existing escape function.
303        fputc('\\', stderr);
304        if (toybuf[i] == '\n') fputc('n', stderr);
305        else if (toybuf[i] == '\r') fputc('r', stderr);
306        else if (toybuf[i] == '\t') fputc('t', stderr);
307        else fprintf(stderr, "x%2.2x", toybuf[i]);
308      }
309      if (++total >= TT.s) {
310        done = 1;
311        break;
312      }
313    }
314    offset += sizeof(v);
315  }
316  fputc('"', stderr);
317}
318
319static void print_bitmask(int bitmask, long v, char *zero, ...)
320{
321  va_list ap;
322  int first = 1;
323
324  if (!v && zero) {
325    fprintf(stderr, "%s", zero);
326    return;
327  }
328
329  va_start(ap, zero);
330  for (;;) {
331    int this = va_arg(ap, int);
332    char *name;
333
334    if (!this) break;
335    name = va_arg(ap, char*);
336    if (bitmask) {
337      if (v & this) {
338        fprintf(stderr, "%s%s", first?"":"|", name);
339        first = 0;
340        v &= ~this;
341      }
342    } else {
343      if (v == this) {
344        fprintf(stderr, "%s", name);
345        v = 0;
346        break;
347      }
348    }
349  }
350  va_end(ap);
351  if (v) fprintf(stderr, "%s%#lx", first?"":"|", v);
352}
353
354static void print_flags(long v)
355{
356#define C(n) n, #n
357  if (strstart(&TT.fmt, "access|")) {
358    print_bitmask(1, v, "F_OK", C(R_OK), C(W_OK), C(X_OK), 0);
359  } else if (strstart(&TT.fmt, "mmap|")) {
360    print_bitmask(1, v, 0, C(MAP_SHARED), C(MAP_PRIVATE), C(MAP_32BIT),
361        C(MAP_ANONYMOUS), C(MAP_FIXED), C(MAP_GROWSDOWN), C(MAP_HUGETLB),
362        C(MAP_DENYWRITE), 0);
363  } else if (strstart(&TT.fmt, "open|")) {
364    print_bitmask(1, v, "O_RDONLY", C(O_WRONLY), C(O_RDWR), C(O_CLOEXEC),
365        C(O_CREAT), C(O_DIRECTORY), C(O_EXCL), C(O_NOCTTY), C(O_NOFOLLOW),
366        C(O_TRUNC), C(O_ASYNC), C(O_APPEND), C(O_DSYNC), C(O_EXCL),
367        C(O_NOATIME), C(O_NONBLOCK), C(O_PATH), C(O_SYNC),
368        0x4000, "O_DIRECT", 0x8000, "O_LARGEFILE", 0x410000, "O_TMPFILE", 0);
369  } else if (strstart(&TT.fmt, "prot|")) {
370    print_bitmask(1,v,"PROT_NONE",C(PROT_READ),C(PROT_WRITE),C(PROT_EXEC),0);
371  } else abort();
372}
373
374static void print_alternatives(long v)
375{
376  if (strstart(&TT.fmt, "rlimit^")) {
377    print_bitmask(0, v, "RLIMIT_CPU", C(RLIMIT_FSIZE), C(RLIMIT_DATA),
378        C(RLIMIT_STACK), C(RLIMIT_CORE), C(RLIMIT_RSS), C(RLIMIT_NPROC),
379        C(RLIMIT_NOFILE), C(RLIMIT_MEMLOCK), C(RLIMIT_AS), C(RLIMIT_LOCKS),
380        C(RLIMIT_SIGPENDING), C(RLIMIT_MSGQUEUE), C(RLIMIT_NICE),
381        C(RLIMIT_RTPRIO), C(RLIMIT_RTTIME), 0);
382  } else if (strstart(&TT.fmt, "seek^")) {
383    print_bitmask(0, v, "SEEK_SET", C(SEEK_CUR), C(SEEK_END), C(SEEK_DATA),
384        C(SEEK_HOLE), 0);
385  } else if (strstart(&TT.fmt, "sig^")) {
386    print_bitmask(0, v, "SIG_BLOCK", C(SIG_UNBLOCK), C(SIG_SETMASK), 0);
387  } else abort();
388}
389
390static void print_args()
391{
392  int i;
393
394  // Loop through arguments and print according to format string
395  for (i = 0; *TT.fmt; i++, TT.arg++) {
396    long v = TT.regs[REG_ORDER[TT.arg]];
397    char *s, ch;
398
399    if (i) fprintf(stderr, ", ");
400    switch (ch = *TT.fmt++) {
401      case 'd': fprintf(stderr, "%ld", v); break; // decimal
402      case 'f': if ((int) v == AT_FDCWD) fprintf(stderr, "AT_FDCWD");
403                else fprintf(stderr, "%ld", v);
404                break;
405      case 'i': fprintf(stderr, "%s", strioctl(v)); break; // decimal
406      case 'm': fprintf(stderr, "%03o", (unsigned) v); break; // mode for open()
407      case 'o': fprintf(stderr, "%ld", v); break; // off_t
408      case 'p': print_ptr(v); break;
409      case 's': print_string(v); break;
410      case 'S': // The libc-reserved signals aren't known to num_to_sig().
411                // TODO: use an strace-only routine for >= 32?
412                if (!(s = num_to_sig(v))) fprintf(stderr, "%ld", v);
413                else fprintf(stderr, "SIG%s", s);
414                break;
415      case 'z': fprintf(stderr, "%zd", v); break; // size_t
416      case 'x': fprintf(stderr, "%lx", v); break; // hex
417
418      case '{': print_struct(v); break;
419      case '|': print_flags(v); break;
420      case '^': print_alternatives(v); break;
421
422      case '/': return; // Separates "enter" and "exit" arguments.
423
424      default: fprintf(stderr, "?%c<0x%lx>", ch, v); break;
425    }
426  }
427}
428
429static void print_enter(void)
430{
431  char *name;
432
433  get_regs();
434  TT.syscall = TT.regs[REG_ORDER[6]];
435  if (TT.syscall == __NR_ioctl) {
436    name = "ioctl";
437    switch (TT.regs[REG_ORDER[1]]) {
438      case FS_IOC_FSGETXATTR: TT.fmt = "fi/{fsxattr}"; break;
439      case FS_IOC_FSSETXATTR: TT.fmt = "fi{fsxattr}"; break;
440      case FS_IOC_GETFLAGS: TT.fmt = "fi/{longx}"; break;
441      case FS_IOC_GETVERSION: TT.fmt = "fi/{long}"; break;
442      case FS_IOC_SETFLAGS: TT.fmt = "fi{long}"; break;
443      case FS_IOC_SETVERSION: TT.fmt = "fi{long}"; break;
444      //case SIOCGIFCONF: struct ifconf
445      case SIOCGIFADDR:
446      case SIOCGIFBRDADDR:
447      case SIOCGIFDSTADDR:
448      case SIOCGIFFLAGS:
449      case SIOCGIFHWADDR:
450      case SIOCGIFMAP:
451      case SIOCGIFMTU:
452      case SIOCGIFNETMASK:
453      case SIOCGIFTXQLEN: TT.fmt = "fi/{ifreq}"; break;
454      case SIOCSIFADDR:
455      case SIOCSIFBRDADDR:
456      case SIOCSIFDSTADDR:
457      case SIOCSIFFLAGS:
458      case SIOCSIFHWADDR:
459      case SIOCSIFMAP:
460      case SIOCSIFMTU:
461      case SIOCSIFNETMASK:
462      case SIOCSIFTXQLEN: TT.fmt = "fi{ifreq}"; break;
463      case TCGETS: TT.fmt = "fi/{termios}"; break;
464      case TCSETS: TT.fmt = "fi{termios}"; break;
465      case TIOCGWINSZ: TT.fmt = "fi/{winsize}"; break;
466      case TIOCSWINSZ: TT.fmt = "fi{winsize}"; break;
467      default:
468        TT.fmt = (TT.regs[REG_ORDER[0]]&1) ? "fip" : "fi/p";
469        break;
470    }
471  } else switch (TT.syscall) {
472#define SC(n,f) case __NR_ ## n: name = #n; TT.fmt = f; break
473    SC(access, "s|access|");
474    SC(arch_prctl, "dp");
475    SC(brk, "p");
476    SC(close, "d");
477    SC(connect, "fpd"); // TODO: sockaddr
478    SC(dup, "f");
479    SC(dup2, "ff");
480    SC(dup3, "ff|open|");
481    SC(execve, "spp");
482    SC(exit_group, "d");
483    SC(fcntl, "fdp"); // TODO: probably needs special case
484    SC(fstat, "f/{stat}");
485    SC(futex, "pdxppx");
486    SC(getdents64, "dpz");
487    SC(geteuid, "");
488    SC(getuid, "");
489
490    SC(getxattr, "sspz");
491    SC(lgetxattr, "sspz");
492    SC(fgetxattr, "fspz");
493
494    SC(lseek, "fo^seek^");
495    SC(lstat, "s/{stat}");
496    SC(mmap, "pz|prot||mmap|fx");
497    SC(mprotect, "pz|prot|");
498    SC(mremap, "pzzdp"); // TODO: flags
499    SC(munmap, "pz");
500    SC(nanosleep, "{timespec}/{timespec}");
501    SC(newfstatat, "fs/{stat}d");
502    SC(open, "sd|open|m");
503    SC(openat, "fs|open|m");
504    SC(poll, "pdd");
505    SC(prlimit64, "d^rlimit^{rlimit}/{rlimit}");
506    SC(read, "d/sz");
507    SC(readlinkat, "s/sz");
508    SC(rt_sigaction, "Sppz");
509    SC(rt_sigprocmask, "^sig^{sigset}/{sigset}z");
510    SC(set_robust_list, "pd");
511    SC(set_tid_address, "p");
512    SC(socket, "ddd"); // TODO: flags
513    SC(stat, "s/{stat}");
514    SC(statfs, "sp");
515    SC(sysinfo, "p");
516    SC(umask, "m");
517    SC(uname, "p");
518    SC(write, "dsz");
519    default:
520      sprintf(name = toybuf, "SYS_%ld", TT.syscall);
521      TT.fmt = "pppppp";
522      break;
523  }
524
525  fprintf(stderr, "%s(", name);
526  TT.arg = 0;
527  print_args();
528}
529
530static void print_exit(void)
531{
532  long result;
533
534  get_regs();
535  result = TT.regs[REG_ORDER[7]];
536  if (*TT.fmt) print_args();
537  fprintf(stderr, ") = ");
538  if (result >= -4095UL)
539    fprintf(stderr, "-1 %s (%s)", strerrno(-result), strerror(-result));
540  else if (TT.syscall==__NR_mmap || TT.syscall==__NR_brk) print_ptr(result);
541  else fprintf(stderr, "%ld", result);
542  fputc('\n', stderr);
543}
544
545static int next(void)
546{
547  int status;
548
549  for (;;) {
550    ptrace(PTRACE_SYSCALL, TT.pid, 0, 0);
551    waitpid(TT.pid, &status, 0);
552    // PTRACE_O_TRACESYSGOOD sets bit 7 to indicate a syscall.
553    if (WIFSTOPPED(status) && WSTOPSIG(status) & 0x80) return 1;
554    if (WIFEXITED(status)) return 0;
555    fprintf(stderr, "[stopped %d (%x)]\n", status, WSTOPSIG(status));
556  }
557}
558
559static void strace_detach(int s)
560{
561  xptrace(PTRACE_DETACH, TT.pid, 0, 0);
562  exit(1);
563}
564
565void strace_main(void)
566{
567  int status;
568
569  if (!FLAG(s)) TT.s = 32;
570
571  if (FLAG(p)) {
572    if (*toys.optargs) help_exit("No arguments with -p");
573    TT.pid = TT.p;
574    signal(SIGINT, strace_detach);
575    // TODO: PTRACE_SEIZE instead?
576    xptrace(PTRACE_ATTACH, TT.pid, 0, 0);
577  } else {
578    if (!*toys.optargs) help_exit("Needs 1 argument");
579    TT.pid = xfork();
580    if (!TT.pid) {
581      errno = 0;
582      ptrace(PTRACE_TRACEME);
583      if (errno) perror_exit("PTRACE_TRACEME failed");
584      raise(SIGSTOP);
585      toys.stacktop = 0;
586      xexec(toys.optargs);
587    }
588  }
589
590  do {
591    waitpid(TT.pid, &status, 0);
592  } while (!WIFSTOPPED(status));
593
594  // TODO: PTRACE_O_TRACEEXIT
595  // TODO: PTRACE_O_TRACEFORK/PTRACE_O_TRACEVFORK/PTRACE_O_TRACECLONE for -f.
596  errno = 0;
597  ptrace(PTRACE_SETOPTIONS, TT.pid, 0, PTRACE_O_TRACESYSGOOD);
598  if (errno) perror_exit("PTRACE_SETOPTIONS PTRACE_O_TRACESYSGOOD failed");
599
600  // TODO: real strace swallows the failed execve()s if it started the child
601
602  for (;;) {
603    if (!next()) break;
604    print_enter();
605    if (!next()) break;
606    print_exit();
607  }
608
609  // TODO: support -f and keep track of children.
610  waitpid(TT.pid, &status, 0);
611  if (WIFEXITED(status))
612    fprintf(stderr, "+++ exited with %d +++\n", WEXITSTATUS(status));
613  if (WIFSTOPPED(status))
614    fprintf(stderr, "+++ stopped with %d +++\n", WSTOPSIG(status));
615}
616