为什么这个ptrace程序显示系统调用返回了-38?

9

这与此问题相同,只是我正在运行execl("/bin/ls", "ls",NULL);

结果显然是错误的,因为每个系统调用都返回-38

[user@ test]# ./test_trace 
syscall 59 called with rdi(0), rsi(0), rdx(0)
syscall 12 returned with -38
syscall 12 called with rdi(0), rsi(0), rdx(140737288485480)
syscall 9 returned with -38
syscall 9 called with rdi(0), rsi(4096), rdx(3)
syscall 9 returned with -38
syscall 9 called with rdi(0), rsi(4096), rdx(3)
syscall 21 returned with -38
syscall 21 called with rdi(233257948048), rsi(4), rdx(233257828696)
...

请问有人知道原因吗?

更新

现在的问题是:

execve called with rdi(4203214), rsi(140733315680464), rdx(140733315681192)
execve returned with 0
execve returned with 0
...

execve返回了0 两次,为什么?


测试跟踪的源代码在哪里? - Matt Joiner
可能与此类似:https://dev59.com/01XTa4cB1Zd3GeqP4sTQ,但这里更好,因为它有代码。 - Ciro Santilli OurBigBook.com
3个回答

14
该代码没有考虑到来自子进程的exec通知,因此会将系统调用入口处理为系统调用退出,将系统调用退出处理为系统调用入口。这就是为什么你会看到"syscall 12 returned"在"syscall 12 called"之前等等。(-38ENOSYS,作为默认返回值放入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)
{
    /* Request tracing by parent: */
    ptrace(PTRACE_TRACEME, 0, NULL, NULL);

    /* Stop before doing anything, giving parent a chance to catch the exec: */
    kill(getpid(), SIGSTOP);

    /* Now exec: */
    execl("/bin/ls", "ls", NULL);
}

static void parent(pid_t child_pid)
{
    int status;
    long sc_number, sc_retcode;

    while (1)
    {
        /* Wait for child status to change: */
        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) {
            /* Note that there are *three* reasons why the child might stop
             * with SIGTRAP:
             *  1) syscall entry
             *  2) syscall exit
             *  3) child calls exec
             */
            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);

        /* Resume child, requesting that it stops again on syscall enter/exit
         * (in addition to any other reason why it might stop):
         */
        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非常复杂并具有微妙的怪癖”....


现在的问题是execve返回了两次0,为什么? - lexer
@new_perl:execve系统调用只会发生一次。在跟踪该系统调用时,子进程会停止三次(一次为进入,一次为退出,一次为通知已发生exec)。这就是上面示例中的三行syscall 59 - Matthew Slattery
eax 寄存器中的值 -38 不足以区分系统调用的进入和退出吗?(ptrace 手册指出:“跟踪器无法通过 syscall-enter-stop 和 syscall-exit-stop 区分它们。”) - Matheus Santana

0
您可以使用 perror 或 strerror 打印出可读性强的最后一个系统错误描述。这个错误描述会对您有很大的帮助。

0

我猜你正在检查eax或其64位等效项(可能是rax)以获取系统调用的返回代码。还有一个用于保存此寄存器的附加插槽,名为orig_eax,用于重新启动系统调用。

我深入研究了这些东西,但却找不到我的发现。以下是一些相关问题:

更新0

我再次搜索后发现我的记忆是正确的。你可以在这里的内核源代码中找到所有需要的内容(主站点已经关闭,幸运的是Torvalds现在在GitHub上镜像了Linux)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接