一,接口
1.接口的定义
用户使用计算机的三种方式:
1.命令行:即通过命令程序,linux系统中常用此种方式
2.图形按钮:通过鼠标点击等操作实现对计算机的操控。windows系统在这方面做的就非常优秀。这种方式通过消息框架程序和消息处理程序实现
3.应用程序
不管采用何种方式,我们都需要让操作系统和应用程序之间建立联系。如何建立连接 ? 操作系统接口
接口其实是一种抽象,比如插排,它将内部的电路全部封装起来,只提供两个插口,用电设备插上就能用;不用管插座内部是如何实现的。
操作系统接口也具有连接两个东西、屏蔽细节、方便用户使用的特点。它连接上层应用软件和底层硬件,屏蔽细节,用户直接通过程序(应用软件)使用计算机,方便用户使用。操作系统的接口其实就是一个个函数,知道它的功能然后直接调用就行,而不用管它内核里面是怎么实现的,因为这个函数是系统调用的,所以也称为系统调用。比如:write()、read()等等
2.接口的分类
操作系统接口的功能就是提供一个用户使用系统的界面。根据服务对象的不同,操作系统的接口可以划分为两类:一是供用户使用的用户级接口,二是供程序使用的程序级接口。
(1)用户接口
用户接口就是操作系统向用户提供的使用界面。分为脱机接口与交互式接口两种
(2) 程序接口
程序级接口是为程序访问系统资源而提供的,它由一组系统调用组成。系统调用(System Call)可以看作是由操作系统内核提供的一组广义指令。程序员在设计程序时,凡涉及到系统资源访问的操作,如文件读/写、数据输入/输出、网络传输等,都必须通过系统调用来实现。所以说,系统调用是操作系统提供给应用程序的唯一接口。
从层次上来看,用户接口属于高层接口,是用户与操作系统之间的接口。而程序接口则是低级接口,是任何核外程序(包括应用程序和系统程序)与操作系统内核之间的接口。用户接口的功能最终是通过程序接口来实现的。
二,系统调用的实现
思考下面这个问题:
========================
= main{} =
= {whoami();} =
=========================
=whoami() =
={printf(100,8)} =
="xxxxx" =
=========================
如图,上面一层是用户程序调用whoami,一个字符串“xxxxx”放在下一层操作系统内核中(系统引导时载入),取出来打印。
那用户为什么不可以直接从操作系统内核中那这个数据,直接显示 出来呢?他们不是都在内存中吗?
答案当然是否定的,因为操作系统不允许我们随意调用数据,不能随意jmp,否则我们机器不小心被病毒感染,就可能被盗取root密码并且可以修改它。
所以如何实现这层“不随意的调用”呢?
将内核程序和用户程序隔离
为了确保系统的安全性,计算机硬件通过一些方法来定义此时计算机是工作在内核态和用户态。
内核态可以访问用户态信息,而用户态不能访问内核态信息。
CPL(Current Privilege Level)寄存器表示当前程序执行在什么态,0表示内核态,3表示用户态;
DPL(Descriptor Privilege Level)寄存器表示即将访问的数据在什么段,同样0表示内核段,3表示用户段。
每一次jmp和mov都要访问GDT,DPL就在GDT中,CPL在CS选择子中。
系统初始化最后要推倒用户态去执行,这时将用户程序的CPL初始化结果是3,而head.s将操作系统内核里函数的DPL是0,所以用户对内核的访问权限不够。
每次访问数据的时候检查两个寄存器的大小关系,若DPL≥CPL,则可以访问,反之,则不能访问
执行ppt程序时处于用户态CPL=3无法访问内核态的DPL=0。
对于intel x86,硬件提供了“主动进入内核的方法“:中断指令int:
int指令将使得CS中的CPL改成0,“进入内核”,这是用户程序发起的调用内核的唯一方式。
系统调用的核心:
(1)用户程序中包含一段包好int指令的代码
(2)操作系统写中断处理,获取想调用程序的编号
(3)操作系统根据编号执行响应的代码
这种能够进入内核态的中断并不多,只有int 0x80可以
应用程序调用printf---->C库函数printf—>库函数write()—>系统OS内核调用write()
最终展开成包含int指令的代码:
#include <unistd.h>
_syscall3(int, write,int ,fd ,const char *buf , off_t ,count)
#define _syscall3(type,name,...) typename(...) \
{__asm__ ("int 0x80" : "=a"(__res)...}
int 0x80实现的细节
在linux/include/unistd.h中,对write有声明:
int write(int fildes, const char * buf, off_t count);
在write.c中
/*
* linux/lib/write.c
*
* (C) 1991 Linus Torvalds
*/
#define __LIBRARY__
#include <unistd.h>
//定义write的实现
_syscall3(int,write,int,fd,const char *,buf,off_t,count)
为什么要有#define LIBRARY:
因为在unistd.h有
#ifdef __LIBRARY__
/*中间省略*/
#define __NR_write 4
/*中间省略*/
//有3个參数的系统调用宏函数
#define _syscall3(type,name,atype,a,btype,b,ctype,c) \
type name(atype a,btype b,ctype c) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "0" (__NR_##name),"b" ((long)(a)),"c" ((long)(b)),"d" ((long)(c))); \
if (__res>=0) \
return (type) __res; \
errno=-__res; \
return -1; \
}
#endif /* __LIBRARY__ */
为了搞懂这段代码的运行机制,做了如下简化:
这种宏的写法值得学习!
显然,__NR_write是系统调用号,放在eax中
#define __NR_write 4 // 一一堆连续正整数(数组下表,函数表索引)
同时eax也存放返回值,ebx,ecx,edx存放3个参数
将一个系统调用号置给eax然后调用0x80
int 0x80之后就意味着我们进入了内核态,这就是进入内核的唯一方法
int 0x80要查IDT表,和GDT表很像,查到中断要到哪儿个地方执行。int 0x80的中断的处理执行过程:
void sched_init(void)
{
set_system_gate(0x80,&system_call); //设置中断处理门,此后0x80代表的中断处理函数就是system_call
}
linux/include/asm/system.h中
#define set_system_gate(n,addr) \ //n就是中断号,addr就是中断处理函数的地址
_set_gate(&idt[n],15,3,addr); //idt是中断向量表基地址,DPL置为3,降低门槛了,用户程序CPL=3可以进来了
#define _set_gate(gate_addr,type,dpl,addr) \
__asm__("movw %%dx,%%ax\n\t" "movw %0,%%dx\n\t" \
"movl %%eax,%1\n\t" "movl %%edx,%2":\
:"i"( (short)(0x8000+(dpl<<13)+type<<8))),
"o"(*((char*)(gate_addr))), //gate_addr低四位
"o"(*(4+(char*)(gate_addr))),\ //gate_addr高四位
"d"((char*)(addr),"a"(0x00080000)) //
上面的代码就是在给IDT初始化,初始化后一般遇到0x80中断,就可以通过IDT表找到中断处理函数,从而执行中断处理
处理函数入口点偏移就是IDT中的每个表项
用段和偏移形成行的PC,此时CS=8,IP=system_call (还记得jmpi 0,8吗?) 8是内核的代码段
CS的最后两位bit 0 和bit 1就是CPL,CS等于8则CPL就变为0,也就是说DPL=3让用户态能进来之后又让CPL=0,就什么都可以做了。返回的时候CPL又会变为3
#在linux/kernel/system_call.s中:
nr_system_calls=72
.global _system_call
_system_call:cmpl $nr_system_calls-1,%eax //eax中存放的是系统调用号
ja bad_sys_call
push %ds push %es push %fs
pushl $edx pushl pushl %ebx //调用的参数
movl $0x10,%edx
mov %dx,%dx
mov %dx,%es //内核数据 0x10 16 内核的数据段
movl $0x17,%edx
mov %dx,%fs //fs可以找到用户数据
call _sys_call_table(,%eax,4) //a(,%eax,4)=a+4*eax _sys_call_table+4*%eax就是系统调用处理函数的入口
pushl %eax //返回值压栈,留着ret_from_sys_call时使用
*** //其他代码
ret_from_sys_call:
popl %eax,
//还有其他pop
iret
在include/linux/sys.h中
fn_ptr sys_table[]={sys_setup,sys_exit,sys_fork,sys_read,sys_write,...} //sys_write对应的数组下表为4,__NR_write=4
在include/linux/sched.h中
typedef int (*fn_ptr)();
call_sys_call_table(,%eax,4)就是在call sys_write
综上所述-直观过程: