因为最近工作涉及到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标志意味着文件的搜索路径不是从进程文件系统的根路径开始,而是从进程当前路径开始,在后面打开文件时会用到。
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函数,下面开始重点分析这个函数。
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
void
start_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 void
start_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函数中,下一章开始分析该函数。