18. 程序的执行 2010-03-02 12:56 131人阅读 评论(0) 收藏
“程序”以可执行文件的形式存于磁盘,它包含执行函数的目标代码与其它数据。其中部分函数包含在称为“库”的特殊函数中。一个库中函数的目标代码可被静态拷到可执行文件中或是在运行时连接进来。装入并运行一程序时,用户可提供两种影响程序执行的信息:命令行参数与环境变量。main(int argc, char *argv[], char *envp[]),第三个参数即为环境参数。
先解释下述行为:在shell中输入ls时,它命令shell创建一个进程,新的进程调用exec()系统调用,它的参数是ls的全路径名,系统调用进入服务例程找到文件,检查可执行格式,按信息修改进程的执行上下文,将之前的大部分资源丢弃,用系统调用execve()传的环境变量参数代替shell的参数,释放父进程继承的页、甚至改变进程特权,然而PID与已打开文件描述符不变。
信任状将进程与特定用户或组绑定,它要进程数据结构的支持,也要资源(文件)的支持。进程的信任状放在进程描述符的uid、euid、fsuid、suid等字段。UID为0的给root,凡遇0则不检查权限。新创建进程继承父进程的信任状。而在文件一端,可执行文件setuid设置时,euid、fsuid字段被设置为此文件拥有者的标识符,即在装入时会改变进程的相应字段。Setuid程序就是设置文件的此位的。特殊地,当euid=0时执行setuid会把4个信任状都改变,即超级用户进程变为普通进程,如shell程序就是由超级用户进程退化为普通进程的。
库是当代程序的重要组成部分,源码文件中编译成包含机器码的目标文件时,它不能执行,因为源码文件中的一些外部的符号的线性地址还不可确定(如库函数或其它源码文件中的函数与变量),接下来链接程序完成这些线性地址的分配或解析。它把所有目标文件收集起来构造可执行文件,其中最重要的是分析库函数。传统的Unix系统中所有的可执行文件都基于静态库,这时link程序产生的可执行文件不仅包括原程序代码,还包含库函数的代码,由于每个这种可执行文件都拷贝库中的某些部分,因此它占磁盘空间大。现代的unix引入共享库,这时link程序不把库的目标代码拷入可执行文件,而仅指向库名,当程序装入内存执行时,叫动态链接器的程序就专注于分析可执行文件中的库名,确定库在目录中的位置,执行一个内存映射,将库文件的相关部分映射到进程的地址空间,使可执行进程可以使用库代码。这种共享库使机器代码所在的页框可被所有进程共享。共享库的缺点是慢一点,可移植性差一点。其中的关键:动态链接程序运行在用户态,它的第一个工作是从内核保存在用户态栈的信息中为自己建立一个基本的执行上下文,再检查被执行程序,以识别哪些共享库被装入,其中哪些函数被请求。之后,会发出几个mmap()系统调用来创建线性区,映射实际要用的库函数,再将相应的线性地址去更新原可执行文件中的共享库符号,这时,动态链接程序转到被执行程序的主入口点并终止自身,这时可执行程序才真正“可执行”。
Unix进程的线性地址空间分为几个段(为何叫段是因为第一个unix系统用不同段寄存器实现线性区间)。有正文段、数据段、未初始化段bss、stack、heap。这些段在线性地址空间的布局有两种。经典布局:从0x08048000开始依次是正文段、数据段、bss段、堆。以0x40000000为分界,往上是文件内存映射与匿名线性区,再往上是从0xc0000000往下生长的栈。灵活布局则到堆处是一样的,但堆以上不从0x40000000开始,而是反过来从栈的下方开始向下,这种方法由于堆大小不在封顶于0x40000000而显得更灵活,但它要求栈大小固定。若栈大小无限制会启用老的经典方式。可编程用brk()系统调用试验,可观察到在堆上栈下地址区间内动态链接程序与共享库的分布及线性区建立的顺序。
最后关注下可执行格式。Linux标准的可执行格式是ELF(Executable and Linking Format),由Unix实验室开发,Linux也可支持其它如MS-DOS的EXE程序、BSD的COFF,这些可执行类型由Linux_binfmt对象描述。这些对象处于一个单向链表中,其中表尾是对解释脚本的可执行格式描述的一个对象,它检查可执行文件是否以#!开头,是则将这行其余部分解释为另一个可执行路径名,如bash,并将它作参数执行。Linux也可让用户注册可执行格式,对该格式的识别通过前128字节。当确定可执行格式为自定义时,内核启动位于用户态的解释器程序,它读入文件路径名,执行计算。如java程序的可执行文件由java虚拟机解释。
Linux可支持外来的程序,分两种:1.程序中系统调用与POSIX不兼容,这时如wine这样的模拟程序被调用,它将每一个API转换为一个模拟的封装函数调用,后者用linux系统调用实现。2.程序中包含的系统调用与POSIX兼容,此时内核要做的仅仅是消除一些细微的差别的工作如系统调用如何调用、信号如何编号等,这些信息存在exec_domain的执行域描述符中。
版权声明:本文为博主原创文章,未经博主允许不得转载。