现在的假设是:hello可执行文件已经存在于磁盘上(存储介质上),并且在可执行文件中包含了被执行的text,同时也包含了这些目标代码使用的数据
,同时上面的分析可得,在elf中定义的只是虚拟的地址(linux中对于每个process的话,否有4GB的虚拟地址空间,当然这些地址只是virtual的,
真正的数据的存储还是在实际的ram中,OS提供虚拟存储空间主要是为了能够在ram容量较小的机器中运行一些占用内存较大的应用程序)。下面
开始今天的旅行。
假设你在shell中键入:./hello,shell创建一个新的进程,新的进程又使用系统调用sys_execve(),sys_execve()系统调用首先需要找打相应的
的可执行文件(对于./hello而言,显然在当前目录中查找就能找到该可执行文件),然后检查可执行文件格式,并根据其中存放的上下文信息来改
变当前进程的上下文,当这个系统调用终止时,cpu开始执行我们的hello程序。当然了程序执行时,用户可以提供命令行参数来影响程序的执行,例如
ls程序,在执行时,通常在其中加上一个命令行参数来制定目录,另外还可以通过环境变量来影响程序的执行。大家中所周知的main函数的原型
其实完整版是:
int main (int argc, char* argv[], char* envp[])
envp参数执行环境变量中的字符串,形式如下:
VAR_NAME = something
sys_execve()函数如下:
/*
* sys_execve() executes a new program.
*/
long sys_execve(char __user *name, char __user * __user *argv,
char __user * __user *envp, struct pt_regs *regs)
{
long error;
char *filename;
// 检查参数name合法性
filename = getname(name);
error = PTR_ERR(filename);
if (IS_ERR(filename))
return error;
error = do_execve(filename, argv, envp, regs); // 真正的主角do_execve,大部分的工作是由则个函数来完成的
#ifdef CONFIG_X86_32
if (error == 0) {
/* Make sure we don't return using sysenter.. */
set_thread_flag(TIF_IRET);
}
#endif
putname(filename);
return error;
}
下面开始do_execve :
/*
* sys_execve() executes a new program.
*/
int do_execve(char * filename,
char __user *__user *argv,
char __user *__user *envp,
struct pt_regs * regs)
{
/*
* This structurelinux_binprm is used to hold the arguments that are used when loading binaries.
* 当加载可执行文件时,使用这个结构来传递参数
*/
struct linux_binprm *bprm;
struct file *file;
struct files_struct *displaced;
bool clear_in_exec;
int retval;
retval = unshare_files(&displaced);
if (retval)
goto out_ret;
retval = -ENOMEM;
/*
* 动态的分配linux_binprm数据结构,并使用新的可执行文件的数据填充这个结构,即是新
* 分配以个页框,在linux中,内存的分配是通过linux的内存分配模块来实现,程序(内核程序
* )通过函数来请求内存,linux内存管理模块来根据内存的使用情况来分配一块内存。
*/
bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);
if (!bprm)
goto out_files;
retval = prepare_bprm_creds(bprm);
if (retval)
goto out_free;
/*
* determine how safe it is to execute the proposed program
* - the caller must hold current->cred_guard_mutex to protect against
* PTRACE_ATTACH
* 检查执行这个文件是否安全,但是首先应该得到current->cred_guard_mutex
*/
retval = check_unsafe_exec(bprm);
if (retval < 0)
goto out_free;
clear_in_exec = retval;
current->in_execve = 1;
/* 获得可执行文件的相关信息 */
file = open_exec(filename);
retval = PTR_ERR(file);
if (IS_ERR(file))
goto out_unmark;
/* 在多处理器中,用来优化程序执行,暂时忽略 */
sched_exec();
/*************填充bprm数据结构***********/
bprm->file = file;
bprm->filename = filename;
bprm->interp = filename;
retval = bprm_mm_init(bprm);
if (retval)
goto out_file;
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;
/*
* 调用函数search_binary_handler对formats链表进行查询,当找到一个
* 应答的load_binary后,停止扫描,调用函数load_binary,函数
*search_binary_handler返回的是load_binary函数的结果
*/
current->flags &= ~PF_KTHREAD;
retval = search_binary_handler(bprm,regs);
if (retval < 0)
goto out;
current->stack_start = current->mm->start_stack;
/* execve succeeded */
current->fs->in_exec = 0;
current->in_execve = 0;
acct_update_integrals(current);
free_bprm(bprm);
if (displaced)
put_files_struct(displaced);
return retval;
/* 错误处理部分 */
...
return retval;
}
接下来开始可执行文件的加载函数load_elf_binary,下面的部分参见:http://blog.youkuaiyun.com/ruixj/archive/2009/11/07/4783637.aspx
和http://www.kerneltravel.net/kernel-book/第六章%20Linux内存管理/6.4.3.htm
load_elf_binary(linux_binprm* bprm,pt_regs* regs)
{
分析ELF文件头
读入程序的头部分(kernel_read函数)
if(存在解释器头部){
读入解释器名(ld*.so)(kernel_read函数)|(zalem note:可用
打开解释器文件(open_exec函数) | objdump -s -j .interp xxx
读入解释器文件的头部(kernel_read函数) |命令查看,
) |linux下是/lib/ld-linux.so.x)
释放空间,清楚信号,关闭指定了close-on-exec标识的文件(flush_old_exec函数)
生成堆栈空间,塞入环境变量/参数部分(setup_arg_pages函数)
for(可引导的所有的程序头)
{
将文件影射入内存空间(elf_map,do_mmap函数)
}
if(为动态联结){
影射动态联结器(load_elf_interp函数)
}
释放文件(sys_close函数)
确定执行中的UID,GID(compute_creds函数)
生成bss领域(set_brk函数)
bss领域清零(padzero函数)
设定从exec返回时的IP,SP(start_thread函数)(动态联结时的IP指向解释器的入口)
}
在上面的整个过程中,最关键的函数是:elf_map,do_mmap,上面的函数只是将可执行文件,下面解释其中的do_mmap函数:
下面摘自:http://www.kerneltravel.net/kernel-book/第六章%20Linux内存管理/6.4.3.htm
当某个程序的映象开始执行时,可执行映象必须装入到进程的虚拟地址空间。如果该进程用到了任何一个共享库,则共享库也必须装入到进程的虚拟地址空间。由此可看出,Linux并不将映象装入到物理内存,相反,可执行文件只是被连接到进程的虚拟地址空间中。随着程序的运行,被引用的程序部分会由操作系统装入到物理内存,这种将映象链接到进程地址空间的方法被称为“内存映射”。
当可执行映象映射到进程的虚拟地址空间时,将产生一组vm_area_struct结构来描述虚拟内存区间的起始点和终止点,每个vm_area_struct结构代表可执行映象的一部分,可能是可执行代码,也可能是初始化的变量或未初始化的数据,这些都是在函数do_mmap()中来实现的。随着vm_area_struct结构的生成,这些结构所描述的虚拟内存区间上的标准操作函数也由Linux初始化。但要明确在这一步还没有建立从虚拟内存到物理内存的影射,也就是说还没有建立页表页目录。
为了对上面的原理进行具体的说明,我们来看一下do_mmap()的实现机制。
函数do_mmap()为当前进程创建并初始化一个新的虚拟区,如果分配成功,就把这个新的虚拟区与进程已有的其他虚拟区进行合并,do_mmap()在include/linux/mm.h中定义如下:
staticinline unsigned long do_mmap(struct file *file, unsigned long addr,
unsignedlong len, unsigned long prot,
unsignedlong flag, unsigned long offset)
{
unsignedlong ret = -EINVAL;
if((offset + PAGE_ALIGN(len)) < offset)
gotoout;
if(!(offset & ~PAGE_MASK))
ret= do_mmap_pgoff(file, addr, len, prot, flag, offset >> PAGE_SHIFT);
out:
returnret;
}
函数中参数的含义如下:
file:表示要映射的文件,file结构将在第八章文件系统中进行介绍;
off:文件内的偏移量,因为我们并不是一下子全部映射一个文件,可能只是映射文件的一部分,off就表示那部分的起始位置;
len:要映射的文件部分的长度
addr:虚拟空间中的一个地址,表示从这个地址开始查找一个空闲的虚拟区;
prot:这个参数指定对这个虚拟区所包含页的存取权限。可能的标志有PROT_READ、PROT_WRITE、PROT_EXEC和PROT_NONE。前三个标志与标志VM_READ、VM_WRITE及VM_EXEC的意义一样。PROT_NONE表示进程没有以上三个存取权限中的任意一个。
Flag:这个参数指定虚拟区的其它标志:
MAP_GROWSDOWN,MAP_LOCKED,MAP_DENYWRITE和MAP_EXECUTABLE:
它们的含义与表6.2中所列出标志的含义相同。
MAP_SHARED和MAP_PRIVATE:
前一个标志指定虚拟区中的页可以被许多进程共享;后一个标志作用相反。这两个标志都涉及vm_area_struct中的VM_SHARED标志。
MAP_ANONYMOUS
表示这个虚拟区是匿名的,与任何文件无关。
MAP_FIXED
这个区间的起始地址必须是由参数addr所指定的。
MAP_NORESERVE
函数不必预先检查空闲页面的数目。
....................
如果对文件的操作不成功,则解除对该虚拟区间的页面映射,这是由zap_page_range()函数完成的。
当你读到这里时可能感到困惑,页面的映射到底在何时建立?实际上,generic_file_mmap( )就是真正进行映射的函数。因为这个函数的实现涉及很多文件系统的内容,我们在此不进行深入的分析,当读者了解了文件系统的有关内容后,可自己进行分析。
这里要说明的是,文件到虚存的映射仅仅是建立了一种映射关系,也就是说,虚存页面到物理页面之间的映射还没有建立。当某个可执行映象映射到进程虚拟内存中并开始执行时,因为只有很少一部分虚拟内存区间装入到了物理内存,可能会遇到所访问的数据不在物理内存。这时,处理器将向Linux报告一个页故障及其对应的故障原因,于是就用到了请页机制。
呵呵,好像现在越来越远了。现在已经到了这里:linux kernel已经将elf文件加载到内存中(当然,这句话可能不是那么正确,因为可能只是一小吧部分的内容在实际的ram中),现在程序能够运行了吗?
很遗憾,现在hello程序还是不能运行,怎么了?因为程序中还有动态链接库需要连接。折下来解释器将程序中需要的动态链接库映射到程序的地址空间,然后跳转到可执行文件hello的入口点,开始执行。
终于,hello开始运行了。
不要高兴地太早了,现在还没有解决下面的问题:
1.使用strace ./hello可以发现出现了许多的系统调用,那么系统调用在linux中是如何实现的?
2.在上面中多次使用到了“映射”,那么映射的含义是什么(将文件映射到进程的process中)?
3.程序中寻址的完整过程是怎样的?
3.进程在物理内存中的”镜像“是怎样的?
4.进程hello的进程调度是如何实现的?
5.进程最终exit时,发生了什么?