操作系统的诞生是位了更好更方便的管理使用计算机,因此对于操作系统一定存在接口供我们使用。这一篇就是为了搞清楚接口及其实现。

接口

用户使用计算机的方式主要有:命令行、图形按钮、应用程序。

其本质都是应用程序,并调用了相关系统接口函数,我们将这些函数称为系统调用

其中最常用的标准有POSIX: Portable Operating System Interface of Unix(IEEE制定的一个标准族)。

为什么要有接口呢?这是因为如果可以随意访问内存像实模式汇编那样,这将产生巨大的安全问题。因此对于那些我们不能直接访问的内存就要通过系统调用来操作。

接口实现

隔离访问

为了用户不能直接访问特定内存,于是引入了内核态用户态,而这两者的实现就需要靠硬件来完成。

内核态和用户态

如上图所示,内核态时就可以访问任何内存,而用户态就有限制。

这两者的区分主要就是靠CPLDPLRPL,这里用前两个举例,DPL是目标段的特权等级,而CPL是当前指令所在段的特权等级也就是处于上面态。只要DPL大于等于CPL时才能访问目标。

DPL存在于各自GDT表项中,在head.s中初始化;而CPL存在于cs寄存器的最后两位。

到此就实现了访问的隔离。

访问内存

操作系统中虽然要隔离特定内存的访问,但在使用时必然要使用那些不可访问的内存,如系统调用。这时就需要方法使接口可以使用不可访问内存,这个方法就是使用中断,就如系统调用内部就是用了中断来实现相关操作。

中断就会使程序进入操作系统中相关的中断处理函数,并将用户态转变为内核态;当中断完成后再从内核态变为用户态

graph TD subgraph 系统调用 r3[用户程序] --执行int中断--> r0[内核] --int中断执行结束--> r[用户程序] end

下面使用printf举例(printf是C库函数,write中的宏展开才能算作系统调用)。

printf调用实现

c库write函数中的关键的宏是__syscall3(int, write, int, fd, const char *, buf, off_t, count)。其展开如下图:

宏_systemcall3

此外搭配宏#define __NR_write 4这是相关系统调用的函数索引表下标。

根据上图,宏展开的就是:

int write(int fd, const char *buf, off_t count)
{
    long __res;
    __asm__ volatile("int 0x80"
                     :"=a"(__res)
                     :""(__NR_write),"b"((long)(fd)),"c"((long)(buf)),"d"((long)(count))
                    );
    if (__res >= 0)
        return (int)__res;
    errno = -__res;
    return -1;
}

这里的第4行就调用了中断执行操作系统中相关的操作。

第5行表示将中断的返回值eax寄存器中的值放入变量__res中。

第7行则为中断需要的参数。第一个""表示沿用上一个寄存器eax,赋值为4;同时以此类推ebx=fd, ecx=buf,edx=count

这样就可以通过中断来将字符串打印出来。

那么这个中断内部又是如何呢?

这首先要回到操作系统初始化的地方,在那里我们有初始化IDT表,这时中断的关键,如下图是对中断0x80 的中断表项初始化。

初始化0x80的IDT表项

在图中的第一个宏展开我们会发现这就是通过第二个宏来设置相应的IDT表项,其中addr是相关中断的回调函数,dpl就是中断的表项的特权等级,为3,这就说明我们用户态的程序是可以使用中断的。

这时在看第二个宏的展开就是为了设置对应的表项,将表项的DPL置为3,将中断回调函数地址放到表项偏移中,将表项的段选择符置为0x0008。这是会发现段选择符的最后两位变为了00,有了访问操作系统的内存的权限。

之后就是进入操作系统的中断回调来执行显示,在执行完成之后就会再次从内核态变为用户态,返回之前的应用程序。