1. 程式人生 > >linux0.11 execve系統呼叫分析

linux0.11 execve系統呼叫分析

    在Linux平臺下,我們一般都是在命令列下鍵入"./hello"來執行一個當前目錄下的hello應用程式("./"指定當前目錄)。雖然看似很簡單,但這麼小小的一個操作其實涉及到了很多的知識。比如:shell是如何將hello調入記憶體的?hello在執行前shell執行了哪些操作?hello的父程序又是哪個?回答這些問題之前我們先來看下面的一個例子:
這個例子包含兩個程式,第一個test_hello程式可以看成是hello的父程序,因為在test_hello中呼叫了fork函式生成一個子程序,並在子程序中呼叫execve函式執行hello程式。最後父程序等待子程序的退出,並打印出子程序的退出碼。第二個hello程式簡單的將main函式的引數和環境變數打印出來。

test_hello.c程式碼:

#include <stdio.h>
#include <stdlib.h>

char * argv[]={ "arg1","arg2", NULL };
char * envp[] = { "PATH=/home", NULL };

int main()
{
	int pid;
	int i;
	if(!(pid=fork()))
	{
		execve("hello",argv,envp);
	}
	else
		printf("child pid %d\n\n",pid);

	while (1)
		if (pid == wait(&i))
			break;
	printf("\n\rchild %d died with code %04x\n\r",pid,i);
	return 0;
}
hello.c程式碼:
#include <stdio.h>

int main(int argc, char **argv, char **envp)
{
	int i;
	printf("hello program start\n");
	for(i=0;i<argc;i++)
		printf("argv: %s\n",argv[i]);
	printf("arge: %s\n",envp[0]);

	printf("hello program end\n");
	return 0x05;
}
在同一個目錄下將上面兩個程式進行編譯連結,然後執行"./test_hello",最後列印結果如下:
[email protected]
:~/gcc$ ./test_hello child pid 3690 hello program start argv: arg1 argv: arg2 arge: PATH=/home hello program end child 3690 died with code 0500
    從執行的結果可以看出,首先test_hello父程序先執行列印處子程序的pid,然後子程序通過執行execve系統呼叫將hello程式裝載進來並執行,打印出execve系統呼叫向hello程式傳遞的引數和環境變數,最後hello退出後父程序也將終止wait並退出。上面程式其實就是一個說明shell執行機制的簡單例子(實際的shell遠比這複雜),為什麼這麼說呢?首先可以把test_hello看成是"shell",然後把hello看成是我們的"命令"(ls, echo 類似的命令),而hello的執行依靠的是test_hello,並且hello的引數也是由test_hello來傳遞,這與我們平常用的shell道理是一致的(比如: ls -l /etc/passwd),只不過我們平常用的shell沒有顯示的呼叫而已(/bin/sh)。
    所以簡單來說,shell執行一個命令就是這麼一個過程:首先fork出一個子程序,然後在子程序中execve這個命令程式,並傳遞命令程式的引數和環境變數,最後在父程序中wait子程序退出。

    下面再通過分析linux0.11的execve系統呼叫來進一步說明一個程式的執行過程。首先來看一下linux0.11的init程序中關於shell啟動的程式碼部分:

static char * argv[] = { "-/bin/sh",NULL };
static char * envp[] = { "HOME=/usr/root", NULL };

void init(void)
{
/*... 省略前面程式碼...*/	

	while (1) {
		if ((pid=fork())<0) {
			printf("Fork failed in init\r\n");
			continue;
		}
		if (!pid) {
			close(0);close(1);close(2);
			setsid();
			(void) open("/dev/tty0",O_RDWR,0);
			(void) dup(0);
			(void) dup(0);
			_exit(execve("/bin/sh",argv,envp));
		}
		while (1)
			if (pid == wait(&i))
				break;
		printf("\n\rchild %d died with code %04x\n\r",pid,i);
		sync();
	}

	_exit(0);	/* NOTE! _exit, not exit() */
}
    上面的init函式其實就是在所謂的init程序中執行,在該函式中可以看到包含一個while迴圈,在迴圈中首先fork出一個子程序,然後在子程序中執行shell終端程式(/bin/sh),父程序一直等待子程序退出。當子程序退出後(shell中執行exit命令),父程序將break等待,然後又重新fork子程序並執行shell程式,父程序又等待,如此形成一個迴圈(似乎只有強制關機才可“退出”)。
    上述程式碼我們只關注execve("/bin/sh",argv,envp)函式,其它的先不管。按照最開始的分析,執行這條語句將啟動/bin/sh程式,並給該程式傳遞argv引數和envp環境變數。注意sh也只是一個正常的具有main函式的可執行程式而已,只不過它的功能與我們平常寫的程式稍有不同,它會讀取使用者在命令列輸入的程式名以及相應引數,然後fork出一個子程序去execve這個程式,子程式執行完成後又回到命令列等待輸入,最後一直迴圈這個過程(似乎跟init函式有點類似,不過概念完全不同)。所以execve所做的工作就是將所要執行的程式調入記憶體並執行,準確的說應該是將fork出來的子程序替換成所要執行的程式。那麼這個替換又是如何進行的呢?下面具體分析execve這個系統呼叫。
    首先execve函式將呼叫system_call.s檔案中的sys_execve函式,該函式如下所示:
sys_execve:
	lea EIP(%esp),%eax
	pushl %eax
	call do_execve
	addl $4,%esp
	ret
    可以看到sys_execve內部其實又呼叫的是exec.c檔案中的do_execve函式,但是注意在呼叫do_execve函式前的pushl %eax語句,這條語句其實是給do_execve函式壓入引數,根據C語言引數傳遞規則,do_execve函式的第一個引數即為eax暫存器的值,而eax暫存器中存放的內容為子程序進行系統呼叫時壓入子程序核心態堆疊的eip指標,也即系統呼叫的返回地址,所以這裡可以猜想do_execve函式內部可能會對該eip指標進行修改。
    接下來再來看一下do_execve函式宣告:
int do_execve(unsigned long * eip,long tmp,char * filename, char ** argv, char ** envp)
    我們已經知道了第一個引數的由來以及含義,第二個引數其實是sys_execve函式的返回地址(call sys_execve指令自動產生),這裡並沒有用,接下來的3個引數即為C程式中執行execve系統呼叫所傳遞進來的3個引數,他們分別是:可執行程式的全路徑名稱,引數以及環境變數。
    在具體研究do_execve函式前,再先大概瞭解一下gcc1.3版本編譯出來的a.out格式的可執行檔案結構。linux0.11支援的可執行檔案格式為a.out(現在採用的是ELF檔案格式),每個二進位制的執行檔案的頭部資料都從下面資料結構開始:
struct exec {
  unsigned long a_magic;	/* Use macros N_MAGIC, etc for access */
  unsigned a_text;		/* length of text, in bytes */
  unsigned a_data;		/* length of data, in bytes */
  unsigned a_bss;		/* length of uninitialized data area for file, in bytes */
  unsigned a_syms;		/* length of symbol table data in file, in bytes */
  unsigned a_entry;		/* start address */
  unsigned a_trsize;		/* length of relocation info for text, in bytes */
  unsigned a_drsize;		/* length of relocation info for data, in bytes */
};
    這個結構就基本上描述了該可執行程式的必要資訊,比如:程式碼段,資料段,bss段的長度,應用程式的入口地址a_entry等。所以可以想象,如果將子程序的eip替換為a_entry,就應該可以執行該可執行檔案了,當然還必須要設定堆疊指標以及程序描述符。因此do_execve函式完成的功能應該包含上述的這些操作(設定堆疊,設定程序描述符,替換eip),其中對於堆疊的操作稍微複雜點。堆疊操作的過程其實是將可執行程式的引數和環境變數複製到64M線性空間的末尾128KB處(128KB足夠放很多引數了),並且為這些引數和環境變數建立對應的指標表(因為要訪問這些引數,肯定要定義指向這些引數的指標了),最終的堆疊指標p將指向指標表的第一個元素(因為在進入main函式時需要從堆疊中彈出3個引數:1個整型引數argc和2個字串指標引數argv,envp,所以p應指向整型引數argc)。最終程序的64M線性空間的佈局如下所示:

    從do_execve函式的原始碼可以看到,如我們想象的那樣,do_execve函式最後修改了子程序的eip和esp,這樣函式返回後將執行可執行程式的內容。

eip[0] = ex.a_entry;       /* eip, magic happens :-) */
eip[3] = p;                /* stack pointer */
    至此,關於do_execve函式的功能介紹到這裡,總結一下:對於一個a.out格式的可執行檔案,首先獲取該檔案的inode節點指標,並判斷該檔案的屬性以及執行許可權,然後獲取可執行檔案的a.out頭結構指標ex,根據ex結構判斷可執行檔案是否符合標準,然後對子程序的程序描述符的相關欄位進行賦值,最後修改子程序核心態堆疊中eip和esp並返回。返回後,fork出來的子程序將被可執行程式替代並執行。其中關於執行指令碼檔案相關的程式碼沒有分析(#!),因為這部分程式碼僅在執行檔案為shell指令碼的時候可用。


附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;
	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 (current->egid == 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[1023], *cp, *interp, *i_name, *i_arg;
		unsigned long old_fs;

		strncpy(buf, bh->b_data+2, 1022);
		brelse(bh);
		iput(inode);
		buf[1022] = '\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 */
	if (current->executable)
		iput(current->executable);
	current->executable = inode;
	for (i=0 ; i<32 ; i++)
		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)-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->euid = e_uid;
	current->egid = e_gid;
	i = ex.a_text+ex.a_data;
	while (i&0xfff)
		put_fs_byte(0,(char *) (i++));
	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);
}