Adventures with ptrace (2)
  • Transfer
On Habré already wrote about the interception of system calls using ptrace; Alex wrote about this much more detailed post, which I decided to translate.

Where to begin

Communication between the program being debugged and the debugger takes place using signals. This greatly complicates the already difficult things; for fun can read the BUGS section in man ptrace.

There are at least two different ways to start debugging:

  1. ptrace(PTRACE_TRACEME, 0, NULL, NULL)will make the parent of the current process a debugger for it. No assistance from the parent is required; manunobtrusively advises: " If you’re not looking for the trace it." (you have seen the phrase "probably shouldn't" ?), if the current process already had a debugger then the challenge will fail.
  2. ptrace(PTRACE_ATTACH, pid, NULL, NULL)will make the current process a debugger for pid. If u pidalready had a debugger, then the call will fail. The process being debugged is sent SIGSTOP, and it will not continue to work until the debugger defrosts it.

These two methods are completely independent; you can use either one or the other, but there is no point in combining them.It is important to note that PTRACE_ATTACHit does not act instantly: after a call ptrace(PTRACE_ATTACH), as a rule, a call follows waitpid(2)to wait until it PTRACE_ATTACH“works”.

You can start a child process under debugging using PTRACE_TRACEME:

staticvoidtracee(int argc, char **argv){
    if (ptrace(PTRACE_TRACEME, 0, NULL, NULL) < 0)
        die("child: ptrace(traceme) failed: %m");
    /* Остановиться и дождаться, пока отладчик отреагирует. */if (raise(SIGSTOP))
        die("child: raise(SIGSTOP) failed: %m");
    /* Запустить процесс. */
    execvp(argv[0], argv);
    /* Сюда выполнение дойти не должно. */
    die("tracee start failed: %m");
staticvoidtracer(pid_t pid){
    int status = 0;
    /* Дождаться, пока дочерний процесс сделает нас своим отладчиком. */if (waitpid(pid, &status, 0) < 0)
        die("waitpid failed: %m");
    if (!WIFSTOPPED(status) || WSTOPSIG(status) != SIGSTOP) {
        kill(pid, SIGKILL);
        die("tracer: unexpected wait status: %x", status);
    /* Если требуются дополнительные опции для ptrace, их можно задать здесь. *//*
     * Обратите внимание, что в предшествующем коде нигде
     * не указывается, что мы собирается отлаживать дочерний процесс.
     * Это не ошибка -- таков API у ptrace!
     *//* Начиная с этого момента можно использовать PTRACE_SYSCALL. */
/* (argc, argv) -- аргументы для дочернего процесса, который мы собираемся отлаживать. */voidshim_ptrace(int argc, char **argv){
    pid_t pid = fork();
    if (pid < 0)
        die("couldn't fork: %m");
    elseif (pid == 0)
        tracee(argc, argv);
    die("should never be reached");

Without a call, raise(SIGSTOP)it might turn out that it execvp(3)will be executed before the parent process is ready for it; and then the debugger actions (for example, interception of system calls) will not start from the beginning of the process.

When debugging is started, each call ptrace(PTRACE_SYSCALL, pid, NULL, NULL)will “defrost” the process being debugged until the first entry into the system call and then until the exit from the system call.

Telekinetic assembler

ptrace(PTRACE_SYSCALL)does not return any information to the debugger ; it simply promises that the process being debugged will stop twice on every system call. To get information about what is happening with the process being debugged - for example, in which particular system call it stopped - you need to crawl into a copy of its registers stored by the kernel in struct usera format dependent on a particular architecture. (For example, on x86_64, the call number will be in the field regs.orig_rax, the first parameter passed in regs.rdi, etc.) Alex comments: “It feels like writing in C an assembler code that works with the registers of the remote processor.”

Instead of the structure described in sys/user.h, it may be more convenient to use constant constants defined in sys/reg.h:

#include<sys/reg.h>/* Получить номер системного вызова. */longptrace_syscall(pid_t pid){
#ifdef __x86_64__return ptrace(PTRACE_PEEKUSER, pid, sizeof(long)*ORIG_RAX);
#else// ...#endif
/* Получить аргумент системного вызова по номеру. */uintptr_t ptrace_argument(pid_t pid, int arg)
#ifdef __x86_64__int reg = 0;
    switch (arg) {
            reg = RDI;
            reg = RSI;
            reg = RDX;
            reg = R10;
            reg = R8;
            reg = R9;
    return ptrace(PTRACE_PEEKUSER, pid, sizeof(long) * reg, NULL);
#else// ...#endif

At the same time, two stops of the process being debugged — at the entrance to the system call and at the exit from it — are in no way different from the point of view of the debugger; so the debugger must remember by itself what state each of the processes being debugged is in: if there are several, then no one guarantees that a pair of signals from one process comes in a row.


One of the options ptrace, namely PTRACE_O_TRACECLONE, ensure that all children are debugging process will be automatically taken at the time of debugging output from fork(2). An additional subtle point here is that descendants, taken under debugging, become “pseudo-children” of the debugger, and waitpidwill respond not only to the stop of “immediate children”, but also to the stop of debugged “pseudo-children”. Man warns about it: «Setting the flag WCONTINUED the when calling the waitpid (2) is not recommended: the" continued The "state is The per-process and consuming IT CAN Confuse the real parent of the Tracee.» - that is, Pseudo-children have two parents who can wait for them to stop. For a debugger programmer, this means thatwaitpid(-1) will wait for the shutdown not only of the immediate children, but of any of the processes being debugged.


(Bonus content from the translator: this information is not in the English-language article)
As it was said at the very beginning, communication between the program being debugged and the debugger takes place with the help of signals. A process receivesSIGSTOPwhen a debugger is connected to it, and thenSIGTRAPevery time something “interesting” occurs in the process being debugged — for example, a system call or an external signal. The debugger, in turn, getsSIGCHLDevery time one of the processes being debugged (not necessarily the immediate child) “freezes” or “razmesa”.

"Defrosting" debugged process is carried out by callingptrace(PTRACE_SYSCALL)(before the first signal or a system call) orptrace(PTRACE_CONT)(before the first signal). When signalsSIGSTOP/SIGCONTare also used for purposes not related to debugging, then ptraceproblems may arise: if the debugger “unfreezes” the debugged process that received SIGSTOP, then from the outside it will look as if the signal was ignored; if the debugger does not “defrost” the process being debugged, then the external SIGCONTone will not be able to “unfreeze” it.

Now the most interesting: Linux forbids processes to debug themselves , but does not prevent the creation of cycles, when the parent and the child debug each other. In this case, when one of the processes receives any external signal, it “freezes” in SIGTRAP- then the second process is sent SIGCHLD, and that one also “freezes” in the SIGTRAP. Pulling such "co-debuggers" from deadlock is impossible by sendingSIGCONTfrom the outside; the only way is to kill ( SIGKILL) the child, then the parent will go out from under the debug and “freeze”. (If you kill a parent, the child will die with him.) If the child turns on the option PTRACE_O_EXITKILL, then with his death the parent that he is debugging will die.

Now you know how to implement a couple of processes that, when receiving any signal, both of them hang up forever and die only together. Why it may be necessary in practice, I will not explain :-)

Also popular now: