我是如何学习写一个操作系统(四):操作系统之系统调用

前言

最近有点事情,马上要开学了,所以学习的脚步就慢下来了。这一篇主要是来说操作系统的系统调用的,像C语言的printf深入到内部就是一个有关屏幕输出的系统调用

什么是系统调用

之前提过操作系统是对硬件的抽象,也是软硬件之间的一层。之前比如如果我们想要在屏幕上输出一些字符,就需要一些指令操作,然后把数据放到显存上。但是在有了操作系统后,就不需要这样做,也不能这样做了。这时候只要操作系统提供一个接口来让我们完成这个任务

由操作系统实现提供的所有系统调用所构成的集合即程序接口或应用编程接口(Application Programming Interface,API)。是应用程序同系统之间的接口。

系统调用的实现

在硬件设计上,通过区分内核态和用户态来把内核程序和用户程序隔离开

CS寄存器最低的两位为0即是内核态,为3是用户态

但是系统调用的代码是处在内核态的,所以就需要提供一种方法来能够让用户程序进入内核态来实现系统调用

在X86里,INT指令就是硬件用来提供由用户态进入内核态的方法,所以系统调用的实现就可以变为:

  • 由用户程序发起一个INT指令,指明要调用的服务
  • 在操作系统里写出相应的中断处理
  • 由操作系统根据用户指明要调用的服务取执行相应的代码

内联汇编

稍微说一下C里的内联汇编,以免之后忘记。

gcc的内联汇编一般都是这个格式

1
2
3
4
5
asm ( 汇编指令
: 输出操作数 // 非必需
: 输入操作数 // 非必需
: 其他被污染的寄存器 // 非必需
);
  1. 第一部分就是汇编指令

  2. 第二部分是输出操作数,都是 “=?”(var) 的形式, var可以是任意内存变量(输出结果会存到这个变量中), ?一般是下面这些标识符 (表示内联汇编中用什么来代理这个操作数):

    a,b,c,d,S,D 分别代表 eax,ebx,ecx,edx,esi,edi 寄存器
    r 上面的寄存器的任意一个(谁闲着就用谁)
    m 内存
    i 立即数(常量,只用于输入操作数)
    g 寄存器、内存、立即数 都行
    在汇编中用%序号来代表这些输入/输出操作数,序号从0开始。为了与操作数区分开来,寄存器用两个%引出,如:%%eax

  3. 第三部分是是输入操作数,都是 “?”(var) 的形式, ? 除了可以是上面的那些标识符,还可以是输出操作数的序号,表示用 var 来初始化该输出操作数,上面的程序中 %0 和 %1 就是一个东西,初始化为 1(a的值)。

  4. 第四部分标出那些在汇编代码中修改了的、 又没有在输入/输出列表中列出的寄存器, 这样 gcc 就不会擅自使用这些”危险的”寄存器。 还可以用 “memory” 表示在内联汇编中修改了内存, 之前缓存在寄存器中的内存变量需要重新读取。

    参考链接

Linux0.11里对系统调用的代码实现

设置中断

  • 首先需要对IDT设置中断调用的处理函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define set_system_gate(n,addr) \
_set_gate(&idt[n],15,3,addr)

#define _set_gate(gate_addr,type,dpl,addr) \
__asm__ ("movw %%dx,%%ax\n\t" \
"movw %0,%%dx\n\t" \
"movl %%eax,%1\n\t" \
"movl %%edx,%2" \
: \
: "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \
"o" (*((char *) (gate_addr))), \
"o" (*(4+(char *) (gate_addr))), \
"d" ((char *) (addr)),"a" (0x00080000))

set_system_gate(0x80,&system_call);

实现中断函数

  • sys_call_table[]是一个指针数组,定义在include/linux/sys.h中,该指针数组中设置了所有72个系统调用C处理函数地址。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
system_call:
cmpl $nr_system_calls-1,%eax
ja bad_sys_call
push %ds
push %es
push %fs
pushl %edx
pushl %ecx # push %ebx,%ecx,%edx as parameters
pushl %ebx # to the system call
movl $0x10,%edx # set up ds,es to kernel space
mov %dx,%ds
mov %dx,%es
movl $0x17,%edx # fs points to local data space
mov %dx,%fs
call sys_call_table(,%eax,4)
pushl %eax
movl current,%eax
cmpl $0,state(%eax) # state
jne reschedule
cmpl $0,counter(%eax) # counter
je reschedule

提供接口

  • 在linux向应用程序提供系统调用接口write
  • _syscall3的本质上是一个宏
1
_syscall3(int,write,int,fd,const char *,buf,off_t,count)
1
2
3
4
5
6
7
8
9
10
11
12
#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) \
: "0" (__NR_##name),"b" ((long)(a)),"c" ((long)(b)),"d" ((long)(c))); \
if (__res>=0) \
return (type) __res; \
errno=-__res; \
return -1; \
}

小结

这样对于一个系统调用就会变成

printf 用户调用

int 0x80 库函数的实现


进入内核


system_call 中断调用

sys_ 系统调用