该代码没有考虑到来自子进程的
exec
通知,因此会将系统调用入口处理为系统调用退出,将系统调用退出处理为系统调用入口。这就是为什么你会看到"
syscall 12 returned
"在"
syscall 12 called
"之前等等。(
-38
是
ENOSYS
,作为默认返回值放入RAX中,由内核的系统调用入口代码执行。)
正如
ptrace(2)
man page所述:
PTRACE_TRACEME
指示此进程由其父进程跟踪。任何信号(除SIGKILL外)传递给此进程都将导致它停止,并通过wait()通知其父进程。此外,此进程的所有后续对exec()的调用都将导致向其发送一个SIGTRAP,使得父进程有机会在新程序开始执行之前获得控制权。[...]
你说你运行的原始代码是"与
this one相同,只是我正在运行
execl("/bin/ls", "ls", NULL);
"。嗯,显然不是这样,因为你正在使用x86_64而不是32位,并且至少改变了消息。
但是,假设您没有太多其他更改,第一次wait()
唤醒父进程时,它不是为了系统调用的进入或退出-父进程尚未执行ptrace(PTRACE_SYSCALL,...)
。相反,您会看到这个通知,表示子进程已经执行了一个exec
(在x86_64上,系统调用59是execve
)。
该代码错误地将其解释为系统调用的进入。然后它调用ptrace(PTRACE_SYSCALL,...)
,下一次父进程被唤醒时,它是为了系统调用的进入(syscall 12),但该代码报告为系统调用的退出。
请注意,在这种原始情况下,您永远不会看到execve
系统调用的进入/退出-仅看到附加的通知-因为父进程直到发生此事件后才执行ptrace(PTRACE_SYSCALL,...)
。
如果您安排代码以捕获execve
系统调用的进入/退出,您将看到您观察到的新行为。父进程将被唤醒三次:一次是由于使用ptrace(PTRACE_SYSCALL,...)
而导致的execve
系统调用进入,一次是由于使用ptrace(PTRACE_SYSCALL,...)
而导致的execve
系统调用退出,第三次是为了exec
通知(无论如何都会发生)。
这是一个完整的示例(适用于x86或x86_64),它通过先停止子进程来展示
exec
本身的行为:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/ptrace.h>
#include <sys/reg.h>
#ifdef __x86_64__
#define SC_NUMBER (8 * ORIG_RAX)
#define SC_RETCODE (8 * RAX)
#else
#define SC_NUMBER (4 * ORIG_EAX)
#define SC_RETCODE (4 * EAX)
#endif
static void child(void)
{
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
kill(getpid(), SIGSTOP);
execl("/bin/ls", "ls", NULL);
}
static void parent(pid_t child_pid)
{
int status;
long sc_number, sc_retcode;
while (1)
{
wait(&status);
if (WIFEXITED(status)) {
printf("Child exit with status %d\n", WEXITSTATUS(status));
exit(0);
}
if (WIFSIGNALED(status)) {
printf("Child exit due to signal %d\n", WTERMSIG(status));
exit(0);
}
if (!WIFSTOPPED(status)) {
printf("wait() returned unhandled status 0x%x\n", status);
exit(0);
}
if (WSTOPSIG(status) == SIGTRAP) {
sc_number = ptrace(PTRACE_PEEKUSER, child_pid, SC_NUMBER, NULL);
sc_retcode = ptrace(PTRACE_PEEKUSER, child_pid, SC_RETCODE, NULL);
printf("SIGTRAP: syscall %ld, rc = %ld\n", sc_number, sc_retcode);
} else {
printf("Child stopped due to signal %d\n", WSTOPSIG(status));
}
fflush(stdout);
ptrace(PTRACE_SYSCALL, child_pid, NULL, NULL);
}
}
int main(void)
{
pid_t pid = fork();
if (pid == 0)
child();
else
parent(pid);
return 0;
}
大致得到以下内容(这是针对64位系统调用号码不同于32位的情况;特别是 execve 是11,而不是59):
由于信号19而停止的子进程
SIGTRAP:syscall 59,rc = -38
SIGTRAP:syscall 59,rc = 0
SIGTRAP:syscall 59,rc = 0
SIGTRAP:syscall 63,rc = -38
SIGTRAP:syscall 63,rc = 0
SIGTRAP:syscall 12,rc = -38
SIGTRAP:syscall 12,rc = 5324800
...
信号19是明确的 SIGSTOP
; 子进程因上述 execve
而停止三次; 然后两次(入口和出口)进行其他系统调用。
如果您真的对 ptrace()
的所有细节感兴趣,则我所知道的最佳文档是
strace
源中的 README-linux-ptrace
文件。正如它所说,“API非常复杂并具有微妙的怪癖”....