前言

通过本系列前几篇文章的梳理,我们对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的编译与执行流程进行梳理:

  1. 首先,gcc编译execc.c这一文件,根据头文件,在链接的过程中,将c标准库libc中的execl与exit实现给予a.out。
  2. libc关于execl的实现,实际是进行了execve的系统调用。而这一系统调用是内核库提供的,在编译内核过程中生成。
  3. 相当于执行a.out时,调用execve,读取/root/hello进行执行
  4. execve实际上是sys_execve系统调用的封装,其具体实现就涉及需求装载这一概念了

需求装载

在 execve()执行过程中,系统会清掉 fork()复制的原程序的页目录和页表项,并释放对应页面。系统仅为新加载的程序代码重新设置进程数据结构中的信息,并申请和映射了命令行参数和环境参数块所占的内存页面,以及设置了执行代码执行点。此时此刻,内核并没有立刻从执行文件所在块设备上加载新程序的代码和数据。当 execve()该过程返回时, 即开始执行新的程序,但一开始执行肯定会引起缺页异常中断发生。因为此时代码和数据还未被从块设备上读入内存。此时缺页异常处理过程就会根据引起异常的线性地址在主内存区为新程序申请内存页面(内存帧),并从块设备上读入引起异常的指定页面。同时还为该线性地址设置对应的页目录项和页表项。这种加载执行文件的方法称为需求加载

根据书中定义,我们总结需求加载需要完成以下几件事

  1. 一开始执行execve()过程中,不将具体程序代码放入内存中执行,而让其留在文件系统中。但页表等内容需要换新
  2. 设置缺页异常中断,设计对应中断处理程序,此时该程序才会将执行文件的正要被执行到的代码从文件系统移入内存中
  3. 既然要延时访问执行文件内容,那么当前进程需要记录执行文件的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;
	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)
		i >>= 6;
	else if (in_group_p(inode->i_gid))
		i >>= 3;
	if (!(i & 1) &&
	    !((inode->i_mode & 0111) && suser())) {
		retval = -ENOEXEC;
		goto exec_error2;
	}
	if (!(bh = bread(inode->i_dev,inode->i_zone[0]))) {
		retval = -EACCES;
		goto exec_error2;
	}
	ex = *((struct exec *) bh->b_data);	/* read exec-header */
	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';
			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) {
			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 */
			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 ||
		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) {
		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) {
			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)
		iput(current->executable);
	current->executable = inode;
	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;
	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;
	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);
exec_error1:
	for (i=0 ; i<MAX_ARG_PAGES ; i++)
		free_page(page[i]);
	return(retval);
}

该函数较长,书中关于该函数的注释已十分清晰。主要完成了上述提到的第一件事一开始执行execve()过程中,不将具体程序代码放入内存中执行,而让其留在文件系统中。但页表等内容需要换新。当然,具体还需要区分待执行的是什么程序,如果是shell则走另一条路,在此就不展开。

接着,设置缺页异常中断,设计对应中断处理程序
mm/page.s

.globl _page_fault

_page_fault:
	xchgl %eax,(%esp)
	pushl %ecx
	pushl %edx
	push %ds
	push %es
	push %fs
	movl $0x10,%edx
	mov %dx,%ds
	mov %dx,%es
	mov %dx,%fs
	movl %cr2,%edx
	pushl %edx
	pushl %eax
	testl $1,%eax
	jne 1f
	call _do_no_page
	jmp 2f
1:	call _do_wp_page
2:	addl $8,%esp
	pop %fs
	pop %es
	pop %ds
	popl %edx
	popl %ecx
	popl %eax
	iret

当发生缺页异常,触发page_fault缺页中断,跳转do_no_page()。在trap.c中,page_fault会被注册到中断描述符表中,该过程在初始化阶段完成
kernel/trap.c

set_trap_gate(14,&page_fault);

我们继续跟踪do_no_page()。该函数对应上述2,3点要求。具体看书即可,十分详细。
mm/memory.c

void do_no_page(unsigned long error_code,unsigned long address)
{
	int nr[4];
	unsigned long tmp;
	unsigned long page;
	int block,i;
	struct m_inode * inode;

	if (address < TASK_SIZE)
		printk("\n\rBAD!! KERNEL PAGE MISSING\n\r");
	if (address - current->start_code > TASK_SIZE) {
		printk("Bad things happen: nonexistent page error in do_no_page\n\r");
		do_exit(SIGSEGV);
	}
	page = *(unsigned long *) ((address >> 20) & 0xffc);
	if (page & 1) {
		page &= 0xfffff000;
		page += (address >> 10) & 0xffc;
		tmp = *(unsigned long *) page;
		if (tmp && !(1 & tmp)) {
			swap_in((unsigned long *) page);
			return;
		}
	}
	address &= 0xfffff000;
	tmp = address - current->start_code;
	if (tmp >= LIBRARY_OFFSET ) {
		inode = current->library;
		block = 1 + (tmp-LIBRARY_OFFSET) / BLOCK_SIZE;
	} else if (tmp < current->end_data) {
		inode = current->executable;
		block = 1 + tmp / BLOCK_SIZE;
	} else {
		inode = NULL;
		block = 0;
	}
	if (!inode) {
		get_empty_page(address);
		return;
	}
	if (share_page(inode,tmp))
		return;
	if (!(page = get_free_page()))
		oom();
/* remember that 1 block is used for header */
	for (i=0 ; i<4 ; block++,i++)
		nr[i] = bmap(inode,block);
	bread_page(page,inode->i_dev,nr);
	i = tmp + 4096 - current->end_data;
	if (i>4095)
		i = 0;
	tmp = page + 4096;
	while (i-- > 0) {
		tmp--;
		*(char *)tmp = 0;
	}
	if (put_page(page,address))
		return;
	free_page(page);
	oom();
}
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐