本文参考书籍
1.操作系统真相还原
2.Linux内核完全剖析:基于0.12内核
3.x86汇编语言 从实模式到保护模式
4.Linux内核设计的艺术
ps:基于x86硬件的pc系统
Linux0.12初始化续
本次主要分析文件的打开与可执行程序的加载。
打开文件与中断的执行过程
上文分析完成了setup函数的执行,此时返回init函数继续执行,此时会执行到;
(void) open("/dev/tty1",O_RDWR,0); // 以读写方式打开tty
我们查看open函数的定义,位于lib/open.c中;
int open(const char * filename, int flag, ...) // 打开文件函数 filename文件名,flag文件打开标志
{ // 打开文件返回文件描述符
register int res;
va_list arg;
va_start(arg,flag); // 输入的可变参数
__asm__("int $0x80"
:"=a" (res)
:"0" (__NR_open),"b" (filename),"c" (flag),
"d" (va_arg(arg,int))); // 将参数传入并调用读文件的系统调用
if (res>=0) // 返回文件大于等于0 则表示是一个文件描述符
return res; // 返回打开的文件描述符
errno = -res;
return -1;
}
此时进行系统调用后,会在内核态下执行sys_open函数;
int sys_open(const char * filename,int flag,int mode) // 打开文件系统调用
{
struct m_inode * inode;
struct file * f;
int i,fd;
mode &= 0777 & ~current->umask; // 将用户设置的模式与当前进程屏蔽码相与
for(fd=0 ; fd<NR_OPEN ; fd++) // 从20中查找一个当前进程没有使用的fd
if (!current->filp[fd])
break;
if (fd>=NR_OPEN) // 如果打开文件的个数超过20则返回出错码
return -EINVAL;
current->close_on_exec &= ~(1<<fd); // 设置当前进程的执行时关闭文件句柄位图
f=0+file_table; // 获取file_table数组的首地址,赋值给f
for (i=0 ; i<NR_FILE ; i++,f++) // 搜索空闲文件结构项
if (!f->f_count) break; // 如果找到空闲则停止循环
if (i>=NR_FILE) // 如果超过64个,即没有找到空闲的则返回出错码
return -EINVAL;
(current->filp[fd]=f)->f_count++; // 如果找到则将当前进程对应的文件句柄fd指向搜索到的文件结构
if ((i=open_namei(filename,flag,mode,&inode))<0) { // 执行打开操作,如果返回小于0说明出错
current->filp[fd]=NULL; // 释放刚刚找到的结构
f->f_count=0; // 将找到的文件结构引用计数置空
return i; // 返回出错码
}
/* ttys are somewhat special (ttyxx major==4, tty major==5) */
if (S_ISCHR(inode->i_mode)) // 检查是否是字符设备
if (check_char_dev(inode,inode->i_zone[0],flag)) { // 检查是否能打开该字符设备
iput(inode); // 如果不能打开则释放inode并重新置为返回出错码
current->filp[fd]=NULL;
f->f_count=0;
return -EAGAIN;
}
/* Likewise with block-devices: check for floppy_change */
if (S_ISBLK(inode->i_mode)) // 如果打开的是块设备
check_disk_change(inode->i_zone[0]); // 检查盘片是否更换
f->f_mode = inode->i_mode; // 设置文件属性
f->f_flags = flag; // 设置文件标志
f->f_count = 1; // 设置文件句柄引用计数为1
f->f_inode = inode; // 设置i节点为打开文件的i节点
f->f_pos = 0; // 初始化文件读写指针
return (fd); // 返回文件句柄
}
读函数,主要是找到当前进程读写的空闲的文件描述符,然后将数据放入inode中,然后让打开的文件结构指向打开的i节点,在本函数中比较核心的函数是open_namei;
int open_namei(const char * pathname, int flag, int mode,
struct m_inode ** res_inode)
{
const char * basename;
int inr,dev,namelen;
struct m_inode * dir, *inode;
struct buffer_head * bh;
struct dir_entry * de;
if ((flag & O_TRUNC) && !(flag & O_ACCMODE)) // 如果文件访问模式是只读但是截位标志置位了添加只写标志,
flag |= O_WRONLY;
mode &= 0777 & ~current->umask; // 屏蔽给定模式的位
mode |= I_REGULAR; // 并添加上普通文件标志位
if (!(dir = dir_namei(pathname,&namelen,&basename,NULL))) // 根据指定的路径名寻找对应的i节点
return -ENOENT;
if (!namelen) { /* special case: '/usr/' etc */ // 如果长度为0
if (!(flag & (O_ACCMODE|O_CREAT|O_TRUNC))) { // 如果不是读写、创建和文件长度截0
*res_inode=dir; // 则是打开一个目录名文件操作
return 0; // 直接赋值返回
}
iput(dir); // 否则释放该节点
return -EISDIR; // 返回错误码
}
bh = find_entry(&dir,basename,namelen,&de); // 根据最顶层目录名的iJ节点dir,在取得路径名中最后的文件名对应的目录项结构de,并获取该目录的缓冲区指针
if (!bh) { // 如果返回为空
if (!(flag & O_CREAT)) { // 如果不是创建文件则释放返回
iput(dir);
return -ENOENT;
}
if (!permission(dir,MAY_WRITE)) { // 如果该用户在目录下没有写的权利则释放并返回错误码
iput(dir);
return -EACCES;
}
inode = new_inode(dir->i_dev); // 是创建操作并且有写操作权限,申请该目录下一个i节点
if (!inode) { // 如果申请失败则释放并返回错误码
iput(dir);
return -ENOSPC;
}
inode->i_uid = current->euid; // 设置节点的用户id
inode->i_mode = mode; // 设置节点的模式
inode->i_dirt = 1; // 设置修改标志
bh = add_entry(dir,basename,namelen,&de); // 添加指定目录添加一个新目录项
if (!bh) { // 如果添加失败
inode->i_nlinks--; // 节点的引用连接计数减1
iput(inode); // 释放该节点
iput(dir); // 释放申请的目录
return -ENOSPC; // 返回出错码
}
de->inode = inode->i_num; // 置i节点号为新申请的iJ节点号码
bh->b_dirt = 1; // 置高速缓冲区已修改标志
brelse(bh); // 释放缓冲块
iput(dir); // 放回目录9节点
*res_inode = inode; // 返回新目录项的i节点指针
return 0;
}
inr = de->inode; // 若去文件名对应的目录项结构成功则该文件存在,取该节点的节点号
dev = dir->i_dev; // 取所在设备的设备号
brelse(bh); // 释放缓冲块
if (flag & O_EXCL) { // 如果此时独占操作标志置位,但文件已经存在则返回文件出错码
iput(dir);
return -EEXIST;
}
if (!(inode = follow_link(dir,iget(dev,inr)))) // 读取该目录项的i节点内容
return -EACCES;
if ((S_ISDIR(inode->i_mode) && (flag & O_ACCMODE)) ||
!permission(inode,ACC_MODE(flag))) { // 如果该i节点是一个目录的i节点并且访问模式是只写或读写或者没有访问权限则放回该节点并返回错误码
iput(inode);
return -EPERM;
}
inode->i_atime = CURRENT_TIME; // 更新该i节点的访问时间
if (flag & O_TRUNC) // 如果设立了截0标志
truncate(inode); // 则将该i节点的文件长度截为0并返回目录项i节点的指针
*res_inode = inode;
return 0;
}
该函数相对比较复杂,我们依次来分析所调用的函数dir_namei,该函数主要是找到指定目录名的i节点指针,以及在最顶层目录的名称,其中涉及的调用函数分析如下;
static struct m_inode * get_dir(const char * pathname, struct m_inode * inode)
{
char c;
const char * thisname;
struct buffer_head * bh;
int namelen,inr;
struct dir_entry * de;
struct m_inode * dir;
if (!inode) { // 检查传入inode是否为空
inode = current->pwd; // 如果为空则使用当前进程的当前工作目录
inode->i_count++; // 当前inode引用计数加1
}
if ((c=get_fs_byte(pathname))=='/') { // 如果用户指定路径的第一个字符是/,则是绝对路径
iput(inode); // 放回原节点
inode = current->root; // 从当前任务的根节点开始查找
pathname++; // 路径名指针向下移动一位
inode->i_count++; // inode引用计数加1
}
while (1) { // 路径名中各个目录名部分和文件名进行循环处理
thisname = pathname; // 当前路径名
if (!S_ISDIR(inode->i_mode) || !permission(inode,MAY_EXEC)) {
iput(inode); // 如果当前节点不是目录名或者没有进入该目录的权限放回并返回
return NULL;
}
for(namelen=0;(c=get_fs_byte(pathname++))&&(c!='/');namelen++)
/* nothing */ ; // 找到下一个/之前的名称
if (!c) // 如果c为空则直接返回该inode
return inode;
if (!(bh = find_entry(&inode,thisname,namelen,&de))) {
iput(inode); // 找到当前目录项后然后查找该目录项inode如果没找到则放回并返回
return NULL;
}
inr = de->inode; // 获取该节点的节点号
brelse(bh); // 释放缓冲块
dir = inode;
if (!(inode = iget(dir->i_dev,inr))) { // 取节点的内容
iput(dir);
return NULL;
}
if (!(inode = follow_link(dir,inode))) // 如果当前目录是一个符号链接则取符号链接指向的i节点
return NULL;
}
}
/*
* dir_namei()
*
* dir_namei() returns the inode of the directory of the
* specified name, and the name within that directory.
*/
static struct m_inode * dir_namei(const char * pathname,
int * namelen, const char ** name, struct m_inode * base) // 返回指定目录名的i节点指针,以及在最顶层目录的名称
{
char c;
const char * basename;
struct m_inode * dir;
if (!(dir = get_dir(pathname,base))) // base是指定的起始目录i节点
return NULL; // 如果没找到则返回为空
basename = pathname; //
while (c=get_fs_byte(pathname++)) //
if (c=='/')
basename=pathname;
*namelen = pathname-basename-1; // 返回名长度
*name = basename; // 返回名称
return dir;
}
此时我们继续分析find_entry函数,该函数主要是查找指定目录和文件名的目录项;
static struct buffer_head * find_entry(struct m_inode ** dir,
const char * name, int namelen, struct dir_entry ** res_dir)
{
int entries;
int block,i;
struct buffer_head * bh;
struct dir_entry * de;
struct super_block * sb;
#ifdef NO_TRUNCATE // 如果定义了NO_TRUNCATE
if (namelen > NAME_LEN) // 如果文件名长度超过最大长度则返回
return NULL;
#else
if (namelen > NAME_LEN) // 如果文件名长度超过定义则截取NAME_LEN长度的名字
namelen = NAME_LEN;
#endif
entries = (*dir)->i_size / (sizeof (struct dir_entry)); // 计算目录中目录项项数
*res_dir = NULL;
/* check for '..', as we might have to do some "magic" for it */
if (namelen==2 && get_fs_byte(name)=='.' && get_fs_byte(name+1)=='.') { // 如果是 . 和 .. 这种情况
/* '..' in a pseudo-root results in a faked '.' (just change namelen) */
if ((*dir) == current->root) // 如果是 .. 则转换为 .
namelen=1;
else if ((*dir)->i_num == ROOT_INO) { // 如果该目录节点号为1
/* '..' over a mount-point results in 'dir' being exchanged for the mounted
directory-inode. NOTE! We set mounted, so that we can iput the new dir */
sb=get_super((*dir)->i_dev); // 读取文件系统的根i节点
if (sb->s_imount) { // 如果已经安装了根节点
iput(*dir); // 则放回dir及节点
(*dir)=sb->s_imount; // 将安装的根节点指向dir
(*dir)->i_count++; // 引用计数加1
}
}
}
if (!(block = (*dir)->i_zone[0])) // 如果0不包含数据则返回
return NULL;
if (!(bh = bread((*dir)->i_dev,block))) // 读取节点0节点的数据
return NULL;
i = 0;
de = (struct dir_entry *) bh->b_data; // 获取读取的数据让de指向该数据
while (i < entries) {
if ((char *)de >= BLOCK_SIZE+bh->b_data) { // 如果搜索完成还没找到
brelse(bh); // 释放该缓冲块
bh = NULL; // 指针置空
if (!(block = bmap(*dir,i/DIR_ENTRIES_PER_BLOCK)) ||
!(bh = bread((*dir)->i_dev,block))) { // 通过读取目录的下一个逻辑块
i += DIR_ENTRIES_PER_BLOCK; // 继续寻找
continue;
}
de = (struct dir_entry *) bh->b_data;
}
if (match(namelen,name,de)) { // 判断是否匹配目录
*res_dir = de; // 如果匹配上则让res_dir指向找到的de
return bh; // 返回缓冲块
}
de++; // 否则进行下一次寻找
i++;
}
brelse(bh); // 如果最终没找到则释放该缓冲块
return NULL; // 返回为空
}
继续执行时,由于此时传入的参数为’/dev/tty1’,打开的是终端设备,此时会执行check_char_dev函数,该函数主要是检查如果打开的文件是tty终端字符设备时,对当前进程的设置和tty表的设置。
至此open的系统调用就完成了。
输入输出重定向
(void) dup(0); // 复制句柄,产生句柄1号标准输出设备
(void) dup(0);
此时也会使用系统调用,此时调用sys_dup()函数;
int sys_dup(unsigned int fildes)
{
return dupfd(fildes,0);
}
dupfd函数如下;
static int dupfd(unsigned int fd, unsigned int arg) // 复制文件句柄函数
{
if (fd >= NR_OPEN || !current->filp[fd]) // 检查函数参数的有效性,如果大于20或者该文件描述符对应的文件结构为空
return -EBADF; // 返回错误码
if (arg >= NR_OPEN) // 如果输入的参数小于20则返回错误码
return -EINVAL;
while (arg < NR_OPEN) // 循环遍历,找到一个可用的文件描述符
if (current->filp[arg])
arg++;
else
break;
if (arg >= NR_OPEN) // 如果找到的文件描述符大于20,则没有找到空闲的则返回错误码
return -EMFILE;
current->close_on_exec &= ~(1<<arg); // 找到空闲后关闭标志,即在exec的过程中不会关闭dup的文件描述符
(current->filp[arg] = current->filp[fd])->f_count++; // 将文件引用计数增1
return arg; // 返回文件描述符
}
该函数主要是复制文件句柄。
可执行程序的加载
在初始化程序中继续执行,此时会生成子进程,子进程中执行,将/etc/rc文件中的内容输出到终端上,然后调用execve执行/etc/rc文件中的命令。
if (!(pid=fork())) { // 执行/etc/rc中的命令参数
close(0);
if (open("/etc/rc",O_RDONLY,0)) // 将打开文件重定向到标准输入
_exit(1); // 若文件打开失败则立刻退出
execve("/bin/sh",argv_rc,envp_rc); // 系统调用执行命令
_exit(2); // 若执行出错则退出
}
此时我们来分析一下execve函数的执行过程,该函数也是通过系统调用调用sys_execve函数;
_sys_execve:
lea EIP(%esp),%eax # eax指向堆栈中保存用户程序的eip
pushl %eax # 将eax压栈
call _do_execve # 调用c函数do_execve函数
addl $4,%esp # 丢弃压入的值
ret
此时我们继续查看do_execve函数;
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; // 有效用户ID和有效组ID
int retval;
int sh_bang = 0; // 控制是否需要执行脚本程序
unsigned long p=PAGE_SIZE*MAX_ARG_PAGES-4; // p指向参数和环境的最后部分
if ((0xffff & eip[1]) != 0x000f) // 段选择符是否是当前任务代码段的段选择符
panic("execve called from supervisor mode");
for (i=0 ; i<MAX_ARG_PAGES ; i++) /* clear page-table */ // 清空Page数组
page[i]=0;
if (!(inode=namei(filename))) /* get executables inode */ // 获取文件名对应的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; // 获取i节点的属性
e_uid = (i & S_ISUID) ? inode->i_uid : current->euid; // 判断进程是否有运行的权利
e_gid = (i & S_ISGID) ? inode->i_gid : current->egid;
if (current->euid == inode->i_uid) // 如果当前进程的euid与节点的uid相同
i >>= 6; // 则文件属性值右移6位
else if (in_group_p(inode->i_gid)) // 如果是同组用户
i >>= 3; // 则文件属性值右移3位
if (!(i & 1) &&
!((inode->i_mode & 0111) && suser())) { // 根据属性i的最低3位来判断当前进程是否有权运行这个文件
retval = -ENOEXEC; // 如果不能运行则跳转
goto exec_error2;
}
if (!(bh = bread(inode->i_dev,inode->i_zone[0]))) { // 读取可执行文件的第1块数据到缓冲区
retval = -EACCES;
goto exec_error2;
}
ex = *((struct exec *) bh->b_data); /* read exec-header */ // 将读入的数据让ex指向该数据
if ((bh->b_data[0] == '#') && (bh->b_data[1] == '!') && (!sh_bang)) { // 如果是以#!开头则是脚本文件
/*
* This section does the #! interpretation.
* Sorta complicated, but hopefully it will work. -TYT
*/
char buf[128], *cp, *interp, *i_name, *i_arg;
unsigned long old_fs;
strncpy(buf, bh->b_data+2, 127); // 提取脚本程序名与参数并把解释程序名、解释程序的参数和脚本文件名组合放入环境参数块中
brelse(bh); // 释放该缓冲块
iput(inode); // 放回脚本文件节点
buf[127] = '\0';
if (cp = strchr(buf, '\n')) {
*cp = '\0'; // 第1个换行符并去掉空格制表符
for (cp = buf; (*cp == ' ') || (*cp == '\t'); cp++);
}
if (!cp || *cp == '\0') { // 若改行没内容则报错
retval = -ENOEXEC; /* No interpreter name found */
goto exec_error1;
}
interp = i_name = cp; // 得到程序的内容
i_arg = 0; // 获取程序的名称和程序执行的输入参数
for ( ; *cp && (*cp != ' ') && (*cp != '\t'); cp++) {
if (*cp == '/')
i_name = cp+1;
}
if (*cp) {
*cp++ = '\0';
i_arg = cp;
}
/*
* OK, we've parsed out the interpreter name and
* (optional) argument.
*/
if (sh_bang++ == 0) { // sh_bang加1
p = copy_strings(envc, envp, page, p, 0); // 把函数的参数放入空间中
p = copy_strings(--argc, argv+1, page, p, 0); // 除了执行文件名其他都放入空间中
}
/*
* Splice in (1) the interpreter's name for argv[0]
* (2) (optional) argument to interpreter
* (3) filename of shell script
*
* This is done in reverse order, because of how the
* user environment and arguments are stored.
*/
p = copy_strings(1, &filename, page, p, 1); // 接着逆向复制文件名
argc++;
if (i_arg) { // 复制解释程序的多个参数
p = copy_strings(1, &i_arg, page, p, 2);
argc++;
}
p = copy_strings(1, &i_name, page, p, 2);
argc++;
if (!p) { // 如果复制不成功则返回错误码
retval = -ENOMEM;
goto exec_error1;
}
/*
* OK, now restart the process with the interpreter's inode.
*/
old_fs = get_fs(); // 让段寄存器指向内核空间
set_fs(get_ds()); // 设置段寄存器
if (!(inode=namei(interp))) { /* get executables inode */ // 获取可执行i节点
set_fs(old_fs); // 如果出错则设置成原值并设置错误码返回
retval = -ENOENT;
goto exec_error1;
}
set_fs(old_fs); // 设置成原段寄存器
goto restart_interp; // 重新处理新的执行文件
}
brelse(bh); // 释放缓冲块
if (N_MAGIC(ex) != ZMAGIC || ex.a_trsize || ex.a_drsize ||
ex.a_text+ex.a_data+ex.a_bss>0x3000000 || // 此时可执行文件的头机构数据已经复制到了ex中
inode->i_size < ex.a_text+ex.a_data+ex.a_syms+N_TXTOFF(ex)) { // 检查头部文件格式如果不对则设置错误码返回
retval = -ENOEXEC;
goto exec_error2;
}
if (N_TXTOFF(ex) != BLOCK_SIZE) { // 如果文件代码开始处没有位于1个页面边界处,则不能执行
printk("%s: N_TXTOFF != BLOCK_SIZE. See a.out.h.", filename); // 因为需求页需要加载执行文件时以页面为单位
retval = -ENOEXEC;
goto exec_error2;
}
if (!sh_bang) { // 如果该标志没有被设置,
p = copy_strings(envc,envp,page,p,0); // 复制指定个数的命令行参数和环境字符串到参数和环境空间中
p = copy_strings(argc,argv,page,p,0);
if (!p) { // 如果此时p为0则表示空间已满
retval = -ENOMEM;
goto exec_error2;
}
}
/* OK, This is the point of no return */
/* note that current->library stays unchanged by an exec */
if (current->executable) // 如果当前可执行有值则放回对应的inode
iput(current->executable);
current->executable = inode; // 让进程executable指向新执行文件的i节点
current->signal = 0; // 复位所有的信号位图
for (i=0 ; i<32 ; i++) { // 复位原进程的所有信号处理句柄
current->sigaction[i].sa_mask = 0;
current->sigaction[i].sa_flags = 0;
if (current->sigaction[i].sa_handler != SIG_IGN)
current->sigaction[i].sa_handler = NULL;
}
for (i=0 ; i<NR_OPEN ; i++) // 根据设定的执行时关闭位图标志,是否关闭指定的打开文件
if ((current->close_on_exec>>i)&1)
sys_close(i);
current->close_on_exec = 0; // 复位该标志
free_page_tables(get_base(current->ldt[1]),get_limit(0x0f)); // 设置当前进程的基地址和限长
free_page_tables(get_base(current->ldt[2]),get_limit(0x17));
if (last_task_used_math == current) // 是否使用了协处理器
last_task_used_math = NULL; // 如果该进程使用了则重置
current->used_math = 0; // 复位该标志
p += change_ldt(ex.a_text,page); // 根据执行文件头的代码长度字段修改局部表中描述符基址和段限长
p -= LIBRARY_SIZE + MAX_ARG_PAGES*PAGE_SIZE; // 并将128KB的参数和环境空间放置在数据段末端
p = (unsigned long) create_tables((char *)p,argc,envc); // 在栈空间中创建环境和参数变量指针表
current->brk = ex.a_bss +
(current->end_data = ex.a_data +
(current->end_code = ex.a_text)); // 指向当前任务数据段的末端
current->start_stack = p & 0xfffff000; // 进程栈开始字段所在页面
current->suid = current->euid = e_uid; // 重新设置有效用户id和有效组id
current->sgid = current->egid = e_gid;
eip[0] = ex.a_entry; /* eip, magic happens :-) */ // 将系统中断在堆栈上的代码指针换为新执行程序的入口点
eip[3] = p; /* stack pointer */ // 将栈指针替换为新执行文件的栈指针
return 0;
exec_error2:
iput(inode); // 放回i节点
exec_error1:
for (i=0 ; i<MAX_ARG_PAGES ; i++) // 释放存放参数的内存页面
free_page(page[i]);
return(retval); // 返回出错码
}
该函数主要的工作是,执行对命令行参数和环境参数空间页面的初始化操作,根据执行文件开始部分的头结构、对其中信息进行处理,对当前调用进程运行新文件前进行初始化操作,替换堆栈上原调用程序的返回地址为新执行程序运行地址,运行新加载的程序。
至此文件打开与可执行程序的加载分析流程已完成。