行业新闻

Linux的系统调用机制

Linux的系统调用机制

 

概述

处于用户态的程序只能执行非特权指令, 如果需要使用某些特权指令, 比如: 通过io指令与硬盘交互来读取文件, 则必须通过系统调用向内核发起请求, 内核会检查请求是否安全, 从而保证用户态进程不会威胁整个系统

write(1, ptr, 0x10)系统调用为例子, 汇编可以写为如下, 内核收到请求后会向显存中写入数据, 从而在显示器上显示出来

mov rax, 1
mov rdi, 1
mov rsi, ptr
mov rdx, 0x10
syscall

C库会首先实现一个write的包裹函数, 为这个系统调用进行一些简单的参数检查和错误处理

由于write的功能十分简单, 不方面使用因此还会根据write衍生出更高级的函数printf()供用户使用

整体结构如下:

接下来我们主要研究系统调用是怎么进入和退出的, 并不研究具体处理函数的实现

 

i386下处理系统调用

i386的系统调用是通过中断实现的, 因此放在了arch/i386/traps.c里面, 通过system_call()处理int 0x80的中断

system_call()声明

system_call()定义

sys_call_table就是一个函数指针数组, 定义在arch/i386/syscall.c中, 通过包含文件完成数组的初始化

unistd.h中定义了系统调用号与处理函数的句柄, 这个文件位于源码顶层, 是所有架构都必须满足的, 处理函数的举报由各个架构自己实现

总结

 

预备知识

段选择子

实模式下下: 实际物理地址 = (段地址<<4) + 偏移地址

保护模式下: 逻辑地址由两部分组成

  • 段标识符: 16位字段, 放在段寄存器中, 为全局段表的索引
  • 段内偏移: 32位

保护模式下寻址过程

  • 先根据段寄存器找到段选择子(16bit)
  • 再根据GDT找到段表, 用选择子作为索引, 从而找到段描述符(64bit)
  • 根据段描述符找到段基址
  • 段基址+ 偏移地址 得到线性地址
  • 线性地址到物理地址:
    • 如果没有分页那么线性地址就是物理地址,
    • 如果启用了分页, 那么还要通过页表得到物理地址
  • 物理地址才是类似于实模式中地址的概念, 可以直接送到总线上用于CPU访问内存

段选择子/段描述符索引

  • 为 段描述表 中 段描述符 的 序号
  • 由于一共13bit可用, 因此可以区分8192个段
  • TI: Table Indicator, 引用描述表的指示位
    • T1 = 0表示从全局段表GDT中读取
    • TI = 1 表示从局部段表LDT中读取
  • RPL: Requested Privilege Level, 表示请求者的特权等级, 当试图访问一个段时, 会自动当前特权等级和段要求的特权等级

由于分页机制比分段机制更加灵活, 因此现在的操作系统并不开启分段, 但是处于兼容性的考虑, 段寄存器还是被保留了下来

对于分段linux采用平坦模式, 也就是说所有的段的基址都是0, 地址空间相同, 分段只用于鉴权: 每当执行某些特权指令时CPU就会自动检查CS寄存器的RPL

  • 如果为0则说明当前是内核态, 允许执行
  • 如果不为0则说明是用户态, CPU会抛出一个异常, 交由内核的异常处理程序处理, 通常会向此用户进程发送SIGSEV信号

因此狭义上来说陷入内核态就是CPU令CS的RPL为0, 从而可以执行特权指令. 切换到内核态的执行环境则就是后话了

syscall指令

64位下的系统调用就和中断没关系了, 主要依赖于syscall指令的支持, syscall指令依靠MSR寄存器找到处理系统的入口点

MSR寄存器用来对CPU进行设置, 通过WRMSR和RDMSR指令读写

x86_64寄存器架构

当syscall指令执行时, 有如下操作

  • RCX保存用户态的RIP
  • 从MSR寄存器中的IA32_LASAR获取RIP
  • R11保存标志寄存器
  • 用IA32_STAR[47:32]设置CS的选择子, 同时把RPL设置为0, 表示现在开始执行内核态代码, 这是进入内核态的第一步, 由CPU完成
  • 用IA32_STAR[47:32]+8设置SS的选择子, 这也就要求GDT中栈段描述符就在代码段描述符上面

指令操作

https://www.felixcloutier.com/x86/syscall.html

swapgs指令

swapgs指令: 把gs的值与IA32_KERNEL_GS_BASE MSR进行交换

https://www.felixcloutier.com/x86/swapgs

刚刚切换到内核态时, 所有的通用寄存器与段寄存器都被用户使用, 内核需要想办法找到内核相关信息, 解决方法为:

  • 令gs指向描述每个cpu相关的数据结构
  • 当要切换到用户态时就调用swapgs把值保存在MSR寄存器中. 由于操作MSR的指令为特权指令, 因此用户态下是无法修改的MSR

 

x86_64下处理系统调用

kernel初始化时, 调用arch/x86/kernel/s.c:syscall_init()对MSR进行初始化, 设置entry_SYSCALL_64为处理系统调用的入口点

由于有些指令entry_SYSCALL_64的任务可以分为三部分

  • 进入路径: 汇编实现, 目的是保存syscall的现场, 切换到内核态的执行环境, 创建一个适当的环境, 然后调用处理程序
  • 处理程序: C实现, 负责具体的处理工作
  • 退出路径: 汇编实现, 目的是从中断环境中退出, 切换到用户态, 恢复用户态的程序执行

进入路径部分:

  • 先通过swapgs指令切换到内核态的gs, 并保存用户态的gs
    • 这是一个特权指令, 但是CPU处理system指令时已经把CS的RPL设为00, 因此现在运行在内核态, 可以执行特权指令
  • 然后通过gs保存用户的rsp, 并找到内核态的rsp, 至此切换到内核态堆栈
  • 然后保存所有内核态会使用的寄存器到栈上

接下来涉及到slow_path和fast_path相关, 这只是一个优化, 其本质工作就是下面这条指令

sys_call_table是一个函数指针数组, 指向各个系统调用的处理函数

处理函数结束后会ret到entry_SYSCALL_64中, 进入退出路径部分, 这部分进行的工作为

  • 把处理函数的返回值写入到内核栈上的pt_regs中
  • 利用pt_regs结构恢复用户态的执行环境
  • 交换gs, 切回用户态的gs, 并把内核态的gs保存在MSR中
  • sysretq, 从syscall中退出

 

总结

i386:

x86_64

关闭