第七课 可执行程序的装载(2)

时间:2022-09-28 20:04:15

可执行程序的装载之前的工作
通过shell程序启动一个可执行程序,shell程序到底做了什么?在执行exec调用可执行程序之前,shell要做哪些工作?
举个例子:
ls -l /usr/bin
实际上shell调用了可执行程序ls。-l 和/usr/bin是ls程序的入口参数。shell命令将会调用exec函数并把参数传递给可执行程序的main函数。

int execve(const char * filename,char * const argv[ ],char * const envp[ ]);

shell首先创建一个子进程,然后在子进程中调用exec函数,从而执行新的程序。

命令行参数是如何传递给新的程序的main函数呢?换句话说,是如何进入到新的进程的堆栈中的呢?再来看一下函数的调用过程。
shell函数调用execve函数,调用sys_execve系统调用,在初始化新的程序堆栈的时候,将会依据系统调用和入参的指针值拷贝信息到自己的堆栈中。此时旧的堆栈信息将会被清空。仅仅在内存中存储了一份,是新的一份参数。

粘贴实验结果。
动态库有两种使用方法,一种是动态链接,一种是动态装载。
动态链接是直接调用函数
动态装载是直接操作*.so文件。此种是程序自身去状态库。

装载过程

execve是一种系统调用,来完成可执行程序的执行。但是他是一种特殊的系统调用。与fork函数类似,都是特殊的。
当前的可执行程序在执行中,当执行到execve时,陷入到内核态,把当前可执行程序被覆盖掉之后,函数在返回时将会进入到新的可执行程序的执行起点。因此main函数的执行环境必须在内核中首先创建好,才能正确的执行。
下面我们来追踪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_execve_common(filename, argv, envp);
}

上面是函数实现,调用了do_execve_common这个函数。

/* * sys_execve() executes a new program. */
static int do_execve_common(struct filename *filename,
                struct user_arg_ptr argv,
                struct user_arg_ptr envp)
{
    struct linux_binprm *bprm;
    struct file *file;
    struct files_struct *displaced;
    int retval;

    if (IS_ERR(filename))
        return PTR_ERR(filename);

    /* * We move the actual failure in case of RLIMIT_NPROC excess from * set*uid() to execve() because too many poorly written programs * don't check setuid() return code. Here we additionally recheck * whether NPROC limit is still exceeded. */
     这里是在函数之前首先检查一下用户权限,是否是可执行的,因为在一般的程序调用过程中都不会设置此标志,所以这里又做了一次检查。
    if ((current->flags & PF_NPROC_EXCEEDED) &&
        atomic_read(&current_user()->processes) > rlimit(RLIMIT_NPROC)) {
        retval = -EAGAIN;
        goto out_ret;
    }

    /* We're below the limit (still or again), so we don't want to make * further execve() calls fail. */
    current->flags &= ~PF_NPROC_EXCEEDED;

    //为当前的进程复制一份已经打开的文件句柄,这个函数是对copy_files的进一步的封装。
    retval = unshare_files(&displaced);
    if (retval)
        goto out_ret;
    //结构体开辟空间
    retval = -ENOMEM;
    bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);
    if (!bprm)
        goto out_files;
    //给出新的credit,叫做证书或者权限
    retval = prepare_bprm_creds(bprm);
    if (retval)
        goto out_free;
    //调用者必须拿到一把锁才能安全的执行下面的系统调用函数
    check_unsafe_exec(bprm);
    current->in_execve = 1;
    //打开二进制文件
    file = do_open_exec(filename);
    retval = PTR_ERR(file);
    if (IS_ERR(file))
        goto out_unmark;

    //因为接下来要执行新的程序,在多cpu的情况下,为了负载均衡,就要用到调度器来处理。这个函数还应用在fork函数中。这是linux下的创建时负载均衡机制。
    sched_exec();

    bprm->file = file;
    bprm->filename = bprm->interp = filename->name;

    retval = bprm_mm_init(bprm);
    if (retval)
        goto out_unmark;
//传递argc个数
    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;

//填充bprm其他字段
    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;

    /* execve succeeded */
    current->fs->in_exec = 0;
    current->in_execve = 0;
    acct_update_integrals(current);
    task_numa_free(current);
    free_bprm(bprm);
    putname(filename);
    if (displaced)
        put_files_struct(displaced);
    return retval;

out:
    if (bprm->mm) {
        acct_arg_size(bprm, 0);
        mmput(bprm->mm);
    }

out_unmark:
    current->fs->in_exec = 0;
    current->in_execve = 0;

out_free:
    free_bprm(bprm);

out_files:
    if (displaced)
        reset_files_struct(displaced);
out_ret:
    putname(filename);
    return retval;
}

上述代码中,在为下载做准备的时候,仅仅围绕着结构体linux_binprm 展开。linux_binprm 结构体定义如下:

/* * This structure is used to hold the arguments that are used when loading binaries. */
struct linux_binprm {
    char buf[BINPRM_BUF_SIZE];
#ifdef CONFIG_MMU
    struct vm_area_struct *vma;
    unsigned long vma_pages;
#else
# define MAX_ARG_PAGES 32
    struct page *page[MAX_ARG_PAGES];
#endif
    struct mm_struct *mm;
    unsigned long p; /* current top of mem */
    unsigned int
        cred_prepared:1,/* true if creds already prepared (multiple * preps happen for interpreters) */
        cap_effective:1;/* true if has elevated effective capabilities, * false if not; except for init which inherits * its parent's caps anyway */
#ifdef __alpha__
    unsigned int taso:1;
#endif
    unsigned int recursion_depth; /* only for search_binary_handler() */
    struct file * file;
    struct cred *cred;  /* new credentials */
    int unsafe;     /* how unsafe this exec is (mask of LSM_UNSAFE_*) */
    unsigned int per_clear; /* bits to clear in current->personality */
    int argc, envc;
    const char * filename;  /* Name of binary as seen by procps */
    const char * interp;    /* Name of the binary really executed. Most of the time same as filename, but could be different for binfmt_{misc,script} */
    unsigned interp_flags;
    unsigned interp_data;
    unsigned long loader, exec;
};

代码最后调用函数exec_binprm继续执行,函数实现为:

static int exec_binprm(struct linux_binprm *bprm)
{
    pid_t old_pid, old_vpid;
    int ret;

    /* Need to fetch pid before load_binary changes it */
    old_pid = current->pid;
    rcu_read_lock();
    old_vpid = task_pid_nr_ns(current, task_active_pid_ns(current->parent));
    rcu_read_unlock();

    ret = search_binary_handler(bprm);
    if (ret >= 0) {
        audit_bprm(bprm);
        trace_sched_process_exec(current, old_pid, bprm);
        ptrace_event(PTRACE_EVENT_EXEC, old_vpid);
        proc_exec_connector(current);
    }

    return ret;
}

这里search_binary_handler寻找二进制handler是指寻找二进制文件的处理函数。
寻找函数中一段关键代码是:

list_for_each_entry(fmt, &formats, lh) {
        if (!try_module_get(fmt->module))
            continue;
        read_unlock(&binfmt_lock);
        bprm->recursion_depth++;
        retval = fmt->load_binary(bprm);
        read_lock(&binfmt_lock);
        put_binfmt(fmt);
        bprm->recursion_depth--;

寻找到能够解析当前文件的fmt之后,执行对应的load_binary函数。load_binary函数在别处初始化,此时elf对应的解析函数是:


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,
};

在函数load_elf_binary最后调用了如下函数:

 start_thread(regs, elf_entry, bprm->p); retval = 0;

start_kernel中函数分析:

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;
    regs->flags     = X86_EFLAGS_IF;
    /*
     * force it to the iret return path by making it look as if there was
     * some work pending.
     */
    set_thread_flag(TIF_NOTIFY_RESUME);
}
EXPORT_SYMBOL_GPL(start_thread);

regs实际上是内核的栈底的位置,这里在不断的修改regs中值。栈底的ip值被赋予了新的值,sp被赋予了新的值。
ip就是新的程序的入口点,通过上述的调用流程可知是在函数load_elf_binary中elf_entry这个值。而在静态链接文件中,这个值就是在elf文件中的函数入口点的定义值。