sys_execv源码分析

来源:互联网 发布:数据库工程师培训机构 编辑:程序博客网 时间:2024/06/11 17:04

因为最近工作涉及到linux的反汇编,做了一阵子Java搜索引擎的工作,再回到C很不适应,因此借着看源码回忆一点linux的知识,上一篇博文分析了_dl_runtime_resolve的源码,本章从头开始研究在linux下程序装载、运行的全部过程,因此从glibc回到linux的源码,来看看linux的系统调用sys_execv是如何装载程序的,后面会回到glibc陆续分析_dl_start、_dl_main函数的源码。

因为linux源码涉及到的知识太多,本文在尽量保证代码原样的情况下删除一些不重要的代码,并且不过多深入其他知识领域,例如文件系统、负载均衡、安全等等,以后有时间了再分析其他的源码。

fs/exec.c
sys_execv

SYSCALL_DEFINE3(execve,        const char __user *, filename,        const char __user *const __user *, argv,        const char __user *const __user *, envp){    return do_execve(getname(filename), argv, envp);}

SYSCALL_DEFINE3宏定义展开就是sys_execv函数,getname函数进而调用getname_flags函数,该函数会在内核空间分配一块内存用于存储用户空间传入的文件名filename。

fs/exec.c
sys_execv->do_execve

int do_execve(struct filename *filename,    const char __user *const __user *__argv,    const char __user *const __user *__envp){    struct user_arg_ptr argv = { .ptr.native = __argv };    struct user_arg_ptr envp = { .ptr.native = __envp };    return do_execveat_common(AT_FDCWD, filename, argv, envp, 0);}

user_arg_ptr是对普通指针的一种封装,用于表示用户空间的指针。这里分别封装了程序的调用参数__argv和环境变量__envp。
AT_FDCWD标志意味着文件的搜索路径不是从进程文件系统的根路径开始,而是从进程当前路径开始,在后面打开文件时会用到。

do_execveat_common函数

fs/exec.c
sys_execv->do_execve->do_execveat_common

static int do_execveat_common(int fd, struct filename *filename,                  struct user_arg_ptr argv,                  struct user_arg_ptr envp,                  int flags){    char *pathbuf = NULL;    struct linux_binprm *bprm;    struct file *file;    struct files_struct *displaced;    int retval;    if (IS_ERR(filename))        return PTR_ERR(filename);    if ((current->flags & PF_NPROC_EXCEEDED) &&        atomic_read(&current_user()->processes) > rlimit(RLIMIT_NPROC)) {        retval = -EAGAIN;        goto out_ret;    }    current->flags &= ~PF_NPROC_EXCEEDED;    retval = unshare_files(&displaced);    retval = -ENOMEM;    bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);    if (!bprm)        goto out_files;    retval = prepare_bprm_creds(bprm);    if (retval)        goto out_free;    check_unsafe_exec(bprm);    current->in_execve = 1;    file = do_open_execat(fd, filename, flags);    retval = PTR_ERR(file);    if (IS_ERR(file))        goto out_unmark;    sched_exec();    bprm->file = file;    if (fd == AT_FDCWD || filename->name[0] == '/') {        bprm->filename = filename->name;    } else {        ...    }    bprm->interp = bprm->filename;    retval = bprm_mm_init(bprm);    if (retval)        goto out_unmark;    bprm->argc = count(argv, MAX_ARG_STRINGS);    if ((retval = bprm->argc) < 0)        goto out;    bprm->envc = count(envp, MAX_ARG_STRINGS);    if ((retval = bprm->envc) < 0)        goto out;    retval = prepare_binprm(bprm);    if (retval < 0)        goto out;    retval = copy_strings_kernel(1, &bprm->filename, bprm);    if (retval < 0)        goto out;    bprm->exec = bprm->p;    retval = copy_strings(bprm->envc, envp, bprm);    if (retval < 0)        goto out;    retval = copy_strings(bprm->argc, argv, bprm);    if (retval < 0)        goto out;    retval = exec_binprm(bprm);    if (retval < 0)        goto out;    ...}

参数user_arg_ptr是对用户空间指针的一种分装,linux内核在特定时候(例如sparse)会检查该指针的合法性。
IS_ERR宏用来检测文件名指针是否合法,判断该指针是否指向内核空间的最后一个页面,即错误页面,如果是则直接返回。
接下来根据PF_NPROC_EXCEEDED标志位检查当前用户的进程数是否超过限制,如果超过限制就不能再创建新的进程了,直接返回。
unshare_files函数用于备份当前进程的文件表至displaced中,即当前进程的files_struct结构,当出错或者返回时用于恢复当前进程的文件表。
接下来创建linux_binprm并分配内存空间,后面就要对该结构进行初始化。
prepare_bprm_creds函数会从当前进程内复制一份cred结构,封装了进程的安全信息。
check_unsafe_exec检查程序执行后是否存在潜在的风险,进而设置linux_binprm的unsafe标志位。
do_open_execat函数内部通过do_filp_open函数打开文件,返回file结构。
sched_exec函数在多核计算机中找到最小负载的CPU,用来执行该二进制文件。
再往下通过bprm_mm_init函数分配新进程的内存空间mm_struct。
然后计算用户和环境变量的字符串个数,分别赋值给bprm的argc和envc。count函数内部会通过get_user函数检查用户空间指针的合法性(根据thread_info中的addr_limit变量进行检查)。
prepare_binprm用于设置进程的授权,并将可执行文件的内容读取到bprm的buf缓存中。
再往下的copy_strings_kernel和copy_strings函数都是讲用户空间的数据拷贝到内核空间,其内部通过kmap系统调用获取内核空间的页面。
最后通过exec_binprm函数开始执行新进程。

kernel/fork.c
sys_execv->do_execve->do_execveat_common->unshare_files

int unshare_files(struct files_struct **displaced){    struct task_struct *task = current;    struct files_struct *copy = NULL;    unshare_fd(CLONE_FILES, &copy);    *displaced = task->files;    task->files = copy;    return 0;}static int unshare_fd(unsigned long unshare_flags, struct files_struct **new_fdp){    struct files_struct *fd = current->files;    if ((unshare_flags & CLONE_FILES) &&        (fd && atomic_read(&fd->count) > 1)) {        *new_fdp = dup_fd(fd, &error);    }    return 0;}

unshare_files继而通过unshare_fd函数复制当前进程current的文件表files,赋值操作通过dup_fd函数执行,这里就不往下看了。

kernel/cred.c
sys_execv->do_execve->do_execveat_common->prepare_exec_creds

int prepare_bprm_creds(struct linux_binprm *bprm){    bprm->cred = prepare_exec_creds();    if (likely(bprm->cred))        return 0;    return -ENOMEM;}struct cred *prepare_exec_creds(void){    return prepare_creds();}struct cred *prepare_creds(void){    struct task_struct *task = current;    const struct cred *old;    struct cred *new;    validate_process_creds();    new = kmem_cache_alloc(cred_jar, GFP_KERNEL);    if (!new)        return NULL;    old = task->cred;    memcpy(new, old, sizeof(struct cred));    atomic_set(&new->usage, 1);    set_cred_subscribers(new, 0);    get_group_info(new->group_info);    get_uid(new->user);    get_user_ns(new->user_ns);    key_get(new->session_keyring);    key_get(new->process_keyring);    key_get(new->thread_keyring);    key_get(new->request_key_auth);    new->security = NULL;    if (security_prepare_creds(new, old, GFP_KERNEL) < 0)        goto error;    validate_creds(new);    return new;error:    abort_creds(new);    return NULL;}

prepare_exec_creds用于复制当前进程的cred结构,并相应地增加引用计数,验证后返回。linux后面的版本将安全信息封装在cred结构里,例如uid不再和进程结构task_struct粘在一起,而是抽象出来。

fs/exec.c
sys_execv->do_execve->do_execveat_common->check_unsafe_exec

static void check_unsafe_exec(struct linux_binprm *bprm){    struct task_struct *p = current, *t;    unsigned n_fs;    if (p->ptrace) {        if (p->ptrace & PT_PTRACE_CAP)            bprm->unsafe |= LSM_UNSAFE_PTRACE_CAP;        else            bprm->unsafe |= LSM_UNSAFE_PTRACE;    }    if (task_no_new_privs(current))        bprm->unsafe |= LSM_UNSAFE_NO_NEW_PRIVS;    t = p;    n_fs = 1;    spin_lock(&p->fs->lock);    rcu_read_lock();    while_each_thread(p, t) {        if (t->fs == p->fs)            n_fs++;    }    rcu_read_unlock();    if (p->fs->users > n_fs)        bprm->unsafe |= LSM_UNSAFE_SHARE;    else        p->fs->in_exec = 1;    spin_unlock(&p->fs->lock);}

ptrace标志位决定了该进程是否被跟踪。
task_no_new_privs是个宏定义,定义在include/linux/sched.h中,用于检测task_struct结构中atomic_flags的标志位PFA_NO_NEW_PRIVS。
while_each_thread遍历同一个线程组中的其他线程,查找具有相同fs_struct结构的线程,检查fs_struct结构体的使用者计数(即current->fs->users)是否超过线程组中所有线程的数量,若是,则标记bprm->unsafe为LSM_UNSAFE_SHARE,若不是,则标记current->fs->in_exec为1。

fs/exec.c
sys_execv->do_execve->do_execveat_common->do_open_execat

static struct file *do_open_execat(int fd, struct filename *name, int flags){    struct file *file;    struct open_flags open_exec_flags = {        .open_flag = O_LARGEFILE | O_RDONLY | __FMODE_EXEC,        .acc_mode = MAY_EXEC | MAY_OPEN,        .intent = LOOKUP_OPEN,        .lookup_flags = LOOKUP_FOLLOW,    };    file = do_filp_open(fd, name, &open_exec_flags);    ...    return file;}

因为将要打开的是可执行文件,因此要设置相应的标志位,然后调用do_filp_open打开文件,下面涉及到文件系统的知识了,不往下看了。

kernel/sched/core.c
sys_execv->do_execve->do_execveat_common->sched_exec

void sched_exec(void){    struct task_struct *p = current;    unsigned long flags;    int dest_cpu;    raw_spin_lock_irqsave(&p->pi_lock, flags);    dest_cpu = p->sched_class->select_task_rq(p, task_cpu(p), SD_BALANCE_EXEC, 0);    if (dest_cpu == smp_processor_id())        goto unlock;    if (likely(cpu_active(dest_cpu))) {        struct migration_arg arg = { p, dest_cpu };        raw_spin_unlock_irqrestore(&p->pi_lock, flags);        stop_one_cpu(task_cpu(p), migration_cpu_stop, &arg);        return;    }unlock:    raw_spin_unlock_irqrestore(&p->pi_lock, flags);}

select_task_rq函数选择负载最小的cpu,其内部通过select_task_rq_fair函数进行筛选,最后通过stop_one_cpu函数执行切换。

fs/exec.c
sys_execv->do_execve->do_execveat_common->bprm_mm_init

static int bprm_mm_init(struct linux_binprm *bprm){    struct mm_struct *mm = NULL;    bprm->mm = mm = mm_alloc();    __bprm_mm_init(bprm);    return 0;}static int __bprm_mm_init(struct linux_binprm *bprm){    int err;    struct vm_area_struct *vma = NULL;    struct mm_struct *mm = bprm->mm;    bprm->vma = vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);    down_write(&mm->mmap_sem);    vma->vm_mm = mm;    vma->vm_end = STACK_TOP_MAX;    vma->vm_start = vma->vm_end - PAGE_SIZE;    vma->vm_flags = VM_SOFTDIRTY | VM_STACK_FLAGS | VM_STACK_INCOMPLETE_SETUP;    vma->vm_page_prot = vm_get_page_prot(vma->vm_flags);    INIT_LIST_HEAD(&vma->anon_vma_chain);    insert_vm_struct(mm, vma);    mm->stack_vm = mm->total_vm = 1;    arch_bprm_mm_init(mm, vma);    up_write(&mm->mmap_sem);    bprm->p = vma->vm_end - sizeof(void *);    return 0;err:    up_write(&mm->mmap_sem);    bprm->vma = NULL;    kmem_cache_free(vm_area_cachep, vma);    return err;}

bprm_mm_init函数首先通过mm_alloc创建新进程的mm_struct结构,然后执行__bprm_mm_init继续进行初始化。
__bprm_mm_init函数创建虚拟内存结构vm_area_struct,STACK_TOP_MAX在64位的计算机上定义为

#define TASK_SIZE_MAX   ((1UL << 47) - PAGE_SIZE)

因此是用户空间的结束地址减去一个保护页面PAGE_SIZE,即0x0000,7fff,ffff,f000。
vm_end即虚拟空间的结束地址指向STACK_TOP_MAX。
vm_start即虚拟内存的开始地址指向vm_end向下一个页面的地址,即0x0000,7fff,ffff,e000。
insert_vm_struct将分配的虚拟内存vm_area_struct插入进程的内存管理结构mm_struct中。
然后通过arch_bprm_mm_init函数对具体的计算机类型执行相应的初始化操作。
最后设置堆栈起始指针,大小为将STACK_TOP_MAX减去一个指针的大小,即8个字节,因此最后的地址为0x0000,7fff,ffff,eff8。

fs/exec.c
sys_execv->do_execve->do_execveat_common->prepare_binprm

int prepare_binprm(struct linux_binprm *bprm){    int retval;    bprm_fill_uid(bprm);    retval = security_bprm_set_creds(bprm);    if (retval)        return retval;    bprm->cred_prepared = 1;    memset(bprm->buf, 0, BINPRM_BUF_SIZE);    return kernel_read(bprm->file, 0, bprm->buf, BINPRM_BUF_SIZE);}

bprm_fill_uid设置即将运行的进程的uid和gid。security_bprm_set_creds函数设置授权。最后通过kernel_read函数将file中的内容读取到bprm的缓存buf中。

fs/exec.c
sys_execv->do_execve->do_execveat_common->prepare_binprm->bprm_fill_uid

static void bprm_fill_uid(struct linux_binprm *bprm){    struct inode *inode;    unsigned int mode;    kuid_t uid;    kgid_t gid;    bprm->cred->euid = current_euid();    bprm->cred->egid = current_egid();    if (bprm->file->f_path.mnt->mnt_flags & MNT_NOSUID)        return;    if (task_no_new_privs(current))        return;    inode = file_inode(bprm->file);    mode = READ_ONCE(inode->i_mode);    if (!(mode & (S_ISUID|S_ISGID)))        return;    mutex_lock(&inode->i_mutex);    mode = inode->i_mode;    uid = inode->i_uid;    gid = inode->i_gid;    mutex_unlock(&inode->i_mutex);    if (!kuid_has_mapping(bprm->cred->user_ns, uid) ||         !kgid_has_mapping(bprm->cred->user_ns, gid))        return;    if (mode & S_ISUID) {        bprm->per_clear |= PER_CLEAR_ON_SETID;        bprm->cred->euid = uid;    }    if ((mode & (S_ISGID | S_IXGRP)) == (S_ISGID | S_IXGRP)) {        bprm->per_clear |= PER_CLEAR_ON_SETID;        bprm->cred->egid = gid;    }}

首先设置euid和egid未当前进程的euid和egid。
在文件模式字mode中,有两个位bit10和bit11分别称为设置用户组ID位和设置用户ID位,分别有两个测试常量S_ISGID和S_ISUID与其对应。若此文件为可执行文件:当设置用户组ID位为1,则文件执行时,内核将其进程的有效用户组ID设置为文件的所有组ID。当设置用户ID位为1,则文件执行时,内核将其进程的有效用户ID设置为文件所有者的用户ID。

fs/exec.c
sys_execv->do_execve->do_execveat_common->exec_binprm

static int exec_binprm(struct linux_binprm *bprm){    int ret;    ...    ret = search_binary_handler(bprm);    ...    return ret;}int search_binary_handler(struct linux_binprm *bprm){    struct linux_binfmt *fmt;    int retval = -ENOENT;    list_for_each_entry(fmt, &formats, lh) {        bprm->recursion_depth++;        retval = fmt->load_binary(bprm);        put_binfmt(fmt);        bprm->recursion_depth--;    }    return retval;}

exec_binprm主要调用search_binary_handler处理可执行文件,search_binary_handler函数遍历format格式,对于linux下的elf格式的可执行文件而言,会找到elf_format,调用其load_binary函数,最终其实调用的是load_elf_binary函数,下面开始重点分析这个函数。

load_elf_binary

fs/binfmt_elf.c
load_elf_binary第一部分

static int load_elf_binary(struct linux_binprm *bprm){    struct file *interpreter = NULL;     unsigned long load_addr = 0, load_bias = 0;    int load_addr_set = 0;    char * elf_interpreter = NULL;    unsigned long error;    struct elf_phdr *elf_ppnt, *elf_phdata, *interp_elf_phdata = NULL;    unsigned long elf_bss, elf_brk;    int retval, i;    unsigned long elf_entry;    unsigned long interp_load_addr = 0;    unsigned long start_code, end_code, start_data, end_data;    unsigned long reloc_func_desc __maybe_unused = 0;    int executable_stack = EXSTACK_DEFAULT;    struct pt_regs *regs = current_pt_regs();    struct {        struct elfhdr elf_ex;        struct elfhdr interp_elf_ex;    } *loc;    struct arch_elf_state arch_state = INIT_ARCH_ELF_STATE;    loc = kmalloc(sizeof(*loc), GFP_KERNEL);    if (!loc) {        retval = -ENOMEM;        goto out_ret;    }    loc->elf_ex = *((struct elfhdr *)bprm->buf);    retval = -ENOEXEC;    if (memcmp(loc->elf_ex.e_ident, ELFMAG, SELFMAG) != 0)        goto out;    if (loc->elf_ex.e_type != ET_EXEC && loc->elf_ex.e_type != ET_DYN)        goto out;    if (!elf_check_arch(&loc->elf_ex))        goto out;    if (!bprm->file->f_op->mmap)        goto out;    elf_phdata = load_elf_phdrs(&loc->elf_ex, bprm->file);    if (!elf_phdata)        goto out;    ...}

第一部分代码主要是检查elf的文件头信息。
首先检查e_ident,即前4个字节,魔数。
接下来查看类型e_type是否为ET_EXEC(2)和ET_DYN(3)的其中一种。

#define elf_check_arch(x)           \    ((x)->e_machine == EM_X86_64)

elf_check_arch检查e_machine 的值是否为EM_X86_64(0x3E)。
load_elf_phdrs函数读取elf文件中的Segment头信息到elf_phdata中。
为了方便说明,下面首先看一下elf64文件的文件头elf64_hdr结构和Segment头elf64_phdr结构。

typedef struct elf64_hdr {  unsigned char e_ident[EI_NIDENT];  Elf64_Half e_type;  Elf64_Half e_machine;  Elf64_Word e_version;  Elf64_Addr e_entry;  Elf64_Off e_phoff;  Elf64_Off e_shoff;  Elf64_Word e_flags;  Elf64_Half e_ehsize;  Elf64_Half e_phentsize;  Elf64_Half e_phnum;  Elf64_Half e_shentsize;  Elf64_Half e_shnum;  Elf64_Half e_shstrndx;} Elf64_Ehdr;typedef struct elf64_phdr {  Elf64_Word p_type;  Elf64_Word p_flags;  Elf64_Off p_offset;  Elf64_Addr p_vaddr;  Elf64_Addr p_paddr;  Elf64_Xword p_filesz;  Elf64_Xword p_memsz;  Elf64_Xword p_align;} Elf64_Phdr;

结构中成员的具体意义可以到网上查,不一一说明了,下面碰到重要的时会说明。

fs/binfmt_elf.c
load_elf_binary->load_elf_phdrs

static struct elf_phdr *load_elf_phdrs(struct elfhdr *elf_ex,                       struct file *elf_file){    struct elf_phdr *elf_phdata = NULL;    int size;    size = sizeof(struct elf_phdr) * elf_ex->e_phnum;    elf_phdata = kmalloc(size, GFP_KERNEL);    kernel_read(elf_file, elf_ex->e_phoff,                 (char *)elf_phdata, size);    return elf_phdata;}

elf文件头的e_phnum表示Segment头的个数,e_phentsize是Segment头的大小,两者的乘积size即所有Segment头的大小和,所以要先分配size大小的内存空间,e_phoff是第一个Segment头在文件中的偏移。接着调用kernel_read从文件中读取Segment信息到elf_phdata中。

fs/binfmt_elf.c
load_elf_binary第二部分

static int load_elf_binary(struct linux_binprm *bprm){    ...    elf_ppnt = elf_phdata;    elf_bss = 0;    elf_brk = 0;    start_code = ~0UL;    end_code = 0;    start_data = 0;    end_data = 0;    for (i = 0; i < loc->elf_ex.e_phnum; i++) {        if (elf_ppnt->p_type == PT_INTERP) {            if (elf_ppnt->p_filesz > PATH_MAX ||                 elf_ppnt->p_filesz < 2)                goto out_free_ph;            elf_interpreter = kmalloc(elf_ppnt->p_filesz,                          GFP_KERNEL);            retval = kernel_read(bprm->file, elf_ppnt->p_offset,                         elf_interpreter,                         elf_ppnt->p_filesz);            interpreter = open_exec(elf_interpreter);            retval = kernel_read(interpreter, 0, bprm->buf,                         BINPRM_BUF_SIZE);            loc->interp_elf_ex = *((struct elfhdr *)bprm->buf);            break;        }        elf_ppnt++;    }    ...}

load_elf_binary函数的第二部分主要是查找文件中的解释器信息。
首先遍历所有的Segment头信息,查看其p_type是否为PT_INTERP(3)。
对于解释器信息的Segment头,p_filesz其实是解释器的路径,p_offset是路径信息在elf文件中的偏移,接下来检查该路径信息是否合理。
再往下分配用于存储解释器路径的内存空间elf_interpreter,并通过kernel_read函数从文件中读取路径信息到elf_interpreter中。
然后通过open_exec函数打开解释器,返回一个file结构,open_exec函数内部也是调用do_open_execat函数打开文件。
通过kernel_read函数将解释器的头部128个字节BINPRM_BUF_SIZE写入bprm的buf结构中,因为之前已经将文件头读取出来,这里覆盖了bprm的buf。
然后将loc的interp_elf_ex设置为解释器的头部在缓存buf中的起始地址。

fs/binfmt_elf.c
load_elf_binary第三部分

static int load_elf_binary(struct linux_binprm *bprm){    ...    elf_ppnt = elf_phdata;    for (i = 0; i < loc->elf_ex.e_phnum; i++, elf_ppnt++)        switch (elf_ppnt->p_type) {        case PT_GNU_STACK:            if (elf_ppnt->p_flags & PF_X)                executable_stack = EXSTACK_ENABLE_X;            else                executable_stack = EXSTACK_DISABLE_X;            break;        case PT_LOPROC ... PT_HIPROC:            retval = arch_elf_pt_proc(&loc->elf_ex, elf_ppnt,                          bprm->file, false,                          &arch_state);            if (retval)                goto out_free_dentry;            break;        }    ...}

这部分代码首先通过查找类型为PT_GNU_STACK的Segment,检查堆栈的可执行性,设置在executable_stack中。PT_LOPROC和PT_HIPROC类型的Segment用来提供给特定的计算机体系进行检查,这里不往下看了。

fs/binfmt_elf.c
load_elf_binary第四部分

static int load_elf_binary(struct linux_binprm *bprm){    ...    if (elf_interpreter) {        retval = -ELIBBAD;        if (memcmp(loc->interp_elf_ex.e_ident, ELFMAG, SELFMAG) != 0)            goto out_free_dentry;        if (!elf_check_arch(&loc->interp_elf_ex))            goto out_free_dentry;        interp_elf_phdata = load_elf_phdrs(&loc->interp_elf_ex,                           interpreter);        elf_ppnt = interp_elf_phdata;        for (i = 0; i < loc->interp_elf_ex.e_phnum; i++, elf_ppnt++)            switch (elf_ppnt->p_type) {            case PT_LOPROC ... PT_HIPROC:                retval = arch_elf_pt_proc(&loc->interp_elf_ex,                              elf_ppnt, interpreter,                              true, &arch_state);                if (retval)                    goto out_free_dentry;                break;            }    }    ...}

因为解释器也是elf文件,被读取到内存中后,这部分代码首先检查解释器的elf文件头信息,然后再次调用load_elf_phdrs函数读取解释器文件的Segment信息,找到类型为PT_LOPROC和PT_HIPROC的Segment以供特定的体系结构检查。

fs/binfmt_elf.c
load_elf_binary第五部分

static int load_elf_binary(struct linux_binprm *bprm){    ...    flush_old_exec(bprm);    SET_PERSONALITY2(loc->elf_ex, &arch_state);    if (elf_read_implies_exec(loc->elf_ex, executable_stack))        current->personality |= READ_IMPLIES_EXEC;    if (!(current->personality & ADDR_NO_RANDOMIZE) && randomize_va_space)        current->flags |= PF_RANDOMIZE;    setup_new_exec(bprm);    retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP),                 executable_stack);    if (retval < 0)        goto out_free_dentry;    current->mm->start_stack = bprm->p;    ...}

flush_old_exec主要用来进行新进程地址空间的替换,并删除同线程组中的其他线程。
再往下继续设置进程task_struct的personality。
然后通过setup_new_exec函数对刚刚替换的地址空间进行简单的初始化。
randomize_stack_top对栈的指针进行随机的移动,然后通过setup_arg_pages函数随机调整堆栈的位置。
最后记录栈的起始地址至mm_struct结构中。

fs/exec.c
load_elf_binary->flush_old_exec

int flush_old_exec(struct linux_binprm * bprm){    de_thread(current);    set_mm_exe_file(bprm->mm, bprm->file);    exec_mmap(bprm->mm);    bprm->mm = NULL;    set_fs(USER_DS);    current->flags &= ~(PF_RANDOMIZE | PF_FORKNOEXEC | PF_KTHREAD |                    PF_NOFREEZE | PF_NO_SETAFFINITY);    flush_thread();    current->personality &= ~bprm->per_clear;    return 0;}

因为即将要替换新进程的地址空间,所以首先通过de_thread函数用来删除同线程组中的其他线程。
set_mm_exe_file函数设置新进程的路径,即mm_struct中的exe_file成员变量。
再往下就通过exec_mmap函数将新进程的地址空间设置为bprm中创建并设置好的地址空间。
flush_thread函数主要用来初始化thread_struct中的TLS元数据信息。
最后设置进程的标志位flags和personality,personality用来兼容linux的旧版本或者BSD等其他版本。

fs/exec.c
load_elf_binary->flush_old_exec->de_thread

static int de_thread(struct task_struct *tsk){    struct signal_struct *sig = tsk->signal;    struct sighand_struct *oldsighand = tsk->sighand;    spinlock_t *lock = &oldsighand->siglock;    if (thread_group_empty(tsk))        goto no_thread_group;    if (signal_group_exit(sig)) {        spin_unlock_irq(lock);        return -EAGAIN;    }    sig->group_exit_task = tsk;    sig->notify_count = zap_other_threads(tsk);    if (!thread_group_leader(tsk))        sig->notify_count--;    while (sig->notify_count) {        __set_current_state(TASK_KILLABLE);        spin_unlock_irq(lock);        schedule();        if (unlikely(__fatal_signal_pending(tsk)))            goto killed;        spin_lock_irq(lock);    }    spin_unlock_irq(lock);    if (!thread_group_leader(tsk)) {        ...    }    sig->group_exit_task = NULL;    sig->notify_count = 0;no_thread_group:    tsk->exit_signal = SIGCHLD;    exit_itimers(sig);    flush_itimer_signals();    if (atomic_read(&oldsighand->count) != 1) {        struct sighand_struct *newsighand;        newsighand = kmem_cache_alloc(sighand_cachep, GFP_KERNEL);        if (!newsighand)            return -ENOMEM;        atomic_set(&newsighand->count, 1);        memcpy(newsighand->action, oldsighand->action,               sizeof(newsighand->action));        rcu_assign_pointer(tsk->sighand, newsighand);        __cleanup_sighand(oldsighand);    }    return 0;    ...}

首先通过thread_group_empty函数检查线程组中是否有其他线程,如果没有就直接返回。
接下来通过signal_group_exit函数检查是否已经开始删除线程组,如果是,也直接返回。
zap_other_threads开始执行删除线程组的操作,其内部会向除了本线程外的所有其他线程发送SIGKILL信号。
接下来通过while循环等待其他线程的退出。
如果不是线程组leader,就要等待该leader的退出,省略的代码用来将当前task替换成线程组leader。
再往下为新的进程分配新的sighand_struct结构并初始化。

fs/exec.c
load_elf_binary->setup_new_exec

void setup_new_exec(struct linux_binprm * bprm){    arch_pick_mmap_layout(current->mm);    current->sas_ss_sp = current->sas_ss_size = 0;    if (uid_eq(current_euid(), current_uid()) && gid_eq(current_egid(), current_gid()))        set_dumpable(current->mm, SUID_DUMP_USER);    else        set_dumpable(current->mm, suid_dumpable);    perf_event_exec();    __set_task_comm(current, kbasename(bprm->filename), true);    current->mm->task_size = TASK_SIZE;    if (!uid_eq(bprm->cred->uid, current_euid()) ||        !gid_eq(bprm->cred->gid, current_egid())) {        current->pdeath_signal = 0;    } else {        would_dump(bprm, bprm->file);        if (bprm->interp_flags & BINPRM_FLAGS_ENFORCE_NONDUMP)            set_dumpable(current->mm, suid_dumpable);    }    current->self_exec_id++;    flush_signal_handlers(current, 0);    do_close_on_exec(current->files);}

既然前面已经替换了新进程的mm_struct结构,下面就要对该结构进行设置。arch_pick_mmap_layout函数对设置了mmap的起始地址和分配函数。
然后更新mm的标志位,通过kbasename函数根据文件路径bprm->filename获得最后的文件名,再调用__set_task_comm函数设置进程的文件路径,最终设置到task_struct的comm变量中。
flush_signal_handlers用于清空信号的处理函数。最后调用do_close_on_exec关闭对应的文件。

arch/x86/mm/mmap.c
load_elf_binary->setup_new_exec->arch_pick_mmap_layout

void arch_pick_mmap_layout(struct mm_struct *mm){    unsigned long random_factor = 0UL;    if (current->flags & PF_RANDOMIZE)        random_factor = arch_mmap_rnd();    mm->mmap_legacy_base = mmap_legacy_base(random_factor);    if (mmap_is_legacy()) {        mm->mmap_base = mm->mmap_legacy_base;        mm->get_unmapped_area = arch_get_unmapped_area;    } else {        mm->mmap_base = mmap_base(random_factor);        mm->get_unmapped_area = arch_get_unmapped_area_topdown;    }}

arch_mmap_rnd获得线性区的随机起始地址,其为get_random_int() % (1<<28)。
mmap_legacy_base修改该地址,加上TASK_UNMAPPED_BASE,值为((1UL << 47) - PAGE_SIZE)/3。
最后设置mmap_base地址和get_unmapped_area函数指针,get_unmapped_area函数用于分配虚拟内存。

load_elf_binary->setup_arg_pages

int setup_arg_pages(struct linux_binprm *bprm,            unsigned long stack_top,            int executable_stack){    unsigned long ret;    unsigned long stack_shift;    struct mm_struct *mm = current->mm;    struct vm_area_struct *vma = bprm->vma;    struct vm_area_struct *prev = NULL;    unsigned long vm_flags;    unsigned long stack_base;    unsigned long stack_size;    unsigned long stack_expand;    unsigned long rlim_stack;    stack_top = arch_align_stack(stack_top);    stack_top = PAGE_ALIGN(stack_top);    stack_shift = vma->vm_end - stack_top;    bprm->p -= stack_shift;    mm->arg_start = bprm->p;    bprm->exec -= stack_shift;    ...    if (stack_shift) {        shift_arg_pages(vma, stack_shift);    }    stack_expand = 131072UL;    stack_size = vma->vm_end - vma->vm_start;    rlim_stack = rlimit(RLIMIT_STACK) & PAGE_MASK;    if (stack_size + stack_expand > rlim_stack)        stack_base = vma->vm_end - rlim_stack;    else        stack_base = vma->vm_start - stack_expand;    current->mm->start_stack = bprm->p;    expand_stack(vma, stack_base);}

传入的参数stack_top添加了随机因子,首先对该stack_top进行页对齐,然后计算位移stack_shift,再将该位移添加到栈的指针bprm->p也即当前参数的存放地址mm->arg_start。省略的部分是对标志位的修改,再往下既然修改了栈的指针,就要通过shift_arg_pages函数修改堆栈对应的虚拟内存了。最后需要通过expand_stack函数拓展栈的大小,默认为stack_expand即4个页面。

fs/binfmt_elf.c
load_elf_binary第六部分

前面已经根据elf文件头进行了相应的初始化和设置工作,并加载了解释器的Segment头,下面这部分代码要查找elf文件中类型为PT_LOAD的Segment,将其装载进内存。

static int load_elf_binary(struct linux_binprm *bprm){    ...    for(i = 0, elf_ppnt = elf_phdata;        i < loc->elf_ex.e_phnum; i++, elf_ppnt++) {        int elf_prot = 0, elf_flags;        unsigned long k, vaddr;        unsigned long total_size = 0;        if (elf_ppnt->p_type != PT_LOAD)            continue;        if (unlikely (elf_brk > elf_bss)) {            unsigned long nbyte;            retval = set_brk(elf_bss + load_bias,                     elf_brk + load_bias);            if (retval)                goto out_free_dentry;            nbyte = ELF_PAGEOFFSET(elf_bss);            if (nbyte) {                nbyte = ELF_MIN_ALIGN - nbyte;                if (nbyte > elf_brk - elf_bss)                    nbyte = elf_brk - elf_bss;                if (clear_user((void __user *)elf_bss +                            load_bias, nbyte)) {                }            }        }        if (elf_ppnt->p_flags & PF_R)            elf_prot |= PROT_READ;        if (elf_ppnt->p_flags & PF_W)            elf_prot |= PROT_WRITE;        if (elf_ppnt->p_flags & PF_X)            elf_prot |= PROT_EXEC;        elf_flags = MAP_PRIVATE | MAP_DENYWRITE | MAP_EXECUTABLE;        vaddr = elf_ppnt->p_vaddr;        if (loc->elf_ex.e_type == ET_EXEC || load_addr_set) {            elf_flags |= MAP_FIXED;        } else if (loc->elf_ex.e_type == ET_DYN) {            load_bias = ELF_ET_DYN_BASE - vaddr;            if (current->flags & PF_RANDOMIZE)                load_bias += arch_mmap_rnd();            load_bias = ELF_PAGESTART(load_bias);            total_size = total_mapping_size(elf_phdata,                            loc->elf_ex.e_phnum);        }        elf_map(bprm->file, load_bias + vaddr, elf_ppnt,                elf_prot, elf_flags, total_size);        if (!load_addr_set) {            load_addr_set = 1;            load_addr = (elf_ppnt->p_vaddr - elf_ppnt->p_offset);            if (loc->elf_ex.e_type == ET_DYN) {                load_bias += error -                             ELF_PAGESTART(load_bias + vaddr);                load_addr += load_bias;                reloc_func_desc = load_bias;            }        }        k = elf_ppnt->p_vaddr;        if (k < start_code)            start_code = k;        if (start_data < k)            start_data = k;        if (BAD_ADDR(k) || elf_ppnt->p_filesz > elf_ppnt->p_memsz ||            elf_ppnt->p_memsz > TASK_SIZE ||            TASK_SIZE - elf_ppnt->p_memsz < k) {            retval = -EINVAL;            goto out_free_dentry;        }        k = elf_ppnt->p_vaddr + elf_ppnt->p_filesz;        if (k > elf_bss)            elf_bss = k;        if ((elf_ppnt->p_flags & PF_X) && end_code < k)            end_code = k;        if (end_data < k)            end_data = k;        k = elf_ppnt->p_vaddr + elf_ppnt->p_memsz;        if (k > elf_brk)            elf_brk = k;    }    loc->elf_ex.e_entry += load_bias;    elf_bss += load_bias;    elf_brk += load_bias;    start_code += load_bias;    end_code += load_bias;    start_data += load_bias;    end_data += load_bias;    set_brk(elf_bss, elf_brk);    ...}

首先循环遍历Segment头,查找类型为PT_LOAD的头,
然后根据Segment头的flag标志位设置内存的标志位elf_prot。
接下来如果要加载的文件数据类型为ET_EXEC,则在固定地址上分配虚拟内存,因此要加上MAP_FIXED标志,而如果要加载的数据类型为ET_DYN,则需要从ELF_ET_DYN_BASE地址处开始映射时,在设置了PF_RANDOMIZE标志位时,需要加上arch_mmap_rnd()随机因子,将偏移记录到load_bias中。total_size为计算的需要映射的内存大小。
再往下就通过elf_map函数将文件映射到虚拟内存中。如果是第一次映射,则需要记录虚拟的elf文件装载地址load_addr,如果是ET_DYN类型的数据,需要加上偏移load_bias。
每次映射后,都要修改bss段、代码段、数据段、堆的起始位置,对同一个elf文件而言,start_code向上增长,start_data向下增长,elf_bss向上增长,end_code 向上增长,end_data 向上增长,elf_brk向上增长,因此从虚拟内存中看,从低地址到高地址依次为代码段,数据段,bss段和堆的起始地址。当装载完毕退出循环后需要将这些变量加上偏移load_bias。
最后通过set_brk在elf_bss到elf_brk之间分配内存空间。

fs/binfmt_elf.c
load_elf_binary->elf_map

static unsigned long elf_map(struct file *filep, unsigned long addr,        struct elf_phdr *eppnt, int prot, int type,        unsigned long total_size){    unsigned long map_addr;    unsigned long size = eppnt->p_filesz + ELF_PAGEOFFSET(eppnt->p_vaddr);    unsigned long off = eppnt->p_offset - ELF_PAGEOFFSET(eppnt->p_vaddr);    addr = ELF_PAGESTART(addr);    size = ELF_PAGEALIGN(size);    if (!size)        return addr;    if (total_size) {        total_size = ELF_PAGEALIGN(total_size);        map_addr = vm_mmap(filep, addr, total_size, prot, type, off);        if (!BAD_ADDR(map_addr))            vm_munmap(map_addr+size, total_size-size);    } else        map_addr = vm_mmap(filep, addr, size, prot, type, off);    return(map_addr);}

传入的参数filep是文件指针,addr是即将映射的内存中的虚拟地址,size是文件映像的大小,off是映像在文件中的偏移。elf_map函数主要通过vm_mmap为文件申请虚拟空间并进行相应的映射,然后返回虚拟空间的起始地址map_addr。

fs/binfmt_elf.c
load_elf_binary第七部分

前面一部分将elf文件中类型为PT_LOAD的数据映射到虚拟内存中,这一部分代码采用相同的方法将解释器的数据映射到虚拟内存中,其内部都是通过elf_map函数进行映射,最后将程序的起始地址保存在elf_entry中,这里就不往下分析了。

static int load_elf_binary(struct linux_binprm *bprm){    ...    if (elf_interpreter) {        unsigned long interp_map_addr = 0;        elf_entry = load_elf_interp(&loc->interp_elf_ex,                        interpreter,                        &interp_map_addr,                        load_bias, interp_elf_phdata);        interp_load_addr = elf_entry;        elf_entry += loc->interp_elf_ex.e_entry;        reloc_func_desc = interp_load_addr;        allow_write_access(interpreter);        fput(interpreter);        kfree(elf_interpreter);    } else {        elf_entry = loc->elf_ex.e_entry;    }    kfree(interp_elf_phdata);    kfree(elf_phdata);    ...}

fs/binfmt_elf.c
load_elf_binary第八部分

static int load_elf_binary(struct linux_binprm *bprm){    ...    set_binfmt(&elf_format);    install_exec_creds(bprm);    create_elf_tables(bprm, &loc->elf_ex,              load_addr, interp_load_addr);    current->mm->end_code = end_code;    current->mm->start_code = start_code;    current->mm->start_data = start_data;    current->mm->end_data = end_data;    current->mm->start_stack = bprm->p;    if ((current->flags & PF_RANDOMIZE) && (randomize_va_space > 1)) {        current->mm->brk = current->mm->start_brk =            arch_randomize_brk(current->mm);    }    start_thread(regs, elf_entry, bprm->p);    ...}

set_binfmt将elf_format记录到mm_struct的binfmt变量中。
install_exec_creds会设置新进程的cred结构。
create_elf_tables函数在将启动解释器或者程序前,向用户空间的堆栈添加一些额外信息,例如应用程序Segment头的起始地址,入口地址等等。
接着向新进程mm_struct结构中设置前面计算的代码段、数据段、bss段和堆的位置。
再往下根据标志位PF_RANDOMIZE选择是否要为堆起始地址添加随机变量。
最后调用start_thread函数将执行权交给解释器或者应用程序了。

arch/x86/kernel/process_64.c
load_elf_binary->start_thread

voidstart_thread(struct pt_regs *regs, unsigned long new_ip, unsigned long new_sp){    start_thread_common(regs, new_ip, new_sp,                __USER_CS, __USER_DS, 0);}static voidstart_thread_common(struct pt_regs *regs, unsigned long new_ip,            unsigned long new_sp,            unsigned int _cs, unsigned int _ss, unsigned int _ds){    loadsegment(fs, 0);    loadsegment(es, _ds);    loadsegment(ds, _ds);    load_gs_index(0);    regs->ip        = new_ip;    regs->sp        = new_sp;    regs->cs        = _cs;    regs->ss        = _ss;    regs->flags     = X86_EFLAGS_IF;    force_iret();}

传入的参数regs为保存的寄存器,new_ip为解释器或者应用程序的起始代码地址,new_sp为用户空间的堆栈指针。设置完这些变量后,最后通过force_iret强制返回,跳到new_ip指向的地址处开始执行。对于glibc而言,最终就会跳转到_start函数中,下一章开始分析该函数。

0 0