操作系统接口与系统调用的实现 (李治军操作系统课笔记2)

什么是接口

用户使用计算机的两种方式:命令行、图形。从这两种方式理解接口。

  • 命令行
    在这里插入图片描述
    1、命令其实就是程序,经过编译后生成可执行文件,在命令行键入可执行文件名与参数输出结果。而实际上shell也是一个程序,开机时在main.c文件的所有xx_init()函数之后,通过/bin/sh这个可执行文件启动shell程序
    2、关键是这些程序里面调用的函数:fork()控制进程 , printf()控制屏幕,scanf()控制键盘……通过这些函数对计算机硬件进行使用

  • 图形
    在这里插入图片描述
    鼠标、硬盘点击之后,通过中断放入消息队列;然后应用程序写一个循环,不断从消息队列中取消息,又对不同的消息产生不同的反应(函数)

所以不管是命令行还是图形,用户实质上都是通过程序来使用计算机。而这些程序的关键就是里面的函数,比如read() , write()等,这些函数调用就是应用程序和操作系统的接口。又因为这些函数调用时系统提供的,所以又叫系统调用(system_call)

POSIX标准

POSIX(Portable Operating System Interface of Unix),POSIX标准定义了操作系统应该为应用程序提供的接口标准,目的是为了增强程序的可移植性,在不同的操作系统上都能跑。

为什么需要接口?

如果用户程序能直接通过jmp跳到内核或者mov访问内核的数据,那么你从网上下载一段程序就可能进入系统内核获取你的root密码等等。

如何隔离应用程序与系统内核?

利用硬件将内存划分为用户段和内存段,在用户态下的程序不能直接访问内核段的数据。如何实现对这种访问的阻止呢?

  • GDT表:
    GDT的由来详见
    总结下来:
    1、对一个内存地址的访问以段Base Address为单位。
    2、在实模式下,使用16-bit段寄存器CS(Code Segment),DS(Data Segment),SS(Stack Segment)来指定段,直接左移4位与偏移量相加
    3、在保护模式下,对一个段的描述则包括【Base Address, Limit, Access】,它们加在一起被放在一个64-bit长的数据结构中,被称为段描述符。而段寄存器只有16bit,所以将段寄存器中的值作为下标索引来间接引用(事实上,是将段寄存器中的高13 bit的内容作为索引)。这个全局的数组就是GDT。
    在这里插入图片描述
    注意此处的DPL(description privilege level描述特权级)那一个格,表示表示访问该段时CPU所需处于的最低特权级,与隔离原理相关,下面详述。

  • 段选择子
    在保护模式下,cs叫段选择子。
    在这里插入图片描述
    如上图,高13位作为被引用的段描述符在GDT/LDT中的下标索引,bit 2用来指定被引用段描述符被放在GDT中还是到LDT中,bit 0和bit 1是RPL——请求特权等级,请求特权级(RPL)则代表选择子的特权级,共有4个特权级(0级、1级、2级、3级)。
    在这里插入图片描述
    这个特权级与访问权限相关:
    在head.s里面建立GDT表的时候就将内核段DPL置为0,而CPL(current privilege level)是当前指令的特权级,如果是在用户态,那么CPL就为3,否则为00的特权级是高于3,在访问某个地址的时候,如果CPL的特权级小于等于DPL的特权级,那么就不能访问。由此实现用户段和内核段的隔离。

在保护模式下如何利用段选择子将逻辑地址准换为线性地址?举个例子
(1)选择子SEL=21h=0000000000100 0 01b 它代表的意思是:选择子的index=4即选择GDT中的第4个描述符;TI=0代表选择子是在GDT选择;最后的01代表特权级RPL=1
(2)OFFSET=12345678h,若此时GDT第四个描述符中描述的段基址(Base)为11111111h,则线性地址=11111111h+12345678h=23456789h
流程如下图所示(图源
在这里插入图片描述

有了隔离,用户程序如何才能调用系统内核

硬件提供了用户态访问内核态的唯一方法——中断,int指令将使CS中的CPL从3变为0,这样就可以进入内核,并且这个中断号只能是0x80(后面解释)。
在这里插入图片描述

具体理解:以printf为例

c代码里面的printf是printf(“%d”,a),在printf()内部其实调用了系统函数write,而write函数的函数头其实是这样的:

size_t write(int fd, const void *buf, size_t count);

可以看到,printf()函数的形参和write()的形参是不一样的,因此如果printf(“%d”,a)能调用write函数的话,肯定要对printf的形参进行处理,库函数printf()就起到这个作用——格式化输出write所需要的参数,再调用包含中断int 0x80的write(),最后在内核系统调用wirte()
在这里插入图片描述

  • 那么格式化参数之后是如何调用write()?
    通过_syscall3这个宏展开成_syscall3(int, write, int, fd, const char* buf, off_t, count)
    _syscall3的定义如下:
#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):””(__NR_##name),
”b”((long)(a)),”c”((long)(b)),“d”((long)(c)))); if(__res>=0) return
(type)__res; errno=-__res; return -1;}

要得到_syscall3(int, write, int, fd, const char* buf, off_t, count),就是进行如下替换:

type=int,name=write,atype=int,a=fd,btype=const char * ,b=buf,ctype=off_t,c=count;

因此type name(atype a, btype b, ctype c)就变成了int write(int fd,const char * buf, off_t count)调用库函数wirte()。

现在调用的还是库函数wirte(),由该函数代码可以看到目的就是展开成上面的一段汇编代码。这是一段内嵌汇编,:”=a”(__res):””(__NR_##name)意思是把存储了返回值的eax赋给_res,把__NR_write赋值给eax,__NR_write称为系统调用号,后面有用;”b”((long)(a)),”c”((long)(b)),“d”((long)(c))就是把形参的a、b、c依次赋值给ebx、ecx、edx。
这段代码的意思就是在int 0x80进入内核进行中断处理,处理完之后,返回_res,write这个系统调用就结束了。

  • 什么是系统调用号呢?
    int 0x80是唯一让应用程序进入内核的途径,所以所有的系统调用都是通过int 0x80这个中断来调用的,那么如何区分是write调用还是read调用or else?就是根据这个系统调用号来区分的,__NR_write表示write调用,会接着执行write对应的内核代码,其他同理。通过宏定义 #define __NR_write 4 将真正的中断处理函数地址在函数指针表中的索引赋给系统调用号。

  • int 0x80具体是如何操作的呢?
    1、int 0x80是进入中断服务函数的一条指令。 凡是int 指令都要要idt(interrupt description table)表转去哪里执行。
    2、void sched_init(void){ set_system_gate(0x80,&system_call); }这个初始化语句意思是int 0x80对应的中断处理程序就是system_call。那么系统是如何初始化IDT表,让能够通过int 0x80找到system_call呢?
    通过set_system_gate这个宏,set_system_gate这个宏又调用了_set_gate这个宏,如下图:
    在这里插入图片描述
    关于上图的几处解释:
    1、“movl %%eax,%1\n\t” “movl %%edx,%2”:意思就是把gate_addr也就是&idt[n]的前4位赋给%eax,后四位赋给%edx
    2、_set_gate(&idt[n],15,3,addr)这里的3替换掉#define _set_gate(gate_addr, type, dpl, addr)的dpl,也就是int0x80的表项在定义时就把DPL定义成了3,所以在用户态时也能通过int 0x80进入内核段——跳到idt表开始进一步查找。
    3、对于表项的构成,0-15和49-64位存储的是addr也就是&system_call,16-31存储的是段选择符,在这里是”a”(0x00080000))的前四位0x0008,也就是cs=0x8。在setup.s里面有一行jmpi 0,8,这条指令表示根据gdt表跳转到内核代码的地址0处。所以在此处,CS=8,ip=system_call就是跳到内核代码0处,然后根据入口点偏移量找到system_call这个函数;同时因为CS=8=0x1000,那么CPL=0,也就是说当前程序的特权级变了,变成内核态的了,这样就什么都能干了(这就是0x80是唯一途径的原因)。将来int 0x80返回之后,CS最后两位又要变成3,变回用户态。

中断处理程序system_call又做了什么呢
……
call _sys_call_table(,%eax,4) 
……

_sys_call_table(,%eax,4)=_sys_call_table+4*%eax:这是一种寻址方式。eax是系统调用号__NR_write,那_sys_call_table是什么?

fn_ptr sys_call_table[]=
{sys_setup, sys_exit, sys_fork, sys_read, sys_write, ...};

sys_call_table是一个fn_ptr类型的全局函数表,fn_ptr是一个函数指针,4个字节,所以_sys_call_table+4*%eax要*4,得到的结果就是真正的中断服务函数sys_write的入口地址了。



总结:

在这里插入图片描述



参考:
操作系统(二) – 操作系统的接口与实现

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值