您的位置:首页 > 运维架构 > Linux

Linux内核如何装载和启动一个可执行程序

2015-04-19 17:03 330 查看
罗晓波 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000

linux下,一个可执行文件需要通过预处理、编译、汇编、链接等步骤才能形成,这里的可执行文件在linux下,一般认为就是ELF文件,简单分析一下ELF文件的格式吧。ELF文件。文件头:



(上图引用http://www.cnblogs.com/xmphoenix/archive/2011/10/23/2221879.html)

ELF头之后就烦着一个个sections,则改写sections保存着ELF的信息,从连接的角度来看,是指令、数据、符号表、重定位信息等。细看ELF的头,这跟java程序编译之后,形成的java bytecode 的header有几分相似,只是这里由内核对ELF进行文件解析以及系统调用,java
bytecode则是通过jvm中的类装载来进行class文件的解析和初始化等。关于这一块有时间再来写点东西记录一下自己的学习心路历程吧。

还是从一个实验开始吧:

一、实验:

这里的实验环境还是和之前提到的实验环境一样,在实验楼的环境下进行实验,本次实验是针对execve系统调用内核处理函数sys_execve,内核启动之后,运行exec命令。



接下来,用gdb来跟踪一下,看看里面核心的几个地方发生了什么。

首先,断点打在sys_execve这个地方,这是exec函数簇的一个服务例程,进入这个系统调用服务例程之后,我们可以看到,它实质上是调用了do_execve这个内核态的函数。





gdb跟到这个函数之后,在进行参数赋值之后,又调用了do_execve_common函数,这个函数,我们跟一下,看看发生了什么。

1434	struct linux_binprm *bprm;
1435	struct file *file;
1436	struct files_struct *displaced;
上面三行是do_execve_common的变量声明,这个linux_binprm结构体通过新的这个可执行文件的数据来对这个结构体进行填充。也就是说,对应着do_execve_common这个函数的下面的绝大多数代码都是对这个数据结构的填充,我简单的跟踪了几个:

retval = prepare_bprm_creds(bprm);
1142int prepare_bprm_creds(struct linux_binprm *bprm)
1143{
1144	if (mutex_lock_interruptible(&t->signal->cred_guard_mutex))
1145		return -ERESTARTNOINTR;
1146
1147	bprm->cred = prepare_exec_creds();
1148	if (likely(bprm->cred))
1149		return 0;
1150
1151	mutex_unlock(&t->signal->cred_guard_mutex);
1152	return -ENOMEM;
1153}
这个prepare_bprm_creds函数是设置新的执行进程的信任状(credential),我的理解是这个信任状是进程所在的用户或者用户组的一些数据结构信息,在多用户的状态下,可以决定这个进程可以做什么,不可以做什么。

其中,下面这些就是bprm结构体指针的具体的填充。

<pre name="code" class="plain">	bprm->file = file;
1482	bprm->filename = bprm->interp = filename->name;
1483
1484	retval = bprm_mm_init(bprm);
1485	if (retval)
1486		goto out_unmark;
1487
1488	bprm->argc = count(argv, MAX_ARG_STRINGS);
1489	if ((retval = bprm->argc) < 0)
1490		goto out;
1491
1492	bprm->envc = count(envp, MAX_ARG_STRINGS);
1493	if ((retval = bprm->envc) < 0)
1494		goto out;
1495
1496	retval = prepare_binprm(bprm);
1497	if (retval < 0)
1498		goto out;
1499
1500	retval = copy_strings_kernel(1, &bprm->filename, bprm);
1501	if (retval < 0)
1502		goto out;
1503
1504	bprm->exec = bprm->p;
1505	retval = copy_strings(bprm->envc, envp, bprm);
1506	if (retval < 0)
1507		goto out;
1508
1509	retval = copy_strings(bprm->argc, argv, bprm);
1510	if (retval < 0)
1511		goto out;
1512
1513	retval = exec_binprm(bprm);



在1513行的exec_binprm打个断点,跟踪一下:

这里主要调用了search_binary_handler,跟踪到这一点:



由上图可看到这是在对这个formats链表进行扫描,看一下这个linux_binfmt是什么?这个结构体是

<pre name="code" class="plain">70struct linux_binfmt {
71	struct list_head lh;
72	struct module *module;
73	int (*load_binary)(struct linux_binprm *);
74	int (*load_shlib)(struct file *);
75	int (*core_dump)(struct coredump_params *cprm);
76	unsigned long min_coredump;	/* minimal dump size */
77};



对这个formats列表进行扫描,先是misc_format、再是script_format、终于到了elf_format,对应着这个elf_format的类型也是一个linux_binfmt这个结构体类型,其中这个load_binary是个函数指针,当列表扫描到这个elf_format的时候,也就是执行了这个elf_format所对应的函数指针,这个函数(load_binary),主要是用来解析elf文件。这个load_binary方法相当庞大,解析这个elf文件,并修改进程描述符以及做一些数据段或者正文段的映射等等,elf程序被装入的起始线性地址是0x08048000。这里,我跟踪了do_brk函数以及start_thread函数,do_brk函数主要是映射程序的bss段(用来存放程序中未初始化的全局变量和静态变量的一块内存区域)。以及start_thread函数:



这里主要修改了pt_regs这个内核栈中的结构体的ip值,打印出了new_ip的值,下面两张图,即可说明,其实地址是elf文件中的Entry入口的线性地址。





很明显,是一一对应的。也就是说,新的可执行程序是从ELF文件的这个入口地址开始执行的。

二、总结与浅析:

当execve系统调用从内核态返回到用户态的时候,上下文被新的可执行的文件大面积更改和替换,原本调用系统调用的用户态代码也不会再继续执行了,上面实验分析对应的是一个静态链接的情况,在动态链接的情况下,动态链接程序ld,检查被执行程序,识别哪个共享库要必须装入以及哪个函数被有效的请求。然后,同样会用mmap系统调用来创建线性区,将正文和数据段的页进行映射。最后一样,都是跳转到被执行程序的主入口点,开始执行新的程序咯。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: