读薄《Linux 内核设计与实现》(3) - 系统调用

这篇文章是《读薄「Linux 内核设计与实现」》系列文章的第 III 篇,本文主要讲了以下问题:系统调用的概念、系统调用的实现原理与过程以及如何在 Linux 中增加一个系统调用。

0x00 系统调用的概念

系统调用是为了和用户空间上的进程进行交互,内核提供的一组界面。

  • 应用程序通过这组界面访问硬件和其他操作系统资源

  • 完成对硬件和资源的访问控制

  • 硬件设备的抽象(提供设备的独立性)

0x01 系统调用简介

I 常用系统调用

  • fork(), exec(), open(), read(), write(), close(),……

  • 目前 Linux 系统调用 300 多个

II 应用程序及系统调用的层次关系

应用程序通过在用户空间实现的 API 而不是直接通过系统调用来编程

例:调用 printf() 函数时,应用程序、C 库和内核的关系:
应用程序调用 printf() -> C 库中的 printf() -> C 库中的 write() -> 内核中的 write() 系统调用

0x02 Linux 系统调用实现原理

I 相关概念

  • int 80H:软中断,通知内核的机制是靠软中断实现的,第128号中断处理程序

  • IVT(Interrupt Vector Table):中断向量表,包括所有中断程序入口地址,它固定存放于内存中(实模式下应用)

  • IDT(Interrupt Descriptor Table):中断描述符表,不固定内存位置,通过 IDTR 寄存器定位该表(保护模式下应用,int 80H 占据其中一项)

  • syscall table:系统调用表

  • 系统调用号: 在 Linux 中,每个系统调用被赋予一个系统调用号,表示它在表中的编号

II 系统调用的加载

操作系统在加载时做的有关系统调用的加载:

  • int 80H 处理程序地址的加载:start_kernel()中的 trap_init()和 set_system_gate()

  • 各系统调用处理程序的加载(entry.s)

III 系统调用过程(以 x86 为例)

首先,通过软中断陷入到 int 80h 中断中,促使系统切换到内核态去执行异常处理程序(系统调用处理程序);之后,系统通过读取 eax 寄存器的值来获取系统调用号;之后,系统通过读取寄存器来获取传递的参数(ebx, ecx, edx, esi, edi)按照顺序存放前五个参数,如果参数为6个或以上,则将其中一个寄存器的值指向内存空间;最后,执行相应系统调用代码,完成系统调用

IV 系统调用的参数验证

系统调用必须仔细检查他们所有参数是否合法有效,如果用户将不合法的参数传递给内核,那么系统的安全和稳定将面临极大考验。

  • 权限验证:系统调用的调用者可以使用 capable() 函数来检查是否有权能对制定的资源进行操作

  • 指针合法性验证:在接受一个用户空间的指针之前,内核需要验证:

    • 指针指向的内存区域属于用户空间
    • 指针指向的内存区域在进程的地址空间里
    • 如果是读,该内存应被标记为可读;如果是写,该内存应被标记为可写;如果是可执行,进程决不能绕过内存访问限制

0x03 如何增加一个系统调用

  • 增加系统调用函数(/kernel/sys.c)

  • 把系统调用函数入口添加到 sys_call_table(entry.s)

  • 添加系统调用号

0x04 系统调用的意义

  • 它为用户提供了一种硬件的抽象接口

  • 在保证系统稳定和安全的前提下提供服务,避免应用程序恣意横行


本文的版权归作者 罗远航 所有,采用 Attribution-NonCommercial 3.0 License。任何人可以进行转载、分享,但不可在未经允许的情况下用于商业用途;转载请注明出处。感谢配合!