操作系统实践-3 添加系统调用

操作系统实践-3 添加系统调用

1. 在Linux 0.11中添加系统调用

1.1 在kernel下编写所添加系统调用的源码

目标:在Linux中添加两个系统调用set_osname和get_osname,原型如下

int set_osname(const char * name,int size);//将系统名设为name
int get_osname(char * name);//将系统名写入name中 

在kernel文件夹中新建一个osname.c文件
osname.c
代码如下,注意这里声明的是sys_set_osnamesys_get_osname

#include <errno.h> //错误码
#include <asm/segment.h> //要用get_fs_byte和push_fs_byte
#define OSNAME_MAX 101 //定义最大长度
char osname[OSNAME_MAX] = "You didn't set any osname"; //存储OS_NAME
int sys_set_osname(const char * name,int size){
	if(size > OSNAME_MAX - 1){
		errno = EINVAL;
		return ERROR;
	}
	//memset(osname,sizeof(osname),'\0');
	int i = 0;
	for(i = 0;i < OSNAME_MAX;i++){
		osname[i] = '\0';
	}
	char c;
	for(i = 0;i < size;i++){
		c = get_fs_byte(&name[i]);//获取用户段的数据,为什么这样做在第二节
		osname[i] = c;
	}
	return 0;
}
int sys_get_osname(char * name){
	int i = 0;
	char c;
	do{
		c = osname[i];
		put_fs_byte(c,&name[i]);
		i++;
	}while(c != '\0');
	return 0;
}

1.2 在include/linux/sys.h中注册所添加的系统调用

添加两个函数声明

extern int sys_set_osname();
extern int sys_get_osname();

在sys_call_table中添加两项
在这里插入图片描述

1.3 修改kernel/system_call.s中的系统调用数量

将nr_system_calls从72改为74
在这里插入图片描述

1.4 修改include/unistd.h,让用户程序能够调用

添加两行

#define __NR_set_osname 72 //对应sys_set_osname在sys_call_table中的位置
#define __NR_get_osname 73 //

在这里插入图片描述

1.5 修改Makefile文件并编译

OBJS添加一项osname.o
在这里插入图片描述
依赖项添加osname.s osname.o : osname.c 头文件
在这里插入图片描述
执行make命令
在这里插入图片描述

1.6 添加时的注意&补充

  • 一定要注意osname.c中引入的头文件
  • 现在还不能直接调用get_osname和set_osname,还要通过syscall宏才可
  • 我这里在编译好的linux-0.11中的gcc的头文件/usr/include/unistd.h并没有修改过,要手动用vi修改,保持和上面的一致。
  • 执行过程正确无误,return 0,否则return errno.h中的一个数的相反数。

1.6 在Linux-0.11中写使用了这个调用的程序

设置osname的.setname.c

//setname.c
//-------------------这些可以放在一个osname.c文件中--------------------------
#define __LIBRARY__ //因为在unistd.h中有defif __LIBRARY__,表面引入系统调用库
#include <unistd.h> //插入unistd.h,主要是为了用__NR_##name和syscall
_syscall1(int,get_osname,char*,name) //宏展开
_syscall2(int,set_osname,char*,name,int,size) //宏展开
//---------------------------------------------------------------------------

/*
如果懒得去修改unistd.h,可以这样写
#define __LIBRARY__ //因为在unistd.h中有defif __LIBRARY__,表面引入系统调用库
#include <unistd.h> //插入unistd.h,主要是为了用__NR_##name和syscall
#ifndef __NR_set_osname
#define __NR_set_osname 72
#endif
#ifndef __NR_get_osname
#define __NR_get_osname 73
#endif
_syscall1(int,get_osname,char*,name) //宏展开
_syscall2(int,set_osname,char*,name,int,size) //宏展开
*/

#include <stdio.h>
#include <string.h>
int main(int args,char * argv[]){
    set_osname(argv[1],strlen(argv[1]));
    return 0;
}

显示osname的showname.c

//setname.c
//-------------------这些可以放在一个osname.c文件中--------------------------
#define __LIBRARY__ //因为在unistd.h中有defif __LIBRARY__,表面引入系统调用库
#include <unistd.h> //插入unistd.h,主要是为了用__NR_##name和syscall
_syscall1(int,get_osname,char*,name) //宏展开
_syscall2(int,set_osname,char*,name,int,size) //宏展开
//---------------------------------------------------------------------------

/*
如果懒得去修改unistd.h,可以这样写
#define __LIBRARY__ //因为在unistd.h中有defif __LIBRARY__,表面引入系统调用库
#include <unistd.h> //插入unistd.h,主要是为了用__NR_##name和syscall
#ifndef __NR_set_osname
#define __NR_set_osname 72
#endif
#ifndef __NR_get_osname
#define __NR_get_osname 73
#endif
_syscall1(int,get_osname,char*,name) //宏展开
_syscall2(int,set_osname,char*,name,int,size) //宏展开
*/

#include <stdio.h>
#include <string.h>
int main(int args,char * argv[]){
    char str[100];
    get_osname(str);
    printf("%s\n",str);
    return 0;
}

在linux中用gcc编译

gcc -o setname setname.c
gcc -o showname showname.c

测试

./setname lmcismyfather
./showname

在这里插入图片描述

1.7总结

  • 朝系统中添加一个fun()系统调用的步骤如下
    1. 找个位置编写fun.c,其中的函数定义为int sys_fun()
    2. 在include/linux/sys.h中extern int fun();在sys_call_table[]中添加fun
    3. 修改kernel/system_call.s中的系统调用数
    4. 在include/linux/sys.h中注册所添加的系统调用
    5. 修改kernel/system_call.s中的系统调用数量
    6. 修改include/unistd.h,让用户程序能够调用
    7. 修改Makefile文件并编译
  • 使用此fun()系统调用见1.6

2. 系统调用原理

2.1 CPL和DPL

实模式和保护模式,见书《x86汇编语言——从实模式到保护模式》。
CPL(Current privilege level,当前特权级),表示当前指令的特权级。当前指令由CS:IP指示,CPL就存储在CS的最低2位。
eg:jmpi 0,8指令使得CS = 8 = 0000 0000 0000 1000,则当前特权级位0,最高特权级,8在GDT中对应的代码段具有最高特权级。

DPL(Description privilege level,描述符特权级),表示一个目标段的特权级。DPL存储在GDT表或IDT表(中断向量表)中。x86在寻址的时候会自动对比当前特权级和目标描述符的特权级,不符合要求无法寻址。
eg:
当前CS = 0000 0000 0000 0111b = 0x0007,DPL = 11b = 3,当前特权级为3。CS:IP指向的指令为jmpi 0,8。CPU现在GDT表中查到8对应的描述符,发现描述符中的DPL = 0,拒绝访问。

在操作系统初始化GDT时,会把内核所在的内存区域的DPL设为0,用户态的指令设为3。

2.2 系统调用与int 0x80

应用程序通过int 0x80指令进入中断处理程序,用这唯一的入口进入内核。操作系统会利用这个中断处理程序来实施各种检查,让上层应用只能按照操作系统规定的格式来使用系统。
0x80号中断的初始化工作如下:
在sched_init()中展开宏set_system_gate(0x80,&system_call)为set_gate(&idt[0x80],15,3,&system_call)
set_gate函数完成的任务是填写好IDT表中0x80号中断表项,DPL是3,段描述符是0x0008,中断处理函数偏移是system_call
当应用程序调用int 0x80时,当前CPL = 3,CPU到IDT中查找0x80项的DPL,发现DPL = 3,就设置CS:IP为0x80项中的CS:IP,0x80中存储的CS = 8,IP = system_call,此时CS = 8,DPL = 0,进入了内核,可以访问任意内存区域

2.3 应用程序如何使用int 0x80 - 以write为例

write函数的原型为

int write(int fd,char * buf,int count);

write函数库在/lib/write.c中,只有这短短的几行

/*
 *  linux/lib/write.c
 *
 *  (C) 1991  Linus Torvalds
 */

#define __LIBRARY__
#include <unistd.h>

_syscall3(int,write,int,fd,const char *,buf,off_t,count)

第一行

#define __LIBRARY__

表面这是一个库文件,与unistd.h中的

#ifdef __LIBRARY__

相呼应。
_syscall3是unistd.h中定义的一个宏。unistd.h中除了这个宏之外还有_syscall1和_syscall2,_syscalln中的n表面此系统调用的参数个数。
_syscall2的宏定义如下

#define _syscall2(type,name,atype,a,btype,b) \
type name(atype a,btype b) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
	: "=a" (__res) \
	: "0" (__NR_##name),"b" ((long)(a)),"c" ((long)(b))); \
if (__res >= 0) \
	return (type) __res; \
errno = -__res; \
return -1; \
}

有一段内嵌汇编。汇编的意义如下:

  • 输入:
    1. eax,系统调用号,这里是__NR_write。
    2. ebx,第一个参数,这里是fd。
    3. ecx,第二个参数,这里是count
  • 输出:
    1. eax存入_res中。
  • 代码:
    1. int 0x80
      接下来就进入了内核态,int 0x80所指向的中断处理函数为kernel/system_call.s中的system_call
_system_call:
	cmpl $nr_system_calls-1,%eax #确保eax是正确的系统调用号,这也是为什么要修改nr_system_calls
	ja bad_sys_call #呼应cmpl
	push %ds
	push %es
	push %fs #把段寄存器压入栈,此时这三个寄存器还为用户的数据段
	pushl %edx #段偏移压入栈,待会还要返回到用户程序执行的
	pushl %ecx		# 把参数ecx压入栈
	pushl %ebx		# 把参数ebx压入栈
	movl $0x10,%edx		# set up ds,es to kernel space
	mov %dx,%ds
	mov %dx,%es
	movl $0x17,%edx		# fs points to local data space
	mov %dx,%fs  #根据fs在GDT中寻址可以查到用户态的数据地址,用以数据交换
	call _sys_call_table(,%eax,4) #调用_sys_call_table[eax],_sys_call_table就是linux/sys.h中的存储系统调用函数的数组
	pushl %eax #返回的eax
	movl _current,%eax
	cmpl $0,state(%eax)		# state,错误处理
	jne reschedule
	cmpl $0,counter(%eax)		# counter,时间片
	je reschedule

在这里插入图片描述
重点是call _sys_call_table(,%eax,4)这一句,即执行_sys_call_table[eax]
,eax在是unistd中宏_syscall2传递过去的##name,这里是write。所以在unistd中必然有__NR_write的定义,在/include/linux/sys.h中必有:

  1. extern int sys_write()
  2. _sys_call_table中的下标为__NR_write对应的函数即为sys_write

2.5 内核段数据与用户段数据的交换

程序一旦进入system_call,ds,es会指向内核,fs会指向用户。用get_fs_byte和put_fs_byte来完成数据访问修改。
具体的定义在/include/asm/segment.h中

static inline unsigned char get_fs_byte(const char * addr)
{
	unsigned register char _v;

	__asm__ ("movb %%fs:%1,%0":"=r" (_v):"m" (*addr));
	return _v;
}

static inline unsigned short get_fs_word(const unsigned short *addr)
{
	unsigned short _v;

	__asm__ ("movw %%fs:%1,%0":"=r" (_v):"m" (*addr));
	return _v;
}

static inline unsigned long get_fs_long(const unsigned long *addr)
{
	unsigned long _v;

	__asm__ ("movl %%fs:%1,%0":"=r" (_v):"m" (*addr)); \
	return _v;
}

static inline void put_fs_byte(char val,char *addr)
{
__asm__ ("movb %0,%%fs:%1"::"r" (val),"m" (*addr));
}

static inline void put_fs_word(short val,short * addr)
{
__asm__ ("movw %0,%%fs:%1"::"r" (val),"m" (*addr));
}

static inline void put_fs_long(unsigned long val,unsigned long * addr)
{
__asm__ ("movl %0,%%fs:%1"::"r" (val),"m" (*addr));
}

/*
 * Someone who knows GNU asm better than I should double check the followig.
 * It seems to work, but I don't know if I'm doing something subtly wrong.
 * --- TYT, 11/24/91
 * [ nothing wrong here, Linus ]
 */

static inline unsigned long get_fs() 
{
	unsigned short _v;
	__asm__("mov %%fs,%%ax":"=a" (_v):);
	return _v;
}

static inline unsigned long get_ds() 
{
	unsigned short _v;
	__asm__("mov %%ds,%%ax":"=a" (_v):);
	return _v;
}

static inline void set_fs(unsigned long val)
{
	__asm__("mov %0,%%fs"::"a" ((unsigned short) val));
}


2.6 总结

用户应用程序调用库函数的流程如下

  1. APP调用一个库函数API
  2. API利用syscall宏把调用号存入eax,通过中断0x80使系统进入内核态
  3. 内核中的中断处理函数system_call根据eax中的调用号,调用相应的内核函数
  4. 系统调用完成后,把返回值放入eax,返回到中断处理函数
  5. 中断处理函数返回至API,API把eax返回给APP

3. 重要的系统调用

3.1 fork

fork()系统调用的函数原型为

int fork();

调用该函数的进程会再创建一个进程,新创建的进程是原进程的子进程。两个进程都从fork()这个地方继续往下执行,并且执行”同样”的代码。但是父进程执行fork()会返回子进程的ID,而子进程调用fork()会返回0。
fork的含义是叉子,这是因为fork的工作效果很像一个叉子,典型的fork用法如图。
在这里插入图片描述

3.2 exec

exec()再当前进程中执行一段新程序,进程的PID保持不变。fork()创建了一个进程的壳子,并且将父进程的代码装在这个壳子中执行,而exec()是用一个磁盘上的可执行程序替换了壳子里原有的内容。
exec()函数分为以下几种

int execv(const char * pathname, char ** argv);
int execve(const char * filename, char ** argv, char ** envp);
int execvp(const char * file, char ** argv);
int execl(const char * pathname, char * arg0, ...);
int execle(const char * pathname, char * arg0, ...);
int execlp(const char * file, char * arg0, ...);

其中execv表示以数组形式传递参数,execl表示以列举方式传递参数。带e表示是否可以使用extern char **environ来引入环境变量。

3.3 exit

函数原型为

int exit(int status);

程序可以显示的调用exit来终止自己。操作系统再编译程序的main函数时会塞入一个exit在" } "之前。

3.4 open、read、write

函数原型为

//在/include/linux/sys.h中
int open(const char * filename, int flag, ...);
int read(int fildes, char * buf, off_t count);
int write(int fildes, const char * buf, off_t count);
int close(int fildes);

4. 总结

  1. 如何添加系统调用
  2. 系统调用的原理
  3. 常见的系统调用
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值