sys_execv源码分析
来源:互联网 发布:数据库工程师培训机构 编辑:程序博客网 时间:2024/06/11 17:04
因为最近工作涉及到linux的反汇编,做了一阵子Java搜索引擎的工作,再回到C很不适应,因此借着看源码回忆一点linux的知识,上一篇博文分析了_dl_runtime_resolve的源码,本章从头开始研究在linux下程序装载、运行的全部过程,因此从glibc回到linux的源码,来看看linux的系统调用sys_execv是如何装载程序的,后面会回到glibc陆续分析_dl_start、_dl_main函数的源码。
因为linux源码涉及到的知识太多,本文在尽量保证代码原样的情况下删除一些不重要的代码,并且不过多深入其他知识领域,例如文件系统、负载均衡、安全等等,以后有时间了再分析其他的源码。
fs/exec.c
sys_execv
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);}
SYSCALL_DEFINE3宏定义展开就是sys_execv函数,getname函数进而调用getname_flags函数,该函数会在内核空间分配一块内存用于存储用户空间传入的文件名filename。
fs/exec.c
sys_execv->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_execveat_common(AT_FDCWD, filename, argv, envp, 0);}
user_arg_ptr是对普通指针的一种封装,用于表示用户空间的指针。这里分别封装了程序的调用参数__argv和环境变量__envp。
AT_FDCWD标志意味着文件的搜索路径不是从进程文件系统的根路径开始,而是从进程当前路径开始,在后面打开文件时会用到。
do_execveat_common函数
fs/exec.c
sys_execv->do_execve->do_execveat_common
static int do_execveat_common(int fd, struct filename *filename, struct user_arg_ptr argv, struct user_arg_ptr envp, int flags){ char *pathbuf = NULL; struct linux_binprm *bprm; struct file *file; struct files_struct *displaced; int retval; if (IS_ERR(filename)) return PTR_ERR(filename); if ((current->flags & PF_NPROC_EXCEEDED) && atomic_read(¤t_user()->processes) > rlimit(RLIMIT_NPROC)) { retval = -EAGAIN; goto out_ret; } current->flags &= ~PF_NPROC_EXCEEDED; retval = unshare_files(&displaced); 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_execat(fd, filename, flags); retval = PTR_ERR(file); if (IS_ERR(file)) goto out_unmark; sched_exec(); bprm->file = file; if (fd == AT_FDCWD || filename->name[0] == '/') { bprm->filename = filename->name; } else { ... } bprm->interp = bprm->filename; 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; ...}
参数user_arg_ptr是对用户空间指针的一种分装,linux内核在特定时候(例如sparse)会检查该指针的合法性。
IS_ERR宏用来检测文件名指针是否合法,判断该指针是否指向内核空间的最后一个页面,即错误页面,如果是则直接返回。
接下来根据PF_NPROC_EXCEEDED标志位检查当前用户的进程数是否超过限制,如果超过限制就不能再创建新的进程了,直接返回。
unshare_files函数用于备份当前进程的文件表至displaced中,即当前进程的files_struct结构,当出错或者返回时用于恢复当前进程的文件表。
接下来创建linux_binprm并分配内存空间,后面就要对该结构进行初始化。
prepare_bprm_creds函数会从当前进程内复制一份cred结构,封装了进程的安全信息。
check_unsafe_exec检查程序执行后是否存在潜在的风险,进而设置linux_binprm的unsafe标志位。
do_open_execat函数内部通过do_filp_open函数打开文件,返回file结构。
sched_exec函数在多核计算机中找到最小负载的CPU,用来执行该二进制文件。
再往下通过bprm_mm_init函数分配新进程的内存空间mm_struct。
然后计算用户和环境变量的字符串个数,分别赋值给bprm的argc和envc。count函数内部会通过get_user函数检查用户空间指针的合法性(根据thread_info中的addr_limit变量进行检查)。
prepare_binprm用于设置进程的授权,并将可执行文件的内容读取到bprm的buf缓存中。
再往下的copy_strings_kernel和copy_strings函数都是讲用户空间的数据拷贝到内核空间,其内部通过kmap系统调用获取内核空间的页面。
最后通过exec_binprm函数开始执行新进程。
kernel/fork.c
sys_execv->do_execve->do_execveat_common->unshare_files
int unshare_files(struct files_struct **displaced){ struct task_struct *task = current; struct files_struct *copy = NULL; unshare_fd(CLONE_FILES, ©); *displaced = task->files; task->files = copy; return 0;}static int unshare_fd(unsigned long unshare_flags, struct files_struct **new_fdp){ struct files_struct *fd = current->files; if ((unshare_flags & CLONE_FILES) && (fd && atomic_read(&fd->count) > 1)) { *new_fdp = dup_fd(fd, &error); } return 0;}
unshare_files继而通过unshare_fd函数复制当前进程current的文件表files,赋值操作通过dup_fd函数执行,这里就不往下看了。
kernel/cred.c
sys_execv->do_execve->do_execveat_common->prepare_exec_creds
int prepare_bprm_creds(struct linux_binprm *bprm){ bprm->cred = prepare_exec_creds(); if (likely(bprm->cred)) return 0; return -ENOMEM;}struct cred *prepare_exec_creds(void){ return prepare_creds();}struct cred *prepare_creds(void){ struct task_struct *task = current; const struct cred *old; struct cred *new; validate_process_creds(); new = kmem_cache_alloc(cred_jar, GFP_KERNEL); if (!new) return NULL; old = task->cred; memcpy(new, old, sizeof(struct cred)); atomic_set(&new->usage, 1); set_cred_subscribers(new, 0); get_group_info(new->group_info); get_uid(new->user); get_user_ns(new->user_ns); key_get(new->session_keyring); key_get(new->process_keyring); key_get(new->thread_keyring); key_get(new->request_key_auth); new->security = NULL; if (security_prepare_creds(new, old, GFP_KERNEL) < 0) goto error; validate_creds(new); return new;error: abort_creds(new); return NULL;}
prepare_exec_creds用于复制当前进程的cred结构,并相应地增加引用计数,验证后返回。linux后面的版本将安全信息封装在cred结构里,例如uid不再和进程结构task_struct粘在一起,而是抽象出来。
fs/exec.c
sys_execv->do_execve->do_execveat_common->check_unsafe_exec
static void check_unsafe_exec(struct linux_binprm *bprm){ struct task_struct *p = current, *t; unsigned n_fs; if (p->ptrace) { if (p->ptrace & PT_PTRACE_CAP) bprm->unsafe |= LSM_UNSAFE_PTRACE_CAP; else bprm->unsafe |= LSM_UNSAFE_PTRACE; } if (task_no_new_privs(current)) bprm->unsafe |= LSM_UNSAFE_NO_NEW_PRIVS; t = p; n_fs = 1; spin_lock(&p->fs->lock); rcu_read_lock(); while_each_thread(p, t) { if (t->fs == p->fs) n_fs++; } rcu_read_unlock(); if (p->fs->users > n_fs) bprm->unsafe |= LSM_UNSAFE_SHARE; else p->fs->in_exec = 1; spin_unlock(&p->fs->lock);}
ptrace标志位决定了该进程是否被跟踪。
task_no_new_privs是个宏定义,定义在include/linux/sched.h中,用于检测task_struct结构中atomic_flags的标志位PFA_NO_NEW_PRIVS。
while_each_thread遍历同一个线程组中的其他线程,查找具有相同fs_struct结构的线程,检查fs_struct结构体的使用者计数(即current->fs->users)是否超过线程组中所有线程的数量,若是,则标记bprm->unsafe为LSM_UNSAFE_SHARE,若不是,则标记current->fs->in_exec为1。
fs/exec.c
sys_execv->do_execve->do_execveat_common->do_open_execat
static struct file *do_open_execat(int fd, struct filename *name, int flags){ struct file *file; struct open_flags open_exec_flags = { .open_flag = O_LARGEFILE | O_RDONLY | __FMODE_EXEC, .acc_mode = MAY_EXEC | MAY_OPEN, .intent = LOOKUP_OPEN, .lookup_flags = LOOKUP_FOLLOW, }; file = do_filp_open(fd, name, &open_exec_flags); ... return file;}
因为将要打开的是可执行文件,因此要设置相应的标志位,然后调用do_filp_open打开文件,下面涉及到文件系统的知识了,不往下看了。
kernel/sched/core.c
sys_execv->do_execve->do_execveat_common->sched_exec
void sched_exec(void){ struct task_struct *p = current; unsigned long flags; int dest_cpu; raw_spin_lock_irqsave(&p->pi_lock, flags); dest_cpu = p->sched_class->select_task_rq(p, task_cpu(p), SD_BALANCE_EXEC, 0); if (dest_cpu == smp_processor_id()) goto unlock; if (likely(cpu_active(dest_cpu))) { struct migration_arg arg = { p, dest_cpu }; raw_spin_unlock_irqrestore(&p->pi_lock, flags); stop_one_cpu(task_cpu(p), migration_cpu_stop, &arg); return; }unlock: raw_spin_unlock_irqrestore(&p->pi_lock, flags);}
select_task_rq函数选择负载最小的cpu,其内部通过select_task_rq_fair函数进行筛选,最后通过stop_one_cpu函数执行切换。
fs/exec.c
sys_execv->do_execve->do_execveat_common->bprm_mm_init
static int bprm_mm_init(struct linux_binprm *bprm){ struct mm_struct *mm = NULL; bprm->mm = mm = mm_alloc(); __bprm_mm_init(bprm); return 0;}static int __bprm_mm_init(struct linux_binprm *bprm){ int err; struct vm_area_struct *vma = NULL; struct mm_struct *mm = bprm->mm; bprm->vma = vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL); down_write(&mm->mmap_sem); vma->vm_mm = mm; vma->vm_end = STACK_TOP_MAX; vma->vm_start = vma->vm_end - PAGE_SIZE; vma->vm_flags = VM_SOFTDIRTY | VM_STACK_FLAGS | VM_STACK_INCOMPLETE_SETUP; vma->vm_page_prot = vm_get_page_prot(vma->vm_flags); INIT_LIST_HEAD(&vma->anon_vma_chain); insert_vm_struct(mm, vma); mm->stack_vm = mm->total_vm = 1; arch_bprm_mm_init(mm, vma); up_write(&mm->mmap_sem); bprm->p = vma->vm_end - sizeof(void *); return 0;err: up_write(&mm->mmap_sem); bprm->vma = NULL; kmem_cache_free(vm_area_cachep, vma); return err;}
bprm_mm_init函数首先通过mm_alloc创建新进程的mm_struct结构,然后执行__bprm_mm_init继续进行初始化。
__bprm_mm_init函数创建虚拟内存结构vm_area_struct,STACK_TOP_MAX在64位的计算机上定义为
#define TASK_SIZE_MAX ((1UL << 47) - PAGE_SIZE)
因此是用户空间的结束地址减去一个保护页面PAGE_SIZE,即0x0000,7fff,ffff,f000。
vm_end即虚拟空间的结束地址指向STACK_TOP_MAX。
vm_start即虚拟内存的开始地址指向vm_end向下一个页面的地址,即0x0000,7fff,ffff,e000。
insert_vm_struct将分配的虚拟内存vm_area_struct插入进程的内存管理结构mm_struct中。
然后通过arch_bprm_mm_init函数对具体的计算机类型执行相应的初始化操作。
最后设置堆栈起始指针,大小为将STACK_TOP_MAX减去一个指针的大小,即8个字节,因此最后的地址为0x0000,7fff,ffff,eff8。
fs/exec.c
sys_execv->do_execve->do_execveat_common->prepare_binprm
int prepare_binprm(struct linux_binprm *bprm){ int retval; bprm_fill_uid(bprm); retval = security_bprm_set_creds(bprm); if (retval) return retval; bprm->cred_prepared = 1; memset(bprm->buf, 0, BINPRM_BUF_SIZE); return kernel_read(bprm->file, 0, bprm->buf, BINPRM_BUF_SIZE);}
bprm_fill_uid设置即将运行的进程的uid和gid。security_bprm_set_creds函数设置授权。最后通过kernel_read函数将file中的内容读取到bprm的缓存buf中。
fs/exec.c
sys_execv->do_execve->do_execveat_common->prepare_binprm->bprm_fill_uid
static void bprm_fill_uid(struct linux_binprm *bprm){ struct inode *inode; unsigned int mode; kuid_t uid; kgid_t gid; bprm->cred->euid = current_euid(); bprm->cred->egid = current_egid(); if (bprm->file->f_path.mnt->mnt_flags & MNT_NOSUID) return; if (task_no_new_privs(current)) return; inode = file_inode(bprm->file); mode = READ_ONCE(inode->i_mode); if (!(mode & (S_ISUID|S_ISGID))) return; mutex_lock(&inode->i_mutex); mode = inode->i_mode; uid = inode->i_uid; gid = inode->i_gid; mutex_unlock(&inode->i_mutex); if (!kuid_has_mapping(bprm->cred->user_ns, uid) || !kgid_has_mapping(bprm->cred->user_ns, gid)) return; if (mode & S_ISUID) { bprm->per_clear |= PER_CLEAR_ON_SETID; bprm->cred->euid = uid; } if ((mode & (S_ISGID | S_IXGRP)) == (S_ISGID | S_IXGRP)) { bprm->per_clear |= PER_CLEAR_ON_SETID; bprm->cred->egid = gid; }}
首先设置euid和egid未当前进程的euid和egid。
在文件模式字mode中,有两个位bit10和bit11分别称为设置用户组ID位和设置用户ID位,分别有两个测试常量S_ISGID和S_ISUID与其对应。若此文件为可执行文件:当设置用户组ID位为1,则文件执行时,内核将其进程的有效用户组ID设置为文件的所有组ID。当设置用户ID位为1,则文件执行时,内核将其进程的有效用户ID设置为文件所有者的用户ID。
fs/exec.c
sys_execv->do_execve->do_execveat_common->exec_binprm
static int exec_binprm(struct linux_binprm *bprm){ int ret; ... ret = search_binary_handler(bprm); ... return ret;}int search_binary_handler(struct linux_binprm *bprm){ struct linux_binfmt *fmt; int retval = -ENOENT; list_for_each_entry(fmt, &formats, lh) { bprm->recursion_depth++; retval = fmt->load_binary(bprm); put_binfmt(fmt); bprm->recursion_depth--; } return retval;}
exec_binprm主要调用search_binary_handler处理可执行文件,search_binary_handler函数遍历format格式,对于linux下的elf格式的可执行文件而言,会找到elf_format,调用其load_binary函数,最终其实调用的是load_elf_binary函数,下面开始重点分析这个函数。
load_elf_binary
fs/binfmt_elf.c
load_elf_binary第一部分
static int load_elf_binary(struct linux_binprm *bprm){ struct file *interpreter = NULL; unsigned long load_addr = 0, load_bias = 0; int load_addr_set = 0; char * elf_interpreter = NULL; unsigned long error; struct elf_phdr *elf_ppnt, *elf_phdata, *interp_elf_phdata = NULL; unsigned long elf_bss, elf_brk; int retval, i; unsigned long elf_entry; unsigned long interp_load_addr = 0; unsigned long start_code, end_code, start_data, end_data; unsigned long reloc_func_desc __maybe_unused = 0; int executable_stack = EXSTACK_DEFAULT; struct pt_regs *regs = current_pt_regs(); struct { struct elfhdr elf_ex; struct elfhdr interp_elf_ex; } *loc; struct arch_elf_state arch_state = INIT_ARCH_ELF_STATE; loc = kmalloc(sizeof(*loc), GFP_KERNEL); if (!loc) { retval = -ENOMEM; goto out_ret; } loc->elf_ex = *((struct elfhdr *)bprm->buf); retval = -ENOEXEC; if (memcmp(loc->elf_ex.e_ident, ELFMAG, SELFMAG) != 0) goto out; if (loc->elf_ex.e_type != ET_EXEC && loc->elf_ex.e_type != ET_DYN) goto out; if (!elf_check_arch(&loc->elf_ex)) goto out; if (!bprm->file->f_op->mmap) goto out; elf_phdata = load_elf_phdrs(&loc->elf_ex, bprm->file); if (!elf_phdata) goto out; ...}
第一部分代码主要是检查elf的文件头信息。
首先检查e_ident,即前4个字节,魔数。
接下来查看类型e_type是否为ET_EXEC(2)和ET_DYN(3)的其中一种。
#define elf_check_arch(x) \ ((x)->e_machine == EM_X86_64)
elf_check_arch检查e_machine 的值是否为EM_X86_64(0x3E)。
load_elf_phdrs函数读取elf文件中的Segment头信息到elf_phdata中。
为了方便说明,下面首先看一下elf64文件的文件头elf64_hdr结构和Segment头elf64_phdr结构。
typedef struct elf64_hdr { unsigned char e_ident[EI_NIDENT]; Elf64_Half e_type; Elf64_Half e_machine; Elf64_Word e_version; Elf64_Addr e_entry; Elf64_Off e_phoff; Elf64_Off e_shoff; Elf64_Word e_flags; Elf64_Half e_ehsize; Elf64_Half e_phentsize; Elf64_Half e_phnum; Elf64_Half e_shentsize; Elf64_Half e_shnum; Elf64_Half e_shstrndx;} Elf64_Ehdr;typedef struct elf64_phdr { Elf64_Word p_type; Elf64_Word p_flags; Elf64_Off p_offset; Elf64_Addr p_vaddr; Elf64_Addr p_paddr; Elf64_Xword p_filesz; Elf64_Xword p_memsz; Elf64_Xword p_align;} Elf64_Phdr;
结构中成员的具体意义可以到网上查,不一一说明了,下面碰到重要的时会说明。
fs/binfmt_elf.c
load_elf_binary->load_elf_phdrs
static struct elf_phdr *load_elf_phdrs(struct elfhdr *elf_ex, struct file *elf_file){ struct elf_phdr *elf_phdata = NULL; int size; size = sizeof(struct elf_phdr) * elf_ex->e_phnum; elf_phdata = kmalloc(size, GFP_KERNEL); kernel_read(elf_file, elf_ex->e_phoff, (char *)elf_phdata, size); return elf_phdata;}
elf文件头的e_phnum表示Segment头的个数,e_phentsize是Segment头的大小,两者的乘积size即所有Segment头的大小和,所以要先分配size大小的内存空间,e_phoff是第一个Segment头在文件中的偏移。接着调用kernel_read从文件中读取Segment信息到elf_phdata中。
fs/binfmt_elf.c
load_elf_binary第二部分
static int load_elf_binary(struct linux_binprm *bprm){ ... elf_ppnt = elf_phdata; elf_bss = 0; elf_brk = 0; start_code = ~0UL; end_code = 0; start_data = 0; end_data = 0; for (i = 0; i < loc->elf_ex.e_phnum; i++) { if (elf_ppnt->p_type == PT_INTERP) { if (elf_ppnt->p_filesz > PATH_MAX || elf_ppnt->p_filesz < 2) goto out_free_ph; elf_interpreter = kmalloc(elf_ppnt->p_filesz, GFP_KERNEL); retval = kernel_read(bprm->file, elf_ppnt->p_offset, elf_interpreter, elf_ppnt->p_filesz); interpreter = open_exec(elf_interpreter); retval = kernel_read(interpreter, 0, bprm->buf, BINPRM_BUF_SIZE); loc->interp_elf_ex = *((struct elfhdr *)bprm->buf); break; } elf_ppnt++; } ...}
load_elf_binary函数的第二部分主要是查找文件中的解释器信息。
首先遍历所有的Segment头信息,查看其p_type是否为PT_INTERP(3)。
对于解释器信息的Segment头,p_filesz其实是解释器的路径,p_offset是路径信息在elf文件中的偏移,接下来检查该路径信息是否合理。
再往下分配用于存储解释器路径的内存空间elf_interpreter,并通过kernel_read函数从文件中读取路径信息到elf_interpreter中。
然后通过open_exec函数打开解释器,返回一个file结构,open_exec函数内部也是调用do_open_execat函数打开文件。
通过kernel_read函数将解释器的头部128个字节BINPRM_BUF_SIZE写入bprm的buf结构中,因为之前已经将文件头读取出来,这里覆盖了bprm的buf。
然后将loc的interp_elf_ex设置为解释器的头部在缓存buf中的起始地址。
fs/binfmt_elf.c
load_elf_binary第三部分
static int load_elf_binary(struct linux_binprm *bprm){ ... elf_ppnt = elf_phdata; for (i = 0; i < loc->elf_ex.e_phnum; i++, elf_ppnt++) switch (elf_ppnt->p_type) { case PT_GNU_STACK: if (elf_ppnt->p_flags & PF_X) executable_stack = EXSTACK_ENABLE_X; else executable_stack = EXSTACK_DISABLE_X; break; case PT_LOPROC ... PT_HIPROC: retval = arch_elf_pt_proc(&loc->elf_ex, elf_ppnt, bprm->file, false, &arch_state); if (retval) goto out_free_dentry; break; } ...}
这部分代码首先通过查找类型为PT_GNU_STACK的Segment,检查堆栈的可执行性,设置在executable_stack中。PT_LOPROC和PT_HIPROC类型的Segment用来提供给特定的计算机体系进行检查,这里不往下看了。
fs/binfmt_elf.c
load_elf_binary第四部分
static int load_elf_binary(struct linux_binprm *bprm){ ... if (elf_interpreter) { retval = -ELIBBAD; if (memcmp(loc->interp_elf_ex.e_ident, ELFMAG, SELFMAG) != 0) goto out_free_dentry; if (!elf_check_arch(&loc->interp_elf_ex)) goto out_free_dentry; interp_elf_phdata = load_elf_phdrs(&loc->interp_elf_ex, interpreter); elf_ppnt = interp_elf_phdata; for (i = 0; i < loc->interp_elf_ex.e_phnum; i++, elf_ppnt++) switch (elf_ppnt->p_type) { case PT_LOPROC ... PT_HIPROC: retval = arch_elf_pt_proc(&loc->interp_elf_ex, elf_ppnt, interpreter, true, &arch_state); if (retval) goto out_free_dentry; break; } } ...}
因为解释器也是elf文件,被读取到内存中后,这部分代码首先检查解释器的elf文件头信息,然后再次调用load_elf_phdrs函数读取解释器文件的Segment信息,找到类型为PT_LOPROC和PT_HIPROC的Segment以供特定的体系结构检查。
fs/binfmt_elf.c
load_elf_binary第五部分
static int load_elf_binary(struct linux_binprm *bprm){ ... flush_old_exec(bprm); SET_PERSONALITY2(loc->elf_ex, &arch_state); if (elf_read_implies_exec(loc->elf_ex, executable_stack)) current->personality |= READ_IMPLIES_EXEC; if (!(current->personality & ADDR_NO_RANDOMIZE) && randomize_va_space) current->flags |= PF_RANDOMIZE; setup_new_exec(bprm); retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP), executable_stack); if (retval < 0) goto out_free_dentry; current->mm->start_stack = bprm->p; ...}
flush_old_exec主要用来进行新进程地址空间的替换,并删除同线程组中的其他线程。
再往下继续设置进程task_struct的personality。
然后通过setup_new_exec函数对刚刚替换的地址空间进行简单的初始化。
randomize_stack_top对栈的指针进行随机的移动,然后通过setup_arg_pages函数随机调整堆栈的位置。
最后记录栈的起始地址至mm_struct结构中。
fs/exec.c
load_elf_binary->flush_old_exec
int flush_old_exec(struct linux_binprm * bprm){ de_thread(current); set_mm_exe_file(bprm->mm, bprm->file); exec_mmap(bprm->mm); bprm->mm = NULL; set_fs(USER_DS); current->flags &= ~(PF_RANDOMIZE | PF_FORKNOEXEC | PF_KTHREAD | PF_NOFREEZE | PF_NO_SETAFFINITY); flush_thread(); current->personality &= ~bprm->per_clear; return 0;}
因为即将要替换新进程的地址空间,所以首先通过de_thread函数用来删除同线程组中的其他线程。
set_mm_exe_file函数设置新进程的路径,即mm_struct中的exe_file成员变量。
再往下就通过exec_mmap函数将新进程的地址空间设置为bprm中创建并设置好的地址空间。
flush_thread函数主要用来初始化thread_struct中的TLS元数据信息。
最后设置进程的标志位flags和personality,personality用来兼容linux的旧版本或者BSD等其他版本。
fs/exec.c
load_elf_binary->flush_old_exec->de_thread
static int de_thread(struct task_struct *tsk){ struct signal_struct *sig = tsk->signal; struct sighand_struct *oldsighand = tsk->sighand; spinlock_t *lock = &oldsighand->siglock; if (thread_group_empty(tsk)) goto no_thread_group; if (signal_group_exit(sig)) { spin_unlock_irq(lock); return -EAGAIN; } sig->group_exit_task = tsk; sig->notify_count = zap_other_threads(tsk); if (!thread_group_leader(tsk)) sig->notify_count--; while (sig->notify_count) { __set_current_state(TASK_KILLABLE); spin_unlock_irq(lock); schedule(); if (unlikely(__fatal_signal_pending(tsk))) goto killed; spin_lock_irq(lock); } spin_unlock_irq(lock); if (!thread_group_leader(tsk)) { ... } sig->group_exit_task = NULL; sig->notify_count = 0;no_thread_group: tsk->exit_signal = SIGCHLD; exit_itimers(sig); flush_itimer_signals(); if (atomic_read(&oldsighand->count) != 1) { struct sighand_struct *newsighand; newsighand = kmem_cache_alloc(sighand_cachep, GFP_KERNEL); if (!newsighand) return -ENOMEM; atomic_set(&newsighand->count, 1); memcpy(newsighand->action, oldsighand->action, sizeof(newsighand->action)); rcu_assign_pointer(tsk->sighand, newsighand); __cleanup_sighand(oldsighand); } return 0; ...}
首先通过thread_group_empty函数检查线程组中是否有其他线程,如果没有就直接返回。
接下来通过signal_group_exit函数检查是否已经开始删除线程组,如果是,也直接返回。
zap_other_threads开始执行删除线程组的操作,其内部会向除了本线程外的所有其他线程发送SIGKILL信号。
接下来通过while循环等待其他线程的退出。
如果不是线程组leader,就要等待该leader的退出,省略的代码用来将当前task替换成线程组leader。
再往下为新的进程分配新的sighand_struct结构并初始化。
fs/exec.c
load_elf_binary->setup_new_exec
void setup_new_exec(struct linux_binprm * bprm){ arch_pick_mmap_layout(current->mm); current->sas_ss_sp = current->sas_ss_size = 0; if (uid_eq(current_euid(), current_uid()) && gid_eq(current_egid(), current_gid())) set_dumpable(current->mm, SUID_DUMP_USER); else set_dumpable(current->mm, suid_dumpable); perf_event_exec(); __set_task_comm(current, kbasename(bprm->filename), true); current->mm->task_size = TASK_SIZE; if (!uid_eq(bprm->cred->uid, current_euid()) || !gid_eq(bprm->cred->gid, current_egid())) { current->pdeath_signal = 0; } else { would_dump(bprm, bprm->file); if (bprm->interp_flags & BINPRM_FLAGS_ENFORCE_NONDUMP) set_dumpable(current->mm, suid_dumpable); } current->self_exec_id++; flush_signal_handlers(current, 0); do_close_on_exec(current->files);}
既然前面已经替换了新进程的mm_struct结构,下面就要对该结构进行设置。arch_pick_mmap_layout函数对设置了mmap的起始地址和分配函数。
然后更新mm的标志位,通过kbasename函数根据文件路径bprm->filename获得最后的文件名,再调用__set_task_comm函数设置进程的文件路径,最终设置到task_struct的comm变量中。
flush_signal_handlers用于清空信号的处理函数。最后调用do_close_on_exec关闭对应的文件。
arch/x86/mm/mmap.c
load_elf_binary->setup_new_exec->arch_pick_mmap_layout
void arch_pick_mmap_layout(struct mm_struct *mm){ unsigned long random_factor = 0UL; if (current->flags & PF_RANDOMIZE) random_factor = arch_mmap_rnd(); mm->mmap_legacy_base = mmap_legacy_base(random_factor); if (mmap_is_legacy()) { mm->mmap_base = mm->mmap_legacy_base; mm->get_unmapped_area = arch_get_unmapped_area; } else { mm->mmap_base = mmap_base(random_factor); mm->get_unmapped_area = arch_get_unmapped_area_topdown; }}
arch_mmap_rnd获得线性区的随机起始地址,其为get_random_int() % (1<<28)。
mmap_legacy_base修改该地址,加上TASK_UNMAPPED_BASE,值为((1UL << 47) - PAGE_SIZE)/3。
最后设置mmap_base地址和get_unmapped_area函数指针,get_unmapped_area函数用于分配虚拟内存。
load_elf_binary->setup_arg_pages
int setup_arg_pages(struct linux_binprm *bprm, unsigned long stack_top, int executable_stack){ unsigned long ret; unsigned long stack_shift; struct mm_struct *mm = current->mm; struct vm_area_struct *vma = bprm->vma; struct vm_area_struct *prev = NULL; unsigned long vm_flags; unsigned long stack_base; unsigned long stack_size; unsigned long stack_expand; unsigned long rlim_stack; stack_top = arch_align_stack(stack_top); stack_top = PAGE_ALIGN(stack_top); stack_shift = vma->vm_end - stack_top; bprm->p -= stack_shift; mm->arg_start = bprm->p; bprm->exec -= stack_shift; ... if (stack_shift) { shift_arg_pages(vma, stack_shift); } stack_expand = 131072UL; stack_size = vma->vm_end - vma->vm_start; rlim_stack = rlimit(RLIMIT_STACK) & PAGE_MASK; if (stack_size + stack_expand > rlim_stack) stack_base = vma->vm_end - rlim_stack; else stack_base = vma->vm_start - stack_expand; current->mm->start_stack = bprm->p; expand_stack(vma, stack_base);}
传入的参数stack_top添加了随机因子,首先对该stack_top进行页对齐,然后计算位移stack_shift,再将该位移添加到栈的指针bprm->p也即当前参数的存放地址mm->arg_start。省略的部分是对标志位的修改,再往下既然修改了栈的指针,就要通过shift_arg_pages函数修改堆栈对应的虚拟内存了。最后需要通过expand_stack函数拓展栈的大小,默认为stack_expand即4个页面。
fs/binfmt_elf.c
load_elf_binary第六部分
前面已经根据elf文件头进行了相应的初始化和设置工作,并加载了解释器的Segment头,下面这部分代码要查找elf文件中类型为PT_LOAD的Segment,将其装载进内存。
static int load_elf_binary(struct linux_binprm *bprm){ ... for(i = 0, elf_ppnt = elf_phdata; i < loc->elf_ex.e_phnum; i++, elf_ppnt++) { int elf_prot = 0, elf_flags; unsigned long k, vaddr; unsigned long total_size = 0; if (elf_ppnt->p_type != PT_LOAD) continue; if (unlikely (elf_brk > elf_bss)) { unsigned long nbyte; retval = set_brk(elf_bss + load_bias, elf_brk + load_bias); if (retval) goto out_free_dentry; nbyte = ELF_PAGEOFFSET(elf_bss); if (nbyte) { nbyte = ELF_MIN_ALIGN - nbyte; if (nbyte > elf_brk - elf_bss) nbyte = elf_brk - elf_bss; if (clear_user((void __user *)elf_bss + load_bias, nbyte)) { } } } if (elf_ppnt->p_flags & PF_R) elf_prot |= PROT_READ; if (elf_ppnt->p_flags & PF_W) elf_prot |= PROT_WRITE; if (elf_ppnt->p_flags & PF_X) elf_prot |= PROT_EXEC; elf_flags = MAP_PRIVATE | MAP_DENYWRITE | MAP_EXECUTABLE; vaddr = elf_ppnt->p_vaddr; if (loc->elf_ex.e_type == ET_EXEC || load_addr_set) { elf_flags |= MAP_FIXED; } else if (loc->elf_ex.e_type == ET_DYN) { load_bias = ELF_ET_DYN_BASE - vaddr; if (current->flags & PF_RANDOMIZE) load_bias += arch_mmap_rnd(); load_bias = ELF_PAGESTART(load_bias); total_size = total_mapping_size(elf_phdata, loc->elf_ex.e_phnum); } elf_map(bprm->file, load_bias + vaddr, elf_ppnt, elf_prot, elf_flags, total_size); if (!load_addr_set) { load_addr_set = 1; load_addr = (elf_ppnt->p_vaddr - elf_ppnt->p_offset); if (loc->elf_ex.e_type == ET_DYN) { load_bias += error - ELF_PAGESTART(load_bias + vaddr); load_addr += load_bias; reloc_func_desc = load_bias; } } k = elf_ppnt->p_vaddr; if (k < start_code) start_code = k; if (start_data < k) start_data = k; if (BAD_ADDR(k) || elf_ppnt->p_filesz > elf_ppnt->p_memsz || elf_ppnt->p_memsz > TASK_SIZE || TASK_SIZE - elf_ppnt->p_memsz < k) { retval = -EINVAL; goto out_free_dentry; } k = elf_ppnt->p_vaddr + elf_ppnt->p_filesz; if (k > elf_bss) elf_bss = k; if ((elf_ppnt->p_flags & PF_X) && end_code < k) end_code = k; if (end_data < k) end_data = k; k = elf_ppnt->p_vaddr + elf_ppnt->p_memsz; if (k > elf_brk) elf_brk = k; } loc->elf_ex.e_entry += load_bias; elf_bss += load_bias; elf_brk += load_bias; start_code += load_bias; end_code += load_bias; start_data += load_bias; end_data += load_bias; set_brk(elf_bss, elf_brk); ...}
首先循环遍历Segment头,查找类型为PT_LOAD的头,
然后根据Segment头的flag标志位设置内存的标志位elf_prot。
接下来如果要加载的文件数据类型为ET_EXEC,则在固定地址上分配虚拟内存,因此要加上MAP_FIXED标志,而如果要加载的数据类型为ET_DYN,则需要从ELF_ET_DYN_BASE地址处开始映射时,在设置了PF_RANDOMIZE标志位时,需要加上arch_mmap_rnd()随机因子,将偏移记录到load_bias中。total_size为计算的需要映射的内存大小。
再往下就通过elf_map函数将文件映射到虚拟内存中。如果是第一次映射,则需要记录虚拟的elf文件装载地址load_addr,如果是ET_DYN类型的数据,需要加上偏移load_bias。
每次映射后,都要修改bss段、代码段、数据段、堆的起始位置,对同一个elf文件而言,start_code向上增长,start_data向下增长,elf_bss向上增长,end_code 向上增长,end_data 向上增长,elf_brk向上增长,因此从虚拟内存中看,从低地址到高地址依次为代码段,数据段,bss段和堆的起始地址。当装载完毕退出循环后需要将这些变量加上偏移load_bias。
最后通过set_brk在elf_bss到elf_brk之间分配内存空间。
fs/binfmt_elf.c
load_elf_binary->elf_map
static unsigned long elf_map(struct file *filep, unsigned long addr, struct elf_phdr *eppnt, int prot, int type, unsigned long total_size){ unsigned long map_addr; unsigned long size = eppnt->p_filesz + ELF_PAGEOFFSET(eppnt->p_vaddr); unsigned long off = eppnt->p_offset - ELF_PAGEOFFSET(eppnt->p_vaddr); addr = ELF_PAGESTART(addr); size = ELF_PAGEALIGN(size); if (!size) return addr; if (total_size) { total_size = ELF_PAGEALIGN(total_size); map_addr = vm_mmap(filep, addr, total_size, prot, type, off); if (!BAD_ADDR(map_addr)) vm_munmap(map_addr+size, total_size-size); } else map_addr = vm_mmap(filep, addr, size, prot, type, off); return(map_addr);}
传入的参数filep是文件指针,addr是即将映射的内存中的虚拟地址,size是文件映像的大小,off是映像在文件中的偏移。elf_map函数主要通过vm_mmap为文件申请虚拟空间并进行相应的映射,然后返回虚拟空间的起始地址map_addr。
fs/binfmt_elf.c
load_elf_binary第七部分
前面一部分将elf文件中类型为PT_LOAD的数据映射到虚拟内存中,这一部分代码采用相同的方法将解释器的数据映射到虚拟内存中,其内部都是通过elf_map函数进行映射,最后将程序的起始地址保存在elf_entry中,这里就不往下分析了。
static int load_elf_binary(struct linux_binprm *bprm){ ... if (elf_interpreter) { unsigned long interp_map_addr = 0; elf_entry = load_elf_interp(&loc->interp_elf_ex, interpreter, &interp_map_addr, load_bias, interp_elf_phdata); interp_load_addr = elf_entry; elf_entry += loc->interp_elf_ex.e_entry; reloc_func_desc = interp_load_addr; allow_write_access(interpreter); fput(interpreter); kfree(elf_interpreter); } else { elf_entry = loc->elf_ex.e_entry; } kfree(interp_elf_phdata); kfree(elf_phdata); ...}
fs/binfmt_elf.c
load_elf_binary第八部分
static int load_elf_binary(struct linux_binprm *bprm){ ... set_binfmt(&elf_format); install_exec_creds(bprm); create_elf_tables(bprm, &loc->elf_ex, load_addr, interp_load_addr); current->mm->end_code = end_code; current->mm->start_code = start_code; current->mm->start_data = start_data; current->mm->end_data = end_data; current->mm->start_stack = bprm->p; if ((current->flags & PF_RANDOMIZE) && (randomize_va_space > 1)) { current->mm->brk = current->mm->start_brk = arch_randomize_brk(current->mm); } start_thread(regs, elf_entry, bprm->p); ...}
set_binfmt将elf_format记录到mm_struct的binfmt变量中。
install_exec_creds会设置新进程的cred结构。
create_elf_tables函数在将启动解释器或者程序前,向用户空间的堆栈添加一些额外信息,例如应用程序Segment头的起始地址,入口地址等等。
接着向新进程mm_struct结构中设置前面计算的代码段、数据段、bss段和堆的位置。
再往下根据标志位PF_RANDOMIZE选择是否要为堆起始地址添加随机变量。
最后调用start_thread函数将执行权交给解释器或者应用程序了。
arch/x86/kernel/process_64.c
load_elf_binary->start_thread
voidstart_thread(struct pt_regs *regs, unsigned long new_ip, unsigned long new_sp){ start_thread_common(regs, new_ip, new_sp, __USER_CS, __USER_DS, 0);}static voidstart_thread_common(struct pt_regs *regs, unsigned long new_ip, unsigned long new_sp, unsigned int _cs, unsigned int _ss, unsigned int _ds){ loadsegment(fs, 0); loadsegment(es, _ds); loadsegment(ds, _ds); load_gs_index(0); regs->ip = new_ip; regs->sp = new_sp; regs->cs = _cs; regs->ss = _ss; regs->flags = X86_EFLAGS_IF; force_iret();}
传入的参数regs为保存的寄存器,new_ip为解释器或者应用程序的起始代码地址,new_sp为用户空间的堆栈指针。设置完这些变量后,最后通过force_iret强制返回,跳到new_ip指向的地址处开始执行。对于glibc而言,最终就会跳转到_start函数中,下一章开始分析该函数。
- sys_execv源码分析
- 源码分析
- 源码分析
- 源码分析
- 源码分析
- 源码分析
- 源码分析
- 源码分析
- 源码分析
- 源码分析:SparseArray分析
- 源码- Spark Broadcast源码分析
- Android源码/框架源码分析
- 【Android应用源码分析】HandlerThread 源码分析
- 【Android应用源码分析】IntentService 源码分析
- java源码分析01-Object源码分析
- VC++源码分析 - 中国象棋源码分析
- [Java源码分析]ArrayList源码分析
- [java源码分析]LinkedList源码分析
- 俞军跟我聊了三个小时产品(1):用户不是人,是需求的集合
- 翻译:走出类加载器迷宫
- android同时使用wifi和以太网
- Excel个人所得税简洁计算公式
- pl/sql 导出insert语句和pl/sql导出表结构
- sys_execv源码分析
- Vuex源码阅读笔记
- WWDC心得与延伸:iOS图形性能
- Android sqlite插入一条数据时,时间自动写入数据库
- 初识PHP(10)
- 用Qtl编写媒体软件到安卓(待续)
- 数据结构——图 动态规划
- C语言之尾队列tailq
- 第十二课 数据文件与持久性