Debugging

  • IV.1 Tracing programs
  • IV.2 What happens when the traced process receives a signal
  • IV.3 Reading/writing the process' address space
  • IV.4 Reading and writing the process' registers

IV.1 Tracing programs

MiNT provides a special pseudo-drive, u:\proc, in which processes appear as files. This drive may be used by debuggers much as the Unix /proc file system is used, although the specific details of implementation will be different.

The entry for a process in u:\proc has a name like SOMEPROG.nnn, where nnn is a 3 digit number which is the (decimal) process ID of the process, and SOMEPROG is the name of the process. When opening a process, only the process ID matters; a process does not need to know another process' name in order to open it, only its process id. So, for example,

fd = Fopen("u:\\proc\\foo.032",2);
fd = Fopen("u:\\proc\\.032",2);

and

fd = Fopen("u:\\proc\\tcsh.032",2);

will all open the process with process id #32 for reading and writing, regardless of the actual name of that process. Also, note that a process id of -1 refers to the current process, and a process id of -2 refers to the parent of the current process; thus,

fd = Fopen("u:\\proc\\.-1",2);

may always be used to open oneself.

Before a process may be debugged (or traced) it must first be marked as a traced process, and a specific process (often its parent) must be marked as the tracer, the program which will be notified when the traced process receives signals.

  • (1) If the process to be traced is started with Pexec(), and the high bit of the Pexec() mode is set, then the child process begins as a traced process automatically, with its parent as tracer. Example: childpid = Pexec(0x8000|100, "foo.prg", "", 0L);
  • (2) The traced process may indicate that it wishes to be traced by opening its own entry in u:\proc and performing an Fcntl(...PTRACESFLAGS) call which enables tracing. In this case, the child's parent will become the tracer automatically. Note that if the parent is not prepared to perform debugging functions, there could be undesireable results, so the child must be certain that the parent wishes to debug it; for example, this is the case if the child and parent are both executing from the same program image (e.g. the child was started with the Pvfork() system call). This method of indicating tracing is quite similar to Unix's ptrace(0,...) function.

Example:

if ((pid = Pvfork()) == 0) {
    short flag = 1;
    int fd;

/* here we are in the child */
/* open self; see notes above */
    fd = Fopen("u:\\proc\\a.-1",2);
/* perform Fcntl */
    Fcntl(fd, &flag, PTRACESFLAGS);
/* close self, we don't need the handle any more */
    Fclose(fd);
/* now overlay the child with a new program image */
    Pexec(200, "foo.prg", "", 0L);
}
  • (3) A process may force another process to be traced by opening that process' entry in u:\proc, and doing an Fcntl(...PTRACESFLAGS) on it. This is the only way in which to trace a process that is not the child of the debugging process.

Example:

short flag = 1;
/* open process "pid" for tracing */
sprintf(name, "u:\\proc\\a.%03d", pid);
fd = Fopen(name, 2);
Fcntl(fd, &flag, PTRACESFLAGS);
/* leave fd open so that we can read and write it... */

IV.2 What happens when the traced process receives a signal

If a traced process receives a signal (for example, a bus error), then it is placed into a STOP condition and a SIGCHLD signal is sent to the tracer. If the tracer performs a Pwait3() or Pwaitpid() system call, it can then retrieve the pid of the stopped process, and the signal which caused that process to stop. For example:

#define WIFSTOPPED(x) (((int)((x) & 0xFF) == 0x7F) && ((int)(((x) >> 8) & 0xFF) != 0))
#define WSTOPSIG(x) ((int)(((x) >> 8) & 0xFF))

unsigned long r;
/* 0x2 is a flag that indicates that we want to see
 * stopped processes
 */
r = Pwait3(0x2, 0L);
if (WIFSTOPPED(r)) {
    pid = r >> 16;
    signal = WSTOPSIG(r);
}

The tracer can then examine the stopped process' address space and registers (using the Fread(), Fwrite(), and Fcntl() system calls; see below) and/or make modifications to the stopped process' state. It can then restart that process with either the PTRACEGO, PTRACEFLOW, or PTRACESTEP Fcntl commands. The PTRACEGO command Fcntl(fd, &sig, PTRACEGO), where sig is a 16 bit integer, restarts the process. If sig == 0, then all pending signals in the traced process are cleared; otherwise, the signal represented by sig will be delivered to the process after it starts again. Normally, this signal would be the same one which caused the process to stop. Note that this second time the signal is delivered it will not cause the process to stop, but rather will cause whatever action is normally associated with the signal (for example, SIGKILL will kill the process). PTRACEFLOW and PTRACESTEP are similar to PTRACEGO, but they also set some bits in the status register of the stopped process which will cause a SIGTRAP (trace trap) signal to be raised either on the next instruction to be executed (PTRACESTEP) or the next branch or jump instruction to be executed (PTRACEFLOW; this only works on a 68030 or 68040 processor). Note that if the traced process was executing a system call at the time it stopped, the trace trap signal will not take effect until the process leaves the kernel. PTRACESTEP, then, makes it possible to single step through the traced program.

IV.3 Reading/writing the process' address space

Traditional debuggers for the ST have directly accessed the address space of the debugged process. This is undesireable because it assumes that both processes share a common address space, something which may not be the case if memory protection is enabled; also, in a virtual memory system logical addresses in the child and parent may be translated differently. The u:\proc file system may be used to avoid both of these difficulties. If the debugger opens the process to be debugged for reading and writing, it may then use Fread() and Fwrite() system calls to transfer data (e.g. breakpoint instructions) to and from the debugged process' address space. To read 100 bytes from address 0x0123456 in the address space of process 12, for example, one would do:

char buf[100];
fd = Fopen("u:\\proc\\a.012", 2);
Fseek(0x0123456L, fd, 0);
Fread(fd, 100L, buf);

Note that error checking should be performed like in any real application; for example, the Fopen() could very well fail if the process being opened is another user's process.

IV.4 Reading and writing the process' registers

The registers of the stopped process are also available for inspection. The PPROCADDR and PCTXTSIZE Fcntl() commands are used to find the address of the process context block in the traced process' address space; the registers of the process can then be read from the address space with Fread(), or written to the address space with Fwrite(). Example:

long curprocaddr;
long ctxtsize;
struct context {
    long regs[15];      /* registers d0-d7, a0-a6 */
    long usp;           /* user stack pointer */
    short sr;           /* status register */
    long pc;            /* program counter */
    long ssp;           /* supervisor stack pointer */
    long tvec;          /* GEMDOS terminate vector */
    char fstate[216];   /* internal FPU state */
    long fregs[3*8];    /* registers fp0-fp7 */
    long fctrl[3];      /* FPCR/FPSR/FPIAR */
/* there are some more, undocumented, fields, in the
 * MiNT context structure
 */
} c;

/* get the address of the process control structure */
Fcntl(fd, &curprocaddr, PPROCADDR);

/* get the size of the process context structure.
 * there are 2 of these, located immediately before
 * the process control structure. The first one
 * is the one we're interested in (the second one
 * is used by MiNT, and should never be modified).
 */
Fcntl(fd, &ctxtsize, PCTXTSIZE);

/* make curprocaddr point to the first context struct */
curprocaddr -= 2*ctxtsize;

/* now read the context structure */
Fseek(curprocaddr, fd, 0);
Fread(fd, (long)sizeof(struct context), &c);

/* the various fields of c are now set up properly */
... /* code omitted */

/* now write back the context to reflect whatever changes
 * we made
 */
Fseek(curprocaddr, fd, 0);
Fwrite(fd, (long)sizeof(struct context), &c);