之前写过一篇文章 Linux下c语言中的main函数是如何被调用的,该篇文章侧重于从user space层面讲程序的运行,而文章中提到的有关kernel space层面的相关系统调用,比如fork、execve等,都被一笔带过。
今天我们主要来看下execve系统调用,直接看代码:
// fs/exec.c
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);
}
该方法调用了do_execve:
// fs/exec.c
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);
}
该方法又调用了do_execveat_common:
// fs/exec.c
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_file:
// fs/exec.c
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)
{
...
struct linux_binprm *bprm;
...
bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);
...
if (!file)
file = do_open_execat(fd, filename, flags);
...
bprm->file = file;
if (!filename) {
...
} else if (fd == AT_FDCWD || filename->name[0] == '/') {
bprm->filename = filename->name;
} else {
...
}
...
retval = bprm_mm_init(bprm);
...
retval = prepare_binprm(bprm);
...
retval = copy_strings_kernel(1, &bprm->filename, bprm);
...
retval = copy_strings(bprm->envc, envp, bprm);
...
retval = copy_strings(bprm->argc, argv, bprm);
...
retval = exec_binprm(bprm);
...
return retval;
...
}
该方法的大致逻辑是:
1. 分配struct linux_binprm实例,并赋值给bprm。
2. 打开filename指向的程序,并赋值给file变量。
3. 将file变量赋值给bprm->file。
4. 将filename->name赋值给bprm->filename。
5. 调用bprm_mm_init方法,初始化进程内存的相关信息,并分配一个page作为进程的初始堆栈。
6. 调用prepare_binprm方法,从bprm->file中读取256字节到bprm->buf中。
7. 将程序的文件路径拷贝到堆栈中。
8. 将环境变量拷贝到堆栈中。
9. 将程序参数拷贝到堆栈中。
10. 调用exec_binprm方法继续执行该程序。
在看exec_binprm方法之前,我们先看下bprm_mm_init方法。
// fs/exec.c
static int bprm_mm_init(struct linux_binprm *bprm)
{
int err;
struct mm_struct *mm = NULL;
bprm->mm = mm = mm_alloc();
...
err = __bprm_mm_init(bprm);
...
return 0;
...
}
该方法先分配了一个类型为struct mm_struct的实例,该实例就是用来存放有关进程内存的相关信息。
之后,又调用了__bprm_mm_init方法。
// fs/exec.c
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 = vm_area_alloc(mm);
...
/*
* Place the stack at the largest stack address the architecture
* supports. Later, we'll move this to an appropriate place. We don't
* use STACK_TOP because that can depend on attributes which aren't
* configured yet.
*/
vma->vm_end = STACK_TOP_MAX;
vma->vm_start = vma->vm_end - PAGE_SIZE;
...
err = insert_vm_struct(mm, vma);
...
return 0;
...
}
该方法设置了堆栈的位置,并初始化堆栈大小为一个page。
堆栈的位置及大小后面的代码还会调整。
好,我们再回到__do_execve_file方法,该方法的最后又调用了exec_binprm方法。
// fs/exec.c
static int exec_binprm(struct linux_binprm *bprm)
{
...
ret = search_binary_handler(bprm);
...
return ret;
}
该方法又调用了search_binary_handler方法:
// fs/exec.c
int search_binary_handler(struct linux_binprm *bprm)
{
...
struct linux_binfmt *fmt;
...
list_for_each_entry(fmt, &formats, lh) {
...
retval = fmt->load_binary(bprm);
...
}
...
return retval;
}
EXPORT_SYMBOL(search_binary_handler);
该方法遍历linux中可识别的可执行文件格式,找到对应的文件格式,并调用其load_binary方法。
linux下可执行文件的格式一般为elf,所以我们直接看其load_binary方法:
// fs/binfmt_elf.c
static int load_elf_binary(struct linux_binprm *bprm)
{
struct file *interpreter = NULL; /* to shut gcc up */
...
struct elf_phdr *elf_ppnt, *elf_phdata, *interp_elf_phdata = NULL;
...
unsigned long elf_entry;
...
struct {
struct elfhdr elf_ex;
struct elfhdr interp_elf_ex;
} *loc;
...
loc = kmalloc(sizeof(*loc), GFP_KERNEL);
...
// 将之前从file中读出来的buf的内容,转成elf的header
loc->elf_ex = *((struct elfhdr *)bprm->buf);
...
// 从程序文件中读取elf的program header
elf_phdata = load_elf_phdrs(&loc->elf_ex, bprm->file);
...
// 遍历program header,找到其中的interpreter
elf_ppnt = elf_phdata;
for (i = 0; i < loc->elf_ex.e_phnum; i++, elf_ppnt++) {
char *elf_interpreter;
loff_t pos;
if (elf_ppnt->p_type != PT_INTERP)
continue;
...
elf_interpreter = kmalloc(elf_ppnt->p_filesz, GFP_KERNEL);
...
pos = elf_ppnt->p_offset;
// 从程序文件中读取interpreter的路径,一般为 /lib64/ld-linux-x86-64.so.2
// 有关interpreter的信息,请看 http://man7.org/linux/man-pages/man8/ld.so.8.html
retval = kernel_read(bprm->file, elf_interpreter,
elf_ppnt->p_filesz, &pos);
...
// 打开interpreter文件
interpreter = open_exec(elf_interpreter);
...
pos = 0;
// 读取interpreter的elf header
retval = kernel_read(interpreter, &loc->interp_elf_ex,
sizeof(loc->interp_elf_ex), &pos);
...
break;
...
}
...
// 关闭当前进程使用的资源,比如线程、内存、文件等
retval = flush_old_exec(bprm);
...
// 设置新程序的各种信息
setup_new_exec(bprm);
...
// 重新设置当前堆栈的位置及大小
retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP),
executable_stack);
...
// 初始化
elf_bss = 0;
elf_brk = 0;
start_code = ~0UL;
end_code = 0;
start_data = 0;
end_data = 0;
// 遍历program header,将程序文件中的代码段、data段等映射到内存
for(i = 0, elf_ppnt = elf_phdata;
i < loc->elf_ex.e_phnum; i++, elf_ppnt++) {
...
if (elf_ppnt->p_type != PT_LOAD)
continue;
...
vaddr = elf_ppnt->p_vaddr;
...
// 映射程序代码等信息到内存的虚拟地址,类似于mmap系统调用
//该操作会在进程的struct mm_struct实例中添加一个struct vm_area_struct实例
error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt,
elf_prot, elf_flags, total_size);
...
// 设置程序的各个segment位置信息
k = elf_ppnt->p_vaddr;
if (k < start_code)
start_code = k;
if (start_data < k)
start_data = k;
...
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) {
bss_prot = elf_prot;
elf_brk = k;
}
}
// 调整程序各个segment的具体位置
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;
...
// 设置堆的地址
retval = set_brk(elf_bss, elf_brk, bss_prot);
...
if (interpreter) {
...
// 加载interpreter的入口地址
elf_entry = load_elf_interp(&loc->interp_elf_ex,
interpreter,
&interp_map_addr,
load_bias, interp_elf_phdata);
if (!IS_ERR((void *)elf_entry)) {
...
interp_load_addr = elf_entry;
elf_entry += loc->interp_elf_ex.e_entry;
}
...
} else {
// 如果该程序没有interpreter,则使用程序自己的入口地址
elf_entry = loc->elf_ex.e_entry;
...
}
...
// 进一步设置堆栈的各种信息,比如 auxiliary vector、环境变量、程序参数等
retval = create_elf_tables(bprm, &loc->elf_ex,
load_addr, interp_load_addr);
...
// 设置程序各个segment的地址
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);
...
}
...
// 开始执行elf_entry指向的代码
// 如果该程序有interpreter,则是执行interpreter中的入口地址
// 如果没有,则是执行程序自己的入口地址
// interpreter会检查该程序依赖的动态链接库,加载这些库,并解析相应的函数地址
// 之后再调用源程序自己的入口函数,这样,也就对应到文章开始提到的
// main函数是如何被调用的那篇文章了。
start_thread(regs, elf_entry, bprm->p);
...
return retval;
...
}
由于该方法比较长,关于方法的描述已用注释的形式在方法内部标注出来,请参考方法中的中文注释。
在阅读该方法之前,要先了解下elf的具体格式:
http://man7.org/linux/man-pages/man5/elf.5.html
参照该格式以及之前的一篇文章 Linux进程的内存分布,对照着看代码,会更好理解一些。
好了,到这里,整个程序的内核部分的执行流程就讲完了,结合本文开始提到的那篇文章 Linux下c语言中的main函数是如何被调用的,有关linux下程序的执行就全部讲清楚了。
希望对这方面感兴趣的朋友有所帮助。
在结束本文之前,推荐两篇相关文章,也是写的非常好的,如果你对本文还有不太明白地方,没准能在这里找到答案。
https://lwn.net/Articles/630727/
https://lwn.net/Articles/631631/
完。
本文分享自 Linux内核及JVM底层相关技术研究 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体分享计划 ,欢迎热爱写作的你一起参与!