操作系统:系统调用的实现

2025-06-26 16:44:43

内核态与用户态、内核段与用户段

内核态与用户态是保护模式下的概念

内核态:具有较高特权,可以访问所有寄存器和存储区,执行所有指令;OS一般运行在内核态

用户态:较低权限的执行状态,仅能执行规定的指令(如不能随意 jmp ),访问指定的寄存器,应用程序一般只能在用户态运行

计算机中用两个 bit 来表示四种特权状态,硬件将 0 作为内核态,3 作为用户态,Windows 与 Linux 均只使用两个状态

如何保证应用程序不能进入到内核态?

特权保护:

用户态不能直接转向内核态从而执行特权操作,是由硬件保证的

操作系统将内存分段看待,内核态执行在受保护的内核段,用户态对应用户段,

用户态的程序不能由用户段直接跳到内核段,而段是由段寄存器来表示

CPL(Current Privilege Level)当前特权级别,CS 寄存器的最低两位

DPL(Destination/Descriptor Privilege Level)目标特权级别,段描述符的权限位

RPL (Request Privilege Level)请求特权级别,访问的数据段 DS 的最低两位,段选择子的最低两位

简单地说,DPL描述目标内存段的特权级别,CPL表示当前特权级别,当 DPL>=CPL 时,才允许访问

稍微详细一点说,RPL 可以与 CPL 不同,比如说 CPL=0,以 RPL = 3 请求访问 DPL = 3 的段,当然是允许的

即 DPL >= RPL 且 DPL >= CPL 时允许访问,这里涉及到较为复杂的与 GDT 相关的实现,暂且不表

进一步说明:段选择子与段描述符

进一步说明:数据段与代码段权限检查

即每次访问时 通过硬件检查 DPL 与 CPL 是否满足条件,进行特权保护

系统调用

应用程序不能随意访问内核区域,但是它又需要特权级别完成一些任务,

中断 是硬件提供的进入内核的唯一方法, int 指令使 CS 中的 CPL 改成 0,可以"进入内核"

系统调用就是一段包含 int 指令的代码,表现为一系列的内核函数,

由应用程序来调用,在内核中执行,将结果返回给应用程序

即系统调用是操作系统提供给上层程序访问内核的接口

系统调用:

应用程序以系统调用的方式访问内核,防止程序随意更改、访问数据与指令

屏蔽底层细节,使应用程序有更好的移植性

系统调用的过程:

① 用户程序通过系统调用触发相应的中断

② 操作系统进行中断处理,获取系统调用号(入口地址)

③ 操作系统根据系统调用号执行相应的程序代码

系统调用的实现

下面是 Linux 0.11 中,write系统调用的实现

可以看到,系统调用通过内联汇编传递参数、调用中断

进一步说明:GCC内联汇编简介

//

#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; \

}

//write.c

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

即 write实现为

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

long __res;

__asm__ volatile(

"int $0x80"

: "=a"(__res)

: "0" (__NR_write),"b" ((long)(fd)),"c" ((long)(buf)),"d" ((long)(count))

);

if(__res>=0)

return (int) __res;

errno =- __res;

return -1;

}

int 0x80 又是如何利用中断执行相应的程序的呢?

系统在 main 中进行初始化时执行的 sched_init 函数中设置了 0x80 的中断处理

void sched_init(){

set_system_gate(0x80, &system_call);//设置0x80的中断处理

}

//linux/include/asm/system.h

#define set_system_gate(n, addr) _set_gate(&idt[n], 15, 3, addr);

//idt中断向量表基址, n中断处理号, 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))), \ eax的值放到idt[n]的前四个字节

"o" (*(4+(char *) (gate_addr))), \ edx的值放到idt[n]的后四个字节

"d" ((char *) (addr)), \

"a" (0x00080000)

)

相当于

eax = 0x00080000 edx = addr

eax = 0x00080000 | (addr << 16)

edx = addr & 0x0000 | (0x8000+(dpl<<13)+(type<<8)

eax -> idt[n]前四个字节 edx -> idt[n]后四个字节

这段代码将 system_call 的入口地址与相应的段选择符、DPL 及其他信息

填充为 IDT 的一个表项

即设置成为

斯巴拉西

注意这里 DPL 被置为 3,是用户态可访问的

那么取到表项中的段选择子(Selector)与偏移地址(Offset)合成新的 PC(CS : IP)

CS = 0x0008,CPL = 0,IP = addr = system_call

system_call 就是中断 int 0x80 的处理程序,也就是说

系统调用是通过中断机制实现的

继续分析,system_call 如何执行用户程序调用的系统调用

简单地来看,现实检查了系统调用号小于 nr_system_call - 1

然后压栈保存 ds、es、fs,edx、ecx、ebx 的值

再将 ds、es 设置为内核数据段,cs 已经被设置为内核代码段,fs 指向用户数据段

通过 _sys_call_table + eax * 4 得到系统调用处理函数的入口地址

eax 存放系统调用号,4 为 x86 系统地址字节数,_sys_call_table 为系统调用表基址

pushl eax 压栈保存系统调用返回值,在 ret_from_sys_call 中使用

最后恢复寄存器值,使用 iret 指令从中断返回

再看 _sys_call_table 是如何组织的

在 include/linux/sys.h 中

所有的系统调用处理函数的指针,组织成这样的表

且 fn_ptr 定义为返回值为 int 的函数指针,这里参数列表为空

在 include/linux/sched.h 中

就这样 call _sys_call_table + eax * 4 执行真正的系统调用功能,这里 eax 为 _NR_write = 4,

执行 sys_write 函数

在系统调用返回时 iret 指令会将 CPL 置回3

再简单说一下 system_call 还做了哪些事

在系统调用功能函数结束后,检查当前任务的运行状态,如果不在就绪状态就去执行调度程序

如果在就绪态,但是时间片用完,也会执行调度程序

即 系统调用返回时会检查进程调度

若继续执行,则返回用户程序系统调用

总结一下

因为安全问题,用户程序不能直接访问内核,由硬件检查 DPL >= CPL 保证

而有些任务又必须在内核态下完成,系统调用是用户程序访问内核的接口,由中断机制实现

因此,系统调用实际上是 int 0x80 对应的中断处理程序,再根据系统调用号执行不同的程序,完成相应的功能

2019/12/15