I made some significant improvements to my userspace ptrace prober
today. The new way it works is pretty interesting.
There are essentially two major changes. The first is that instead of single
stepping through the program, I am now using the x86 TRAP facility (a.k.a.
3) to more efficiently pause at the right point. This is the same mechanism
that a debugger like GDB uses to implement break points. The idea is pretty
simple. You insert the value
0xcc at the instruction that you want to break
on. Then when the program hits that instruction the kernel will deliver
SIGTRAP to the process. From the tracer process you can use
waitid() to wait until the child is delivered
that point you can do whatever you need to do.
The reason that using a trap is more efficient is because in single stepping
mode the process gets interrupted after literally every x86 instruction it
executes, which imposes a significant overhead as you might imagine. When using
a trap there is essentially no overhead, the only overhead is when you actually
process the trap.
The second problem that I had was that I was encoding the CALL instruction to
fprintf, and the format string data, directly into the code segment of the
executable at the current instruction pointer. This actually works fine, but
there’s one caveat. If you had run my tracer exactly when the tracee was in the
middle of executing
fprintf() (or a routine called by
fprintf()) then the
fprintf() code would actually be corrupted and the program would crash.
Here’s how the new tracer works:
- Attach to the tracee as usual.
- Insert a
SYSCALL instruction at
%rip (this takes two bytes,
- Insert a
JMP %rax instruction right after
SYSCALL (this is also two
- Modify the registers to encode the arguments to the
mmap(2) system call to
allocate a 4096 byte anonymous page that is marked
PROT_READ | PROT_EXEC.
PTRACE_SINGLESTEP to execute the system call.
- Verify that the
mmap(2) call worked by reading the address out of
and checking that it did not return -1.
PTRACE_SINGLESTEP to execute the
JUMP %rax instruction we already
poked into memory; conveniently, the address is already in
%rax since the
kernel returns system call.
- Copy a
CALL instruction into the mmap’ed region followed by a
- Copy the
fprintf() format string right after the
- Modify the registers to hold the values in
%rdx needed to do the
PTRACE_CONT and then
wait() for the process to return to the
- Replace the
TRAP with a
JMP %rax to return back to the original value of
PTRACE_SINGLESTEP to execute the jump.
- The current instruction will still be the
SYSCALL we encoded earlier for
mmap(2) system call.
- Set up the registers to hold the right arguments to
munmap(2) to unmap the
page we allocated earlier.
- Execute the
munmap(2) system call by using
- Verify that the
munmap(2) call worked by checking that
%rax is 0.
- Poke the value at the original
%rip to replace it with the single word we
previously overwrote with the
JMP %rax instructions.
- Restore the original register state.
PTRACE_DETACH to detach from the process.
This new tracer is both faster and more correct than the previous
implementation. As before, you can find the code on GitHub at