余星光 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
一、预备知识
1.1 编译链接的过程
预处理:(.c---->.cpp)(注意:这里的CPP不是C Plus Plus的意思)
gcc -E -o hello.cpp hello.c -m32编译:(.cpp---->.s汇编)
gcc -x cpp-output -S -o hello.s hello.cpp -m32编译:(.s---->.o二进制代码)
gcc -x assembler -c hello.s -o hello.o -m32链接:(.o---->a.out)共享库
gcc -o hello hello.o -m32静态编译:
gcc -o hello.static hello.o -m32 -static
int main(int argc, char *argv[])
int main(int argc, char *argv[], char *envp[])
execve函数格式
int execve(const char *filename, char *const argv[], char *const envp[])
系统调用过程:sys_execve -> do_execve -> do_execve_common -> exec_binprm
动态链接:
(1)装载时动态链接
gcc -shared shlibexample.c -o libshlibexample.so -m32
(2)运行时动态链接
gcc -shared dllibexample.c -o libdllibexample.so -m32
1.2 ELF可执行文件格式
ELF文件有三种类型:
(1)可重定位文件(relocatable),即通常所说的目标文件(.o)
(2)可执行文件(executable)
(3)共享文件(object),即通常所说的库文件(.so)
ELF文件的总体布局:
ELF header(头)
program header table(程序头表)
segment 1(段1)
……
segment n(段n)
section header table(节头表)(可选)
用readelf可以看可执行文件的ELF信息
ELF可执行文件默认映射地址为:0x8048000
但实际的入口地址为:0x8048300
二、实验过程
2.1 打开shell终端,执行如下命令:
cd LinuxKernel rm menu -rf git clone https://github.com/mengning/menu.git cd menu mv test_exec.c test.c make rootfs效果如下:
2.2 开启调试模式
关闭上面的qemu,在menu目录下执行如下命令:
qemu -kernel ../linux-3.18.6/arch/x86/boot/bzImage -initrd ../rootfs.img -s -S另开一个shell窗口,在menu目录下开启gdb
gdb file ../linux-3.18.6/vmlinux target remote:1234
2.3 设置断点跟踪调试
b sys_execve b do_execve b search_binary_handler b load_elf_binary b start_thread
2.4 分析
源码:/linux-3.18.6/fs/exec.c
装载和启动一个可执行程序依次调用以下函数:
sys_execve -> do_execve -> do_execve_common -> exec_binprm ->search_binary_handler ->load_elf_binary -> start_thread
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); }
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); }
/* * 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; retval = unshare_files(&displaced); if (retval) goto out_ret; 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_exec(filename); retval = PTR_ERR(file); if (IS_ERR(file)) goto out_unmark; sched_exec(); bprm->file = file; bprm->filename = bprm->interp = filename->name; 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; /* 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; }
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); }
/* * cycle the list of binary formats handler, until one recognizes the image */ int search_binary_handler(struct linux_binprm *bprm) { bool need_retry = IS_ENABLED(CONFIG_MODULES); struct linux_binfmt *fmt; int retval; /* This allows 4 levels of binfmt rewrites before failing hard. */ if (bprm->recursion_depth > 5) return -ELOOP; retval = security_bprm_check(bprm); if (retval) return retval; retval = -ENOENT; retry: read_lock(&binfmt_lock); 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--; if (retval < 0 && !bprm->mm) { /* we got to flush_old_exec() and failed after it */ read_unlock(&binfmt_lock); force_sigsegv(SIGSEGV, current); return retval; } if (retval != -ENOEXEC || !bprm->file) { read_unlock(&binfmt_lock); return retval; } } read_unlock(&binfmt_lock); if (need_retry) { if (printable(bprm->buf[0]) && printable(bprm->buf[1]) && printable(bprm->buf[2]) && printable(bprm->buf[3])) return retval; if (request_module("binfmt-%04x", *(ushort *)(bprm->buf + 2)) < 0) return retval; need_retry = false; goto retry; } return retval; } EXPORT_SYMBOL(search_binary_handler);对于ELF格式的可执行文件fmt->load_binary(bprm);执行的应该是load_elf_binary其内部是和ELF文件格式解析的部分需要和ELF文件格式标准结合起来阅读。load_elf_binary在/linux-3.18.6/fs/binfmt_elf.c中。
static struct linux_binfmt elf_format = { .module = THIS_MODULE, .load_binary = load_elf_binary, .load_dump = elf_core_dump, .min_coredump = ELF_EXEC_PAGESIZE, };
三、总结
新的可执行程序是从new_ip开始执行,start_thread实际上是返回到用户态的位置从Int 0x80的下一条指令,变成了新加载的可执行文件的入口位置。当执行到execve系统调用时,陷入内核态,用execve加载的可执行文件覆盖当前进程的可执行程序,当execve系统调用返回时,返回新的可执行程序的执行起点(main函数位置),所以execve系统调用返回后新的可执行程序能顺利执行。对于静态链接的可执行程序和动态链接程序execve系统调用返回时,如果是静态链接,elf_entry指向可执行文件规定的头部0x8048000;如果需要依赖动态链接库,elf_entry指向动态连接器的起点。