前言
通过本系列前几篇文章的梳理,我们对linux0.12内核功能有了初步地了解。进一步地,我们看看linux如何在前述基础上执行用户自定义程序。其中的重点为execve
与需求加载
。
样例
在进入正题之间,我们可以在linux0.12中编译并执行下面一段程序
#include <unistd.h>
#include <stdlib.h>
int main()
{
execl("/root/hello","hello",NULL);
exit(0);
}
其中/root/hello为预先编译好的,用于向屏幕输出hello,world!的可执行文件,结果如下,可以顺利执行。相当于我们可以通过exec函数族实现自定义程序的运行
本篇文章主要以上述过程为例,简单分析一下整个过程
函数库
在分析之前,我们对函数库要有一定的理解。简单地说(个人理解,未必正确)
C标准库:
将常用的C函数封装成库,提供用户快速调用
。例如,通过fopen封装系统调用open等。意味着fopen的具体实现是固定的,但open的进一步实现由不同内核决定。
内核库:
将常用的内核C函数封装成库,供内核调用
。主要提供给内核进行快速调用
posix标准:
定义编程接口,使得上层应用具备可移植性
。比如,定义open的参数和返回值,遵循posix协议的内核必须根据定义的参数和返回值实现open的系统调用。如此一来,C标准库的fopen便具备可以移植性。也因此,想要让自己的内核多人使用,就要具备很好的可移植性,又由于目前大部分软件基于C标准库,而C标准库使用posix接口。因此遵循posix协议设计内核,C标准库能直接拿来用,对应的其他软件也可以
具体地,在linux0.12/lib/execve.c中实现了的execve()。
#define __LIBRARY__
#include <unistd.h>
_syscall3(int,execve,const char *,file,char **,argv,char **,envp)
syscall实际上是一段宏定义的汇编代码,用于执行系统调用int 0x80。其最终会被编译成内核库函数。相当于linux自己对于sys_execve这一系统调用进行了封装,封装为execve()。那么C标准库的exec函数族就会直接调用execve()。这便是内核与标准C函数库的关系。
我们可以在现今linux系统下,执行strace跟踪execl的执行过程 strace ./a.out
,如下(ps:这里执行的程序我换成了/bin/date,这不影响结论。
简单地,我们对上述例子a.out的编译与执行流程进行梳理:
- 首先,gcc编译execc.c这一文件,根据头文件,在链接的过程中,将c标准库libc中的execl与exit实现给予a.out。
- libc关于execl的实现,实际是进行了execve的系统调用。而这一系统调用是内核库提供的,在编译内核过程中生成。
- 相当于执行a.out时,调用execve,读取/root/hello进行执行
- execve实际上是sys_execve系统调用的封装,其具体实现就涉及
需求装载
这一概念了
需求装载
在 execve()执行过程中,系统会清掉 fork()复制的原程序的页目录和页表项,并释放对应页面。系统仅为新加载的程序代码重新设置进程数据结构中的信息,并申请和映射了命令行参数和环境参数块所占的内存页面,以及设置了执行代码执行点。此时此刻,内核并没有立刻从执行文件所在块设备上加载新程序的代码和数据。当 execve()该过程返回时, 即开始执行新的程序,但一开始执行肯定会引起缺页异常中断发生。因为此时代码和数据还未被从块设备上读入内存。此时缺页异常处理过程就会根据引起异常的线性地址在主内存区为新程序申请内存页面(内存帧),并从块设备上读入引起异常的指定页面。同时还为该线性地址设置对应的页目录项和页表项。这种加载执行文件的方法称为需求加载
根据书中定义,我们总结需求加载需要完成以下几件事
- 一开始执行execve()过程中,不将具体程序代码放入内存中执行,而让其留在文件系统中。但页表等内容需要换新
- 设置缺页异常中断,设计对应中断处理程序,此时该程序才会将执行文件的正要被执行到的代码从文件系统移入内存中
- 既然要延时访问执行文件内容,那么当前进程需要记录执行文件的i节点,及其库函数i节点
我们从源码看看是否如此,便能对这部分内容进行掌握
execve
在跟踪该系统调用前,
上述我们从execl跟踪到execve,再到sys_execve
kernel/sys_call.s
_sys_execve:
lea EIP(%esp),%eax
pushl %eax
call _do_execve
addl $4,%esp
ret
由上可以进一步跟踪到do_execve
,其为真正干活的函数
fs/exec.c
int do_execve(unsigned long * eip,long tmp,char * filename,
char ** argv, char ** envp)
{
struct m_inode * inode;
struct buffer_head * bh;
struct exec ex;
unsigned long page[MAX_ARG_PAGES];
int i,argc,envc;
int e_uid, e_gid;
int retval;
int sh_bang = 0;
unsigned long p=PAGE_SIZE*MAX_ARG_PAGES-4;
if ((0xffff & eip[1]) != 0x000f)
panic("execve called from supervisor mode");
for (i=0 ; i<MAX_ARG_PAGES ; i++) /* clear page-table */
page[i]=0;
if (!(inode=namei(filename))) /* get executables inode */
return -ENOENT;
argc = count(argv);
envc = count(envp);
restart_interp:
if (!S_ISREG(inode->i_mode)) {
/* must be regular file */
retval = -EACCES;
goto exec_error2;
}
i = inode->i_mode