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

来源:互联网 发布:天下粮仓 知乎 编辑:程序博客网 时间:2024/05/19 07:07
张瑜原创作品转载请注明出处 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000

当我们在用户空间调用execve(),使用一个软件中断调用系统调用的过程。你提出一个软件中断通过INT instruc-tion (在X86架构中),那么CPU咨询另一个IDT结构(中断描述符表),去查询它需要调用的例程。INT指令采取的唯一的操作数是一个索引表。这意味着执行的int 0X80 h时,CPU咨询IDT与并执行存储在索引0x80哪里的函数。在Linux系统中,系统调用处理程序存储在索引0x80。然后处理程序看看递交内容知道哪个系统调用会调用

“进程”是用来便是正在运行的一组程序竞争系统资源的行为。所以呢这次我们来说说进程和程序之间的关系。
从整体的意义上来看,尽管这就是一组指令装入内存中并让CPU开始执行的过程,但其实还是有很多的细节我们需要考虑。
一种方式就是在编译这个程序的时候就把他添加到我们的执行文件中,这样我们在实际使用时就可以直接找到他了,但是这种方式有一个不好的地方就是当我们的程序很庞大,我们需要用到很多的库函数的时候,如果采用运行之前就直接静态的全部加载过来这会导致我们执行的程序占用过高的存储空间,这有的时候是我们不想看到的。
所以另一种方式就是当我们实际运行到比如说printf的时候,我们再到库里面去找到它加载它。内核转入并运行一个程序的时候,我们可以有两种方式来控制它的运行,还记得Java里面的那个public static void main(String args[])么 实际上C语言的主函数也是可以有参数的呢,这就是所谓的控制它的运行。两种方式分别是:环境变量和命令行参数。我们只要在输入文件名字之后输入命令行参数就可以运用命令行参数进行控制程序运行。环境变量是从shell继承过来的所以我们在装入并运行陈旭之前可以修改任何环境变量。为满足这个请求而装入的程序可以从shell接收一些命令行参数。传递命令行参数的时候我们约定程序的main()函数把穿的给程序的参数个数和指向字符船只真的地址作为参数。

首先 父进test_hello程先运行打印子进程的pid,然后子进程通过执行execve系统调用将hello程序装载进来并运行,打印出execve系统调用向hello程序传递的参数和环境变量,最后hello退出后父进程也将终止wait并退出。这个程序其实就是一个说明shell运行机制的简单例子(实际的shell远比这复杂),为什么这么说呢?首先可以把test_hello看成是”shell”,然后把hello看成是我们的”命令”(ls, echo 类似的命令),而hello的执行依靠的是test_hello,并且hello的参数也是由test_hello来传递,这与我们平常用的shell道理是一致的,只不过我们平常用的shell没有显示的调用而已。所以简单来说,shell执行一个命令就是这么一个过程:首先fork出一个子进程,然后在子进程中execve这个命令程序,并传递命令程序的参数和环境变量,最后在父进程中wait子进程退出。

关于execve系统调用
下面我们开看一下这个具体执行程序的函数execve是什么样子的,然后来进一步说明程序的执行过程。首先我们看一下关于shell启动的代码部分:
这里写图片描述
我们看到init函数其实就是在所谓的init进程中执行,在该函数中可以看到包含一个while循环,在循环中首先fork出一个子进程,然后在子进程中执行shell终端程序(/bin/sh),父进程一直等待子进程退出。当子进程退出后(shell中执行exit命令),父进程将break等待,然后又重新fork子进程并运行shell程序,父进程又等待,如此形成一个循环(似乎只有强制关机才可“退出”)。 上述代码我们只关注execve(“/bin/sh”,argv,envp)函数,其它的先不管。按照最开始的分析,执行这条语句将启动/bin/sh程序,并给该程序传递argv参数和envp环境变量。注意sh也只是一个正常的具有main函数的可执行程序而已,只不过它的功能与我们平常写的程序稍有不同,它会读取用户在命令行输入的程序名以及相应参数,然后fork出一个子进程去execve这个程序,子程序执行完成后又回到命令行等待输入,最后一直循环这个过程(似乎跟init函数有点类似,不过概念完全不同)。所以execve所做的工作就是将所要运行的程序调入内存并运行,准确的说应该是将fork出来的子进程替换成所要运行的程序。那么这个替换又是如何进行的呢?下面具体分析execve这个系统调用。
在execve函数调用system_call.s中的sys_execve函数,我们看到实际上sys——execve的定义中首先是将EIP传入eax中然后将其入栈,之后调用do_execve函数之后将esp指针向上移动一个单位(+4)。 可以看到sys_execve内部其实又调用的是exec.c文件中的do_execve函数,但是注意在调用do_execve函数前的pushl %eax语句,这条语句其实是给do_execve函数压入参数,根据C语言参数传递规则,do_execve函数的第一个参数即为eax寄存器的值,而eax寄存器中存放的内容为子进程进行系统调用时压入子进程内核态堆栈的eip指针,也即系统调用的返回地址,所以这里可以猜想do_execve函数内部可能会对该eip指针进行修改。
接下来再来看一下do_execve函数声明:int do_execve(unsigned long * eip,long tmp,char * filename, char * argv, char * envp) 我们已经知道了第一个参数的由来以及含义,第二个参数其实是sys_execve函数的返回地址(call sys_execve指令自动产生),这里并没有用,接下来的3个参数即为C程序中执行execve系统调用所传递进来的3个参数,他们分别是:可执行程序的全路径名称,参数以及环境变量。在具体研究do_execve函数前,再先大概了解一下gcc1.3版本编译出来的a.out格式的可执行文件结构。

实验过程:
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
首先我们开到程序停在了load_elf_binary。这是个什么呢?我们知道在Linux系统中运行一个可执行的ELF文件时,内核首先需要识别这个文件,然后解析并装载它以构建进程的内存空间,最后切换到新的进程来运行。在fs / binfmt_elf.c 中定义了函数load_elf_binary()和load_elf_library()分别用于装载和解析ELF格式的可执行文件和动态连接库。下面来研究一下在load_elf_binary()中做了哪些事情,一个新的进程的内存空间是布局是怎样计算出来的。
一个典型的Linux程序的进程空间模型中一般包含这样一些东西: 代码区(text):存放可执行的代码;数据区(data):存放经过初始化的数据;数据区(bss):bss区存放的也是数据,不过在这里的数据是没有初始它的,而且是全局的。即那些在代码里面声明了但是没有赋初始值全局变量。未初始化全局变量在ELF文件中不占有存储空间,但是在内存空间里必须占有一席之地。堆(heap):进程运行期间动态分配内存的区域,当进程需要分配更多的内存时,它将向上扩展;栈(stack):进程的栈,它的扩展方向与堆刚好相反,当有新的函数调用时,它将向下扩展。
我们装载ELF的目的一般是确定各个区域的边界:text区的起始和终止位置,data区的起始和终止位置,bss区的起始和终止位置,heap和stack的起始位置(它们的终止位置是动态变化的)。还有就是把text区和data区的内容做mmap映射:ELF文件的内容不会被真地拷贝到内存,只有当真正需要的时候,内核才会通过page fault的形式把文件内存复制到内存中去。
接下来我们继续执行让MenuOS启动起来:然后我们开始执行exec这个命令,我们看到程序会停在Chlid Process
这里写图片描述

0 0