pos机出现r0如何解除(Linux内核之execve函数执行流程)

快鱼网 13 0

中,我们引出了execve系统调用函数,我们依然以hello world为例子程序。

点击(此处)折叠或打开

#include int main (int argc, char *argv[]){ printf ("Hello World\n"); return 0;}

保存为hello.c,我们可以通过gcc hello.c -o hello得到可执行程序hello.

当我们在shell命令行终端调用./hello的时候, 实际上busybox执行execve函数来执行hello程序。用strace ./hello可以看到它的系统调用。

点击(此处)折叠或打开

$strace ./helloexecve("./hello", ["./hello"], [/* 33 vars */]) = 0brk(NULL) = 0xeaccess("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3fstat(3, {st_mode=S_IFREG|0644, st_size=, ...}) = 0mmap(NULL, , PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f5181fce000close(3) = 0access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)open("/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0P\t\2\0\0\0\0\0"..., 832) = 832fstat(3, {st_mode=S_IFREG|0755, st_size=, ...}) = 0mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f5181fcd000mmap(NULL, , PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7ffc000mprotect(0x7f5181bbc000, , PROT_NONE) = 0mmap(0x7f5181dbc000, , PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1c0000) = 0x7f5181dbc000mmap(0x7f5181dc2000, , PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0)= 0x7f5181dc2000close(3) = 0mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f5181fcc000mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f5181fcb000arch_prctl(ARCH_SET_FS, 0x7f5181fcc700) = 0mprotect(0x7f5181dbc000, , PROT_READ) = 0mprotect(0x, 4096, PROT_READ) = 0mprotect(0x7f5181feb000, 4096, PROT_READ) = 0munmap(0x7f5181fce000, ) = 0fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 21), ...}) = 0brk(NULL) = 0xebrk(0xeb2000) = 0xeb2000write(1, "Hello World\n", 12Hello World) = 12exit_group(0) = ?+++ exited with 0 +++

图 1

由图1, 我们可以看出执行hello程序的第一步就是将hello的路径作为参数,传入execve函数。下面,我们需要一行一行的去看看execve函数的内核实现(系统调用陷入的详细过程将在以后的文章中说明)。注:由于篇幅限制,以下无法详细贴出所有代码,只是选择关键部分贴出来。

在Linux当中,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);}// Code in "fs/exec.c"

其中SYSCALL_DEFINE3是一个宏,表示这是一个含有3个参数的系统调用,这个宏会产生一个sys_execve函数。也就是当应用程序调用execve函数的时候,CPU产生系统调用中断,中断处理函数通过查表(sys_call_table),通过对应的系统调用号找到sys_execve函数并开始执行(R7/W8存放的系统调用号,参数以此类推),sys_execve函数到__do_execve_file并没有做什么操作,只是简单调用,因此现在直接到__do_execve_file函数,调用栈如下:

点击(此处)折叠或打开

execve (user space)---|---------------------------------------------------------------------------- v (kernel space)el0_sync (arm64同步异常中断处理函数) el0_svc (查找sys_call_table,获取到sys_execve函数地址,并运行) sys_execve do_execve do_execveat_common __do_execve_file

这里以hello可执行程序为例进行讲解,__do_execve_file函数实现如下:

点击(此处)折叠或打开// 这里fd值默认为AT_FDCWD,表示当前进程的工作空间static int __do_execve_file(int fd, struct filename *filename, struct user_arg_ptr argv, struct user_arg_ptr envp, int flags, struct file *file){ ........... // 为hello文件的bprm申请内存空间, bprm是可执行程序文件,底层的一个结构描述 bprm = kzalloc(sizeof(*bprm), GFP_KERNEL); if (!bprm) goto out_files; ............ // 打开hello文件,并返回文件描述结构体struct file if (!file) file = do_open_execat(fd, filename, flags); retval = PTR_ERR(file); if (IS_ERR(file)) goto out_unmark; // SMP才使能,多核应用程序负载均衡调用。 sched_exec(); bprm->file = file; // 获取hello文件的路径 if (!filename) { bprm->filename = "none"; } else if (fd == AT_FDCWD || filename->name[0] == '/') { bprm->filename = filename->name; } else { if (filename->name[0] == '\0') pathbuf = kasprintf(GFP_KERNEL, "/dev/fd/%d", fd); else pathbuf = kasprintf(GFP_KERNEL, "/dev/fd/%d/%s", fd, filename->name); if (!pathbuf) { retval = -ENOMEM; goto out_unmark; } if (close_on_exec(fd, rcu_dereference_raw(current->files->fdt))) bprm->interp_flags |= BINPRM_FLAGS_PATH_INACCESSIBLE; bprm->filename = pathbuf; } bprm->interp = bprm->filename; // 根据当前进程信息,为新程序hello的bprm结构初始化应用程序需要的内存,主要是mm_struct结构 retval = bprm_mm_init(bprm); if (retval) goto out_unmark; // 将hello程序环境变量,参数和bprm结构做出预处理,主要是获取参数个数和环境变量个数 retval = prepare_arg_pages(bprm, argv, envp); if (retval < 0) goto out; // 读取hello文件的前128字节到bprm的buf,这里elf头部只有64字节 retval = prepare_binprm(bprm); if (retval < 0) goto out; // 将hello文件名字拷贝的新进程的内存空间 retval = copy_strings_kernel(1, &bprm->filename, bprm); if (retval < 0) goto out; bprm->exec = bprm->p; // 将hello的环境变量拷贝到新进程的上下文内存空间 retval = copy_strings(bprm->envc, envp, bprm); if (retval < 0) goto out; // 将hello的参数拷贝到新进程上下文的内存空间 retval = copy_strings(bprm->argc, argv, bprm); if (retval < 0) goto out; would_dump(bprm, bprm->file); // 运行bprm指向的应用程序, 这里是hello程序 retval = exec_binprm(bprm); if (retval < 0) goto out; return retval;}

exec_binprm函数就是用于为bprm结构找到合适的加载函数,定义如下:

点击(此处)折叠或打开

static int exec_binprm(struct linux_binprm *bprm){ .......... // 通过search_binary_handler函数来查找bprm到底是什么文件格式,以便于调用对应的处理函数 ret = search_binary_handler(bprm); .......... return ret;}

search_binary_handler函数的实现如下:

点击(此处)折叠或打开/* * cycle the list of binary formats handler, until one recognizes the image */int search_binary_handler(struct linux_binprm *bprm){ ........ // 遍历formats链表,去查找链表formats里面的所有linux支持的文件处理格式 list_for_each_entry(fmt, &formats, lh) { if (!try_module_get(fmt->module)) continue; // 调用load_binary函数去加载指定格式 retval = fmt->load_binary(bprm); ........ } read_unlock(&binfmt_lock); ......... return retval;}

从上面,我们可以看到formats链表,这个链表是怎么来的呢?通过查找代码,我们可以发现内核实现了一个函数register_binfmt, 这个函数用于注册内核支持的处理程序(目前主要为a.out, elf, script等),如我们的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,};static int __init init_elf_binfmt(void){ // 注册一种可执行的文件格式 register_binfmt(&elf_format); return 0;}static void __exit exit_elf_binfmt(void){ unregister_binfmt(&elf_format);}core_initcall(init_elf_binfmt);module_exit(exit_elf_binfmt);// 代码在 fs/binfmt_elf.c

其中register_binfmt的实现如下:

点击(此处)折叠或打开

void __register_binfmt(struct linux_binfmt * fmt, int insert){ write_lock(&binfmt_lock); // 添加到链表 insert ? list_add(&fmt->lh, &formats) : list_add_tail(&fmt->lh, &formats); write_unlock(&binfmt_lock);}static inline void register_binfmt(struct linux_binfmt *fmt){ __register_binfmt(fmt, 0);}

可以发现register_binfmt其实就是把指定的结构插入到formats链表当中。我们继续看看elf的load_binary函数实现,至于其他格式文件,我们这里不考虑讲解。elf的load_binary实现如下:

点击(此处)折叠或打开static int load_elf_binary(struct linux_binprm *bprm){ ........ // 从前面知道bprm->buf为elf文件的前128字节,因此,这里的操作是获取elf头部信息 loc->elf_ex = *((struct elfhdr *)bprm->buf); retval = -ENOEXEC; // 比较elf幻术magic,判断是否为elf,不是就退出 if (memcmp(loc->elf_ex.e_ident, ELFMAG, SELFMAG) != 0) goto out; ............ // 获取elf的program header elf_phdata = load_elf_phdrs(&loc->elf_ex, bprm->file); ........ // 这个for循环中间省略许多判断条件,主要是读取elf的动态解释器路径,并保存到elf_interpreter for (i = 0; i < loc->elf_ex.e_phnum; i++) { if (elf_ppnt->p_type == PT_INTERP) { ....... retval = kernel_read(bprm->file, elf_interpreter, elf_ppnt->p_filesz, &pos); ....... } } ........ // 加载动态解释器的program header if (elf_interpreter) { ....... interp_elf_phdata = load_elf_phdrs(&loc->interp_elf_ex,interpreter); ....... } ........ // 设置起始栈 current->mm->start_stack = bprm->p; // 根据program header提供的信息对elf做必要的内存映射 for(i = 0, elf_ppnt = elf_phdata; i < loc->elf_ex.e_phnum; i++, elf_ppnt++) { ....... error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt, elf_prot, elf_flags, total_size); ....... } ........ // 从动态解释器中获取动态解释器的入口地址,并赋值给elf_entry if (elf_interpreter) { ........ elf_entry = load_elf_interp(&loc->interp_elf_ex,interpreter,&interp_map_addr,load_bias, interp_elf_phdata); ........ } ........... // 设置pc指针为动态解释器的入口地址 start_thread(regs, elf_entry, bprm->p); ........}

最后,load_elf_binary函数调用start_thread去设置对应的regs结构,也就是内核空间返回到应用空间需要读取的结构(user_regs主要存放r0-r15相关的寄存器信息,用于恢复应用空间上下文),start_thread的实现如下:

点击(此处)折叠或打开

static inline void start_thread_common(struct pt_regs *regs, unsigned long pc){ memset(regs, 0, sizeof(*regs)); regs->syscallno = ~0UL; regs->pc = pc;}static inline void start_thread(struct pt_regs *regs, unsigned long pc, unsigned long sp){ start_thread_common(regs, pc); regs->pstate = PSR_MODE_EL0t; regs->sp = sp;}

这样,当cpu从系统调用返回到用户空间时,就从regs->pc确定的地址开始执行,达到了间接跳转的目的,而这个pc地址正好是动态解释器的入口地址(不再是execve的调用者地址),所以当execve函数返回的时候,就直接跳转到了动态解释器中运行,这个动态解释器通常是/lib64/ld-linux-x86-64.so.2。动态解释器怎么执行,怎么符号重定向,以后会有文章加以说明。

调用栈如下:

标签: bprm

抱歉,评论功能暂时关闭!