目前我们至少知道在内核空间执行用户空间的一段应用程序有两种方法:
1. call_usermodehelper
2. kernel_execve
它们最终都通过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等程序:
- static noinline int init_post(void)
- {
- ...
- run_init_process("/sbin/init");
- run_init_process("/etc/init");
- run_init_process("/bin/init");
- run_init_process("/bin/sh");
- panic("No init found. Try passing init= option to kernel. "
- "See Linux Documentation/init.txt for guidance.");
- }
- int kernel_execve(const char *filename,
- const char *const argv[],
- const char *const envp[])
- {
- long __res;
- asm volatile ("int $0x80"
- : "=a" (__res)
- : "0" (__NR_execve), "b" (filename), "c" (argv), "d" (envp) : "memory");
- return __res;
- }
system_call是一段纯汇编代码:
- <arch/x86/kernel/entry_32.s>
- ENTRY(system_call)
- RING0_INT_FRAME # can't unwind into user space anyway
- pushl_cfi %eax # save orig_eax
- SAVE_ALL
- GET_THREAD_INFO(%ebp)
- # system call tracing in operation / emulation
- testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp)
- jnz syscall_trace_entry
- cmpl $(nr_syscalls), %eax
- jae syscall_badsys
- syscall_call:
- call *sys_call_table(,%eax,4)
- movl %eax,PT_EAX(%esp) # store the return value
- syscall_exit:
- ...
- restore_nocheck:
- RESTORE_REGS 4 # skip orig_eax/error_code
- irq_return:
- INTERRUPT_RETURN #iret instruction for x86_32
核心的调用发生在call *sys_call_table(,%eax,4)这条指令上,sys_call_table是个系统调用表,本质上就是一个函数指针数组,我们这里的系 统调用号是__NR_execve=11, 所以在sys_call_table中对应的函数为:
- ENTRY(sys_call_table)
- .long sys_restart_syscall /* 0 - old "setup()" system call, used for restarting */
- .long sys_exit
- .long ptregs_fork
- .long sys_read
- .long sys_write
- .long sys_open /* 5 */
- .long sys_close
- ...
- .long sys_unlink /* 10 */
- .long ptregs_execve //__NR_execve
- ...
ptregs_execve其实就是sys_execve函数:
- #define ptregs_execve sys_execve
- /*
- * sys_execve() executes a new program.
- */
- long sys_execve(const char __user *name,
- const char __user *const __user *argv,
- const char __user *const __user *envp, struct pt_regs *regs)
- {
- long error;
- char *filename;
- filename = getname(name);
- error = PTR_ERR(filename);
- if (IS_ERR(filename))
- return error;
- error = do_execve(filename, argv, envp, regs);
- #ifdef CONFIG_X86_32
- if (error == 0) {
- /* Make sure we don't return using sysenter.. */
- set_thread_flag(TIF_IRET);
- }
- #endif
- putname(filename);
- return error;
- }
- int search_binary_handler(struct linux_binprm *bprm,struct pt_regs *regs)
- {
- ...
- for (try=0; try<2; try++) {
- read_lock(&binfmt_lock);
- list_for_each_entry(fmt, &formats, lh) {
- int (*fn)(struct linux_binprm *, struct pt_regs *) = fmt->load_binary;
- ...
- retval = fn(bprm, regs);
- ...
- }
- ...
- }
- }
- static int load_elf_binary(struct linux_binprm *bprm, struct pt_regs *regs)
- {
- ...
- start_thread(regs, elf_entry, bprm->p);
- ...
- }
- void
- start_thread(struct pt_regs *regs, unsigned long new_ip, unsigned long new_sp)
- {
- set_user_gs(regs, 0);
- regs->fs = 0;
- regs->ds = __USER_DS;
- regs->es = __USER_DS;
- regs->ss = __USER_DS;
- regs->cs = __USER_CS;
- regs->ip = new_ip;
- regs->sp = new_sp;
- /*
- * Free the old FP and other extended state
- */
- free_thread_xstate(current);
- }
所以我们看到,start_kernel在最后调用rest_init,而后者通过对kernel_thread的调用产生一个新进程(pid=1),新进程在其kernel_init()-->init_post()调用链中将通过run_init_process来执行用户空间的/sbin /init,run_init_process的核心是个系统调用,当系统调用返回时代码将从/sbin/init的入口点处开始执行,所以虽然我们知道 post_init中有如下几个run_init_process的调用:
- run_init_process("/sbin/init");
- run_init_process("/etc/init");
- run_init_process("/bin/init");
- run_init_process("/bin/sh");
最后,我们来验证一下,所谓眼见为实,耳听为虚。再者,如果验证达到预期,也是很鼓舞人好奇心的极佳方法。验证的方法我打算采用“Linux设备驱动模型中的热插拔机制及实验” 中的路线,通过call_usermodehelper来做,因为它和kernel_execve本质上都是一样的。我们自己写个应用程序,在这个应用程序里读取cs寄存器的值,程序很简单:
- #include <stdio.h>
- #include <fcntl.h>
- #include <unistd.h>
- #include <syslog.h>
- int main()
- {
- unsigned short ucs;
- asm(
- "movw %%cs, %0\n"
- :"=r"(ucs)
- ::"memory");
- syslog(LOG_INFO, "ucs = 0x%x\n", ucs);
- return 0;
- }
Mar 10 14:20:23 build-server main: ucs = 0x33
0x33正好就是x86 64位系统(我实验用的环境)下的__USER_CS.
所以第一个内核进程(pid=1)通过执行用户空间程序,期间通过cs的转变(从__KERNEL_CS到__USER_CS)来达到特权级的更替。