sys_execv源码分析

因为最近工作涉及到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

void
start_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 void
start_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函数中,下一章开始分析该函数。

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值