深入浅出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_READPROT_WRITEPROT_EXECPROT_NONE。前三个标志与标志VM_READVM_WRITE VM_EXEC的意义一样。PROT_NONE表示进程没有以上三个存取权限中的任意一个。

     Flag:这个参数指定虚拟区的其它标志:

 

MAP_GROWSDOWNMAP_LOCKEDMAP_DENYWRITEMAP_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时,发生了什么?

posted @ 2010-03-23 18:19  qiang.xu  阅读(623)  评论(0编辑  收藏  举报