Linux中ELF文件启动过程
linux注册支持运行的文件类型
struct linux_binfmt {
struct list_head lh;
struct module *module;
int (*load_binary)(struct linux_binprm *);
int (*load_shlib)(struct file *);
int (*core_dump)(struct coredump_params *cprm);
unsigned long min_coredump; /* minimal dump size */
};
linux主要的可执行文件类型为ELF,当然linux实际是并不止可以运行ELF文件这一种文件格式,实际其还可以运行coff等文件格式的可执行文件。
Linux中所有支持的可执行文件类型在内核中都有一个对应的linux_binfmt类型的对象。所有的Linux_binfmt对象都保存在一个双向链表中,此链表第一个元素的地址保存在formatis内核全局变量中。(可以通过register_fmt和unregister_fmt从添加中插入和删除一个Linux_binfmt对象)
load_binary //装入并执行新程序
load_shlib //装入共享库
core_dump //崩溃时进行内存转储
每一个linux_binfmt对象都需要包含3个函数指针,用于对此类型的可执行文件进行进行相关操作,供操作系统调用。load_binary()负责装载此文件类型的二进制可执行文件并执行,load_shlib()用来装入这种文件类型的共享库,core_dump()则会在崩溃时对此文件类型进行转储。
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,
};
ELF可执行文件类型对应的linux_binfmt如上,其中其.load_binary函数指针为load_elf_binary()函数。当我们运行ELF文件时就是由load_elf_binary()加载并启动此ELF文件。
ELF文件启动过程
do_execve
在linux中运行一个ELF可执行文件通常通过shell命令行。而shell命令行程序实际会先调用fork() 复制一个当前进程的副本为新的进程,fork()从内核中返回会返回两次,分别在父进程和子进程中各返回一次(子进程返回值为0,父进程中返回值为子进程PID)。子进程fock()返回后会继续调用exec()进入到内核中, exec()对应的系统调用为do_execve()。总的来说exec()作用就是清空新创建的进程的.text,.data,.bss段等,然后装载新进程并运行。
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);
}
static int do_execveat_common(int fd, struct filename *filename,
struct user_arg_ptr argv,
struct user_arg_ptr envp,
int flags)
{
return __do_execve_file(fd, filename, argv, envp, flags, NULL);
}
do_execve函数会调用do_execveat_common函数,而do_execveat_common()又会进一步调用__do_execve_file()函数去加载ELF并执行。
AT_FDCWD是一个宏,标识当前目录的文件描述符,所以__do_execve_file()去搜索ELF文件就是相对于当前路径。
int do_execve_file(struct file *file, void *__argv, void *__envp)
{
struct user_arg_ptr argv = { .ptr.native = __argv };
struct user_arg_ptr envp = { .ptr.native = __envp };
return __do_execve_file(AT_FDCWD, NULL, argv, envp, 0, file);
}
除了do_execveat_common()函数会调用__do_execve_file()函数去加载ELF并执行外,do_execve_file()函数也会调用__do_execve_file(),其用于User Mode Helper,是内核主动执行应用程序的一种机制。
__do_execve_file
struct linux_binprm {
char buf[BINPRM_BUF_SIZE]; //用于保存ELF文件的前128个字节
#ifdef CONFIG_MMU
struct vm_area_struct *vma; //新进程默认栈空间的线性区间地址描述符(相当于windows中的VAD)
unsigned long vma_pages;
#else
# define MAX_ARG_PAGES 32
struct page *page[MAX_ARG_PAGES];
#endif
struct mm_struct *mm; //指向新进程内存地址描述符
unsigned long p; //默认栈的栈顶地址
unsigned long argmin;
unsigned int
called_set_creds:1,
cap_elevated:1,
secureexec:1;
#ifdef __alpha__
unsigned int taso:1;
#endif
unsigned int recursion_depth;
struct file * file; //ELF文件的文件指针
struct cred *cred;
int unsafe;
unsigned int per_clear;
int argc, envc; //程序的argc和envc
const char * filename; //ELF文件的路径
const char * interp; //链接器的路径
unsigned interp_flags;
unsigned interp_data;
unsigned long loader, exec;
struct rlimit rlim_stack;
} __randomize_layout;
linux_binprm 结构体用于在加载ELF文件之前保存文件的一些基本信息
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)
{
int retval;
char *pathbuf = NULL;
struct linux_binprm *bprm;
//当前进程task_struct的files字段
//PCB进程控制块task_struct的files字段指向当前进程打开的文件表
struct files_struct *displaced;
//将父进程task_struct->files中保存的打开文件的描述符复制一份到当前进程(子进程)的task_struct->files中
retval = unshare_files(&displaced);
//为bprm申请内存用于保存加载的二进制可执行文件的信息到linux_binprm结构体中
bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);
//当前进程的in_execve标志,表示do_execve()已经调用
current->in_execve = 1;
//打开ELF文件
file = do_open_execat(fd, filename, flags);
//将ELF文件文件指针file,文件路径filename保存到bprm中
bprm->file = file;
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);
bprm->filename = pathbuf;
}
//interp链接器的文件路径先暂时设置为ELF文件的路径
bprm->interp = bprm->filename;
//bprm_mm_init调用mm_alloc()分配一个内存描述符mm_struct并赋值给bprm->mm, 用来描述新进程的整个0-3G的内存空间信息。
//调用vm_area_alloc()分配一个vm_area_struct线性区间描述符并赋值给bprm->vma中,用来描述新进程初始栈的内存空间信息。(相当于windows中的虚拟地址描述符VAD)
//调用instert_vm_struct将新进程初始栈的vm_area_struct插入到进程内存描述符mm->mmap链表中(此链表是一个红黑树,相当于windows中的VAD树也就是EPROCESS的ActiveProcessLinks链表)
retval = bprm_mm_init(bprm);
//设置bprm->argv和envp字段
retval = prepare_arg_pages(bprm, argv, envp);
//调用bprm_fill_uid()设置进程的用户ID(UID),用户组ID(GID)等信息
//调用kernel_read()将ELF文件头前128个字节读取到bprm->buf中
retval = prepare_binprm(bprm);
//将ELF文件路径,envp和argv拷贝到bprm->p指向的栈顶中,bprm->p会发生变化
retval = copy_strings_kernel(1, &bprm->filename, bprm);
retval = copy_strings(bprm->envc, envp, bprm);
retval = copy_strings(bprm->argc, argv, bprm);
//内部会加载并执行ELF文件
retval = exec_binprm(bprm);
//新进程创建完成,释放无关内存
/* execve succeeded */
current->fs->in_exec = 0;
current->in_execve = 0;
task_numa_free(current);
free_bprm(bprm);
kfree(pathbuf);
return retval;
}
__do_execve_file的主要核心代码如上。
- unshare_files:将父进程task_struct->files中保存的打开文件的描述符复制一份到当前进程(子进程)的task_struct->files中
- kzalloc:为bprm申请内存用于保存加载的二进制可执行文件的信息到linux_binprm结构体中
- 将ELF文件文件指针file,文件路径filename保存到bprm中,interp链接器的文件路径先暂时设置为ELF文件的路径
- bprm_mm_init(bprm):
bprm_mm_init调用mm_alloc()分配一个内存描述符mm_struct并赋值给bprm->mm, 用来描述新进程的整个0-3G的内存空间信息。
调用vm_area_alloc()分配一个vm_area_struct线性区间描述符并赋值给bprm->vma中,用来描述新进程初始栈的内存空间信息。(相当于windows中的虚拟地址描述符VAD)
调用instert_vm_struct将新进程初始栈的vm_area_struct插入到进程内存描述符mm->mmap链表中(此链表是一个红黑树,相当于windows中的VAD树) - prepare_arg_pages(bprm, argv, envp):设置bprm->argv和envp字段
- prepare_binprm(bprm):调用kernel_read()将ELF文件头前128个字节读取到bprm->buf
- exec_binprm(bprm):内部会调用search_binary_handler(),search_binary_handler会进一步加载并执行ELF文件
exec_binprm(bprm) 主要就是调用 search_binary_handler()
- search_binary_handler会调用list_for_each_entry枚举formatis链表中的linux_binfmt对象,
- 调用linux_binfmt.load_binary加载ELF文件直到能加载成功。对于ELF文件来说linux_binfmt.load_binary就是函数load_elf_binary()
load_elf_binary
load_elf_binary()会先检查加载的文件头是否为ELF magic,文件类型是否为ET_EXEC(可执行文件)或者ET_DYN(共享文件)类型。
接着会获取各个program header table并得到PT_INTERP类型。
利用readelf -l查看elf文件的program信息,PT_INTERP类型program保存的是此ELF文件需要加载的加载器路径。
只要ELF文件需要进行动态链接其他库就需要PT_INTERP类型的program,如果在链接时传入链接参数-static则会静态链接所有的库也就不需要此program了(因为其不需要动态链接,所以不需要加载对应的linker加载器)。
得到ELF待执行文件的PT_INTERP类型的program后,其会将对应路径的加载器的ELF文件头加载到内存中保存在loc->interp_elf_ex中
接着会判断加载的interpreter是否是标准的ELF文件,并将其e_phoff程序头的文件偏移保存在interp_elf_phdata中
接着遍历待执行ELF文件的所有program,并加载所有PT_LOAD类型的program。对于ET_EXEC类型的文件采用默认的加载基地址0,而对于ET_DYN需要计算得到其随机加载基地址。
//遍历ELF待执行文件的program程序段
for(i = 0, elf_ppnt = elf_phdata;
i < loc->elf_ex.e_phnum; i++, elf_ppnt++) {
int elf_prot = 0, elf_flags, elf_fixed = MAP_FIXED_NOREPLACE;
unsigned long k, vaddr;
unsigned long total_size = 0;
//如果程序段不是PT_LOAD类型的就不加载到内存中
if (elf_ppnt->p_type != PT_LOAD)
continue;
//获得对应PT_LOAD程序段的内存属性
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;
//load_bias为ELF加载到内存的基地址,初始化为0
//对于ET_EXEC可执行的文件类型直接使用默认的加载基地址load_bias = 0
if (loc->elf_ex.e_type == ET_EXEC || load_addr_set) {
elf_flags |= elf_fixed;
}
//对于ET_DYN可重定位的文件类型(动态库文件),需要计算获得当前程序的实际内存加载基地址load_bias
else if (loc->elf_ex.e_type == ET_DYN) {
if (elf_interpreter) {
load_bias = ELF_ET_DYN_BASE;
if (current->flags & PF_RANDOMIZE)
load_bias += arch_mmap_rnd();
elf_flags |= elf_fixed;
} else
load_bias = 0;
load_bias = ELF_PAGESTART(load_bias - vaddr);
total_size = total_mapping_size(elf_phdata,
loc->elf_ex.e_phnum);
if (!total_size) {
retval = -EINVAL;
goto out_free_dentry;
}
}
//将当前段MAP到进程地址空间中,实际内存加载地址为load_bias + vaddr,设置内存属性为elf_port,
error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt,
elf_prot, elf_flags, total_size);
//k此时为当前段默认的虚拟地址
k = elf_ppnt->p_vaddr;
if (k < start_code)
start_code = k; //代码段的起始为最小的段地址
if (start_data < k)
start_data = k; //数据段的起始为最大的段地址
//k此时为当前段的文件段尾默认的虚拟地址
k = elf_ppnt->p_vaddr + elf_ppnt->p_filesz;
if (k > elf_bss)
elf_bss = k; //bss段的起始地址为最大的段的文件段尾
if ((elf_ppnt->p_flags & PF_X) && end_code < k)
end_code = k; //代码段的结束地址为最大可执行的段的文件段尾
if (end_data < k)
end_data = k; //数据段的结束地址为最大的段的文件段尾
//k当前为当前段的内存段尾默认的虚拟地址
k = elf_ppnt->p_vaddr + elf_ppnt->p_memsz;
if (k > elf_brk) {
bss_prot = elf_prot; //bss段的结束地址为最大的段的内存段尾
elf_brk = k;
}
}
刚才计算出的各种段(代码段,数据段,bss段)都是默认的虚拟地址(默认加载基地址为0),所以其对应的实际内存地址都需要加上load_bias实际的内存加载基地址。
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;
- 继续判断ELF文件是否采用动态链接,也就是含有PT_INTERP段并需要加载interpreter。如果需要就调用load_elf_interp()加载interpreter(其对应的加载流程和ELF上方的PT_LOAD类型的段的加载相似),并返回interpreter的程序入口点作为新进程应用层的入口点。
- 如果判断ELF文件采用静态链接(编译器参数使用-static),那么则直接返回此ELF文件的程序入口点作为新进程应用层的入口点。
最后的最后调用start_thread并设置regs寄存器,elf_entry应用层的入口点,bprm->p默认栈的栈顶。
start_thread()->compat_start_thread()->start_thread_common()设置应用层的ip和sp,以及默认的cs,ds(x86架构而言)
- android
对于android而言,如果存在PT_INTERP类型的program,函数就会将其加载到内存中并将应用层入口设置为/system/lib/linker程序的入口(__dl_start)。如果不存在PT_INTERP类型的program也就是编译时采用静态链接,则将应用层入口点设置为ELF文件的入口点,一般为__start(ELF被UPX加壳之后就是这样的)。
linux版本为5.0