当前位置: 首页 > news >正文

Linux Kernel 4

系统调用(System Calls)


一、Linux 系统调用实现(System Call Implementation)

在这里插入图片描述

在宏观层面,系统调用是内核向用户程序提供的“服务”接口,其形式类似于库函数调用:

  • 具有函数名、参数列表和返回值;
  • 例如: r e a d ( ) read() read() w r i t e ( ) write() write() o p e n ( ) open() open()

📌 但本质上,系统调用不是普通函数调用,而是特定的汇编指令(依赖于架构与内核实现),它们完成以下操作:

  1. 设置标识系统调用及其参数的信息;
  2. 触发从用户态到内核态的模式切换;
  3. 在内核中处理系统调用,并返回结果。

二、系统调用参数传递机制

在 Linux 中:

  • 每个系统调用通过一个编号system call number来唯一标识;
  • 系统调用参数的数量最多为 6 个;
  • 所有参数及编号均通过寄存器传递,且每个参数为一个机器字大小(32 或 64 位)。

示例:32 位 x86 架构

内容所用寄存器
系统调用编号 E A X EAX EAX
参数 1 ~ 参数 6 E B X , E C X , E D X , E S I , E D I , E B P EBX, ECX, EDX, ESI, EDI, EBP EBX,ECX,EDX,ESI,EDI,EBP

三、系统调用与用户态接口(libc)

系统调用较底层,因此标准库(如 l i b c libc libc)提供了包装函数,使用户应用更容易使用:

例如:

// 实际调用 read 系统调用
ssize_t read(int fd, void *buf, size_t count);

该函数最终会触发底层汇编,向内核请求服务。

三、用户态到内核态的切换流程

当用户程序调用系统调用时:

  • 当前执行流会被中断;

  • 跳转至内核预定义的系统调用入口点(entry point);

  • 执行流程如下:

用户态执行
  ↓
系统调用触发(如 int 0x80 或 syscall 指令)
  ↓
切换至内核态
  ↓
保存用户寄存器状态(包括系统调用编号和参数)
  ↓
调用系统调用分发器(dispatcher)
  ↓
进入对应的内核服务函数
💡 这种切换方式类似于中断/异常机制(某些架构中确实是通过异常触发)。

📌 栈的切换

在用户态到内核态的切换过程中:

  • 栈也会从用户栈切换到内核栈(kernel stack);

  • 这是为了保护内核空间,避免用户栈被恶意或错误修改;

  • 内核为每个进程准备了单独的内核栈,见中断与异常章节。

在这里插入图片描述

系统调用流程分析图(以 dup2 为例)

如图, Linux 32 位架构下,用户态程序通过 libc 触发 dup2() 系统调用后,如何通过 int $0x80 中断进入内核,并最终完成系统调用处理


1️⃣ 应用层(Application)

用户程序调用 $dup2(oldfd, newfd)$,这是标准的 POSIX API,用于复制文件描述符。


2️⃣ C 函数库(libc)

在标准库中(如 glibc),dup2 被封装为一个普通的 C 函数。在汇编层面,做了如下工作:

C7592 movl 0x8(%esp),%ecx   ; 将第2个参数 newfd 放入寄存器 %ecx
C7596 movl 0x4(%esp),%ebx   ; 将第1个参数 oldfd 放入寄存器 %ebx
C759a movl $0x3f,%eax       ; 设置系统调用号 eax = 0x3f(即 dup2)
C759f int $0x80             ; 执行软中断,触发系统调用

🧠 int $0x80 是 32 位 Linux 中用于触发系统调用的中断号。

3️⃣ 内核入口点(Kernel Entry)

内核中断处理程序的入口函数是 entry_INT80_32:

ENTRY(entry_INT80_32)
ASM_CLAC                     ; 清除 AC 标志位,防止用户空间访问 SMAP 内存
pushl %eax                   ; 保存 eax
SAVE_ALL pt_regs_ax $ENOSYS ; 保存全部寄存器到内核栈(构造 pt_regs)
movl %esp,%eax               ; 将当前栈顶(pt_regs 结构)传给 eax
call do_int80_syscall_32    ; 跳转至系统调用分发函数
RESTORE_REGS 4              ; 恢复寄存器,跳过 orig_eax 和 error_code
INTERRUPT_RETURN            ; 从中断返回,回到用户态

🧠 在进入内核时,内核使用专用的内核栈,并通过 SAVE_ALL 宏保存用户态寄存器状态。

系统调用调度器(System Call Dispatcher)


🎯 系统调用调度器的主要任务是:

  • 检查系统调用号是否合法;
  • 调用与该系统调用号对应的内核函数(即执行实际的内核服务逻辑)。

🔧 内核实现分析(int 0 x 80 0x80 0x80 调用路径)

以下为处理 $int $0x80$ 的简化代码流程:

1️⃣ 内核入口函数:do_int80_syscall_32

__visible void do_int80_syscall_32(struct pt_regs *regs)
{
    enter_from_user_mode();   // 切换用户态到内核态的准备
    local_irq_enable();       // 开启中断
    do_syscall_32_irqs_on(regs); // 执行系统调用调度器
}

2️⃣ 简化调度器函数:do_syscall_32_irqs_on

static __always_inline void do_syscall_32_irqs_on(struct pt_regs *regs)
{
    unsigned int nr = regs->orig_ax;   // 获取系统调用号

    if (nr < IA32_NR_syscalls) {
        regs->ax = ia32_sys_call_table[nr](regs->bx, regs->cx,
                                           regs->dx, regs->si,
                                           regs->di, regs->bp);
    }
    syscall_return_slowpath(regs); // 返回用户态处理(含错误检查)
}

系统调用表(System Call Table)

系统调用表是系统调用调度器(dispatcher)使用的核心数据结构,用于将系统调用编号映射到对应的内核函数实现

在 Linux 32 位(x86)内核中,这张表定义如下:

#define __SYSCALL_I386(nr, sym, qual) [nr] = sym,

const sys_call_ptr_t ia32_sys_call_table[] = {
  [0 ... __NR_syscall_compat_max] = &sys_ni_syscall,
  #include <asm/syscalls_32.h>
};

上述代码中:

ia32_sys_call_table[] 是一个函数指针数组;

  • 每个数组下标 nr 对应一个系统调用号;

  • 数组中初始用 sys_ni_syscall 填充所有项,表示默认“不支持该调用”;

  • 然后通过宏 __SYSCALL_I386(nr, sym, qual) 替换特定编号为具体函数,如:

__SYSCALL_I386(0, sys_restart_syscall)
__SYSCALL_I386(1, sys_exit)
__SYSCALL_I386(2, sys_fork)
__SYSCALL_I386(3, sys_read)
__SYSCALL_I386(4, sys_write)

当用户程序执行如 $int 0 x 80 0x80 0x80 并传入系统调用号 e a x = 3 eax=3 eax=3(即 read()),内核就会通过:

ia32_sys_call_table[3](arg1, arg2, ..., arg6);

来调用 s y s _ r e a d sys\_read sys_read 函数。

另外,根据架构配置不同,同一个调用号可能映射到不同函数。例如:

#ifdef CONFIG_X86_32
__SYSCALL_I386(5, sys_open)
#else
__SYSCALL_I386(5, compat_sys_open)
#endif

系统调用参数处理(System Call Parameters Handling)

系统调用参数的处理是一个非常棘手且关键的问题。由于这些参数由用户空间传入,内核不能假设它们是合法的,因此必须进行严格验证,尤其是指针类型的参数。


📌 指针参数的特殊检查要求:

  • 绝不能允许指针指向内核空间(kernel space)
  • 必须检测无效指针(如未映射地址、只读内存等)

由于系统调用在内核态执行,拥有对整个内核地址空间的访问权限,因此如果未正确检查用户提供的指针,可能造成严重的安全问题或系统崩溃。


🚨 示例:read/write 系统调用中的漏洞风险

  • 如果用户向 $write$ 传入一个指向内核空间的指针,系统会把内核数据写到用户可读的文件中,从而泄露内核信息
  • 如果用户向 $read$ 传入内核空间指针,读取的数据将写入该区域,可能破坏内核内存

🚧 无效指针的危害

  • 用户传入的指针如果指向:
    • 未映射内存
    • 只读页面但用于写操作
    • 空指针
      都可能导致内核崩溃或异常行为。

✅ 两种处理策略:

  1. 主动检查法:在使用指针前,对其进行地址空间范围验证(是否在用户空间)
  2. 延迟触发法:不主动检查,而是依赖 MMU 产生页错误(page fault),然后通过异常处理器判断是否非法

🧠 页错误处理机制(Fault Handling)

尽管延迟触发法实现更复杂,但 Linux 更倾向于使用该方式,并配合以下机制提升健壮性和安全性:

  • 所有访问用户空间的函数都使用专门的 API,如 $copy_to_user()$$copy_from_user()$
  • 内核会记录访问用户空间的精确指令地址,保存在异常表exception table中;
  • 当发生页错误时,内核检查该地址是否存在于异常表中,决定是否是用户空间访问导致的错误;
  • 如果命中异常表,则认为是“受控”的合法页错误,否则可能是内核 bug。

📊 成本对比分析

情况主动检查法(地址范围)延迟触发法(异常表)
有效地址地址空间查找成本几乎为 0
无效地址地址空间查找异常表查找(略慢)

✅ 为什么 Linux 采用延迟触发策略?

虽然异常处理路径略复杂,但:

  • 常见路径中(即参数有效时)性能开销非常低;
  • 减少每次调用前的地址空间遍历;
  • 更加通用、集中、可维护,适用于多种系统调用场景;
  • 因此是 Linux 内核中处理用户空间指针的首选策略

虚拟动态共享对象(VDSO:Virtual Dynamic Shared Object)

VDSO 机制的设计初衷是优化系统调用的性能,同时避免让 $libc$ 必须同时考虑 CPU 功能与内核版本的匹配问题。


📌 背景:x86 系统调用的两种方式

$x86$ 架构中,有两种主要的系统调用触发方式:

  1. $int\ 0x80$:传统方法,兼容性强,但性能较低;
  2. $sysenter$:新方法,性能显著更好,但仅在 Pentium II 之后的处理器与 $Linux\ kernel\ >2.6$ 中才可用。

为了根据实际情况动态选择调用方式,同时不影响 $libc$ 的实现与维护,Linux 引入了 VDSO 机制。


⚙️ VDSO 的工作原理

  • 内核在用户地址空间的高地址区域映射一块特殊内存,该内存区域:
    • 由内核动态生成;
    • 以 ELF 格式组织;
    • 类似共享库(shared object),但不依赖磁盘文件;
  • $libc$ 在运行时会搜索 VDSO 页面,如果找到:
    • 将使用其中的汇编指令流来执行系统调用;
    • 从而避开 $int\ 0x80$ 等传统中断路径,提高调用效率。

🚀 VDSO 的扩展:虚拟系统调用(vsyscall)

VDSO 的一个重要演进是虚拟系统调用机制(vsyscall),它允许某些“系统调用”完全在用户态执行,无需切换到内核态。

✅ vsyscall 的特性:

  • 是 VDSO 的一部分;
  • 访问的数据来自 VDSO 页面中的:
    • 静态数据区(如 $getpid()$
    • 动态更新区域(如 $gettimeofday()$$time()$),这些区域由内核维护一个独立的读写映射页。

🧠 举例说明:

  • $getpid()$
    • 返回当前进程的 PID;
    • 该值在进程创建时就确定,不变,因此可通过 VDSO 提供的只读静态值直接获取;
  • $gettimeofday()$$time()$
    • 由于涉及到实时信息,数据由内核定期更新;
    • 用户程序读取的是映射到 VDSO 的“共享内存”区域,效率远高于真正发起系统调用。

从系统调用中访问用户空间(Accessing User Space from System Calls)

在内核中访问用户空间是非常敏感的操作,必须通过专门的 API 函数来完成,如: g e t _ u s e r ( ) get\_user() get_user() p u t _ u s e r ( ) put\_user() put_user() c o p y _ f r o m _ u s e r ( ) copy\_from\_user() copy_from_user() c o p y _ t o _ u s e r ( ) copy\_to\_user() copy_to_user()。这些 API 会检查用户传入的指针是否落在用户空间,并在指针无效时优雅失败(返回非零值),从而避免内核崩溃。

例如:

// 正确做法:若 user_ptr 非法则返回 -EFAULT
if (copy_from_user(&kernel_buffer, user_ptr, size))
    return -EFAULT;

// 错误做法:若 user_ptr 非法直接崩溃内核
memcpy(&kernel_buffer, user_ptr, size);

🧱 get_user 简要实现(x86 架构)

get_user 使用内联汇编实现,根据变量大小自动选择调用 _ g e t _ u s e r _ 1 \_get\_user\_1 _get_user_1 _ _ g e t _ u s e r _ 2 \_\_get\_user\_2 __get_user_2 _ _ g e t _ u s e r _ 4 \_\_get\_user\_4 __get_user_4

#define get_user(x, ptr) ({ \
  int __ret_gu; \
  register __inttype(*(ptr)) __val_gu asm("%"_ASM_DX); \
  __chk_user_ptr(ptr); \
  might_fault(); \
  asm volatile("call __get_user_%P4" \
    : "=a" (__ret_gu), "=r" (__val_gu), \
      ASM_CALL_CONSTRAINT \
    : "0" (ptr), "i" (sizeof(*(ptr)))); \
  (x) = (__force __typeof__(*(ptr))) __val_gu; \
  __builtin_expect(__ret_gu, 0); \
})

其等效伪代码如下:

movl ptr, %eax        // 将用户指针加载到 eax
call __get_user_1     // 调用访问函数
movl %edx, x          // 结果保存至变量 x
movl %eax, result     // 保存返回码

🧠 __get_user_1 实现逻辑(x86)

ENTRY(__get_user_1)
    mov PER_CPU_VAR(current_task), %edx
    cmp TASK_addr_limit(%edx), %eax
    jae bad_get_user
    ASM_STAC
1:  movzbl (%eax), %edx
    xor %eax, %eax
    ASM_CLAC
    ret

bad_get_user:
    xor %edx, %edx
    mov $(-EFAULT), %eax
    ASM_CLAC
    ret
ENDPROC(__get_user_1)

解释如下:

  • 检查当前进程的 a d d r _ l i m i t addr\_limit addr_limit 是否大于用户传入指针地址 e a x eax eax,防止指针越界访问内核空间;

  • 调用 A S M _ S T A C ASM\_STAC ASM_STAC(开启用户空间访问权限,解除 SMAP 限制);

  • 使用 m o v z b l movzbl movzbl 指令从用户空间读取数据;

  • 若读取成功,返回值存于 e d x edx edx,将 e a x eax eax 置零表示成功;

  • 若非法访问则跳转到 b a d _ g e t _ u s e r bad\_get\_user bad_get_user,返回 − E F A U L T -EFAULT EFAULT 表示出错

🧾 异常表机制(Exception Table)

对于访问用户空间的指令,内核通过“异常表”机制保证安全:

# define _ASM_EXTABLE_HANDLE(from, to, handler) \
  .pushsection "__ex_table","a"; \
  .balign 4; \
  .long (from) - .; \
  .long (to) - .; \
  .long (handler) - .; \
  .popsection
  • 每个访问用户空间的指令地址(如 movzbl)被标记为异常表项的 from;

  • 如果访问失败,将跳转至 to 地址并调用 handler;

  • 所有异常表项集中保存在 _ _ e x _ t a b l e \_\_ex\_table __ex_table section;

  • 链接脚本通过 _ _ s t a r t _ _ _ e x _ t a b l e \_\_start\_\_\_ex\_table __start___ex_table _ _ s t o p _ _ _ e x t a b l e \_\_stop\_\_\_ex_table __stop___extable 定位整张表。

🛠️ 异常处理函数示例

bool ex_handler_default(const struct exception_table_entry *fixup,
                        struct pt_regs *regs, int trapnr)
{
    regs->ip = ex_fixup_addr(fixup); // 设置跳转地址
    return true;
}

当用户空间访问失败触发页错误时:

  • f i x u p _ e x c e p t i o n fixup\_exception fixup_exception 函数会查询 _ _ e x _ t a b l e \_\_ex\_table __ex_table

  • 找到对应项后跳转至 b a d _ g e t _ u s e r bad\_get\_user bad_get_user

  • 返回 − E F A U L T -EFAULT EFAULT,避免内核崩溃。

相关文章:

  • spring-boot nacos
  • deepin使用autokey添加微信快捷键一键显隐ctrl+alt+w
  • CExercise_12_单链表面试题_1求链表中间结点的值,判断单链表是否有环
  • 代码随想录训练营第31天 || 56. 合并区间 738. 单调递增的数字
  • gitee基本使用
  • Shell编程之循环语句
  • 【前端样式】使用Flexbox实现经典导航栏:自适应间距与移动端折叠实战
  • MATLAB基本数据类型
  • 如何一键自动提取CAD图中的中心线(如墙体、道路、巷道中心线等)
  • Android常见界面控件、程序活动单元Activity练习
  • LeetCode算法题(Go语言实现)_46
  • 3.2.2.3 Spring Boot配置拦截器
  • C++学习之数据库操作
  • AI日报 - 2025年4月15日
  • 华为OD机试真题——阿里巴巴找黄金宝箱 IV(2025A卷:200分)Java/python/JavaScript/C++/C语言/GO六种最佳实现
  • 子串-滑动窗口的最大值
  • 科研软件分享
  • AI agents系列之全从零开始构建
  • 批处理(Batch Processing)的详解、流程及框架/工具的详细对比
  • 前端工程化之自动化构建
  • 持续更新丨伊朗官员:港口爆炸事件已致5人死亡
  • 甘肃张掖至重庆航线开通,串起西北与西南文旅“黄金走廊”
  • 上海虹桥至福建三明直飞航线开通,飞行时间1小时40分
  • “五一”假期云南铁路预计发送旅客超330万人次
  • 三部门提出17条举措,全力促进高校毕业生等青年就业创业
  • 韩国首尔江南区一大型商场起火,消防部门正在灭火中