Linux 系统执行与隔离体系
├─ 1. 用户态进入内核
│ ├─ syscall 追踪
│ ├─ io_uring 提交项
│ ├─ epoll 边缘触发
│ └─ netlink 套接字
│
├─ 2. 内核观测与动态插桩
│ ├─ eBPF 钩子
│ └─ kprobes 动态插桩
│
├─ 3. 资源控制与安全隔离
│ ├─ cgroups v2 层级
│ ├─ 命名空间隔离
│ └─ SELinux 上下文
│
├─ 4. 文件系统对象
│ └─ inode 吊销 / 失效 / 回收
│
├─ 5. 内存管理
│ ├─ 页表遍历
│ ├─ TLB 刷新
│ ├─ 缺页中断
│ ├─ OOM killer 策略
│ └─ ASLR 布局随机化
│
├─ 6. 调度与并发
│ ├─ 上下文切换
│ └─ RCU 宽限期
│
└─ 7. ELF 二进制与动态链接
└─ GOT / PLT 表项
0. 先建立一条完整运行路径
一个普通 Linux 程序大致这样运行:
用户进程
├─ 调用 libc 函数,例如 read(), write(), socket()
│ └─ 进入 syscall
│ └─ 内核检查权限、namespace、cgroup、SELinux
│ ├─ 访问文件:VFS → dentry → inode → page cache / block device
│ ├─ 访问网络:socket → TCP/IP stack → netlink / eBPF / tc / XDP
│ ├─ 访问内存:页表遍历 → TLB → 缺页中断 → 回收 / OOM
│ └─ 被调度:上下文切换 / RCU / 锁 / 抢占
└─ 被观测:strace / perf / ftrace / eBPF / kprobes
cgroup 用来控制“你能用多少资源”,namespace 用来控制“你能看到什么世界”,SELinux 用来控制“你即使看得到也是否允许访问”。Linux 官方文档把 cgroup 描述为一种把进程组织成层级并沿层级分配资源的机制;namespace 文档则列出 Linux 支持的多种隔离类型。(Kernel Documentation)
1. eBPF 钩子
1.1 它是什么
eBPF 钩子
├─ eBPF:运行在内核中的受限小程序
├─ 钩子 hook:内核执行到某个事件点时触发
├─ verifier:加载前检查安全性
├─ map:用户态和 BPF 程序共享数据
└─ attach type:决定挂到哪里
eBPF 的核心思想是:不改内核源码、不写传统内核模块,也能把一段受限程序挂到内核路径上执行。挂载点可以是 tracepoint、kprobe、uprobes、LSM hook、cgroup hook、XDP、tc、socket 等。Linux BPF 文档列出了不同 program type、attach type 和 ELF section 名称,bpf() syscall 文档也说明 eBPF 程序可以附着到指定 attach type 的 hook 上。(Kernel Documentation)
1.2 递归拆解
eBPF 程序
├─ 输入:内核事件上下文
│ ├─ 网络包上下文
│ ├─ syscall tracepoint 上下文
│ ├─ kprobe 函数参数
│ └─ LSM 安全检查上下文
├─ 执行环境
│ ├─ 受限指令集
│ ├─ verifier 静态检查
│ ├─ helper function
│ └─ JIT 编译成本地机器码
├─ 状态保存
│ ├─ hash map
│ ├─ array map
│ ├─ ring buffer
│ └─ per-cpu map
└─ 输出
├─ 计数
├─ 事件日志
├─ 丢包 / 放行
├─ 安全拒绝
└─ 性能剖析数据
1.3 典型用途
观测类
├─ 统计某个 syscall 调用次数
├─ 跟踪 TCP 连接延迟
├─ 找出慢 I/O
└─ 采样 CPU 火焰图
网络类
├─ XDP 早期丢包
├─ tc 层流量整形
├─ socket 级别策略
└─ cgroup 网络访问控制
安全类
├─ LSM hook
├─ execve 审计
├─ 文件访问审计
└─ 容器逃逸检测
1.4 常见坑
常见误解
├─ “eBPF 可以任意访问内核”
│ └─ 不行,verifier 和 helper 限制很强
├─ “eBPF 没有开销”
│ └─ 有,只是通常比用户态轮询/ptrace 更低
├─ “所有内核都支持同样功能”
│ └─ 不同 kernel 版本、config、BTF 支持差异很大
└─ “eBPF 等于 kprobe”
└─ kprobe 只是 eBPF 可挂载的一类观测点
2. io_uring 提交项
2.1 它是什么
io_uring
├─ SQ:Submission Queue,提交队列
├─ SQE:Submission Queue Entry,提交项
├─ CQ:Completion Queue,完成队列
└─ CQE:Completion Queue Entry,完成项
io_uring_setup() 会创建提交队列和完成队列,并把它们作为用户态和内核共享的 ring buffer;程序把 I/O 请求写成 SQE 放到 SQ,内核处理完成后把结果放到 CQE。man page 明确说明 SQ 和 CQ 是用户态与内核共享的队列,SQE 描述类似 read、write、accept 等操作,CQE 返回结果。(man7.org)
2.2 递归拆解
一个 SQE
├─ opcode:要做什么
│ ├─ READ
│ ├─ WRITE
│ ├─ ACCEPT
│ ├─ SEND / RECV
│ ├─ TIMEOUT
│ └─ FSYNC
├─ fd:操作哪个文件描述符
├─ buffer:读写缓冲区
├─ offset:文件偏移
├─ flags:链式、固定缓冲区、跳过成功 CQE 等
└─ user_data:用户自定义标识,CQE 会带回来
2.3 执行路径
用户态
├─ 准备 SQE
├─ 放入 SQ tail
├─ io_uring_enter() 通知内核
│
内核态
├─ 从 SQ head 取出 SQE
├─ 执行对应 I/O
├─ 可能异步 offload 到 worker
└─ 完成后写入 CQE
│
用户态
└─ 从 CQ 读取 CQE,检查 res 字段
需要注意:提交顺序不等于完成顺序。io_uring 文档明确提醒,I/O 请求可以以任意顺序完成,因此通常要靠 user_data 把提交和完成对应起来。(man7.org)
2.4 和传统 syscall 的区别
传统 read()
├─ 每次 read 都陷入内核
├─ 同步等待结果
└─ syscall 数量多时上下文切换成本高
io_uring
├─ 批量提交多个 SQE
├─ 批量收割 CQE
├─ 用户态 / 内核态共享 ring
└─ 可减少 syscall 和数据结构复制成本
3. cgroups v2 层级
3.1 它是什么
cgroups v2
├─ cgroup:控制组
├─ hierarchy:树形层级
├─ controller:资源控制器
├─ process:进程归属到某个 cgroup
└─ resource policy:资源限制和统计
cgroup v2 的关键点是 统一层级。Linux 文档说明 cgroup 把进程组织成层级,并沿层级以可配置方式分配资源;每个进程属于一个 cgroup,控制器启用后会影响对应子树中的进程。(Kernel Documentation)
3.2 递归拆解
cgroup v2 树
/
├─ system.slice
│ ├─ sshd.service
│ └─ docker.service
├─ user.slice
│ └─ user-1000.slice
└─ machine.slice
└─ container scope
常见 controller
├─ cpu
│ ├─ cpu.max:CPU 配额
│ └─ cpu.weight:相对权重
├─ memory
│ ├─ memory.max:硬限制
│ ├─ memory.high:软压力限制
│ └─ memory.current:当前使用
├─ io
│ └─ io.max / io.weight
├─ pids
│ └─ pids.max:进程数限制
└─ cpuset
├─ 可用 CPU 集合
└─ 可用 NUMA 节点集合
3.3 典型路径
进程申请内存
├─ 内核查当前 task 所属 cgroup
├─ memory controller 计费
├─ 未超过 memory.max
│ └─ 分配成功
└─ 超过 memory.max
├─ 尝试回收
├─ 回收失败
└─ 触发 cgroup 内 OOM
3.4 和容器的关系
容器
├─ namespace:让容器看到独立世界
├─ cgroup:限制容器资源
├─ SELinux/AppArmor/seccomp:限制访问能力
└─ overlayfs/rootfs:提供文件系统视图
cgroups 不是“隔离视图”,而是“资源控制”。namespace 才主要负责“进程看到的世界不同”。
4. 命名空间隔离
4.1 它是什么
namespace
├─ 隔离进程看到的系统资源视图
└─ 同一个内核上制造多个“局部世界”
Linux namespace 文档列出了不同 namespace 类型,并说明它们隔离不同系统资源。(man7.org)
4.2 递归拆解
namespace 类型
├─ PID namespace
│ └─ 隔离进程号视图,容器里可以看到 PID 1
├─ mount namespace
│ └─ 隔离挂载点视图
├─ network namespace
│ ├─ 独立网卡
│ ├─ 独立路由表
│ ├─ 独立 iptables/nftables 视图
│ └─ 独立 socket namespace
├─ UTS namespace
│ └─ 隔离 hostname / domainname
├─ IPC namespace
│ └─ 隔离 System V IPC / POSIX message queue
├─ user namespace
│ └─ 隔离 UID/GID 映射
├─ cgroup namespace
│ └─ 隔离进程看到的 cgroup 路径
└─ time namespace
└─ 隔离某些时钟偏移视图
4.3 容器启动时大概发生什么
clone/unshare/setns
├─ 创建新的 namespace
├─ 设置 rootfs / mount
├─ 配置 veth / network namespace
├─ 设置 UID/GID map
├─ 加入 cgroup
└─ exec 容器 init 进程
4.4 常见误解
namespace 隔离 ≠ 安全边界完整
├─ namespace 只是视图隔离
├─ 仍共享同一个内核
├─ 内核漏洞可能跨 namespace
└─ 需要配合 cgroup、capability、seccomp、SELinux
5. SELinux 上下文
5.1 它是什么
SELinux context
├─ user
├─ role
├─ type
└─ level
SELinux 给进程和对象打安全标签,并根据策略做强制访问控制。Red Hat 文档中的示例展示了 user:role:type:level 形式,并说明 SELinux 策略检查发生在传统 DAC 权限之后;文件上下文可以通过 chcon、semanage fcontext、restorecon 等工具管理。(Red Hat Documentation)
5.2 递归拆解
system_u:object_r:httpd_sys_content_t:s0
├─ system_u
│ └─ SELinux user
├─ object_r
│ └─ role,对文件对象通常是 object_r
├─ httpd_sys_content_t
│ └─ type,最关键,决定访问策略
└─ s0
└─ MLS/MCS level
5.3 访问判断路径
进程 httpd_t 访问文件 httpd_sys_content_t
├─ 先检查 Unix DAC
│ ├─ UID
│ ├─ GID
│ └─ mode bits
├─ DAC 允许后再检查 SELinux
│ ├─ subject type:进程类型
│ ├─ object type:文件类型
│ ├─ class:file / dir / socket 等
│ └─ permission:read / write / execute
└─ policy 允许才真正放行
5.4 常见问题
权限明明 777 但还是 Permission denied
├─ DAC 允许
└─ SELinux 拒绝
├─ 文件 type 不对
├─ 进程 domain 不对
├─ boolean 未开启
└─ MCS label 不匹配
6. inode 吊销 / 失效 / 回收
6.1 先纠正术语
“inode 吊销”不是一个特别统一的 Linux 官方中文术语。更常见的相关概念是:
inode 生命周期变化
├─ inode lookup:查找 inode
├─ inode cache:缓存 inode
├─ inode invalidation:让缓存状态失效
├─ inode eviction:从 inode cache 回收
├─ stale file handle:旧句柄失效
└─ revocation:撤销旧引用或旧权限语义
VFS 文档说明,路径查找会通过 dentry 找到 inode,然后 open、stat 等操作基于 inode 元数据继续执行;文件系统 API 文档也描述了 inode cache 中查找 inode 的接口。(Kernel Documentation)
6.2 inode 是什么
inode
├─ 文件类型
├─ 权限 mode
├─ owner UID/GID
├─ size
├─ timestamps
├─ link count
├─ block mapping / extent
└─ inode operations
注意:inode 不直接等于文件名。
文件名
└─ dentry
└─ inode
└─ 文件元数据和数据块映射
硬链接就是多个 dentry 指向同一个 inode。
6.3 “吊销”可能对应的场景
场景 A:unlink 一个正在打开的文件
├─ 目录项消失
├─ link count 下降
├─ 已打开 fd 仍持有 file/inode 引用
└─ 最后一个 fd 关闭后 inode 才可真正释放
场景 B:远程文件系统句柄失效
├─ 客户端缓存了 inode/file handle
├─ 服务端文件被删除或替换
└─ 客户端再次访问得到 stale handle
场景 C:权限或安全标签变化
├─ chmod/chown/chcon 改变元数据
├─ 旧访问假设失效
└─ 后续访问需重新检查
场景 D:内存压力下 inode cache 回收
├─ inode 不再活跃
├─ shrinker 回收缓存
└─ 后续访问重新 lookup
6.4 核心理解
inode 吊销不是“删除一个文件名”
而是“让某个 inode 相关的引用、缓存、权限或句柄不再按旧状态继续有效”
7. syscall 追踪
7.1 它是什么
syscall tracing
├─ 观察进程调用了哪些系统调用
├─ 观察参数
├─ 观察返回值
└─ 观察错误码 errno
ptrace() man page 说明,一个 tracer 可以观察和控制另一个 tracee 的执行,检查和修改其内存与寄存器,并常用于断点调试和系统调用追踪。(man7.org)
7.2 递归拆解
syscall
├─ 用户态 ABI
│ ├─ syscall number
│ ├─ 参数寄存器
│ └─ 返回值寄存器
├─ 入口
│ ├─ syscall instruction
│ ├─ CPU 切到内核态
│ └─ 进入 syscall table
├─ 内核处理
│ ├─ 参数拷贝
│ ├─ 权限检查
│ ├─ 执行内核逻辑
│ └─ 返回结果
└─ 退出
├─ 返回用户态
└─ 设置 errno 语义
7.3 工具体系
strace
├─ 通常基于 ptrace
├─ 精确但开销较高
└─ 适合调试单进程
perf trace
├─ 基于 perf/tracepoint
├─ 开销较低
└─ 适合系统级观察
eBPF syscall trace
├─ 挂 sys_enter/sys_exit tracepoint
├─ 可以过滤和聚合
└─ 适合生产环境观测
7.4 看什么
openat("/etc/passwd", O_RDONLY) = 3
├─ syscall:openat
├─ 参数:路径、flag
├─ 返回:fd 3
└─ 如果失败:-1 ENOENT / EACCES / EPERM
8. netlink 套接字
8.1 它是什么
netlink
├─ AF_NETLINK socket
├─ 用户态 ↔ 内核态通信
├─ 消息式
└─ 常用于网络、审计、SELinux、uevent
netlink(7) 说明 Netlink 用于在内核和用户空间进程之间传递信息,并提供标准 socket 风格的用户态接口;例如 NETLINK_ROUTE 可接收路由和链路更新,也可修改路由表、地址、链路参数等。(man7.org)
8.2 递归拆解
netlink socket
├─ family
│ ├─ NETLINK_ROUTE
│ ├─ NETLINK_AUDIT
│ ├─ NETLINK_KOBJECT_UEVENT
│ ├─ NETLINK_SELINUX
│ └─ NETLINK_SOCK_DIAG
├─ message header
│ ├─ type
│ ├─ flags
│ ├─ sequence
│ └─ pid
├─ payload
│ └─ 属性 TLV
└─ multicast group
└─ 订阅内核事件广播
8.3 典型路径
ip link set eth0 up
├─ ip 命令创建 NETLINK_ROUTE socket
├─ 构造 RTM_NEWLINK 消息
├─ sendmsg 发给内核
├─ 内核修改网卡状态
└─ 返回 ACK / error
8.4 和 ioctl 的区别
ioctl
├─ 老接口多
├─ 类型不够自描述
└─ 扩展性一般
netlink
├─ 消息格式更适合复杂对象
├─ 支持 dump
├─ 支持 multicast
└─ 网络栈管理大量使用
9. epoll 边缘触发
9.1 它是什么
epoll
├─ interest list:关心哪些 fd
├─ ready list:哪些 fd 已就绪
├─ LT:level-triggered,水平触发
└─ ET:edge-triggered,边缘触发
epoll(7) 说明 epoll 用于监控多个文件描述符是否可 I/O,支持 level-triggered 和 edge-triggered 两种模式,并维护 interest list 与 ready list。(man7.org)
9.2 LT vs ET
LT 水平触发
├─ 只要 fd 仍然可读
├─ epoll_wait 就会继续返回
└─ 容错好,开销略高
ET 边缘触发
├─ 只有状态变化时通知
├─ 不会因为“还剩数据没读完”反复提醒
└─ 必须一次读到 EAGAIN
9.3 ET 正确写法
epoll ET 正确模式
├─ fd 设置 O_NONBLOCK
├─ epoll_ctl 添加 EPOLLET
├─ epoll_wait 收到事件
├─ 循环 read / recv
│ ├─ 读到数据:继续读
│ ├─ 返回 EAGAIN:停止
│ └─ 返回 0:对端关闭
└─ 等待下一次边缘变化
man page 也明确建议使用 EPOLLET 时配合非阻塞 fd,并在 read/write 返回 EAGAIN 后再等待事件。(man7.org)
9.4 常见 bug
错误模式
├─ 收到 EPOLLIN
├─ 只 read 一次
├─ 缓冲区里还有数据
└─ ET 不再提醒 → 连接卡死
10. kprobes 动态插桩
10.1 它是什么
kprobes
├─ kprobe:函数入口或任意指令地址
├─ kretprobe:函数返回点
└─ handler:命中探针时执行的处理函数
Linux kprobes 文档说明,kprobes 可以动态打入几乎任意内核代码地址,在命中断点时调用 handler,用于非侵入式收集调试和性能信息。(Kernel Documentation)
10.2 递归拆解
kprobe 工作机制
├─ 注册 probe
├─ 保存原始指令
├─ 替换为 breakpoint 指令
├─ CPU 执行到该位置触发异常
├─ kprobe handler 执行
├─ 单步执行原始指令
└─ 恢复正常执行流
10.3 kprobe 与 eBPF 的关系
kprobe
├─ 是一种内核插桩机制
└─ 可以被 eBPF 程序作为 attach 点使用
eBPF
├─ 是一种可验证程序运行时
└─ 可以挂到 kprobe、tracepoint、LSM、XDP 等很多点
10.4 kprobe vs tracepoint
tracepoint
├─ 内核开发者预留
├─ ABI 相对稳定
└─ 更适合长期工具
kprobe
├─ 几乎可插任意函数
├─ 灵活
└─ 容易受内核版本和符号变化影响
11. 页表遍历
11.1 它是什么
虚拟地址
└─ 页表遍历
└─ 物理地址
Linux 页表文档说明,页表负责把 CPU 看到的虚拟地址映射到外部内存总线看到的物理地址;层级页表用于节省页表内存,并避免为巨大但稀疏的虚拟地址空间建立线性大表。(Kernel Documentation)
11.2 递归拆解:以 x86-64 常见多级页表为例
虚拟地址
├─ PGD index
│ └─ 找到顶层页表项
├─ P4D/PUD index
│ └─ 找到下一级页表
├─ PMD index
│ └─ 找到中间页表项
├─ PTE index
│ └─ 找到最终页表项
└─ page offset
└─ 页内偏移
11.3 页表项里有什么
PTE
├─ present:页是否在内存
├─ writable:是否可写
├─ user/supervisor:用户态是否可访问
├─ executable / NX:是否可执行
├─ dirty:是否被写过
├─ accessed:是否被访问过
└─ PFN:物理页框号
11.4 路径
CPU 访问虚拟地址
├─ 查 TLB
│ ├─ 命中:直接得到物理地址
│ └─ 未命中:硬件 page walk
├─ 逐级读页表
├─ 找到 PTE
│ ├─ present:填入 TLB,继续执行
│ └─ not present:触发缺页中断
└─ 权限不符:触发 page fault
12. TLB 刷新
12.1 它是什么
TLB
├─ Translation Lookaside Buffer
├─ 缓存虚拟地址到物理地址的翻译结果
└─ 避免每次内存访问都完整走页表
当内核取消映射或修改内存区域属性时,需要处理 TLB 中可能存在的旧翻译。x86 TLB 文档说明,内核可选择刷新整个 TLB,也可用 invlpg 精确失效单页;全局刷新快但会破坏无关 TLB 项,单页失效更精确但可能需要更多指令。(Kernel Documentation)
12.2 递归拆解
什么时候需要 TLB flush
├─ munmap
├─ mprotect
├─ 页面换出
├─ COW 后页表修改
├─ 进程地址空间切换
├─ 内核修改页权限
└─ huge page 拆分/合并
12.3 为什么贵
TLB flush 成本
├─ 当前 CPU 上旧翻译失效
├─ 后续访问重新 page walk
├─ 多核时需要 TLB shootdown
│ ├─ 给其他 CPU 发 IPI
│ ├─ 其他 CPU 执行失效
│ └─ 等待确认
└─ 影响缓存和流水线效率
12.4 和上下文切换的关系
线程切换
├─ 同一进程内线程
│ └─ 共享地址空间,TLB 影响较小
└─ 不同进程
├─ 地址空间不同
├─ 页表根可能变化
└─ TLB 需要切换/标记/刷新
13. 缺页中断
13.1 它是什么
page fault
├─ CPU 访问虚拟地址
├─ 页表项不存在或权限不符
└─ CPU 触发异常,让内核处理
Linux 页表文档说明,访问不属于当前地址空间的内存,或写只读位置等,都可能导致 page fault;如果发生在用户态,内核通常向线程发送 SIGSEGV。(Kernel Documentation)
13.2 递归拆解
缺页中断类型
├─ minor page fault
│ ├─ 不需要从磁盘读
│ ├─ 例如 lazy allocation
│ └─ 例如 page cache 中已有页
├─ major page fault
│ ├─ 需要磁盘 I/O
│ ├─ 例如文件映射页不在内存
│ └─ 例如 swap in
└─ invalid fault
├─ 地址非法
├─ 权限非法
└─ 发送 SIGSEGV / SIGBUS
13.3 常见路径
malloc 后第一次写
├─ 虚拟地址空间已经保留
├─ 物理页还没分配
├─ 第一次写触发 page fault
├─ 内核分配物理页
├─ 填页表
├─ 刷新/更新 TLB
└─ 返回用户态继续执行
13.4 COW 路径
fork 后父子进程共享物理页
├─ 页表标记只读
├─ 子进程写入
├─ 触发 page fault
├─ 内核复制物理页
├─ 子进程页表指向新页
└─ 写入继续
14. OOM killer 策略
14.1 它是什么
OOM killer
├─ Out Of Memory
├─ 内核无法回收足够内存
└─ 选择一个或多个进程杀掉以救系统
Linux 内存管理文档说明,当机器内存耗尽且内核无法回收足够内存继续运行时,会调用 OOM killer;OOM killer 会选择一个任务牺牲,希望释放足够内存让系统恢复。(Kernel Documentation)
14.2 递归拆解
内存压力路径
├─ 进程申请内存
├─ free memory 不足
├─ 尝试 reclaim
│ ├─ 回收 page cache
│ ├─ 写回 dirty page
│ ├─ swap out
│ └─ shrink slab
├─ reclaim 失败
└─ OOM
├─ global OOM
└─ memcg OOM
14.3 选择牺牲者
badness heuristic
├─ 进程占用内存越多越可能被杀
├─ oom_score_adj 可调整倾向
├─ root/关键进程通常有保护因素
└─ cgroup 限制下可能只在 cgroup 内杀
/proc/<pid>/oom_score_adj 可用于调整 OOM 选择进程时的 badness heuristic。(Kernel Documentation)
14.4 常见现象
日志中看到
├─ Out of memory
├─ Killed process 1234
├─ total-vm
├─ anon-rss
├─ file-rss
└─ oom_score_adj
15. GOT / PLT 表项
15.1 它是什么
ELF 动态链接
├─ GOT:Global Offset Table
└─ PLT:Procedure Linkage Table
GOT/PLT 是 ELF 动态链接里用来解析外部符号地址的机制。AMD64 ABI 文档展示了 PLT 项如何跳转到 GOT 中的地址,并说明动态链接器可以在初始化时解析 R_X86_64_JUMP_SLOT,或采用 lazy binding 延迟到第一次调用时解析。
15.2 递归拆解
调用 printf()
├─ 程序里 call printf@plt
├─ 进入 PLT stub
├─ PLT 读取 GOT 表项
│ ├─ 如果已解析:跳到 libc printf
│ └─ 如果未解析:跳到动态链接器 resolver
├─ resolver 查找 printf 地址
├─ 写回 GOT 表项
└─ 后续调用直接跳 libc printf
15.3 GOT vs PLT
PLT
├─ 是代码
├─ 每个外部函数通常有一个 PLT stub
└─ 负责跳转逻辑
GOT
├─ 是数据表
├─ 保存真实函数/对象地址
└─ lazy binding 后会被动态链接器改写
15.4 安全相关
攻击与防护
├─ GOT overwrite
│ └─ 改写 GOT 表项劫持函数调用
├─ RELRO
│ ├─ Partial RELRO:部分保护
│ └─ Full RELRO:解析后 GOT 只读
├─ PIE
│ └─ 配合 ASLR 随机化主程序基址
└─ ASLR
└─ 随机化 libc / stack / heap 等地址
16. ASLR 布局随机化
16.1 它是什么
ASLR
├─ Address Space Layout Randomization
├─ 随机化进程地址空间布局
└─ 增加漏洞利用难度
Oracle Linux 文档说明,ASLR 可以把 base、libraries、heap、stack 放到随机位置,使攻击程序难以预测下一条指令地址;Linux 中可通过 /proc/sys/kernel/randomize_va_space 控制。(Oracle Documentation)
16.2 递归拆解
进程地址空间
├─ text segment
├─ data segment
├─ heap
├─ mmap region
├─ shared libraries
├─ stack
└─ vdso
ASLR 会让这些区域的地址在不同执行之间变化。
16.3 randomize_va_space
0
└─ 关闭 ASLR
1
├─ 随机化 stack
├─ 随机化 VDSO
└─ 随机化 shared memory regions
2
├─ 包含 1 的内容
└─ 额外随机化 data segment
这些取值和含义在 Oracle Linux 文档中有列出,其中 2 是默认设置。(Oracle Documentation)
16.4 和 GOT/PLT 的关系
没有 ASLR
└─ libc 地址固定,ret2libc 更容易
有 ASLR
├─ libc 基址随机
├─ stack 地址随机
├─ heap 地址随机
└─ 攻击者通常需要信息泄露来绕过
17. 上下文切换
17.1 它是什么
context switch
├─ 当前任务暂停
├─ 保存 CPU 上下文
├─ 选择下一个任务
├─ 恢复下一个任务上下文
└─ 继续执行
Linux 调度器文档中,context switch 与 switch_to 架构函数相关;调度器在任务切换时会进入架构相关的上下文切换实现。(Kernel Documentation)
17.2 递归拆解
需要保存/切换的内容
├─ 通用寄存器
├─ 栈指针
├─ 指令指针
├─ FPU/SIMD 状态
├─ 当前 task_struct
├─ 内存地址空间 mm_struct
├─ 页表根
└─ 调度统计信息
17.3 为什么会发生
触发原因
├─ 时间片耗尽
├─ 进程阻塞 I/O
├─ mutex / futex 等待
├─ 高优先级任务唤醒
├─ 主动 sleep/yield
└─ 中断后调度
17.4 成本来源
上下文切换成本
├─ 保存/恢复寄存器
├─ 调度器选择任务
├─ cache locality 下降
├─ TLB 影响
├─ branch predictor 影响
└─ 锁竞争和唤醒延迟
18. RCU 宽限期
18.1 它是什么
RCU
├─ Read-Copy-Update
├─ 读路径极轻量
├─ 写路径复制/替换
└─ 等待宽限期后释放旧对象
Linux RCU 文档说明,RCU 的基本思想是把破坏性操作拆成两步:先阻止新读者看到将被删除的数据,再真正销毁它;两步之间必须经过一个 grace period,确保旧读者都已经放弃引用。(Kernel Documentation)
18.2 递归拆解
RCU 更新
├─ 旧指针 p_old
├─ 分配新对象 p_new
├─ 修改 p_new
├─ rcu_assign_pointer() 发布新指针
├─ 老读者可能仍在读 p_old
├─ synchronize_rcu() 等待宽限期
└─ kfree(p_old)
18.3 读侧路径
RCU read side
├─ rcu_read_lock()
├─ p = rcu_dereference(global_ptr)
├─ 读取 p 指向的数据
└─ rcu_read_unlock()
读者通常不阻塞写者,适合“读多写少”的内核数据结构。
18.4 宽限期是什么
grace period
├─ 所有 CPU 上旧的 RCU read-side critical section 都结束
├─ 旧读者不可能再引用旧对象
└─ 写者可以安全释放旧对象
18.5 常见用途
RCU 常见场景
├─ 路由表
├─ 文件描述符表
├─ dentry cache
├─ 网络协议栈
├─ BPF map 某些路径
└─ 内核对象查找表
19. 把所有术语串起来
可以用一个请求的生命周期串起来:
nginx 收到网络请求
├─ epoll 边缘触发通知 socket 可读
├─ nginx read/recv 或 io_uring 提交 SQE
├─ 进入 syscall 或 io_uring 内核路径
├─ net namespace 决定它看到哪张网卡/路由表
├─ cgroup v2 统计 CPU、内存、I/O
├─ SELinux 检查进程是否能访问目标文件
├─ VFS 根据路径找到 dentry/inode
├─ 访问文件页
│ ├─ TLB 命中:直接访问
│ ├─ TLB 未命中:页表遍历
│ └─ 页不存在:缺页中断
├─ 内存不足
│ ├─ 回收 page cache / slab
│ └─ 失败则 OOM killer
├─ 调度器可能发生上下文切换
├─ 内核并发读多写少结构可能用 RCU
├─ 调试时可用 syscall trace / kprobe / eBPF 观察
└─ 用户态二进制调用 libc 函数时经过 GOT/PLT,地址受 ASLR 影响
最短总结:
eBPF / kprobes / syscall tracing:看内核在干什么
io_uring / epoll / netlink:用户态怎么高效和内核交互
cgroups / namespaces / SELinux:限制资源、视图和权限
inode:文件系统对象的核心身份
页表 / TLB / page fault / OOM:虚拟内存运行机制
context switch / RCU:调度与并发机制
GOT/PLT / ASLR:用户态二进制链接与漏洞缓解机制