内核态到用户态切换(二)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/armmfc/article/details/51475569

引言:(一)分析了0号进程(任务0)、内核线程、用户线程相关问题,有了这个铺垫,开始本文的分析。3)如何从内核空间切换到用户空间去的?

假设1号进程(内核态)init中已ramdisk_execute_command已传入,毕竟是要分析android系统的。

static int __init kernel_init(void * unused) {
    if (!ramdisk_execute_command)
        ramdisk_execute_command = "/init";
    init_post();
}   
static noinline int init_post(void)
{
    if (ramdisk_execute_command) {
        run_init_process(ramdisk_execute_command);
}
static void run_init_process(char *init_filename)
{
    argv_init[0] = init_filename;
    kernel_execve(init_filename, argv_init, envp_init);
}

在init内核进程中,直接运行run_init_process,实际是调用 了kernel_execve ,而kernel_execve的实质是通过一个系统调用,同时改变pt_regs,达到切换到用户空间。


/*
 * Do a system call from kernel instead of calling sys_execve so we
 * end up with proper pt_regs.
 */
int kernel_execve(const char *filename, char *const argv[], char *const envp[])
{
    long __res;
    asm volatile ("push %%ebx ; movl %2,%%ebx ; int $0x80 ; pop %%ebx"
    : "=a" (__res)
    : "0" (__NR_execve), "ri" (filename), "c" (argv), "d" (envp) : "memory");
    return __res;
}

在这里需要注意,首次从内核态切换到用户态中,用户态进程的堆栈应该是干净的,即在从0号进程fork时不要调用函数。这可以通过inline内联函数解决。
但是在中断指令INT还是要用到堆栈,这时的堆栈是内核态堆栈 ,每个任务都有自己独立的内核态堆栈,所以系统调用不会影响用户态堆栈。
是时候对系统调用做一个简单梳理了,否则kernel_execve代码也不好往下分析。

系统调用通常称为syscall,用于Linux内核态与用户态交互通信。当发起系统调用时,需要传递一个系统调用号给内核,随后CPU进入内核模式。内核根据该系统调用号执行相应的系统调用,并在执行完成后返回一个整数值给用户程序,0表示系统调用成功,负数表示失败。

系统调用的处理过程:
1.在内核态保存大多数寄存器的内容
2.调用相应的服务程序
3.退出系统调用程序:用保存在内核栈中的值加载寄存器,并切换回用户态

这就意味着想要从内核态切到用户到就需要模拟一个系统调用,假设用户态init进程发起系统调用,执行完后回到用户态init进程,以此达到切换目的,思想简单实用。
下边就看下具体的实施过程:

int $0x80 -> syscall -> sys_call_table -> sys_execve -> iret

int 0x80如何调用到syscall?
这里涉及到中断相关知识,当中断信号来时,处理器停止当前程序,转到称为中断程序中去运行。
中断或异常有不同类型,俗称任务门、中断门、陷阱门和调用门,不同类型的中断或异常赋一个标识号,称为向量。处理维护一个中断描述符表IDT(interrupt descriptor table),通过表中的索引号可以定位到具体的中断程序。

void __init trap_init(void) {
    set_system_trap_gate(SYSCALL_VECTOR, &system_call);
}

# define SYSCALL_VECTOR     0x80

static inline void set_system_trap_gate(unsigned int n, void *addr)
{
    BUG_ON((unsigned)n > 0xFF);
    _set_gate(n, GATE_TRAP, addr, 0x3, 0, __KERNEL_CS);
}

static inline void _set_gate(int gate, unsigned type, void *addr,
                 unsigned dpl, unsigned ist, unsigned seg)
{
    ...
    write_idt_entry(idt_table, gate, &s);
}

在trap初始化中看到了系统调用SYSCALL_VECTOR的初始,为何系统调用要被设置成陷阱门,而不其它?

系统调用设置trap:
当陷入陷阱时,EIP指向的是下一条指令,而当故障(fault)发生时,EIP指向当前指令,当异常发生时,EIP的指向是不固定的。因此根据系统调用后EIP的变化,它应该属于陷阱门的范畴。

基本的中断知识就到这儿,其余的可能单独再写一篇。继续看代码:set_system_trap_gate(SYSCALL_VECTOR, &system_call);
system_call这个函数真身在哪儿?

[arch/x86/kernel/entry_32.s]
    # system call handler stub
ENTRY(system_call)
    RING0_INT_FRAME         # can't unwind into user space anyway
    pushl %eax          # save orig_eax
    SAVE_ALL
    ...
syscall_call:
    call *sys_call_table(,%eax,4)
    movl %eax,PT_EAX(%esp)      # store the return value
syscall_exit:
    LOCKDEP_SYS_EXIT
    DISABLE_INTERRUPTS(CLBR_ANY)    # make sure we don't miss an interrupt
    jne syscall_exit_work
irq_return:
    INTERRUPT_RETURN

由于是汇编代码,这里只列部分,SAVE_ALL保存切换前的进程信息,然后call *sys_call_table(,%eax,4),根据NR号查询系统调用表中相应函数,用这里会调用到sys_execve,调用完成后INTERRUPT_RETURN返回,这里INTERRUPT_RETURN (aka. “iret”)。

ENTRY(sys_call_table)
    .long sys_unlink    /* 10 */
    .long sys_execve

可以看到sys_execve的NR为11,看看sys_execve对前面提到的pt_regs做了什么?

[arch/x86/kernel/process_32.c]
/*
 * sys_execve() executes a new program.
 */
asmlinkage int sys_execve(struct pt_regs regs)
{
    ...
    filename = getname((char __user *) regs.bx);

    error = do_execve(filename,
            (char __user * __user *) regs.cx,
            (char __user * __user *) regs.dx,
            &regs);
    ...
}

首先获取在regs.bx执行程序的路径和名称,然后调用do_execve

int do_execve(char * filename,
    char __user *__user *argv,
    char __user *__user *envp,
    struct pt_regs * regs)
{
    struct linux_binprm *bprm;
    file = open_exec(filename);
    bprm->file = file;
    retval = bprm_mm_init(bprm);
    retval = search_binary_handler(bprm,regs);
    ...
}   

这里打开可执行文件,如用户空间init程序,初始化mm,接着调用search_binary_handler,注意传的regs。

/*
 * cycle the list of binary formats handler, until one recognizes the image
 */
int search_binary_handler(struct linux_binprm *bprm,struct pt_regs *regs)
{
    struct linux_binfmt *fmt;
    list_for_each_entry(fmt, &formats, lh) {
            int (*fn)(struct linux_binprm *, struct pt_regs *) = fmt->load_binary;
            retval = fn(bprm, regs);
    ...
}

这里根据不同linux_binfmt二进制文件去执行函数fn(bprm, regs);那到底有哪些二进制格式,init程序又属于哪种?

static struct linux_binfmt elf_format = {
        .module     = THIS_MODULE,
        .load_binary    = load_elf_binary,
        .load_shlib = load_elf_library,
        .core_dump  = elf_core_dump,
        .min_coredump   = ELF_EXEC_PAGESIZE,
        .hasvdso    = 1
};

static struct linux_binfmt elf_fdpic_format = {
    .module     = THIS_MODULE,
    .load_binary    = load_elf_fdpic_binary,
#if defined(USE_ELF_CORE_DUMP) && defined(CONFIG_ELF_CORE)
    .core_dump  = elf_fdpic_core_dump,
#endif
    .min_coredump   = ELF_EXEC_PAGESIZE,
};

static struct linux_binfmt flat_format = {
    .module     = THIS_MODULE,
    .load_binary    = load_flat_binary,
    .core_dump  = flat_core_dump,
    .min_coredump   = PAGE_SIZE
};

有不少linux_binfmt,而init程序属于ELF格式,所以这里的load_elf_binary应为binfmt_elf.c中elf_format 的load_elf_binary。这个函数很长,主要检查ELF格式文件是否正确、权限,然后分配内存, 做映射等 ,最后调用start_thread。

static int load_elf_binary(struct linux_binprm *bprm, struct pt_regs *regs)
{
    retval = kernel_read(bprm->file, loc->elf_ex.e_phoff,
    retval = setup_arg_pages(bprm, executable_stack);
    ...
    current->mm->start_stack = bprm->p;
    ... 
    allow_write_access(interpreter);
    set_binfmt(&elf_format);
    install_exec_creds(bprm);
    error = do_mmap(NULL, 0, PAGE_SIZE, PROT_READ 
    ...
    start_thread(regs, elf_entry, bprm->p);
}

void
start_thread(struct pt_regs *regs, unsigned long new_ip, unsigned long new_sp)
{
    __asm__("movl %0, %%gs" : : "r"(0));
    regs->fs        = 0;
    set_fs(USER_DS);
    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_thread中,regs->cs = __USER_CS;前面传入的pt_regs被重新赋值 ,即从原来的__KERNER_CS变为 __USER_CS;意味着代码段发生了改变,同时regs->ip = elf_entry, regs->sp也被更新,所以当执行完内核代码后,通过iret可以返回到__USER_CS:elf_entry,即init程序的入口处。

int $0x80 -> syscall -> sys_call_table -> sys_execve -> iret

这样就从中断开始到系统调用返回就分析完了,Android系统用户空间的init进程正式开始它的旅程,这才是我真正感兴趣的!!

展开阅读全文

没有更多推荐了,返回首页