深入浅出Hello World 2

本文详细解析了在Linux系统中,ELF可执行文件如何被加载到内存的过程,包括sys_execve系统调用的具体实现、do_execve函数的流程、以及可执行文件如何通过内存映射加载到进程的虚拟地址空间。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

现在的假设是: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_READPROT_WRITEPROT_EXECPROT_NONE。前三个标志与标志VM_READVM_WRITEVM_EXEC的意义一样。PROT_NONE表示进程没有以上三个存取权限中的任意一个。

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

MAP_GROWSDOWNMAP_LOCKEDMAP_DENYWRITEMAP_EXECUTABLE

它们的含义与表6.2中所列出标志的含义相同。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值