余星光 + 原创作品转载请注明出处 + 《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 分析
装载和启动一个可执行程序依次调用以下函数:
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,
};
当调用execve的可执行程序是,系统调用execve陷入内核,这时会创建一个新的用户太堆栈,实际是把参数和环境变量通过指针的方式传递给系统调用内核处理函数,然后内核处理函数在创建可执行程序新的用户态堆栈的时候,会把这些拷贝到用户态堆栈初始化新的可执行程序的执行上下文环境(先函数调用参数传递,在系统调用参数传递)。这时就加载了新的可执行程序。系统调用execve返回用户态的时候,就变成了被execve加载的可执行程序。
三、总结
新的可执行程序是从new_ip开始执行,start_thread实际上是返回到用户态的位置从Int 0x80的下一条指令,变成了新加载的可执行文件的入口位置。当执行到execve系统调用时,陷入内核态,用execve加载的可执行文件覆盖当前进程的可执行程序,当execve系统调用返回时,返回新的可执行程序的执行起点(main函数位置),所以execve系统调用返回后新的可执行程序能顺利执行。对于静态链接的可执行程序和动态链接程序execve系统调用返回时,如果是静态链接,elf_entry指向可执行文件规定的头部0x8048000;如果需要依赖动态链接库,elf_entry指向动态连接器的起点。