寇亚飞 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
exec函数系统调用处理过程
以helloworld.c为例
#include
int main(void)
{
printf("hello, world!\n");
return 0;
}
gcc -E helloworld.c -o helloworld.i
gcc -S helloworld.i -o helloworld.s
gcc -c helloworld.s -o helloworld.o
gcc helloworld.o -o helloworld
./helloworld hello, world!
目标文件格式最早在unix上是.out格式,后来发展成COFF。现在用的最多的是PE(主要用在window)和elf(主要用在linux),ELF格式主要有三种文件格式,可重定位文件(relocatable),可执行文件(execable),一个共享的object文件。
链接是收集和组织程序所需的不同代码和数据的过程,以便程序能被装入内存并被执行。主要完成两部分,一部分是是完成空间与地址的分配,另一部分是符号解析与重定位。链接通常可分为静态链接和动态链接。
静态链接库与动态链接库都是共享代码的方式。如果采用静态链接库,则程序把所有执行需要所依赖的东西都加载到可执行文件中了。但若是使用动态库,则不必被包含在最终执行的文件中,可执行文件执行时可以“动态”的引用和卸载这个与可执行程序独立的动态库文件。
动态链接库:多个应用程序可以使用同一个动态库,启动多个应用程序的时候,只需要将动态库加载到内存一次就好
动态链接库的两种链接方式
装载时动态链接(Load-time Dynamic Linking):这种方法的前提是在编译之前已经明确知道要调用的动态库的哪些函数,编译时在目标文件中只保留必要的链接信息,而不含动态库函数代码;当程序执行时,调用函数的时候利用链接信息加载动态库函数代码并在内存中将其链接入调用程序的执行空间中(全部函数加载进内存),其主要目的是便于代码共享。(动态加载程序,处在加载阶段,主要为了共享代码,共享代码内存)
在sys_execve处设置断点,代码如下:
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);
}
进入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_execve_common(filename, argv, envp);
}
然后进入do_execve_common,然后到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);
execve 是比较特殊的系统调用,exec_binprm 在保存了 bprm 后调用该函数来进一步操作,execve 加载的可执行文件会把当前的进程覆盖掉,返回之后就不是原来的程序而是新的可执行程序起点。这个函数除了保存 pid 以外,还执行了 search_binary_handler 来查询能够处理相应可执行文件格式的处理器,并调用相应的load_binary 方法以启动新进程。
search_binary_handler:
int search_binary_handler(struct linux_binprm *bprm)
{
...
retry:
read_lock(&binfmt_lock);
//循环查找 linux_binfmt
list_for_each_entry(fmt, &formats, lh) {
if (!try_module_get(fmt->module))
continue;
read_unlock(&binfmt_lock);
bprm->recursion_depth++;
//对于 elf 文件,实际上执行的就是 load_elf_binary
retval = fmt->load_binary(bprm);
read_lock(&binfmt_lock);
...
}
load_elf_binary:
static int load_elf_binary(struct linux_binprm *bprm)
{
...
//获取头
loc->elf_ex = *((struct elfhdr *)bprm->buf);
//读取头信息
if (loc->elf_ex.e_phentsize != sizeof(struct elf_phdr))
goto out;
if (loc->elf_ex.e_phnum < 1 ||
loc->elf_ex.e_phnum > 65536U / sizeof(struct elf_phdr))
goto out;
size = loc->elf_ex.e_phnum * sizeof(struct elf_phdr);
retval = -ENOMEM;
elf_phdata = kmalloc(size, GFP_KERNEL);
if (!elf_phdata)
goto out;
...
//读取可执行文件的解析器
for (i = 0; i < loc->elf_ex.e_phnum; i++) {
if (elf_ppnt->p_type == PT_INTERP) {
...
}
...
//如果需要装入解释器,并且解释器的映像是ELF格式的,就通过load_elf_interp()装入其映像,并把将来进入用户空间时的入口地址设置成load_elf_interp()的返回值,那显然是解释器的程序入口。而若不装入解释器,那么这个地址就是目标映像本身的程序入口。
if (elf_interpreter) {
unsigned long interp_map_addr = 0;
elf_entry = load_elf_interp(&loc->interp_elf_ex,
interpreter,
&interp_map_addr,
load_bias);
if (!IS_ERR((void *)elf_entry)) {
interp_load_addr = elf_entry;
elf_entry += loc->interp_elf_ex.e_entry;
}
if (BAD_ADDR(elf_entry)) {
retval = IS_ERR((void *)elf_entry) ?
(int)elf_entry : -EINVAL;
goto out_free_dentry;
}
reloc_func_desc = interp_load_addr;
allow_write_access(interpreter);
fput(interpreter);
kfree(elf_interpreter);
} else {
elf_entry = loc->elf_ex.e_entry;
if (BAD_ADDR(elf_entry)) {
retval = -EINVAL;
goto out_free_dentry;
}
}
}
load_elf_binary 调用 start_thread 函数。修改 int 0x80 压入内核堆栈的 EIP,当 load_elf_binary 执行完毕,返回至 do_execve 再返回至 sys_execve 时,系统调用的返回地址,即 EIP 寄存器,已经被改写成了被装载的 ELF 程序的入口地址了。
Linux 系统通过用户态 execve 函数调用内核态 sys_execve 系统调用,负责将新的程序代码和数据替换到新的进程中,打开可执行文件,载入依赖的库文件,申请新的内存空间,最后执行 start_thread 函数设置 new_ip 和 new_sp,完成新进程的代码和数据替换,然后返回并执行新的进程代码。 具体的执行流程即sys_execve -> do_execve -> do_execve_common -> exec_binprm -> search_binary_handler -> load_binary ->(对于我们这里的ELF,会跳转到)load_elf_binary (也执行了elf_format)-> start_thread
实验截图参考实验报告