gVisor ptrace平台学习

本文是我将gVsior移植到RISC-V架构过程中的学习记录,移植项目在我的Github上:TeddyNight/gvisor-riscv。这篇文章仅代表我写完文章以前对gVisor的认识;同时我深知自己对gVisor的认识还不充分,对Linux等相关知识仍然知之甚少,故保留内省的权利,当然也欢迎朋友们交流和批评指正。

Ptrace是一个系统调用,是类Unix系统上通过原生调试器监测被调试进程的主要机制,使用Ptrace,跟踪进程可以暂停被跟踪进程,检查和设置寄存器和内存,监视系统调用,拦截系统调用。

PTRACE在Linux RISC-V内核中的支持

  • 在这个commit:https://github.com/torvalds/linux/commit/f0bddf50586da81360627a772be0e355b62f071e之后,riscv kernel entry部分的代码切换到了指令集架构无关的通用代码中
  • kernel/entry/common.c中

    • syscall_enter_from_user_mode() 函数最终会调用 syscall_trace_enter(),该函数依次处理 Syscall User Dispatch、ptrace 和 seccomp
    • syscall_trace_enter()中处理ptrace部分,会判断syscall work的option是否是SYSCALL_WORK_SYSCALL_EMU

      • 说明RISC-V上ptrace是支持emu的,不再需要额外的移植工作

PTRACE平台移植到RISC-V上需要完成的工作

  • arch/vdso/time/signal 等 package里架构相关代码的编写
  • ptrace平台pkg/sentry/platform/ptrace中架构相关代码的编写

New()

New()首先会调用stubInit()初始化Stub代码的加载,透过MMAP将Stub加载到内存中的另一个位置

Stub代码是与指令集架构相关的,目前已经完成了Stub代码的编写

需要加载Stub代码到另外的内存区域是为了加载和运行用户程序时能保持原系统中用户进程内存空间布局,不影响用户程序代码的加载和使用

New()中随后会newSubprocess()透过fork创建一个子进程,这个子进程的第一个子线程运行stub,作为master,负责后续其他子进程的创建,因而是全局资源的管理者

stubInit()

stubInit()在进程地址空间walk,调用MMAP在地址空间的指定起始地址中创建一个匿名映射,直到MMAP返回的起始地址与指定的起始地址相同

最初的时候gVisor运行不起来,通过STRACE追踪系统调用发现一直在MMAP并且返回的地址和预期的地址相差很远。

后来排查Linux内核代码与MMAP相关的部分发现了这个patch:[LKML: Charlie Jenkins: [PATCH 0/3] riscv: mm: Use hint address in mmap if available](https://lkml.org/lkml/2024/1/29/1473)

也就是说这里MMAP一直不停重试的原因是MMAP并不会按照提示的地址去创建映射,更换内核(v6.9-rc1之后版本包含该patch)后问题解决

NewSubprocess()

New()和后续NewAddressSpace()都会调用这个函数获取一个可用的子进程,这个函数会首先尝试从进程池里获取一个进程,如果没有则新建一个

NewSubprocess()新建进程过程会启动一个goroutine

这个goroutine先创建一个子进程,负责attach子进程中的一个线程,并在一个循环体中接收后续在子进程中创建子线程的请求

New()中第一个stub线程的创建创建线程调用的是createStub()完成seccomp初始化等步骤最终走到forkStub()调用clone创建子进程,此时子进程中调用stubCall运行stub程序,stub程序调用SIGSTOP通知sentry可用attach了,sentry随后grabInitRegs()将此时的寄存器上下文保存到initRegs,rewind pc到ecall的位置便于后续syscall的注入,便完成了第一个线程的attach

后续NewAddressSpace()创建子进程调用的是globalPool.master.createStub(),globalPool.master是第一个stub线程,globalPool.master.createStub()注入syscall时会通过initChildProcessPPID()设置寄存器S7=master进程pid,S8=1,之后注入syscall执行,子进程stub重新执行一次begin段,SIGSTOP之后sentry完成attach

// stub_riscv64.s
begin:
        // N.B. This loop only executes in the context of a single-threaded
        // fork child.

        MOV $SYS_PRCTL, A7
        MOV $PR_SET_PDEATHSIG, A0
        MOV $SIGKILL, A1
        ECALL

        BNE ZERO, A0, error

        // If the parent already died before we called PR_SET_DEATHSIG then
        // we'll have an unexpected PPID.
        MOV $SYS_GETPPID, A7
        ECALL

        BNE A0, S7, parent_dead

        MOV $SYS_GETPID, A7
        ECALL

        BLT A0, ZERO, error

        MOV ZERO, S8

        // SIGSTOP to wait for attach.
        //
        // The SYSCALL instruction will be used for future syscall injection by
        // thread.syscall.
        MOV $SYS_KILL, A7
        MOV $SIGSTOP, A1
        ECALL

        // The sentry sets S8 to 1 when creating stub process.
        MOV $1, T1
        BEQ T1, S8, clone

done:
        // Notify the Sentry that syscall exited.
        EBREAK
        JMP done // Be paranoid
clone:
        // subprocess.createStub clones a new stub process that is untraced,
        // thus executing this code. We setup the PDEATHSIG before SIGSTOPing
        // ourselves for attach by the tracer.
        //
        // S7 has been updated with the expected PPID.
        BEQ ZERO, A0, begin

        // The clone system call returned a non-zero value.
        JMP done

Switch()

Switch()负责在给定的addressSpace中运行指定的context

Switch()拿到addressSpace中创建好的新进程s,调用s.switchApp()

switchToApp()中首先调用s.sysemuThreads.lookupOrCreate()在sysemuThreads线程池中查找或新建一个与当前线程绑定的sysemuThread线程,如果不存在则会通过channel通知NewSubprocess()创建的goroutine通过向进程的stub线程注入一个clone syscall创建线程

goroutine收到请求之后,调用firstThread.clone(),向stub线程注入一个clone syscall,返回一个包含新线程tid的thread对象

之后SwitchToApp()设置thread的寄存器,执行PTRACE调用启动线程,sentry wait直到线程调用syscall触发SIGTRAP线程停止执行,syscall交由sentry进行处理,处理完成后重新Switch恢复线程的执行

标签: none

添加新评论