LWN:在另一个进程的地址空间内执行代码!
共 2758字,需浏览 6分钟
·
2021-05-01 21:06
关注了就能看到更多这么棒的文章哦~
Running code within another process's address space
By Jonathan Corbet
April 16, 2021
DeepL assisted translation
https://lwn.net/Articles/852662/
描述一个进程的时候,总会提到它的关键资源之一:地址空间(address space),这是一组映射关系用来决定任意一段内存地址(memory address)在该进程中是如何使用的。通常来说地址空间对它所属的进程来说是私有的,但在有些情况下,一个进程需要对另一个进程的内存区域进行修改,比如交互式的 debugger 调试器这种情况。ptrace()系统调用使得这种行为变得可行了,但还是很慢,有时候不那么容易使用,所以长期以来人们一直在寻找其他更好的方案。Andrei Vagin 最近提出的 process_vm_exec() 供大家 review,就是希望作为一种可能的解决方案。
事实上,对于某些场景来说已经有了很好的替代 ptrace()的方案。在 2011 年,3.2 版本内核中合入了一个名为 cross-memory attach system calls 的方案,增加的系统调用名为 process_vm_readv()和 process_vm_writev()。正如它们的名字所示,它们允许一个进程从另一个进程的 memory 中进行读取和写入。这些系统调用满足了许多场景下的需求,但有时候需要对另一个进程的地址空间进行更多的访问,这时就显得不够用了。看起来除了在目标进程地址空间内运行代码之外,没有其他选择。
Vagin 的 patch set 给出了几个例子,来说明这种访问会很有用。User-mode kernel(用户模式 Linux kernel)中,比如User-mode Linux 和 gVisor,必须能够拦截由沙盒中的进程发出的系统调用,并可能需要在该进程的地址空间内运行这段代码。Checkpoint/Restore in User space(用户空间中快照以及恢复)项目需要能够深入到一个进程中来提取 checkpoint 所需的所有信息。目前这两种情况都是用 ptrace() 来处理的,但是我们希望有更好的、更快的替代品。
Vagin 提出的替代方案是一个新的系统调用:
int process_vm_exec(pid_t pid, struct sigcontext uctx, unsigned long flags,
siginfo_t siginfo, sigset_t *sigmask, size_t sizemask);
调用成功的话将导致调用进程的地址空间变成 pid 指定的这个进程的地址空间。Patch 的说明邮件中指出,使用 pidfd 可能更好,但这将会使得这个系统调用与 process_vm_readv()和 process_vm_writev()不再一致了。在从新的地址空间恢复代码执行之前,uctx 中的值被用来加载处理器的寄存器(包括当前指令的指针)。这个步骤非常重要,因为在新的地址空间如果使用之前的指令指针很可能会出问题。
如果 flags 为 0,process_vm_exec()将改变地址空间,然后按照 uctx 的指示恢复执行,持续到进程进行系统调用或收到 signal 为止。这两种情况都会恢复到旧的地址空间,process_vm_exec()将终止执行并返回给调用者。siginfo 结构描述了打断另一个地址空间执行的事件是什么,如果是一个系统调用 e 的话,siginfo 会被设置成好像收到了 SIGSYS 信号一样。
相反,如果 flags 里有设置 PROCESS_VM_EXEC_SYSCALL,那么这个调用的目的是在目标进程的地址空间中调用一个系统调用。在这种情况下,uctx 应该在合适的寄存器中配置好系统调用号及其参数,就像发生过真正的系统调用一样。在系统调用的过程中,地址空间将被切换,然后在返回给调用者之前被恢复。
这组 patch 是作为一个原型来发布的,目的是希望得到大家对这种 API 反馈意见。Jann Horn 很快就回应说,这个新提出的系统调用似乎并不适合所述的应用场景,它对一个应用场景来说做了太多工作,对另一个用例来说又还不够。对于在不同的地址空间内运行代码的情况(如 User-mode Linux),他建议,不需要创建一个全新的进程,可能更好的做法是有一个系统调用来允许单独创建出新的地址空间。不过,对于 checkpoint/restore 的情况,可能仍然需要访问进程中除了地址空间之外的资源,尽管他没有明确指出这些资源可能是什么。Vagin 回应说,一个相对通用的系统调用似乎比创建多个分门别类的的系统调用要更好,哪怕这个通用系统调用并不完全适合所有应用场景。
不过,Florian Weimer 确实想到了另一种资源,会对 GNU C 库很有好处。Linux 实现 setuid()的方式和 POSIX 的要求是有区别的。Linux 只会改变当前调用线程的凭证信息(credential),而 POSIX 规定应该要改变这个进程中所有线程的凭证信息。目前,glibc 是通过向所有线程发送信号从而在 Linux 上按 POSIX 语义实现,这样它们就可以一起调用 setuid(),这种方案不是很理想。如果能在每个线程的上下文中调用 setuid(),而不用实际打断线程,那就最好了。他说,这样的功能对于实现 memory barrier 也很有用。
一方面希望创造一个在某些情况下有用的功能,另一方面又试图解决更大、更复杂的问题,这两种需求之间显然存在一些矛盾。在这种情况下,开发者必须谨慎选择他们后续的道路。如果试图做得太多,那么会非常有效地阻止其进入 mainline kernel,反而一点帮助也没有了。目前看来 process_vm_exec() 今后前景尚不清楚。它可能最终会被接受,但在这之前可能需要进行巨大的改变。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~