2021-08-11hit-oslab2系统调用

问题1:为什么需要有系统调用?
系统分为用户空间和内核空间,内核空间存有重要的数据(比如用户密码)和重要的代码(调度程序,文件管理系统什么的)。用户空间不能随意访问内核空间,否则会有极大的安全隐患(比如得到了内核空间的用户密码,随意下载软件,可能会下载恶意软件,比如更改调度程序,让程序一直运行)

问题2:如何分隔用户态和内核态?
1)用户态无法访问内核态数据,而内核态可以访问所有数据。
2)指令的最后两位标识特权,3为用户态,0为内核态。
3)检查当前指令和目标指令的特权

系统调用的具体过程
1)用户调用系统接口API
(我的理解API是连接用户程序和内核的桥梁,用户程序可使用内核中的函数接口,但是得借助API将数据传入内核中运行)

【系统调用和普通的函数调用没有区别,都是直接调用然后实现一定的功能,
自定义函数是直接通过call指令跳转到该函数的地址上去运行,
但是系统调用则是调用为该系统编写的一个接口函数,也就是API(application programming interface)
而API不能直接的完成系统调用的功能,它只是提供了一个接口去调用真正的系统调用。
————————————————
版权声明:本文为CSDN博主「东瓜lqd」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_41708792/article/details/89604209】

2)API将系统调用号存入EAX(寄存器),其他传入的参数依次存入寄存器EBX、ECX、、、以此类推。
(系统调用号是用来表明调用的具体是内核中的哪一个函数,但是我不明白的是:如果有多个进程同时调用同一个函数,那如何区分是哪个进程在调用该函数,像c++的类一样做区分吗?
【函数本身只是代码,代码是只读的,无论多少个线程同时调用都无所谓,因为是只读嘛.但是函数里面总要用到暑假 ,如果数据属性线程级别(比如函数形参–>局部变量–>存在栈上–>每个线程都有自己的栈),那么同时调用是没关系的,因为用的都是本线程的数据;但是如果函数用到一些全局数据,比如全局变量,根据堆内存首地址去访问的堆内存(形参传入的),同时操作一个数据结构(如对一个链表有什么操作),静态局部变量,那就不行了,必须要加锁!!】上网找的资料不知道是否靠谱。)

4)中断处理程序根据对应的系统调用号,调用对应的系统函数(处理中断)
5)对应的系统调用函数调用完后,将返回值存入到EAX中,返回到中断处理函数
6)中断函数返回到API中
7)API将EAX返回给应用程序
(6、7两点没有看懂)

一、应用程序如何调用系统调用?
自定义函数是通过调用call直接跳转到自定义函数的位置开始执行;
系统调用不能直接跳转到内核函数的位置开始执行,而是调用真正的内核函数,它是联通用户程序和系统函数的一道桥梁。
具体的过程为
1)将系统调用号存入EAX。系统调用号是指具体调用了什么系统函数。
2)将其他传递的参数传入其他通用寄存器。
3)触发int 0x80中断
(我的理解是根据中断的类型查找中断向量表,找到对应的解决的方法)

在这里插入图片描述
_syscallN(type, name,atype,a,btype,b,…)
N表示要调用的函数中有N个参数。
【因为在unsitd.h已经声明了四种参数传递的方式,在32位处理器上有四个寄存器(eax,ebx,ecx,edx),
eax用于传递中断调用号和返回值,使用ebx,ecx,edx三个寄存器传递参数,所以最多只可以三个参数,
如果要增加参数需要采用栈来传递参数,寄存器只需要获取栈的地址和参数的长度。
————————————————
版权声明:本文为CSDN博主「东瓜lqd」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_41708792/article/details/89604209】

以__syscall1(int,close,int ,fd)为例,这是用户程序通过API调用内核中的close()函数

#define __LIBRARY__
#include <unistd.h>
int close(int fd);

其中_NR_##name为系统调用号,指明调用系统中的哪个函数,并将其存放到寄存器eax中,其他的参数依次存放到通用寄存器当中。

int 0x80:=a”(__res):””(__NR_##name),
”b”((long)(a)),”c”((long)(b)),“d”((long)(c)))
//((long)(a))为函数传入的参数,“b”为存储参数的寄存器

例如将close()函数用_syscall1(int,close,int,fb)进行宏展开

int close(int fd)
{
    long __res;
    __asm__ volatile ("int $0x80"
        : "=a" (__res)
        : "0" (__NR_close),"b" ((long)(fd)));
    if (__res >= 0)
        return (int) __res;
    errno = -__res;
    return -1;
}

这就是 API 的定义。它先将宏 __NR_close 存入 EAX,将参数 fd 存入 EBX,然后进行 0x80 中断调用。调用返回后,从 EAX 取出返回值,存入 __res,再通过对 __res 的判断决定传给 API 的调用者什么样的返回值。

因此在本实验中,需要在unistd.h中为系统编写的函数加上系统调用号。

/*
而在应用程序中,要有:
*/

/* 有它,_syscall1 等才有效。详见unistd.h */
#define __LIBRARY__

/* 有它,编译器才能获知自定义的系统调用的编号 */
#include "unistd.h"

/* iam()在用户空间的接口函数 */
_syscall1(int, iam, const char*, name);

/* whoami()在用户空间的接口函数 */
_syscall2(int, whoami,char*,name,unsigned int,size);

Linux的内核中实现了一些API,因为在内核加载完成后,会跳转到用户态中做一些初始化的工作,用户态需要一些系统调用,因此在内核中实现了一部分的API。

二、通过int 0x80进入内核态
以下内容由计算机在开机启动的时候,初始化设置IDT表的表项
在这里插入图片描述
在这里插入图片描述
sched_initi()在init/main.c函数中调用,用于在内核加载完成后的初始化。
【其中15表示此中断号对应的是陷阱门,注意,这个中断向量不是中断门描述符。比如硬盘中断(hd_interrupt)或定时器中断(timer_interrupt)等硬件类的中断才设置为中断门描述符。陷阱门是可被中断的。】
上图中的n为0x80,是在中断向量表基址中查找相应的中断处理程序?
其他的参数设置:
type = 15,dpl = 3,addr = &system_call;
用段选择符和便宜地址设置成新的CS:IP

其他的补充:

void main(void)
{
//    ……
    time_init();
    sched_init();
    buffer_init(buffer_memory_end);
//    ……
}

sched_init() 在 kernel/sched.c 中定义为:

void sched_init(void)
{
//    ……
    set_system_gate(0x80,&system_call);
}

set_system_gate 是个宏,在 include/asm/system.h 中定义为:

  设置系统陷阱门函数。
 // 上面 set_trap_gate()设置的描述符的特权级为 0,而这里是 3。因此 set_system_gate()设置的
 // 中断处理过程能够被所有程序执行。例如单步调试、溢出出错和边界超出出错处理。
 // 参数:n - 中断号;addr - 中断程序偏移地址。(发生该中断的时候需要调用的函数)
 // &idt[n]是中断描述符表中中断号 n 对应项的偏移值;中断描述符的类型是 15,特权级是 3。
#define set_system_gate(n,addr) \
    _set_gate(&idt[n],15,3,addr)

_set_gate 的定义是:

 设置门描述符宏。
 // 根据参数中的中断或异常处理过程地址 addr、门描述符类型 type 和特权级信息 dpl,设置位于
 // 地址 gate_addr 处的门描述符。(注意:下面“偏移”值是相对于内核代码或数据段来说的)。
 // 参数:gate_addr -描述符地址;type -描述符类型域值;dpl -描述符特权级;addr -偏移地址。
 // %0 - (由 dpl,type 组合成的类型标志字);%1 - (描述符低 4 字节地址);
 // %2 - (描述符高 4 字节地址);%3 - edx(程序偏移地址 addr);%4 - eax(高字中含有段选择符 0x8)。
#define _set_gate(gate_addr,type,dpl,addr) \
__asm__ ("movw %%dx,%%ax\n\t" \
// 将偏移地址低字与选择符组合成描述符低 4 字节(eax)。
    "movw %0,%%dx\n\t" \
    // 将类型标志字与偏移高字组合成描述符高 4 字节(edx)。
    "movl %%eax,%1\n\t" \
    // 分别设置门描述符的低 4 字节和高 4 字节。
    "movl %%edx,%2" \
    : \
    : "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \
    "o" (*((char *) (gate_addr))), \
  
    "o" (*(4+(char *) (gate_addr))), \

    "d" ((char *) (addr)),"a" (0x00080000))
    //"a"内放置段选择符

其作用为填写IDT表格(中断描述符表),将 system_call 函数地址写到 0x80 对应的中断描述符中,也就是在中断 0x80 发生后,自动调用函数 system_call。
【汇编注释:
1、mov :为寄存器移动指令,例如movw dx,ax 即为dx-〉ax,mov为移动指令。“w”为长度的指定w=word=16位=2个字节;相应的“l”=long=32位=4字节。
2、% :AT&T汇编在引用寄存器时要在前面加1个%,%%是因为GCC在编译时会将%视为特殊字符,拥有特殊意义,%%仅仅是为了汇编的%不被GCC全部转译掉
3、ax 与 eax :ax与eax之间是有联系的,他们并不是孤立的,eax为32位寄存器,ax为16位寄存器,而ax就是eax的低16位。dx与edx具有相同的关系。
4、%0、%1、%2、%3:0、1、2、3可以看作变量,这些变量在程序的":“之后,程序的两个”:",是定义输入、输出项的。针对这段程序这些变量的前面都加了明确的限定,例如"i"(输入项)、“o”(输出项),剩下的"d"(edx的初始值),“a”(eax的初始值)。而0、1、2、3的概念就是指第几个变量,这里输入项、输出向、寄存器初始混合编号;相应的0(“i”((short)(0x8000+(dpl<<13)+(type<<8)))));1((*((char )(gate_addr))));2(((4+(char *)(gate_addr))));3(“d”((char )(addr)));4(“a”(0x00080000))
5、<<:这是个运算符,如果大家觉得<<不好理解可以用乘2的次方来实现相同的效果,例如14<<13=14
2的13次方
6、\n\t:这是嵌入式汇编一种书写格式】

三、实现系统调用
每个系统调用都有一个 sys_xxxxxx() 与之对应,他们都是实实在在的做他们应该做的事情

四、具体实现

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

#define __LIBRARY__
#include <unistd.h>
//提供对 POSIX 操作系统 API 的访问功能,在Linux网络编程中往往会用到
#include <errno.h>
//定义了错误码来返回错误信息的宏
//#define EINVAL 22 /* Invalid argument */
//Invalid argument of a function call as defined by POSIX.
#include <asm/segment.h>
//<asm/segment.h>:段操作头文件,定义了有关段寄存器操作的嵌入式汇编函数。
char kname[24];
//存入到内核中的字符串的长度为23,最后一个放置'\0'

int sys_iam(const char* name)
//将用户名称从用户空间拷贝到内存空间
{
    int len = 0;

    while (get_fs_byte(name + len) != '\0')
        len++;
//功能:从用户空间addr地址处取出一个字节char
//参数:addr用户空间中的逻辑地址
//返回:fs:[addr]处的一个字节内容
//len为用户地址字符串的偏移量,而name为用户字符串的起始地址,只要没有遇到空字符串就自行向后偏移
    if (len > 23)
    {
        errno = EINVAL;
        //errno 用来保存最后的错误代码,它是一个宏,被展开后是一个 int 类型的数据(在单线程程序中可以认为是一个全局变量),并且当前程序拥有读取和写入的权限。
        //<errno.h> 头文件中有一个 errno 宏,它就用来存储错误代码,当系统调用或者函数调用发生错误时,就会将错误代码写入到 errno 中,再次读取 errno 就可以知道发生了什么错误。
        //errno 被设置为 0;程序在运行过程中,任何一个函数发生错误都有可能修改 errno 的值,让其变为一个非零值,用以告知用户发生了特定类型的错误。
        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;
    //返回读取到的字符串的长度
}
//感受到了:平时写的C语言程序是在用户空间之内运行的,

int sys_whoami(char* name, unsigned int size)
//将sys_iam()所数据从内核空间拷贝到用户空间
//并保证拷贝的字符串的大小不超过size
{
    int len = 0;
    while (kname[len] != '\0')
        len++;
        //统计字符串的长度
    if (len > size)
    {
       errno = EINVAL;
       return -1;
       //如果字符串的长度超出了指定的范围,就将其改为-1
    }
    
    int i;
    for (i = 0; i < len; i++)
        put_fs_byte(kname[i], name + i);
        //name+1为起始地址+偏移地址
    put_fs_byte('\0', name + i); //注意这里一定要使用put_fs_byte函数来存入数据,尽管内核可以直接访问用户数据,但也要使用API,否则会报错
    //记得在字符串的最后加上空字符
    return len;
    //返回数据的长度
}

为什么要加LIBRARY呢?
在这里插入图片描述
这张图片解释了加上__LIBRARY__的原因

2、修改一系列头文件
1)修改/oslab/linux-0.11/kernel的Mikefile
第一处:画线的部分为增加的部分
在这里插入图片描述
第二处:画线的部分为增加的部分
在这里插入图片描述
注意修改完Makefile后要返回到/linux-0.11/中进行编译

cd ../../../
make all

2)修改/oslab/linux-0.11/include/中的unistd.h
画线的部分为添加的部分,加上自己写的函数的系统调用号
在这里插入图片描述
3)修改/oslab/linux-0.11/include/linux中的sys.h
第一处:模仿其他的系统调用,加上函数的声明。
第二处:sys_call_table为函数指针数组的起始地址,需要在这个数组中加上sys_iam和sys_whoami的引用,在数组的中的位置必须和在系统调用号列表中的位置一致。
在这里插入图片描述
【call sys_call_table(,%eax,4)这一句话,根据汇编寻址方式来解释这句话:
=> call sys_call_table + 4 * %\eax (eax中存放的是系统调用号,系统调用号也就是那些_nr_xxx)
这里就是通过 一个函数指针数组的起始地址 + 4 * %eax
表示从函数起始地址开始跳过%eax个项,而且每个项是4个字节,然后对应到一个函数入口,
所以也就是跳转到从sys_call_table中的第%eax个函数开始执行,
也就是开始执行那个系统调用。
————————————————
版权声明:本文为CSDN博主「东瓜lqd」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_41708792/article/details/89604209】

4)修改/oslab/linux-0.11/kernel中的system_call.s
将nr_system_calls = 72改成74,因为新增了两个系统调用号
在这里插入图片描述
3、然后编写iam.c和whoami.c放在hdc/usr/root下的任何位置
iam.c
1)首先挂载硬盘,并进入到hdc/usr/root/当中

sudo mount-hdc
cd /hdc/usr/root/

2)编写程序
iam.c

#define __LIBRARY__
//这句话是什么含义?
//猜测是一个ifndef,然后就不做其他的事情的东西,所以要定义
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <stdio.h>

_syscall1(int, iam, const char*, name)
//是一个宏定义
int main(int argc, char* argv[])
{
    if (iam(argv[1]) != -1)
        printf("Input successfully.\n");
       //命令行的第二个参数为要存储在内核的字符串,如果超出了范围,就会得到-1,意味着输入失败
    return 0;
}

whoami.c:

#define __LIBRARY__
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <stdio.h>

_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;
}

3)修改头文件/hdc/usr/include/中的unistd.h
在这里插入图片描述
注意这里是加上写的测试函数的调用号(这是我困惑的地方,其他的就没有什么要修改的了)
否则会报错
在这里插入图片描述

4、测试
1)先取消挂载硬盘

$ sudo umount hdc

2)进入到/oslab/下执行./run
并编译两个程序

gcc -o iam iam.c
gcc -o whoami whoami.c

在这里插入图片描述

问题:这几个程序分属于不同的程序,究竟是怎么将其联系在一起的?

补充1:在用户态和内核态之间传递地址
问题:为什么要使用get_fs_byte() 和put_fs_byte()?
Linux 操作系统和驱动程序运行在内核空间,应用程序运行在用户空间,两者不能简单地使用指针传递数据。应用程序采用的是虚拟内存机制,段页式管理空间内存。直接通过虚拟地址访问可能访问到的数据已经被换出,或者访问的是内核空间的地址。

内核空间数据段的选择符为0x10,用户空间数据段选择符为0x17。内核空间、用户空间之间的数据传输,是段间数据传输。
在segment.h中定义了一系列用于内核空间和用户空间传输数据的函数。从用户空间取得数据的函数中, mov指令的源操作数段寄存器都明确指出是fs,向用户空间写数据的函数中, mov指令的目的操作数段寄存器都是fs。当系统调用发生时,int 0x80处理函数会把fs设成用户数据段选择符(0x17),
(从这个角度来说,将内存空间分为用户空间和内核空间,也是分段管理内存的体现)

// 功能:从用户空间中addr地址处取出一个字节  
// 参数:addr   用户空间中的逻辑地址  
//*%0-(_v),%1-内存地址
// 返回:fs:[addr]处的一个字节内容  
extern inline unsigned char get_fs_byte(const char * addr)
{
    unsigned register char _v;
    
    __asm__ ("movb %%fs:%1,%0":"=r" (_v):"m" (*addr));
    //该指令会完成将*addr中的数据拷贝到_v中的操作
    return _v;
}
// 功能:向用户空间中addr地址处写一个字节的内容  
// 参数:val   要写入的数据  
//      addr     用户空间中的逻辑地址  
// 返回:(无)  
extern inline void put_fs_byte(char val,char *addr)  
{   // addr是相对于用户数据段的偏移,而当前数据段为内核数据段  
    // 所以要写成fs:[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中返回,达到了取一个字节数据的功能。
————————————————
版权声明:横线中的为CSDN博主「刘维汉」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_44167788/article/details/107942067
————————————————

补充2:
AT&T基础知识
内嵌汇编使用的是AT&T汇编,所以首先稍微讲解下AT&T的汇编指令的基础知识。

操作数前缀

movl   $8,%eax 
movl   $0xffff,%ebx 
int     $0x80
//看到在AT%T汇编中诸如"%eax"、"%ebx"之类的寄存器名字前都要加上"%";"$8"、"$0xffff"这样的立即数之前都要加上"$"。

源/目的操作数顺序
在Intel语法中,第一个操作数是目的操作数,第二个操作数源操作数。而在AT&T中,第一个数是源操作数,第二个数是目的操作数。

// INTEL语法
MOV EAX,8 //EAX是目的操作数, 8是源操作数
// AT&T语法
movl $8,%eax //8是源操作数 EAX是目的操作数
下面是一个内嵌汇编的例子

int main(){
    int input = 8;
    int result = 0;
    __asm__ __violate__  ("movl %1,%0" : "=r" (result) : "r" (input));
    printf("%d\n",result);
    return 0;
}

violate表明不希望编译器对代码进行优化,保持后面的代码原样
“movl %1,%0”是指令模板;“%0”和“%1”代表指令的操作数( 操作数(operand),是计算机指令中的一个组成部分,它规定了指令中进行数字运算的量 。 操作数指出指令执行的操作所需要数据的来源。),称为占位符,“=r”代表它之后是输入变量且需用到寄存器,指令模板后面用小括号括起来的是C语言表达式 ,其中input是输入变量,该指令会完成把input的值复制到result中的操作 。

补充3:CPL、RPL、DPL

补充4:print法调试程序
让运行中的程序向外输出一些信息,告知程序的运行状态也是一种很好的调试方式。
printf()是只有在用户空间中才能运行的函数,在内核空间中得使用printk()。
这两个函数的接口差不多,只是printk()需要特别处理一下 fs 寄存器,它是专用于用户模式的段寄存器。

int printk(const char *fmt, ...)
{
//    ……
    __asm__("push %%fs\n\t"
            "push %%ds\n\t"
            "pop %%fs\n\t"
            "pushl %0\n\t"
            "pushl $buf\n\t"
            "pushl $0\n\t"
            "call tty_write\n\t"
            "addl $8,%%esp\n\t"
            "popl %0\n\t"
            "pop %%fs"
            ::"r" (i):"ax","cx","dx");
//    ……
}

printk() 首先 push %fs 保存这个指向用户段的寄存器,在最后 pop %fs 将其恢复,printk() 的核心仍然是调用 tty_write()。查看 printf() 可以看到,它最终也要落实到这个函数上。

参考文章:
1.内嵌汇编学习
2、哈工大操作系统实验OSLab2-系统调用by刘维汉
3、蓝桥oslab——系统调用
4、

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值