OS0x0

2025年11月15日 · 4998 字 · 10 分钟

总结一些OS知识点,也算是rcore这么久以来的总结以及期末复习参考。笔者能力有限不保证绝对正确(x)

basic

什么是操作系统

rcore中的描述是:

操作系统是一种系统软件,主要功能是向下管理CPU、内存和各种外设等硬件资源,并形成软件执行环境来向上管理和服务应用软件

操作系统本质上是一段运行在物理内存中的代码,特殊在:

硬件赋权 (Hardware-Enforced Privilege): 它运行在 Ring 0 (Kernel Mode),是CPU硬件给予的物理特权,只有处于这个模式,才能执行特权指令(如修改 CR3 寄存器切换页表、读写 MSR、执行 I/O 指令) yQXZhLEHVn1b3uC.png

  • 用户态:CPU 运行在 ring3 + 用户进程运行环境上下文

  • 内核态:CPU 运行在 ring0 + 内核代码运行环境上下文

    而开发者们创造了系统调用(syscall)这个机制,让用户态程序可以通过受控的方式切换到内核态,进而使用操作系统提供的各种服务

抽象的创造者:物理硬件是丑陋且有限的(有限的 RAM、复杂的磁盘扇区、单个 CPU 核心) 操作系统创造了完美的幻觉:

  • 每个进程都觉得拥有无限且连续的内存(虚拟内存)

  • 每个进程都觉得独占了 CPU(时间片调度)

  • 每个进程都觉得磁盘就是一连串字节流(文件系统)

进程

进程 = 运行环境上下文 + 内核数据结构(PCB)

运行环境上下文:

寄存器状态(包括指令指针、栈指针等)
虚拟地址空间(由页表定义)

内核数据结构:

task_struct(进程描述符)
包含PCB、权限凭证、调度信息等


执行上下文通过进程控制块(PCB)保存所有通用寄存器、指针作令和状态寄存器,而进程切换本质就是保存当前进程的执行上下文,加载下一个进程的执行上下文


每个进程都在内核态有一个独立的虚拟地址空间(内核栈)


不同进程的虚拟地址空间相互隔离
通过页表映射到不同的物理内存
实现内存保护和安全隔离

页表是cpu内存管理单元MMU能读懂的内存映射数据结构

操作系统通过维护页表来实现虚拟内存管理,页表记录了虚拟地址到物理地址的映射关系,当进程访问内存时,MMU会根据页表将虚拟地址转换为物理地址

每个进程拥有独立的页表集,不同进程的虚拟地址空间相互隔离,通过页表映射到不同的物理内存,实现内存保护和安全隔离

image-3.png

进程调度

由于需要运行的进程数量远大于CPU数量,不能让某几个进程占据所有CPU,操作系统通过进程调度算法来实现对CPU的合理分配

时钟中断(timer interrupt)是操作系统实现进程调度的关键机制,CPU定期触发时钟中断,操作系统在中断处理程序中保存当前进程的执行上下文,选择下一个进程并加载其执行上下文,从而实现进程切换

在进程调度时,我们保存当前进程的上下文到对应PCB中并取出的一个进程的PCB,从其中恢复带运行进程的上下文

内核

  • 控制并与硬件进行交互
  • 提供应用程式运行环境
  • 调度系统资源

image-31.png

微内核&宏内核&混合内核

维基百科

分级保护域

将计算机不同的资源划分至不同权限

x86-64 架构:CS (代码段) 寄存器的最低 2 位(RPL/CPL)

00 = Ring 0 (Kernel), 11 = Ring 3 (User)

RISC-V架构:CSR寄存器中的状态位(mstatus/sstatus),M-Mode (Machine, 固件级), S-Mode (Supervisor, 内核级), U-Mode (User, 用户级)

虚拟内存空间

计算机的虚拟内存地址空间通常被分为两块——供用户进程使用的用户空间(user space)与供操作系统内核使用的内核空间(kernel space),对于 Linux 而言,通常位于较高虚拟地址的虚拟内存空间被分配给内核使用,而位于较低虚拟地址的虚拟内存空间责备分配给用户进程使用

  • 不同进程的用户空间映射隔离
  • 所有进程共享相同的内核空间映射

运行状态切换与控制流转移

从用户态到内核态的三种途径

系统调用:主动请求内核服务

异常(Exception):非法操作(如除零、缺页等)

中断(Interrupt):外部硬件事件

syscall

用户把参数塞进寄存器(RISC-V a0-a7,x86 rdi, rsi…),把系统调用号塞进 rax/a7,然后执行特殊指令,CPU 硬件会自动跳转到在初始化时设置好的入口地址(stvec/MSR_LSTAR)

中断

中断本质是CPU控制流的强制转移机制

中断 (Interrupt)
├── 外部中断 (External Interrupt) - 硬中断
│   ├── 可屏蔽中断 (Maskable) - IF标志位控制
│   │   ├── 时钟中断 (Timer)
│   │   ├── 键盘中断 (Keyboard)
│   │   ├── 网卡中断 (Network)
│   │   └── 磁盘中断 (Disk I/O)
│   │
│   └── 不可屏蔽中断 (NMI) - 无法通过IF屏蔽
│       ├── 硬件故障
│       └── 内存错误
│
└── 内部中断 - 软中断/异常
    ├── 异常 (Exception) - 错误引发
    │   ├── 故障 (Fault) - 可恢复
    │   │   ├── 缺页异常 (#PF)
    │   │   └── 段错误 (#GP)
    │   │
    │   ├── 陷阱 (Trap) - 调试用
    │   │   ├── 断点异常 (#BP)
    │   │   └── 单步执行 (#DB)
    │   │
    │   └── 终止 (Abort) - 不可恢复
    │       └── 双重故障 (#DF)
    │
    └── 软件中断 (Software Interrupt)
        ├── int n 指令主动触发
        └── 系统调用 (int 0x80)

信号机制

image-32.png 袜SROP还在追我

过程①,内核会向进程发送一个signal(你可以把这个理解为中断信号),意思是接下来该进程被挂起,此刻由内核来接管

过程②,内核会保存该进程在用户态的上下文,并且跳到已经注册好的Signal Handler(信号处理器),当这个Signal Handler返回的时候,内核控制去传递了一串user-space code (用户层代码),这里翻译成用户层代码可能不是特别准确,我想表达的意思是,这就是一串实现函数功能的代码并且处于在了用户层,并且这部分代码被称作signal trampoline

过程③,它是在执行signal trampoline的过程

过程④,内核将恢复之前保存的上下文,并且最后恢复进程的执行

进程权限管理

kernel 调度着一切的系统资源,并为用户应用程式提供运行环境,相应地,应用程式的权限也都是由 kernel 进行管理的

每个进程有三个cred:

real_cred:客体凭证,进程的原始权限
cred:主体凭证,当前有效权限(内核以此判断权限)
ptracer_cred:使用ptrace系统调用跟踪该进程的上级进程的cred(gdb调试便是使用了这个系统调用,常见的反调试机制的原理便是提前占用了这个位置)

cred结构体:

//include/linux/cred.h
struct cred {
    atomic_t    usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
    atomic_t    subscribers;    /* number of processes subscribed */
    void        *put_addr;
    unsigned    magic;
#define CRED_MAGIC    0x43736564
#define CRED_MAGIC_DEAD    0x44656144
#endif
    kuid_t        uid;        /* real UID of the task */
    kgid_t        gid;        /* real GID of the task */
    kuid_t        suid;        /* saved UID of the task */
    kgid_t        sgid;        /* saved GID of the task */
    kuid_t        euid;        /* effective UID of the task */
    kgid_t        egid;        /* effective GID of the task */
    kuid_t        fsuid;        /* UID for VFS ops */
    kgid_t        fsgid;        /* GID for VFS ops */
    unsigned    securebits;    /* SUID-less security management */
    kernel_cap_t    cap_inheritable; /* caps our children can inherit */
    kernel_cap_t    cap_permitted;    /* caps we're permitted */
    kernel_cap_t    cap_effective;    /* caps we can actually use */
    kernel_cap_t    cap_bset;    /* capability bounding set */
    kernel_cap_t    cap_ambient;    /* Ambient capability set */
#ifdef CONFIG_KEYS
    unsigned char    jit_keyring;    /* default keyring to attach requested
                     * keys to */
    struct key    *session_keyring; /* keyring inherited over fork */
    struct key    *process_keyring; /* keyring private to this process */
    struct key    *thread_keyring; /* keyring private to this thread */
    struct key    *request_key_auth; /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
    void        *security;    /* subjective LSM security */
#endif
    struct user_struct *user;    /* real user ID subscription */
    struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
    struct group_info *group_info;    /* supplementary groups for euid/fsgid */
    /* RCU deletion */
    union {
        int non_rcu;            /* Can we skip RCU deletion? */
        struct rcu_head    rcu;        /* RCU deletion hook */
    };
} __randomize_layout;
真实用户ID(real UID):标识一个进程启动时的用户ID
保存用户ID(saved UID):标识一个进程最初的有效用户ID
有效用户ID(effective UID):标识一个进程正在运行时所属的用户ID,一个进程在运行途中是可以改变自己所属用户的,因而权限机制也是通过有效用户ID进行认证的,内核通过 euid 来进行特权判断;为了防止用户一直使用高权限,当任务完成之后,euid 会与 suid 进行交换,恢复进程的有效权限
文件系统用户ID(UID for VFS ops):标识一个进程创建文件时进行标识的用户ID

root的uid/gid均为0

进程权限改变

提权的本质:修改进程的cred结构体

I/O

万物皆文件,linux将一切都使用访问文件的方式进行操作

所有的读取操作都可以通过对文件进行 read 系统调用完成
所有的更改操作都可以通过对文件进行 write 系统调用完成
所有的配置操作都可以通过对文件进行 ioctl 系统调用完成

进程文件系统

进程文件系统(process file system, 简写为procfs)用以描述一个进程,其中包括该进程所打开的文件描述符、堆栈内存布局、环境变量等等

进程文件系统本身是一个伪文件系统,通常被挂载到/proc目录下,并不真正占用储存空间,而是占用一定的内存

当一个进程被建立起来时,其进程文件系统便会被挂载到/proc/[PID]下,我们可以在该目录下查看其相关信息

文件描述符

每个进程都有一个文件描述符表(file descriptor table),用以记录该进程所打开的文件描述符(file descriptor, 简写为fd)

0: stdin(标准输入)
1: stdout(标准输出)
2: stderr(标准错误)
3+: 后续打开的文件

文件描述符是进程级别的,不同进程的fd相互独立
内核维护全局文件表,fd是对该表的索引

ioctl系统调用

int ioctl(int fd, unsigned long request, ...);
fd:设备的文件描述符
request:请求码
其他参数:可选参数,取决于请求码

ioctl系统调用允许用户程序通过文件描述符与设备驱动程序进行交互,发送控制命令或获取设备状态信息

Loadable Kernel Module (LKM)

可加载内核模块(Loadable Kernel Module, 简写为LKM)是一种可以在操作系统运行时动态加载和卸载的内核代码模块

动态加载/卸载:运行时插拔
运行在内核空间:Ring 0权限
ELF格式:类似可执行文件
典型应用:设备驱动程序

常见命令:

lsmod:列出已加载的内核模块
insmod:插入内核模块
rmmod:移除内核模块
modinfo:显示模块信息

内核内存结构&管理

三级结构:页→区→节点

节点(Node) - pglist_data
└─ 区(Zone) - struct zone
    ├─ ZONE_DMA      # 用于DMA的低端内存
    ├─ ZONE_NORMAL   # 正常可用内存
    └─ ZONE_HIGHMEM  # 高端内存(32位系统)
        └─ 页(Page) - struct page
  • NUMA vs UMA:

    • UMA(统一内存访问):所有CPU访问内存延迟相同,只有1个节点
    • NUMA(非统一内存访问):每个CPU有本地内存,访问本地快、远程慢

双重内存分配器

伙伴系统 (Buddy System)

struct zone {
    //...
    struct free_area	free_area[MAX_ORDER];
    //...

在每个 zone 结构体中都有一个 free_area 结构体数组,用以存储 buddy system 按照 order 管理的页面

MAX_ORDER 为一个常量,值为 11

  • 粒度:以页(通常4KB)为单位
  • 机制:按2的幂次方管理连续页框
  • 优点:减少外部碎片
  • Order范围:0~11,对应1页到2048页

image-33.png

  • 分配:
    • 首先会将请求的内存大小向 2 的幂次方张内存页大小对齐,之后从对应的下标取出连续内存页
    • 若对应下标链表为空,则会从下一个 order 中取出内存页,一分为二,装载到当前下标对应链表中,之后再返还给上层调用,若下一个 order 也为空则会继续向更高的 order 进行该请求过程
  • 释放:
    • 将对应的连续内存页释放到对应的链表上
    • 检索是否有可以合并的内存页,若有,则进行合成,放入更高 order 的链表中,一路向上合并

SLAB分配器 (SLAB Allocator)

  • 粒度:以对象(object)为单位
  • 机制:从buddy system获取页面后切分成小对象
  • 优点:减少内部碎片,适合频繁分配/释放的小对象
  • 结构:
    kmem_cache (针对特定大小)
        ├─ kmem_cache_cpu (percpu,无锁快速路径)
        │   └─ 当前使用的slub
        └─ kmem_cache_node (所有CPU共享)
            ├─ partial链表 (部分使用的slub)
            └─ full链表   (完全分配的slub)
  • 优先从kmem_cache_cpu取(无锁,最快)
  • 其次从partial链表取
  • 最后向buddy system申请新页

Slab 的空闲块里存放着“下一个空闲块的地址”

保护机制

KASLR

原理:内核加载时添加随机偏移
局限:内核内部相对偏移不变,泄露一个地址即可计算所有地址
绕过:信息泄露漏洞

FGKASLR

原理:以函数粒度随机化内核代码布局
优势:即使泄露一个函数地址,也无法推断其他函数位置
代价:性能开销增加

STACK PROTECTOR

类似于用户态程序的 canary,通常又被称作是 stack cookie,用以检测是否发生内核堆栈溢出,若是发生内核堆栈溢出则会产生 kernel panic

内核中的 canary 的值通常取自 gs 段寄存器某个固定偏移处的值

SMAP/SMEP

  • SMEP (Supervisor Mode Execution Prevention):防止内核执行用户空间代码
  • SMAP (Supervisor Mode Access Prevention):防止内核访问用户空间数据
  • 实现:CR4寄存器第20位控制
  • 绕过方法:
    • kernel ROP修改CR4寄存器关闭SMEP
    • ret2dir:利用直接映射区访问

KPTI

  • 原理:用户态和内核态使用不同页表
    • 用户页表:映射用户空间+最小内核代码
    • 内核页表:完整映射
  • 代价:上下文切换性能损失(5-30%)

堆保护机制

Hardened Usercopy

保护对象:用户空间↔内核空间的数据拷贝
检查内容:是否越界读写
局限:不保护内核内部拷贝
绕过:使用内核到内核的拷贝函数

Hardened Freelist

原始: next指针 = 下一个free object地址
加固: next指针 = 当前地址 XOR 下一个地址 XOR random值

Random Freelist

原理:新slub的object链接顺序随机化
注意:仅在初始化时随机,运行时仍遵循LIFO

CONFIG_INIT_ON_ALLOC

效果:分配时清零内存
目的:防止未初始化内存泄露
代价:1-7%性能损失

Stack Protector

机制:在栈上放置canary
来源:GS段寄存器某固定偏移处
触发:检测到canary被破坏时kernel panic

参考