哈工大操作系统实验OSLab2-系统调用

HIT OSLab2 系统调用

一、什么是系统调用?

首先,系统调用是操作系统分级制度下的产物。当操作系统内核加载到内存后,它便一直常驻在那里;假设操作系统没有没有分级制度,那么我在编写某一段C语言程序的时候就可以读和写这台机器的任意物理内存,这显然是不能被接受的,因为无限制地修改内存意味着我也可能会更改内存中关于内核的那部分内容,导致死机或更严重的后果。

再来说一下分级制度。操作系统将程序运行的状态和物理内存分为两个(或多个)状态和区域:内核态、内核段&&用户态、用户段。当程序处于用户态时,它只能访问同为用户段数据,不能访问内核段数据;当程序处于内核态时,它可以访问任何数据。分级制度保证了处于用户态的程序不能随意访问或更改内核态的数据,保证了内核的数据安全。

可我们在某些情况下仍然需要访问内核中的数据内容,比如输入输出、读写文件等等,处理这些工作的函数都处于内核中。为了使用户能够使用内核提供的内容,但又不想让用户有意或无意地破坏这些数据,便有了系统调用。简而言之,系统调用就是一些内核提供的访问其中数据或者代码的接口。这和Java中常常把类中的变量设为私有并通过getter和setter来访问是同样的道理——不是不能访问,但要遵守一定的规则,接口就是在遵守这些规则的前提下对这些数据进行访问的一个入口。

二、系统调用的实现思路

好了,有了对于保护内核数据的基本构思,现在我们可以试着去实现它了。前面提到了分级制度,为了实现这个制度,我们需要在计算机中的存储部件储存两个字段,一个为前的特权级别,记为CPL(Current Priviledge Level),另一个是我想要访问的数据的特权级别,记为DPL(Destination Priviledge Level),保存这个两个字段的存储单元可以是寄存器或者内存。CPL被存储在CS或SS段寄存器的第0和第1位,而DPL字段被存储在了内存中的某个段描述符或中断描述符中。这是因为当前执行的代码是可知的,可以直接对寄存器查询特权级别,而目的代码是未知的,需要根据跳转地址去查表看其对应的是哪个段,再根据这个段的段描述符得到代码的特权级别。

x86架构将CPL和DPL字段均设置成了2比特长,允许了4种不同的特权级别,但对于Linux操作系统只用到了其中的两个状态:用户态和内核态,其特权级(Priviledge Level)分别为3和0(特权越高特权级越小)。

举个例子,CPL为3的程序只能访问DPL为3的目的代码或数据,而不能访问DPL为0的内核代码;而CPL为0的内核程序则可以访问任意级别的代码和数据。这在操作系统代码中可以简单地写为一个判断语句。

当执行系统调用时,x86架构首先执行一个中断指令(interrupt),这是用户程序发起的进入内核态的唯一方式。此时CPL=3而DPL=0,为了能够使判断语句通过,操作系统会将中断描述符中的DPL字段改为3,通过人为地更改目的代码的特权级别,使CPL=DPL,从而判断通过,系统进入内核态。进入内核态之后,再将段选择子中的RPL字段改为0,稍后段选择子中的RPL字段将更新CS中的CPL字段使其也置为0。如此一番折腾后,CPL=3而DPL=0,特权级别完全反过来了,代表了系统已经进入了内核态。

当然以上只是操作系统在设计时的实现思路,具体的实现细节见下面的代码。

三、系统调用具体实现代码

哈工大操作系统实验OSLab2-系统调用_第1张图片
哈工大的这页ppt举了printf系统调用的例子。首先printf函数在内部又调用了write函数。write函数在linux/include/unistd.h中用宏来定义,这个宏函数里面因为要产生0x80号中断,所以使用了一些内嵌汇编代码。有关于内嵌汇编的简单知识可以参考本课程的参考书《Linux-0.11内核完全注释》的第159页。上图中的这段代码的大致意思是使用%eax作为输出寄存器存储返回值,其他参数分别存入%eax, %ebx, %ecx, %edx中,然后执行0x80号中断。其中的__NR_##name在传入具体参数后,比如write,会变为__NR_write,这是在unistd.h中定义的另外一组宏,它作为函数数组的指针执行具体的系统调用。

哈工大操作系统实验OSLab2-系统调用_第2张图片
操作系统在接受到0x80号中断后,利用set_system_gate函数来设置0x80号中断的处理。这也是一个宏函数,其使用了另外一个宏函数_set_gate并传入了一些参数。第一个参数&idt[n],它是中断门的地址;第三个参数传入了3,它作为新的DPL,在下面的汇编代码中体现了这个"3"的作用。通过左移13位将3,用二进制表示即为11b,赋给了IDT表中的DPL字段,现在IDT表中的这个中断门描述符中的DPL字段被置为了11b,处于用户态的程序可以访问内核了。与此同时,上面的汇编代码的最后一行"a"(0x00080000)将前四位0x0008的两字节数据赋给了图中的段选择符部分,其低四位的8,表示了二进制是1000b,低两位是00b,是CPL字段的所在位置,所以现在CPL被置为0了,表示系统已经进入了内核态。

四、实验代码

首先在kernel文件夹中新建一个who.c文件,里面写入iam和whoami函数的系统调用sys_iam和sys_whoami。

#define __LIBRARY__
#include 
#include 
#include 

char kname[24];

int sys_iam(const char* name)
{
    int len = 0;

    while (get_fs_byte(name + len) != '\0')
        len++;

    if (len > 23)
    {
        errno = EINVAL;
        return -1;
    }

    //printk("%d\n", i);

    int j;
    for (j = 0; j < len; j++)
        kname[j] = get_fs_byte(name + j);
    kname[j] = '\0';
    return len;
}

int sys_whoami(char* name, unsigned int size)
{
    int len = 0;
    while (kname[len] != '\0')
        len++;
    if (len > size)
    {
       errno = EINVAL;
       return -1;
    }
    
    int i;
    for (i = 0; i < len; i++)
        put_fs_byte(kname[i], name + i);
    put_fs_byte('\0', name + i); //注意这里一定要使用put_fs_byte函数来存入数据,尽管内核可以直接访问用户数据,但也要使用API,否则会报错
    return len;
}

然后我们需要修改一系列的头文件和Makefile文件,这部分见实验手册即可。注意unistd.h需要在linux-0.11和其磁盘文件中同时修改,可以先在linux-0.11中修改完后将其粘贴至hdc中。

然后编写iam.c和whoami.c放在hdc/usr/root下的任何位置
iam.c:

#define __LIBRARY__
#include 
#include 
#include 
#include 

_syscall1(int, iam, const char*, name)

int main(int argc, char* argv[])
{
    if (iam(argv[1]) != -1)
        printf("Input successfully.\n");
    return 0;
}

whoami.c:

#define __LIBRARY__
#include 
#include 
#include 
#include 

_syscall2(int, whoami, const char*, name, unsigned int, size)

int main(int argc, char* argv[])
{
    char uname[24];
    if (whoami(uname, 24) != -1)
        printf("%s\n", uname);
    return 0;
}

最后在Bochs中使用gcc编译并执行即可。

五、补充内容

实验手册中提到了内核态和用户态交换数据使用了两个接口get_fs_byte和set_fs_byte,下面是其具体实现。

extern inline unsigned char get_fs_byte(const char * addr)
{
    unsigned register char _v;
    __asm__ ("movb %%fs:%1,%0":"=r" (_v):"m" (*addr));
    return _v;
}

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

仅以第一个接口get_fs_byte为例,其中"=r"代表函数返回值_v以任意一个寄存器返回,“m"代表输入参数*addr在存入内存当中。%0和%1分别按照寄存器或内存出现的顺序代表它们,所以%1就是那个"m”,%0就是%r。现在程序的意思就比较明了了,其使用%fs作为段寄存器加上 *addr的偏移,取出内存中对应的内容后放入寄存器%r中返回,达到了取一个字节数据的功能。

本次实验和第一个实验有些相似,都是概念的理解大于具体代码的实现。在理解有关系统调用后的这些概念后,实验的代码就很好写了。

很多学校的操作系统课程直接书接计算机组成原理的上文,从进程开始讲起,完全忽略或一笔带过操作系统的引导和系统调用部分的内容。而这两个部分这对于理解操作系统是如何从头开始工作是至关重要的,所以这里我要为哈工大的操作系统课程点个赞。

你可能感兴趣的:(操作系统,linux,内核)