Posts Tagged ‘系统调用’

exec族函数

14 3 月, 2011

通常我们使用了fork函数创建了一个子进程后,子进程会调用exec族函数执行另外一个程序。由于fork函数新建的子进程和父进程几乎一模一样(不过,后来引入了写时复制技术避免了这一点),因此需要使用exec函数使得子进程执行一个新的可执行程序。

我们之所以称exec为族函数是因为它有6种不同的形式,主要区别体现在参数上:

#include <unistd.h>;
extern char **environ;
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg,
             ..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *filename, char *const argv[],
              char *const envp[]);

总体来说,exec族函数的参数分为三类:文件路径,命令行参数,环境变量。不论那个exec函数,最终都是将上述三类参数传递给可执行程序中的main函数。这里有一个新的概念,即环境变量,它定义了用户的工作环境,包括用户的主目录,终端的类型,当前用户,当前目录等信息。事实上,main函数的完整形式应该是:

int main(int argc, char *argv[], char **envp);

通常我们并没有显示指定环境变量,这是因为main函数每次都默认使用系统自定义的全局变量environ。另外,通过在终端输入env命令,也可查看当前的所有环境变量信息。可以看到,main函数中的argv[]和envp与exec函数的参数有对应关系。

初次接触exec族函数,易被这6种不同的调用形式搞得混乱。下面我们将依次从上述三个参数的角度来区分这些函数的细微区别。

1.可执行文件的路径

以p结尾的exec函数可以直接传递可执行程序的文件名,此时这类函数会自动在PATH环境变量所指定的目录下查找这个可执行文件;非p结尾的exec函数则必须显示指定可执行程序的完整路径;

2.命令行参数

以execl开始的exec函数必须以列表的形式传递所有的命令行参数,并且最终以NULL作为命令行参数的结束;而以execv开始的exec函数则以字符串指针数组的形式传递所有命令行参数;

3.环境变量

以e结尾的exec函数必须显示指定环境变量;而非p结尾的exec函数则使用默认的环境变量,也就是main函数中的环境变量;

虽然exec函数有6种不同的调用形式,但是每个函数实现的功能都是一样的,即完成新的可执行程序的装入。下面的这个程序很好的演示了不同exec函数的使用方法:

#include < unistd.h >
#include < stdio.h >

int main()
{
	char *envp[]={
		"PATH = /tmp",
	        "USER = edsionte",
		NULL
	};

	char *argv_execv[] = {"echo", "excuted by execv", NULL};
	char *argv_execvp[] = {"echo", "executed by execvp", NULL};
	char *argv_execve[] = {"env", NULL};

	if (fork() == 0)
		if(execl("/bin/echo", "echo", "executed by execl", NULL) < 0)
			perror("Err on execl");
	if(fork() == 0)
		if(execlp("echo", "echo", "executed by execlp", NULL) < 0)
			perror("Err on execlp");
	if(fork() == 0)
		if(execle("/usr/bin/env", "env", NULL, envp) < 0)
			perror("Err on execle");
	if(fork() == 0)
		if(execv("/bin/echo", argv_execv) < 0)
			perror("Err on execv");
	if(fork() == 0)
		if(execvp("echo", argv_execvp) < 0)
			perror("Err on execvp");
	if(fork() == 0)
		if(execve("/usr/bin/env", argv_execve, envp) < 0)
			perror("Err on execve");

	printf("goodbye!\n");
	return 0;
}
//结果:
edsionte@edsionte-desktop:~/edsionte$ ./sys_process
goodbye!
edsionte@edsionte-desktop:~/edsionte$ executed by execl
excuted by execv
executed by execvp
executed by execlp
PATH = /tmp
USER = edsionte
PATH = /tmp
USER = edsionte

这个程序分别使用不同的exec函数调用了echo或env命令,并根据所传递的不同参数显示不同结果。该程序可能每次运行后的显示顺序都有所不同,这是进程的并发性所导致的。

值得注意的是,我们通常所说的命令行参数是以argv数组的第一个元素开始的,而第0号元素是可执行程序的名称。以第一个if语句为例,execl函数为/bin目录下的echo程序传递了两个参数:”echo”和”executed by execl”。由于我们通过execl函数已经装入了/bin/echo程序,因此在调用execl函数时,第一个参数”echo”可以替换成任意字符串,但是这并不代表就不需要传递该参数。

我们也可以让exec函数去执行我们自己编写的可执行程序。下面这个例子很好的演示了父进程和子进程的关系:

/*
 *Author: edsionte
 *Email:  edsionte@gmail.com
 *Time:   2011/03/14
*/

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

int main()
{
	int pid;
	int val = 0;

	pid = fork();
	if (pid == 0) {
		//child proces;
		printf("[Child %d] I will be a new process!\n", getpid());
		if (execl("./sleeping","first arg", "20", NULL) == -1) {
			printf("execl error!\n");
			exit(1);
		}
		printf("[Child %d] I never go here!\n", getpid());
	} else if (pid > 0) {
		printf("[Parent %d] I am running!\n", getpid());
		/*
		wait(&val);
		printf("[Parent %d] All my child were leaving..\n", getpid());
		*/
	} else {
		printf("fork error!\n");
		exit(1);
	}

	printf("[process %d] I am leaving..\n", getpid());
	return 0;
}
//sleeping.c程序
#include < stdio.h >
#include < unistd.h >

int main(int argc, char** argv)
{
	int sec;

	sec = atoi(argv[1]);
	while (sec != 0) {
		sleep(1);
		printf("[New Child %d] I am running and my parent is %d\n", getpid(), getppid());
		sec--;
	}

	return 0;
}

该程序使用了与进程有关的四个最基本的系统调用函数:fork(),exec(),wait()和exit()。对于该程序的具体分析如下:

1.父进程通过fork函数创建子进程,然后父进程打印自己的pid;

2.子进程首先打印自己的pid,再通过execve函数装入可执行程序sleeping,并通过execve函数向sleeping传递了一个参数“20”;

3.在sleeping程序中,将主函数中传递的参数作为计数器,并依次循环睡眠20s。并且循环打印当前的sleeping进程的pid以及其父进程pid;

4.由于父进程先于子进程退出,则sleeping进程成为孤儿进程,因此在sleeping打印的父进程pid为1;如果去掉程序中的注释部分,则父进程等待子进程退出后再退出,那么sleeping所打印的父进程pid即为父进程的pid;

5.如果子进程执行exec函数成功,则主程序中最后一条语句就不会被子进程执行。说明子进程在执行了exec函数后就与父进程完全脱离。另外,在子进程执行过程中也可以通过ps命令查看当前的进程存在情况;

参考:

1.http://www.ibm.com/developerworks/cn/linux/kernel/syscall/part3/

2.Linux C编程实战;童永清 著;人民邮电出版社;

虚拟映射和mmap()

12 1 月, 2011

虚存映射

我们知道,程序是存储在磁盘上到静态文件;进程是对程序到一次运行过程。在进程开始运行时,进程的代码和数据等内容必须装入到进程用户空间到适当区域。这些区域也就是所谓的代码段和数据段等,而被装入的数据和代码等内容被称为进程的可执行映像。从上面都描述中可以发现,进程在运行时并不是将程序一下子就装入到物理内存,而只是将程序装入到进程的用户空间,这个装入的过程称为虚存映射。

一个源程序在成为可执行文件的过程中会经历预处理、编译、汇编和链接四个阶段。因此,进程要成功运行不仅要在其用户空间装入进程映像,也要装入该进程所用到到函数库以及链接程序等。所以,一个进程到用户空间就被分为若干个内存区域。linux使用mm_struct结构来描述一个进程到用户地址空间,使用vm_area_struct结构来描述进程地址空间中的一个内存区域。因此,一个vm_area_struct结构可能代表进程到数据段,也可能代表链接程序到代码段等。

进程的虚存映射所做的只是将磁盘上到文件映射到该进程的用户地址空间,并没有建立虚拟内存到物理内存的映射。当某个可执行映像映射到进程用户空间并开始执行时,只有很少一部分虚拟页被装入了物理内存。在进程后续到执行过程中,如果需要访问到数据并不在物理内存中,则产生一个缺页中断(其实是异常),将所需页从交换区或磁盘中调入物理内存,这个过程即虚拟内存中到请页机制。

进程到虚存区

那么对于一个任意的进程,我们可以通过下面到方法查看其地址空间中到内存区域。

我们先看一个简单的测试程序:

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

int main()
{
	int i=1;
	char *str=NULL;
	printf("hello,world!\n");
	str=(char *)malloc(sizeof(char)*1119);

	sleep(1000);

	return 0;
}

这个程序中使用到了malloc函数,因此str变量存储于堆中。我们通过打印/proc/3530/maps文件,即可看到该进程的内存空间划分。其中3530是该进程的id。

edsionte@edsionte-desktop:~$ cat /proc/3530/maps
0014a000-00165000 r-xp 00000000 08:07 398276     /lib/ld-2.11.1.so
00165000-00166000 r--p 0001a000 08:07 398276     /lib/ld-2.11.1.so
00166000-00167000 rw-p 0001b000 08:07 398276     /lib/ld-2.11.1.so
001d8000-0032b000 r-xp 00000000 08:07 421931     /lib/tls/i686/cmov/libc-2.11.1.so
0032b000-0032c000 ---p 00153000 08:07 421931     /lib/tls/i686/cmov/libc-2.11.1.so
0032c000-0032e000 r--p 00153000 08:07 421931     /lib/tls/i686/cmov/libc-2.11.1.so
0032e000-0032f000 rw-p 00155000 08:07 421931     /lib/tls/i686/cmov/libc-2.11.1.so
0032f000-00332000 rw-p 00000000 00:00 0
00441000-00442000 r-xp 00000000 00:00 0          [vdso]
08048000-08049000 r-xp 00000000 08:09 326401     /home/edsionte/test
08049000-0804a000 r--p 00000000 08:09 326401     /home/edsionte/test
0804a000-0804b000 rw-p 00001000 08:09 326401     /home/edsionte/test
08958000-08979000 rw-p 00000000 00:00 0          [heap]
b78ce000-b78cf000 rw-p 00000000 00:00 0
b78dd000-b78e0000 rw-p 00000000 00:00 0
bfa6a000-bfa7f000 rw-p 00000000 00:00 0          [stack]

每一行信息依次显示的内容为内存区域其实地址-终止地址,访问权限,偏移量,主设备号:次设备号,inode,文件。

上面的信息不但包含了test可执行对象的各内存区域,而且还分别显示了 /lib/ld-2.11.1.so(动态连接程序)文件和/lib/tls/i686/cmov/libc-2.11.1.so(C库)文件的内存区域信息。

我们从某个内存区域的访问权限上可以大致判断该区域的类型。各个属性符号的意义为:r-read,w-write,x-execute,s-shared,p-private。因此,r-x一般代表程序的代码段,即可读,可执行。rw-可能代表数据段,BSS段和堆栈段等,即可读,可写。堆栈段从行信息的文件名就可以区分;如果某行信息的文件名为空,那么可能是BSS段。另外,上述test进程共享了内核动态库,所以在00441000-00442000行处文件名显示为vdso(Virtual Dynamic Shared Object)。

mmap系统调用

通过mmap系统调用可以在进程到用户空间中创建一个新到虚存区。该系统调用到原型如下:

#include
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

该函数可以将以打开的文件映射到进程用户空间到一片内存区上,执行成功后,该函数返回这段映射区到首地址。用户得到这片虚存的首地址后,就可以像访问内存那样访问文件。

该系统调用的参数说明如下:

addr:映射到用户地址空间到起始地址;
length:映射区以字节为单位到长度;
prot:对映射区到访问模式。包括PROT_EXEC(可执行),PROT_READ (可读),PROT_WRITE(可写),PROT_NONE(文件不可访问)。这个访问模式不能超过所映射文件到打开模式。比如被映射的文件打开模式为只读,那么此处到访问模式不能是可读写的。
flags:这个字段比较灵活,不同到标志有不同的功能,具体如下:
MAP_SHARED:创建一个可被子进程共享的映射区;
MAP_PRIVATE:创建一个“写实复制”的映射区;
MAP_ANONYMOUS:创建一个匿名到映射区,该虚存区与进程无关;
fd:所要映射到进程用户空间的文件描述符,该文件必须为以打开的文件;
offset:文件的起始映射偏移量;

mmap()举例

在该程序中,首先以只读方式打开文件test.c,再通过该文件返回到文件描述符和mmap函数将test.c文件映射到当前进程到用户地址空间中。成功执行mmap函数后,buf被赋值为所映射的虚存区的首地址。注意,mmap函数返回的是void型指针,而buf是char型指针。将mmap返回值赋值给buf变量时,自动将void*转化为char*型。

最后,就像平常我们使用一个char型指针变量那样,依次打印出buf中到数据。

#include < stdio.h >
#include < sys/mman.h >
#include < fcntl.h >
int main()
{
	int i,fd;
	char *buf = NULL;

	fd = open("./test.c", O_RDONLY);
	if(fd < 0)
	{
		printf("open error\n");
		return -1;
	}

	buf = mmap(NULL, 12, PROT_READ, MAP_PRIVATE ,fd, 0);
	for(i = 0;i < 12;i++)
	{
		printf("%c",buf[i]);
	}
	printf("\n");

	return 0;
}

try一下!

系统调用的执行过程

8 12 月, 2010

当用户态的进程调用一个系统调用时,CPU切换到内核态并开始执行一个内核函数。在X86体系中,可以通过两种不同的方式进入系统调用:执行int $0x80汇编命令和执行sysenter汇编命令。后者是Intel在PentiumII中引入的指令,内核从2.6版本开始支持这条命令。本文将集中讨论以int $0x80方式进入系统调用的过程。

通过int $0x80方式调用系统调用实际上是用户进程产生一个中断向量号为0x80的软中断。当用户态进程发出int $0x80指令时,CPU将从用户态切换到内核态并开始执行system_call()。这个函数是通过汇编命令来实现的,它是0x80号软中断对应的中断处理程序。对于所有系统调用来说,它们都必须先进入system_call(),也就是所谓的系统调用处理程序。再通过系统调用号跳转到具体的系统调用服务例程处。

在该函数执行之前,CPU控制单元已经将eflags、cs、eip、ss和esp寄存器的值自动保存到该进程对应的内核栈中。随之,在system_call内部首先将存储在eax寄存器中的系统调用号压入栈中。接着执行SAVE_ALL宏。该宏在栈中保存接下来的系统调用可能要用到的所有CPU寄存器。

 
/linux/arch/i386/kernel/entry.S
 241ENTRY(system_call)
 242        pushl %eax                      # save orig_eax
 243        SAVE_ALL

通过GET_THREAD_INFO宏获得当前进程的thread_inof结构的地址;再检测当前进程是否被其他进程所跟踪,也就是thread_inof结构中flag字段的_TIF_SYSCALL_TRACE或_TIF_SYSCALL_AUDIT被置1。如果发生被跟踪的情况则转向相应的处理命令处。

 
 244        GET_THREAD_INFO(%ebp)
 245        # system call tracing in operation
 246        testb $(_TIF_SYSCALL_TRACE|_TIF_SYSCALL_AUDIT),TI_flags(%ebp)
 247        jnz syscall_trace_entry

接着,对用户态进程传递过来的系统调用号的合法性进行检查。如果不合法则跳入到syscall_badsys标记的命令处。

 
 248        cmpl $(nr_syscalls), %eax
 249        jae syscall_badsys

如果系统调用好合法,则跳入相应系统调用号所对应的服务例程当中,也就是在sys_call_table表中找到了相应的函数入口点。由于sys_call_table表的表项占4字节,因此获得服务例程指针的具体方法是将由eax保存的系统调用号乘以4再与sys_call_table表的基址相加。

当系统调用服务例程结束时,从eax寄存器中获得当前进程的的返回值,并把这个返回值存放在曾保存用户态eax寄存器值的那个栈单元的位置上。这样,用户态进程就可以在eax寄存器中找到系统调用的返回码。

 250syscall_call:
 251        call *sys_call_table(,%eax,4)
 252        movl %eax,EAX(%esp)             # store the return value

至此,用户进程进入系统调用的过程大致分析完毕。

fork系统调用分析(1)-准备工作

7 12 月, 2010

接下来的几篇文章将会分析fork系统调用在内核中的实现,以作为本学期linux操作系统课程总的分析报告。本文所采用的源码均为v2.6.11。和最新内核相比,do_fork函数的变化较大,而核心函数copy_process则几乎保持不变。

操作系统需要一种机制用于创建新进程,fork()就是Linux或Unix提供给程序员用于创建进程的方法。fork函数的相关信息如下:

通常,我们程序中直接使用的fork函数是将fork系统调用封装之后而产生的。通过上面的表格,我们知道fork函数用于创建新的进程,所创建的进程称为当前进程的子进程,而当前进程也随之称为子进程的父进程。通常可以通过父子进程不同的返回值来区分父子进程,并使其执行不同功能的代码。

那么,用户态下的fork函数是如何调用fork系统调用的?内核中是如何创建子进程的?父子进程的返回值是如何产生的?这是本文所重点讨论的。

进程描述符

进程是操作系统中一个重要的基本概念。通常我们认为进程是程序的一次执行过程。为了描述和控制进程的运行,操作系统为每个进程定义了一个数据结构,即进程控制块(Process Control Block,PCB)。我们通常所说的进程实体包含程序段,数据段和PCB三部分,PCB在进程实体中占据重要的地位。所谓的创建进程,实质上就是创建PCB的过程;而撤销进程,实质上也就是对PCB的撤销。

上述内容是我们在操作系统原理课上所学习到的。在Linux内核中,PCB对应着一个具体的结构体——task_struct,也就是所谓的进程描述符(process descriptor)。该数据结构中包含了与一个进程相关的所有信息,比如包含众多描述进程属性的字段,以及指向其他与进程相关的结构体的指针。因此,进程描述符内部是比较复杂的。我们可以通过图1大致的了解进程描述符的结构。

图1(图片来自ULK)

可以看到,进程描述符中有指向mm_struct结构体的指针mm,这个结构体是对该进程用户空间的描述;也有指向fs_struct结构体的指针fs,这个结构体是对进程当前所在目录的描述;也有指向files_struct结构体的指针files,这个结构体是对该进程已打开的所有文件进行描述;另外还有一个小型的进程描述符(low-level information)——thread_info。在这个结构体中,也有指向该进程描述符的指针task。因此,这两个结构体是相互关联的。关于thread_info结构的详细说明可以参考本系列文章的后续分析。

与存储在磁盘上的程序相比,我们认为是进程是动态的。这是因为一个进程在其“一生”中可能处于不同的状态。因此,在进程描述符中,使用state字段对该进程当前所处的状态进行描述。在内核中,使用一组宏来描述进程可能所处的状态。这些宏之间是互斥的,也就是说进程一次最多只能使用一个宏。因为,进程在某一刻只可能处于一种状态。下面对本文中所涉及到的进程状态进行简单的描述:

可运行状态(TASK_RUNNING)
如果进程正在CPU上执行或者正在等待被调度程序所调度,那么它的状态就处于可运行状态。

暂停状态(TASK_STOPPED)
进程的执行被暂定,也就是我们常说的阻塞状态和等待状态。当进程接收到SIGSTOP、SIGSTP等信号时,就进入该状态。

跟踪状态(TASK_TRACED)
当一个进程被另一个进程跟踪监控时,这个进程就处于该状态。最常见的场景就是我们调试一个程序,被调试的程序就处于此状态。

fork系统调用

在用户态下,使用fork()创建一个进程对我们来说已经不再陌生。除了这个函数,一个新进程的诞生还可以分别通过vfork()和clone()。fork、vfork和clone三个API函数均由C库提供,它们分别在C库中封装了与其同名的系统调用fork(),vfork()和clone()。API所封装的系统调用对编程者是隐藏的,编程者只需知道如何使用这些API即可。

上述三个系统调用所对应的系统调用号在linux/include/asm-i386/unistd.h中定义如下:

   #define __NR_restart_syscall      0
   #define __NR_exit                 1
   #define __NR_fork                 2
   …… ……
   #define __NR_clone              120
   …… ……
   #define __NR_vfork              190
   …… ……

传统的创建一个新进程的方式是子进程拷贝父进程所有资源,这无疑使得进程的创建效率低,因为子进程需要拷贝父进程的整个地址空间。更糟糕的是,如果子进程创建后又立马去执行exec族函数,那么刚刚才从父进程那里拷贝的地址空间又要被清除以便装入新的进程映像。

为了解决这个问题,内核中提供了上述三种不同的系统调用。

1. 内核采用写时复制技术对传统的fork函数进行了下面的优化。即子进程创建后,父子以只读的方式共享父进程的资源(并不包括父进程的页表项)。当子进程需要修改进程地址空间的某一页时,才为子进程复制该页。采用这样的技术可以避免对父进程中某些数据不必要的复制。

2. 使用vfork函数创建的子进程会完全共享父进程的地址空间,甚至是父进程的页表项。父子进程任意一方对任何数据的修改使得另一方都可以感知到。为了使得双方不受这种影响,vfork函数创建了子进程后,父进程便被阻塞直至子进程调用了exec()或exit()。由于现在fork函数引入了写时复制技术,在不考虑复制父进程页表项的情况下,vfork函数几乎不会被使用。

3. clone函数创建子进程时灵活度比较大,因为它可以通过传递不同的参数来选择性的复制父进程的资源。具体参数可参见表1

就像一开始所分析的那样,用户程序并不直接使用系统调用,而是通过C库中的API。而系统调用在内核中也并不是直接实现的,而是通过调用各自对应的服务例程。系统调用fork、vfork和clone在内核中对应的服务例程分别为sys_fork(),sys_vfork()和sys_clone()。因此,想要了解fork等系统调用的详细执行过程,就必须查看它们所对应的内核函数(也就是服务例程)是如何实现的。上述三个系统调用对应的服务例程分别定义在linux/arch/i386/kernel/process.c 中,具体如下:

asmlinkage int sys_fork(struct pt_regs regs)
{
	return do_fork(SIGCHLD, regs.esp, ®s, 0, NULL, NULL);
}

asmlinkage int sys_clone(struct pt_regs regs)
{
	unsigned long clone_flags;
	unsigned long newsp;
	int __user *parent_tidptr, *child_tidptr;

	clone_flags = regs.ebx;
	newsp = regs.ecx;
	parent_tidptr = (int __user *)regs.edx;
	child_tidptr = (int __user *)regs.edi;
	if (!newsp)
		newsp = regs.esp;
	return do_fork(clone_flags, newsp, ®s, 0, parent_tidptr, child_tidptr);
}

asmlinkage int sys_vfork(struct pt_regs regs)
{
	return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs.esp, ®s, 0, NULL, NULL);
}

可以看到do_fork()均被上述三个服务例程调用。而在do_fork()内部又调用了copy_process(),因此我们可以通过图2来理解上述的调用关系。

 

                                                                     图2
从上面的分析中,我们已经明确了do_fork()和copy_process()是本文的主要分析对象。但是在这之前,我们有必要先分析一下用户进程进入系统调用的过程。详见下文。

在内核中添加系统调用

1 12 月, 2010

如何往内核中添加自己写的系统调用?其实步骤非常简单:

1.编写一个系统调用;
2.在系统调用表末尾加入一个新表项;
3.在< asm/unistd.h >中添加一个新的系统调用号;
4.重新编译内核;

上述工作完成后,就可以在用户程序中使用自己所编写的系统调用了。接下来,我们将逐步分析如何上述步骤。

1.编写系统调用

我们将要实现一个获得当前进程pid的的系统调用。对于一个进程,我们可以直接通过current->pid来获得。为了使得这个系统调用同样适用于一个线程,我们使用current->tgid。这么做的原因是同一个线程组内的所有线程TGID均相同;而一个进程的PID和TGID是相同的。

asmlinkage long sys_mygetpid(void)
{
	return current->tgid;
}

与普通函数不同的是,这个系统调用的函数名前有asmlinkage修饰符。这个修饰符使得GCC编译器从堆栈中取该函数的参数而不是寄存器中。另外,系统调用函数的命名规则都是sys_XXX的形式。

接下来,我们的要做的是将这个函数放于何处。一种方法是,将上述函数放于/kernel/下的某个文件中;另一种方式是,将这个函数单独存放在/kernel/下的一个新建的.c文件中。不管何种方法,所做的目的都是为了在重新编译内核时将上述我们所编写的系统调用函数编译进内核。

2.在系统调用表中添加新的表项

linux中为每一个系统调用都分配一个系统调用号。也就是说,每一个系统调用号都唯一的对应着一个系统调用。内核中使用系统调用表来记录所有已经注册过的系统调用,这个系统调用表存储在sys_call_table中。在yoursource/arch/x86/kernel/syscall_table_32.S中可以看到系统系统调用表。

我们所要做的就是在该表的末尾添加我们刚编写的系统调用:.long sys_getpid。我们并不需要显式的指定系统调用号,从0开始算起,我们所编写的函数的系统调用号为341。

   ENTRY(sys_call_table)
          .long sys_restart_syscall       /* 0 - old "setup()" system call, used for restarting */
          .long sys_exit
          .long ptregs_fork
          .long sys_read
          .long sys_write
          .long sys_open          /* 5 */
             …… ……
         .long sys_perf_event_open
         .long sys_recvmmsg
         .long sys_fanotify_init
         .long sys_fanotify_mark
         .long sys_prlimit64             /* 340 */
         .long sys_mygetpid

3.在< asm/unistd.h >中添加一个新的系统调用号

上一步,在系统调用表中添加新的表项是为了将系统调用号和系统调用关联起来。现在,我们需要在unistd.h文件中添加具体的系统调用号,这样使得系统可以根据这个系统调用号在系统调用表中查找具体的系统调用。

当用户进程调用一个系统调用时,其实是通过一个中断号为128的软中断来通知内核的。此时,内核会由用户态切换到内核态转而去执行这个软中断对应的中断处理程序。而这个中断处理程序恰好就是系统调用处理程序。也就是说,任何系统调用都会引发CPU去执行这个系统调用处理程序。因此,必须通过系统调用号来识别不同的系统调用。系统调用号通常会存储在eax寄存器中。

现在我们就在yoursource/arch/x86/include/asm/unistd_32.h文件中添加新的系统调用号。在上一步我们所添加的sys_mygetpid系统调用对应的编号为341,因此我们在该文件的末尾添加下面的语句:#define __NR_mygetpid 341。注意这里的宏命名规则,以__NR_开头。

 #define __NR_restart_syscall      0
 #define __NR_exit                 1
 #define __NR_fork                 2
 #define __NR_read                 3
 #define __NR_write                4
        …………
 #define __NR_fanotify_mark      339
 #define __NR_prlimit64          340
 #define __NR_mygetpid         341

4.编译内核

如果上述三个步骤都完成后,那么接下来重新编译内核即可。具体可参见这里

5.编写用户态的程序

#include < linux/unistd.h >

_syscall0(int,mygetpid)

int main()
{
	printf("The current process's pid is %d\n",mygetpid());
	return 0;
}

上述用户程序可能与我们平日里所写的稍有不同。主要区别是增加了_syscall0(int,mygetpid)这个宏。因为我们现在直接在程序中调用系统调用,而我们平时则是通过调用C库中的API来间接调用系统调用。在unistd.h文件中有_syscallN()宏的定义,这里的N可取0~6。N代表的是需要传递给系统调用的参数个数。由于mygetpid系统调用需传递的参数个数为0,因此选取_syscall0。另外,这组宏的内部参数分布有如下特点:第一个参数是系统调用返回值的类型,第二个参数是系统调用函数的名称,接下来的参数按照系统调用参数的次序依次是参数类型和参数名称。对于每个宏来说,都有2+2*N个参数。

OK,上述方法即可以将我们自己编写的系统调用函数加入到内核。try!

windows 7 ultimate product key

windows 7 ultimate product key

winrar download free

winrar download free

winzip registration code

winzip registration code

winzip free download

winzip free download

winzip activation code

winzip activation code

windows 7 key generator

windows 7 key generator

winzip freeware

winzip freeware

winzip free download full version

winzip free download full version

free winrar download

free winrar download

free winrar

free winrar

windows 7 crack

windows 7 crack

windows xp product key

windows xp product key

windows 7 activation crack

windows7 activation crack

free winzip

free winzip

winrar free download

winrar free download

winrar free

winrar free

download winrar free

download winrar free

windows 7 product key

windows 7 product key