本文转载自 chinaunix,高效运维社区致力于陪伴您的职业生涯,与您一起愉快的成长。 Linux 系统中的 init 进程(pid=1)是除了 idle 进程(pid=0,也就是 init_task)之外另一个比较特殊的进程,它是 Linux 内核开始建立起进程概念时第一个通过 kernel_thread 产生的进程,其开始在内核态执行,然后通过一个系统调用,开始执行用户空间的 /sbin/init 程序。 期间 Linux 内核也经历了从内核态到用户态的特权级转变,/sbin/init 极有可能产生出了 shell,然后所有的用户进程都有该进程派生出来(目前尚未阅读过 /sbin/init 的源码)… 目前我们至少知道在内核空间执行用户空间的一段应用程序有两种方法:
它们最终都通过 int $0x80 在内核空间发起一个系统调用来完成,这个过程我在《深入 Linux 设备驱动程序内核机制》第9章有过详细的描述,对它的讨论最终结束在 sys_execve 函数那里,后者被用来执行一个新的程序。 现在一个有趣的问题是,在内核空间发起的系统调用,最终通过 sys_execve 来执行用户 空间的一个程序,比如 /sbin/myhotplug,那么该应用程序执行时是在内核态呢还是用户态呢? 直觉上肯定是用户态,不过因为 cpu 在执行 sys_execve 时cs寄存器还是__KERNEL_CS,如果前面我们的猜测是真的话,必然会有个cs寄存器的值从__KERNEL_CS 到 __USER_CS 的转变过程,这个过程是如何发生的呢?下面我以kernel_execve 为例,来具体讨论一下其间所发生的一些有趣的事情。 start_kernel 在其最后一个函数 rest_init 的调用中,会通过 kernel_thread 来生成一个内核进程,后者则会在新进程环境下调 用 kernel_init 函数,kernel_init一个让人感兴趣的地方在于它会调用 run_init_process 来执行根文件系统下的 /sbin/init 等程序: run_init_process 的核心调用就是 kernel_execve,后者的实现代码是: 里面是段内嵌的汇编代码,代码相对比较简单,核心代码是 int $0x80,执行系统调用,系统调用号 __NR_execve 放在AX里,当然系统调用的返回值也是在AX中,要执行的用户空间应用程序路径名称保存在 BX中。 int $0x80 的执行导致代码向 __KERNEL_CS:system_call 转移(具体过程可参考x86处理器中的特权级检查及 Linux 系统调用的实现一帖). 此处用bx,cx以及dx来保存filename, argv以及envp参数是有讲究的,它对应着 struct pt_regs 中寄存器在栈中的布局,因为接下来就会涉及从汇编到调用C函数过程,所以汇编程序在调用C之前,应该把要传递给C的参数在栈中准备好。 system_call 是一段纯汇编代码: system_call 首先会为后续的C函数的调用在当前堆栈中建立参数传递的环境(x86_64的实现要相对复杂一点,它会将系统调用切换到内核栈 movq PER_CPU_VAR(kernel_stack),%rsp),尤其是接下来对C函数 sys_execve 调用中的 struct pt_regs *regs 参数,我在上面代码中同时列出了系统调用之后的后续操作 syscall_exit,从代码中可以看到系统调用 int $0x80 最终通过iret指令返回,而后者会从当前栈中弹出cs与ip,然后跳转到cs:ip处执行代码。 正常情况下,x86架构上的int n指 令会将其下条指令的cs:ip压入堆栈,所以当通过iret指令返回时,原来的代码将从int n的下条指令继续执行,不过如果我们能在后续的C代码中改变regs- |
|
声明:文章版权归原作者所有 部分文章转自互联网 如有侵权请联系
[邮箱地址] 删除
|