深入浅出Hello World 2
现在的假设是: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 structure linux_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.csdn.net/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 中定义如下:
static inline unsigned long do_mmap(struct file *file, unsigned long addr,
unsigned long len, unsigned long prot,
unsigned long flag, unsigned long offset)
{
unsigned long ret = -EINVAL;
if ((offset + PAGE_ALIGN(len)) < offset)
goto out;
if (!(offset & ~PAGE_MASK))
ret = do_mmap_pgoff(file, addr, len, prot, flag, offset >> PAGE_SHIFT);
out:
return ret;
}
函数中参数的含义如下:
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时,发生了什么?
作者:许强1. 本博客中的文章均是个人在学习和项目开发中总结。其中难免存在不足之处 ,欢迎留言指正。 2. 本文版权归作者和博客园共有,转载时,请保留本文链接。