Linux内核分析(七)——程序的装载和启动

来源:互联网 发布:kali linux能做什么 编辑:程序博客网 时间:2024/06/05 19:50

禹晓博+ 原创作品转载请注明出处 + 欢迎加入《Linux内核分析》MOOC网易云课堂学习

一、关于程序的执行

        我们之前的文章已经说过,“进程”是用来便是正在运行的一组程序竞争系统资源的行为。所以呢这次我们来说说进程和程序晋之间的关系。从整体的意义上来看,尽管这就是一组指令装入内存中并让CPU开始执行的过程,但其实还是有很多的细节我们需要考虑。

       程序在硬盘中的存放形式及时可视性文件,可执行文件包括被执行的函数目标代码和一些数据,这很容易理解。在程序中很多的函数实际上都是我们可以使用的服务例程,他们就是我们通常意义上说的库文件(C库?比如或许大多数人都还在不知道如何实现printf的时候就已经开始使用它了,因为他就是库中已经有的,我们只要调用它就好了)。实际上,一个库文件他的代码在程序执行时候有两种方式我们可以去利用它。一种方式就是在编译这个程序的时候就把他添加到我们的执行文件中,这样我们在实际使用时就可以直接找到他了,但是这种方式有一个不好的地方就是当我们的程序很庞大,我们需要用到很多的库函数的时候,如果采用运行之前就直接静态的全部加载过来这会导致我们执行的程序占用过高的存储空间,这有的时候是我们不想看到的。所以另一种方式就是当我们实际运行到比如说printf的时候,我们再到库里面去找到它加载它。(共享库)

       当内核转入并运行一个程序的时候,我们可以有两种方式来控制它的运行,还记得java里面的那个public static void main(String args[])么 实际上C语言的主函数也是可以有参数的呢,这就是所谓的控制它的运行。两种方式分别是:环境变量和命令行参数。我们只要在输入文件名字之后输入命令行参数就可以运用命令行参数进行控制程序运行。环境变量是从shell继承过来的所以我们在装入并运行陈旭之前可以修改任何环境变量。

       当用户键入一个命令是,为满足这个请求而装入的程序可以从shell接收一些命令行参数。传递命令行参数的时候我们约定程序的main()函数把穿的给程序的参数个数和指向字符船只真的地址作为参数。下面就是一种标准的格式。

int main(int argc, char *argv[ ])

1.1程序的运行参数       

       下面我们来实际举一个例子来正式说明程序的运行。 在Linux系统中,我们一般都是在命令行下键入"./ Hello World"这样的来运行当前目录下的hello应用程序("./"指定当前目录)。虽然看似很简单,但这么小小的一个操作其实涉及到了很多的知识。比如:shell是如何将hello调入内存的?hello在运行前shell执行了哪些操作?hello的父进程又是哪个?回答这些问题之前我们先来看下面的一个例子:这个例子包含两个程序,第一个test_hello程序可以看成是hello的父进程,因为在test_hello中调用了fork函数生成一个子进程,并在子进程中调用execve函数执行hello程序。最后父进程等待子进程的退出,并打印出子进程的退出码。第二个hello程序简单的将main函数的参数和环境变量打印出来。我们来看代码:首先是test_hello.c


然后我们看hello.c


然后我们看到如下的执行过程(./test_hello)


       从运行的结果我们可以看出,首先父进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子进程退出。

1.2 关于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格式的可执行文件结构。linux0.11支持的可执行文件格式为a.out(现在采用的是ELF文件格式),每个二进制的执行文件的头部数据都从下面数据结构开始:


       这个结构就基本上描述了该可执行程序的必要信息,比如:代码段,数据段,bss段的长度,应用程序的入口地址a_entry等。所以可以想象,如果将子进程的eip替换为a_entry,就应该可以运行该可执行文件了,当然还必须要设置堆栈指针以及进程描述符。因此do_execve函数完成的功能应该包含上述的这些操作(设置堆栈,设置进程描述符,替换eip),其中对于堆栈的操作稍微复杂点。堆栈操作的过程其实是将可执行程序的参数和环境变量复制到64 M线性空间的末尾128KB处(128KB足够放很多参数了),并且为这些参数和环境变量创建对应的指针表(因为要访问这些参数,肯定要定义指向这些参数的指针了),最终的堆栈指针p将指向指针表的第一个元素(因为在进入main函数时需要从堆栈中弹出3个参数:1个整型参数argc和2个字符串指针参数argv,envp,所以p应指向整型参数argc )。最终进程的64M线性空间的布局如下所示:


       到现在为止我们关于do_execve函数的功能介绍到这里,总结一下:对于一个a.out格式的可执行文件,首先获取该文件的inode节点指针,并判断该文件的属性以及执行权限,然后获取可执行文件的a.out头结构指针ex,根据ex结构判断可执行文件是否符合标准,然后对子进程的进程描述符的相关字段进行赋值,最后修改子进程内核态堆栈中eip和esp并返回。返回后,fork出来的子进程将被可执行程序替代并运行。

二、实验过程简析

       本周我们仍旧需要进行一次menu的更新。然后我们需要将更新之后的test_exec.c覆盖之前的test.c。具体如下图所示:


       然后我们可以进入test.c看看做了哪些修改。我们发现,在菜单选项中我们增加了一个exec这个命令实际上就是来执行对应的系统调用的。


       我们可以转到对应的函数Exec中去看看他到底是一个什么样的执行过程和命令。如下图所示我们看到实际上我们看到这里面首先就是新创建个一个子进程然后将我们需要运行的东西放进去执行,就像我们前面说的那样。


       最后我们再make一下就可以了,至于它是如何运行的,我么可以在menu中查找一下对应的Makefile文件。如下图所示,实际上我们大体上能看出各个部分是做了什么事情,这个文件日后我们会继续讲(~要是心情好的话偷笑


    我们看到当我们 make rootfs的时候执行了哪些具体的命令,makefile文件的好处之一就是只要我们写好之后就可以免去每次都要敲打冗长的命令了呢。那么接下来我们就要开始进行跟踪调试我们的execve系统调用了。首先还是大小S那个命令。


      然后我们开到程序停在那不动了,接下来我们打开gdb然后读取符号表,以及设置监视。在之后我们就可以进行断点到的设置了。


       首先我们开到程序停在了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,这时候我们可以在看一下调试界面。


       我们看到程序停在了系统调用的地方。也就是sys_execve的地方。


       继续调试我们就会来到刚才说的load_elf_binary的地方。


       之后我们看到了就是在开始进程的地方开始了庄生梦蝶了(额。。。。好吧孟老师是一个文艺的攻城师偷笑


       我们现在看看是不是真的就是改成了我们看到的hello的地址。(好么~化蝶了呢~~大笑啃啃严肃,这是一个严肃的技术文章)恩恩是的改过来了。

 

       接下来我们看看他的修改过程,继续调试哦我们能看到了熟悉的汇编语言,都是一些段赋值语句。


       接下来我们就可以继续把他调试完,我们看到程序全部运行完成了。


三、总结

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


       在程序执行之前,内核需要加载和解析二进制。load_elf_binary的函数被调用时,这个函数检查如果ELF头有效,而且它还设置几个指针:代码,brk和堆栈,下图表示进程内存的设计。


参考借鉴: 

1.Jonathan Salwan's Page : http://shell-storm.org/blog/       

2.FK's Bloghttp://blog.csdn.net/fukai555/


0 0
原创粉丝点击