尽管把一组指令装入内存并让CPU执行看起来并不是什么大问题,但内核还必须灵活处理以下几个方面的问题:
不同的可执行文件格式:
共享库:很多可执行文件并不包含执行程序所需的所有代码,而是期望内核在运行时从共享库中加载函数。
执行上下文的其他信息:这包括程序员熟悉的命令行参数与环境变量。
程序是以可执行文件(executable file)的形式存放在磁盘上的,可执行文件既包括被执行函数的目标代码,也包括这些函数所使用的数据。程序中的很多函数是所有程序员都可使用的服务例程,它们的目标代码包含在所谓“库”的特殊文件中。实际上,一个库函数的代码或被静态地拷贝到可执行文件(静态库),或在运行时被连接到进程(共享库,因为它们的代码由很多独立的进程所共享)。
当装入并运行一个程序时,用户可以提供影响程序执行方式的两种信息:命令行参数和环境变量。用户在shell提示符下紧跟文件名输入的就是命令行参数。环境变量(例如HOME和PATH)是从shell继承来的,但用户在装入并运行程序前可以修改任何环境变量。
一、可执行文件
可执行文件是一个普通文件,它描述了如何初始化一个新的执行上下文,也就是如何开始一个新的计算。
1.1、进程的信任状和权能
从传统上看,Unix系统与每个进程的一些信任状(credential)相关,信任状把进程与一个特定的用户或用户组捆绑在一起。信任状在多用户系统上尤为重要,因为信任状可以决定每个进程能做什么,不能做什么,这样即保证了每个用户的个人数据的完整性,也保证了系统整体上的稳定性。
值为0的UID指定给root超级用户,而值为0的用户GID指定给root超级组。只要有关进程的信任状存放了一个零值,则内核将放弃权限检查,始终允许这个进程做任何事情,如涉及系统管理或硬件处理的那些操作,而这些操作对于非特权进程是不允许的。
1.1.1、进程的权能
一种权能仅仅是一个标志,它表明是否允许进程执行一个特定的操作或一组特定的操作。这个模型不同于传统的“超级用户VS普通用户”模型,在后一种模型中,一个进程要么能做任何事情,要么什么也不能做,这取决于它的有效UID。
权能的主要优点是,任何时候每个进程只需要有限种权能。因此,即使有恶意的用户发现一种利用有潜在错误的程序的方法,他也只能非法地执行有限个操作类型。
1.1.2、Linux安全模块框架
在Linux2.6中,权能是与Linux安全模块(LSM)框架紧密结合在一起的。简单地说,LSM框架允许开发人员定义几种可以选择的内核安全模型。
每个安全模型是由一组安全钩(security hook)实现的。安全钩是由内核调用的一个函数,用于执行与安全有关的重要操作。钩函数决定一个操作是否可以执行。
1.2、命令行参数和shell环境
1.2.1、库
每个高级语言的源码文件都是经过几个步骤才转化为目标文件的,目标文件中包含的是汇编语言指令的机器代码,它们和相应的高级语言指令对应。目标文件并不能被执行,因为它不包含源代码文件所用的全局外部符号名的线性地址(例如库函数或同一程序中的其他源代码文件)。这些地址的分配或解析是由链接程序完成的,链接程序把程序所有的目标文件收集起来并构造可执行文件。链接程序还分析程序所用的库函数,并把它们粘合成可执行文件。
除了C库,Unix系统中还包含很多其他的函数库。一般的Linux系统通常就有几百个不同的库。这里仅仅列举其中的两个:数学库libm包含浮点操作的基本函数,而X11库libX11收集了所有X11窗口系统图形接口的基本底层函数。
传统Unix系统中的所有可执行文件都是基于静态库(static library)的。这就意味着链接程序所产生的可执行文件不仅包含原程序的代码,还包括程序所引用的库函数的代码。
静态库的一大缺点是:它们占用大量的磁盘空间。的确,每个静态链接的可执行文件都复制库代码的某些部分。
现代Unix系统利用共享库(shared library)。可执行文件不用再包含库的目标代码,而仅仅指向库名。当程序被装入内存执行时,一个名为动态链接器(dynamic linker,也叫ld.so)的程序就专注于分析可执行文件的库名,确定所需库在系统目录树中的位置,并使执行进程可以使用所请求的代码。进程也可以使用dlopen()库函数在运行时装入额外的共享库。
共享库对提供文件内存映射的系统尤为方便,因为它们减少了执行一个程序所需的主内存量。当动态链接程序必须把某一共享库链接到进程时,并不拷贝目标代码,而是仅仅执行一个内存映射,把库文件的相关部分映射到进程的地址空间中。这就允许共享库机器代码所在的页框被使用同一代码的所有进程共享。显然,如果程序是静态链接的,那么共享是不可能的。
共享库也有一些缺点。动态链接的程序启动时间通常比静态链接的程序长。此外,动态链接的程序的可移植性也不如静态链接的好,因为当系统中所包含的库版本发生变化时,动态链接的程序运行时就可能出现问题。
用户可以始终请求一个程序被静态地链接。例如,GCC编译器提供-static选项,即告诉链接程序使用静态库而不是共享库。
1.3、程序段和进程的线性区
从逻辑上说,Unix程序的线性地址空间传统上被划分为几个叫做段(segment)的区间:
正文段:包含程序的可执行代码。
已初始化数据段:包含已初始化的数据,也就是初值存放在可执行文件中的所有静态变量和全局变量(因为程序在启动时必须知道它们的值)。
未初始化段:包含未初始化的数据,也就是初值没有存放在可执行文件中的所有全局变量(因为程序在引用它们之前才被赋值);历史上把这个段叫做bss段。
堆栈段:包含程序的堆栈,堆栈中有返回地址、参数和被执行函数的局部变量。
1.3.1、灵活线性区布局
1.4、执行跟踪
执行跟踪(execution tracing)是一个程序监视另一个程序执行的一种技术。被跟踪的程序一步一步地执行,直到接收到一个信号或调用一个系统调用。
ptrace()系统调用修改被跟踪进程描述符的parent字段以使它指向跟踪进程,因此,跟踪进程变为被跟踪进程的有效父进程。当执行跟踪终止时,也就是当以PTRACE_DETACH命令调用ptrace()时,这个系统调用把p_pptr设置为real_parent的值,恢复被跟踪进程原来的父进程。
与被跟踪程序相关的几个监控事件为:
* 一条单独汇编指令执行的结束。
* 进入系统调用
* 退出系统调用
* 接收到一个信号
二、可执行格式
三、执行域
对这些“外来”程序提供两种支持:
* 模拟执行(emulated execuion):程序中包含的系统调用与POSIX不兼容时才有必要执行这种程序。
* 原样执行(native execution):只有程序中所包含的系统调用完全与POSIX兼容时才有效。
四、exec函数
Unix系统提供了一系列函数,这些函数能用可执行文件所描述的新上下文代替进程的上下文。这样的函数名以前缀exec开始,后跟一个或两个字母,因此,家族中的一个普通函数被当作exec函数来引用。
sys_execve()服务例程接收下列参数:
* 可执行文件路径名的地址(在用户态地址空间)。
* 以NULL结束的字符串指针数组的地址(在用户态地址空间)。每个字符串表示一个命令行参数。
* 以NULL结束的字符串指针数组的地址(也在用户态地址空间)。每个字符串以NAME=value形式表示一个环境变量。
sys_execve()把可执行文件路径名拷贝到一个新分配的页框。然后调用do_execve()函数,传递给它的参数为指向这个页框的指针、指针数组的指针及把用户态寄存器内容保存到内核态堆栈的位置。
尽管动态链接程序运行在用户态,但我们还要在这里简要概述一下它是如何运作的。它的第一个工作就是从内核保存在用户态堆栈的信息(处于环境串指针数组和arg_start之间)开始,为自己建立一个基本的执行上下文。然后,动态链接程序必须检查被执行的程序,以识别哪个共享库必须装入及在每个共享库中哪个函数被有效地请求。接下来,解释器发出几个mmap()系统调用来创建线性区,以对将存放程序实际使用的库函数(正文和数据)的页进行映射。然后,解释器根据库的线性区的线性地址更新对共享库符号的所有引用。最后,动态链接程序通过跳转到被执行程序的主入口点而终止它的执行。从现在开始,进程将执行可执行文件的代码和共享库的代码。
阅读(829) | 评论(0) | 转发(0) |