可执行程序的装载之前的工作
通过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(¤t_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文件中的函数入口点的定义值。