【哈尔滨工业大学李治军】操作系统学习笔记:系统调用及【实验 3】系统调用实现

一.学习内容

1、操作系统接口

完成setup之后,操作系统的代码都被读入到从0地址开始的地方,还创建了一些初始的结构,如mem_map(管理内存的数据结构)、GDT、IDT等。而应用程序都放在了内存的上端。

最终,内存的下方放置的为系统代码和数据、上方放置的为应用程序,这样子一个结构情况。

什么是操作系统接口?操作系统直接面对用户吗?用户是怎么用操作系统的?
  • 命令行

首先应用程序编写的程序将编译成一个可执行文件。而与此同时,系统在刚开始的初始化完成后,会循环停留在shell里(可以理解为桌面,不断等你施加命令),当用户输入命令行指令后,系统将运行上面的那个可执行文件。

  • 图形按钮

由getmessage函数把消息从内核的队列中抽出来,然后根据消息调用消息处理函数,做相应的反应。

  • 操作系统接口(系统调用)

    操作系统接口连接谁?连接操作系统和应用软件,并不是直接与硬件交互了。 如何连接?C语言 。所以,操作系统提供这样的重要函数,表现为:函数调用,所以又称为系统调用system_call

2.系统调用的实现

1.假设用户程序内使用printf()函数。 2.根据lib下的_syscalln()和include/unistd.h下的模板,对printf()函数进行宏定义展开。 3.调用展开后的函数,触发int 0x80中断,将kernel下的system_call对应的IDT表中的DPL设为3,从而让用户程序可获取system_call地址作为IP。然后,再设置CS=8,使其对应的CPL=0,从而让用户可以进入内核态。 4.在system_call函数中,会使用从include/unistd.h中获得的存入eax的值,来查询include/linux/sys.h中sys_call_table表里对应的系统调用函数。 5.使用对应的系统调用函数处理数据后,将结果存入eax并返回给用户程序。

不该随意访问内核
  • 应用程序是不可以随意地调用内核的数据,不可以随意jmp。这会导致安全和隐私问题,如:可以看到root密码,可以修改root密码。

如何去访问内核?

  • 操作系统调用提供了能够合理进入内核的一种手段-------“硬件设计”,把非内核的和内核的东西划分成了用户态和内核态,对应的内存中的区域叫用户段和内核段。内核态可以访问任何数据,但用户态不能访问内核数据,只有当前的指令大于或等于目标的特权级,这条指令才被允许执行。

  • 不论是内核段还是用户段都需要通过段寄存器进行访问,主要使用了两个段寄存器CPL(CS低两位)和DPL来实现不同权限的控制。其中CPL存放在CS中,DPL存放在GDT中。当想访问其他段时,会从GDT中查询目标段的DPL来和当前所执行段CS中的CPL进行对比。即当:DPL>=CPL时,才允许执行。

  • 中断是进入内核的唯一方法,该方法通过硬件来实现。因此,如果用户程序想要进入内核,就需要包含一段int中断指令的代码,这段代码由库函数实现,由宏来展开成一段汇编代码。进入内核之后,操作系统就会写中断处理过程,来获取想调程序的编号。然后,操作系统会根据编号执行相应的代码。

3.以printf()为例,详细分析其在Linux0.11中该系统调用的过程。

  • 在lib/write.c中的代码:

#define __LIBRARY__
#include <unistd.h>
​
_syscall3(int,write,int,fd,const char *,buf,off_t,count)
  • 进入include/unistd.h

这是一些宏定义,表示系统调用的编号,在这里我们可以看到write的编号是4.

write.c中调用#define _syscall3()宏,这个宏的作用是封装了通过系统调用号码(_NR##name)来调用系统调用的过程。

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

这段代码执行完毕后,把系统调用的编号(4)放到了eax寄存器中(因为后续要通过编号查表),把参数fd放到了ebx寄存器中,*buf放到了ecx寄存器中,把count放到了edx寄存器中。

  • 当发生了中断以后,CPU就会去Linux内核中找到内核实现的idt表项。通过0x80中断向量找到具体的中断描述符,再通过中断描述符找到具体的回调方法。

  • sched_init()用来初始化,set_system_gate(0x80,&system_call)设置了系统调用门

set_system_gate是个宏,上图是关于它的定义,其中n表示中断号,addr表示地址,它又调用了_set_gate宏。

这段代码主要是初始化IDT表,然后再根据中断指令去查表,跳转到对应地址进行执行.

addr=&system_call组装到了处理函数入口点偏移,把dpl=3组装到了DPL,将0x0008组装到了段选择符。所以现在,CS=8,IP=&system_call。当CS=8时,CS的最后两位CPL就等于00,这时DPL>=CPL,进入内核态。

  • 查看中断处理程序:linux/kernel/system_call.s

此实验只需关心划线代码,意思是call sys_call_table + 4 * %eax,eax中现在存的是系统调用号4,__NR_write的值,sys_call_table就是基址(是一个函数表)。4表示每个系统调用对应的函数占四个字节(32位)。当要查找sys_write(位于第5个函数)时,会设置 eax=4(数组下标从0开始)。

成功找到sys_write。

总结:

1、在main方法启动内核时会通过sched_init方法对gdt、ldt、idt表项以及其他操作初始化。所以这里对0x80中断向量进行了初始化,当触发此中断,会调用到system_call函数来进行处理。 2、当发生int 0x80中断操作后,CPU会去找内核实现的idt表项,通过0x80中断向量找,最后找到0x80对应的处理函数system_call。 3、所以找到system_call,这里是汇编代码,只看最重要的一步。这里通过call指令找sys_call_table表,通过eax寄存器(eax是之前内联汇编传来的索引)*4(因为int数组的一个单元是4个字节)定位到具体的系统调用。最终找到了sys_write系统调用。

过程:

二、实验过程。

内核层面修改:

1、修改kernel/system_call.s 文件
nr_system_calls = 74  # 新增2个系统调用`
2、修改 include/linux/sys.h 文件,进行extern声明,以及添加新的系统调用函数指针
extern int sys_whoami();
extern int sys_iam();

3.添加 kernel/who.c 文件 编写实现 sys_iam函数和sys_whoami函数
#include <string.h>
#include <errno.h>
#include <asm/segment.h>
​
char msg[24]; //23个字符 +'\0' = 24
​
int sys_iam(const char * name)
/***
function:将name的内容拷贝到msg,name的长度不超过23个字符
return:拷贝的字符数。如果name的字符个数超过了23,则返回“•-1”,并置errno为EINVAL。
****/
{
    int i;
    //临时存储 输入字符串 操作失败时不影响msg
    char tmp[30];
    for(i=0; i<30; i++)
    {
        //从用户态内存取得数据
        tmp[i] = get_fs_byte(name+i);
        if(tmp[i] == '\0') break;  //字符串结束
    }
    //printk(tmp);
    i=0;
    while(i<30&&tmp[i]!='\0') i++;
    int len = i;
    // int len = strlen(tmp);
    //字符长度大于23个
    if(len > 23)
    {
        // printk("String too long!\n");
        return -(EINVAL);  //置errno为EINVAL  返回“•-1”  具体见_syscalln宏展开
    }
    strcpy(msg,tmp);
    return i;
}
​
int sys_whoami(char* name, unsigned int size)
/***
function:将msg拷贝到name指向的用户地址空间中,确保不会对name越界访存(name的大小    由size说明)
return: 拷贝的字符数。如果size小于需要的空间,则返回“•-1”,并置errno为EINVAL。
****/
{   
    //msg的长度大于 size
    int len = 0;
    for(;msg[len]!='\0';len++);
    if(len > size)
    {
        return -(EINVAL);
    }
    int i = 0;
    //把msg 输出至 name
    for(i=0; i<size; i++)
    {
        put_fs_byte(msg[i],name+i);
        if(msg[i] == '\0') break; //字符串结束
    }
    return i;
}
​
​
4.修改kernel/Makefile ,将 who.c 编译进内核

5.回到上级目录下make,内核重新编译,出现sync就成功了。

应用层面修改

1、在oslab目录下挂载文件。
sudo ./mount-hdc
2. 在~/oslab/hdc/usr/include/目录下修改unistd.h文件
/* 添加系统调用号 */
#define __NR_whoami 72 
#define __NR_iam    73
3. 在~/oslab/hdc/usr/root目录下写whoami.c和iam.c文件
/* iam.c */
#define __LIBRARY__
#include <unistd.h> 
#include <errno.h>
#include <asm/segment.h> 
#include <linux/kernel.h>
_syscall1(int, iam, const char*, name);
 
int main(int argc, char *argv[])
{
    /*调用系统调用iam()*/
    iam(argv[1]);
    return 0;
}
​
/* whoami.c */
#define __LIBRARY__
#include <unistd.h> 
#include <errno.h>
#include <asm/segment.h> 
#include <linux/kernel.h>
#include <stdio.h>
 
_syscall2(int, whoami,char *,name,unsigned int,size);
 
int main(int argc, char *argv[])
{
    char username[64] = {0};
    /*调用系统调用whoami()*/
    whoami(username, 24);
    printf("%s\n", username);
    return 0;
}
​
4.卸载
sudo umount hdc

测试

在~/oslab目录下./run,依次输入以下命令。

gcc -o iam iam.c
gcc -o whoami whoami.c
./iam zxy.
./whoami

结果:

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值