操作系统基础:05 系统调用实现
一、系统调用概述
- 上节课讲解了系统调用的概念,系统调用是操作系统给上层应用提供的接口,表现为一些函数,如open、read、write 等。上层应用程序通过调用这些函数进入操作系统,使用操作系统功能,就像插座一样,看似简单易用,但本讲要探究其背后的实现机制。
二、系统调用的直观想法及问题
-
以whoami系统调用为例 :
用户程序调用whoami,目的是取出操作系统中(系统引导时载入)的字符串“lizhijun”并打印。直观想法是,应用程序和操作系统都在内存中,为何不能直接访问内核内存,直接调用printf打印内核中的字符串呢?
-
不可行原因:如果允许随意调用内核数据、随意跳转(jmp),会带来严重安全问题。比如可能泄露root密码,导致非法获取用户权限;还可能查看他人word内容,侵犯用户隐私。所以,应用程序不能直接访问内核内存。
三、内核态与用户态及内存分段
-
硬件设计实现隔离:
硬件设计区分了内核态和用户态 ,通过处理器保护环实现。用CS的最低两位表示当前程序执行状态,0是内核态,3是用户态。内核态可访问任何数据,用户态不能访问内核数据,指令跳转也受此限制,实现了内核程序和用户程序的隔离。
-
内存分段及特权级检查:内存对应分为内核段和用户段,通过段寄存器中的CPL(当前特权级)和DPL(目标特权级)等信息来检查访问是否合法。DPL用来表示目标内存段的特权级,操作系统初始化时将内核段的DPL设为0 。CPL表示当前的特权级,数字越大特权级越低,3表示用户态。每次访问时,要求DPL≥CPL且DPL≥RPL(请求特权级 ),若不满足特权级要求则无法访问。
四、进入内核的方法——中断
-
中断指令int:对于Intel x86架构,硬件提供的主动进入内核的方法是中断指令int 。
int指令会使CS中的CPL改成0,从而进入内核,这是用户程序调用内核代码的唯一方式。
-
系统调用核心要点:
- 用户程序中包含一段含int指令的代码,通常由库函数展开生成。
- 操作系统编写中断处理程序,获取想调用程序的编号。
- 操作系统根据编号执行相应代码。
五、系统调用实现流程
-
以printf调用为例 :
应用程序调用printf,经C函数库处理,先由库函数printf转换为库函数write,最终展开成包含int 0x80指令的代码。
在Linux系统中,以write系统调用为例
使用_syscall3宏展开代码,_NR_write是其系统调用号(值为4 )。在展开的汇编代码中,系统调用号放在eax中,同时eax也存放返回值,ebx、ecx、edx存放3个参数,通过int 0x80指令进入内核。
六、int 0x80中断处理
-
中断处理设置:系统通过set_system_gate函数设置0x80的中断处理
将中断处理函数入口地址等信息设置到中断描述符表(idt)中,其中DPL设为3,使得用户态代码(CPL = 3 )能通过int 0x80中断进入内核。执行int 0x80指令时,会根据idt表找到对应的中断处理程序system_call 。
七、中断处理程序system_call
-
处理流程:在system_call中断处理程序中,
首先会检查系统调用号是否合法,然后保存相关寄存器,设置内核数据段,通过系统调用号在_sys_call_table函数表中查找对应的处理函数。比如对于write系统调用,系统调用号为4,就会找到sys_write函数执行相应操作。执行完毕后恢复寄存器,返回用户态。_sys_call_table是一个全局函数数组,
存放着各个系统调用对应的处理函数指针。
八、总结
- 系统调用从用户态应用程序发起,借助库函数将系统调用展开为含int 0x80指令的代码,通过中断进入内核态,在内核中根据系统调用号执行相应处理函数,完成后返回用户态。这一机制既为应用程序提供了访问内核功能的途径,又保障了系统的安全稳定,理解其原理对深入掌握操作系统及开发应用程序意义重大。