在计算机科学中,任何程序都可以被抽象为一个状态机,无论是我们熟知的日常工具(LibreOffice,Chrome)还是开发工具(IDE,GCC,GDB),它们本质没有任何区别。
程序的执行过程本质上是状态的不断变化。具体而言:
main
函数的第一条语句决定。此时,栈中仅包含一个 StackFrame
,表示 main
函数的初始调用,全局变量也被初始化为预设的初始值。--- 程序通过操作系统的 execve
系统调用被加载到内存中,并初始化为一个独立的进程。
--- 程序在内存中运行,经历一系列的状态迁移,完成计算任务,并通过系统调用与操作系统交互(如文件操作、进程管理、存储管理等)。程序通过调用 exit
或 exit_group
系统调用终止执行,操作系统回收其占用的资源。
--- model checking 可以在给定一个系统和一个我们希望它拥有的性质的前提下,model checking 算法会探索这个系统的每个状态,验证系统是否满足这个性质。举个例子:如果我们希望系统满足“无死锁”这个性质,那么 model checking 算法就会遍历系统的每个状态。如果它发现存在一个从初始状态可达的状态没有后继状态,那么它就找到了系统不满足“无死锁”的证据,然后给用户报错,否则它就可以告诉我们,这个系统确实是满足“无死锁”这个性质的。
一个直观的状态机迁移:
int main() {
int sum = 0;
for (int i = 0; i < 1000000; i++) { // 用户态循环
sum += i; // 用户态加法
}
printf(「Sum: %d\n」, sum); // 库函数(内部含系统调用)
return 0;
}
在这个程序中:
printf
的 write
系统调用)用户态执行
┌───────────────────────┐ 系统调用入口
│ 指令 1: mov eax, 0 │ │
│ 指令 2: add ebx, ecx │ ▼
│ 指令 3: cmp edx, 100 │ ┌───────────┐
│ ... (百万条指令) │ │ 内核态执行 │
│ │ │ sys_read │
│ 指令 N: call printf ├─────►│ sys_write │
└───────────────────────┘ └───────────┘
▲ │
│ ▼
└───────┘
系统调用返回
# 启动新进程并跟踪
strace <命令> [参数]
# 示例:跟踪 `ls -l`
strace ls -l
# 跟踪已运行的进程(按 PID)
strace -p <PID>
# 示例:跟踪 PID 为 1234 的进程
strace -p 1234
# 常用选项:
strace -f # 跟踪子进程(适用于多进程程序)
strace -o log.txt # 将输出保存到文件(避免刷屏)
strace -T # 显示系统调用耗时
strace -s 1024 # 显示更长的参数内容(默认只显示 32 字节)
strace -e trace=<类别> # 只跟踪特定系统调用(如文件操作:`-e trace=file`)
系统调用指令
进程管理: fork, execve, exit, waitpid, getpid, ...
操作系统对象和访问: open, close, read, write, pipe, mount, mkfifo, mknod, stat, socket, ...
地址空间管理: mmap, sbrk (mmap 的前身)
以及一些其他的机制: pivot_root, chmod, chown, ..
状态机是一个具有严格数学定义的概念。这意味着我们可以用形式化的方法来描述和分析程序的行为。这种形式化的方法为程序的正确性验证和优化提供了理论基础。
Curry-Howard Correspondence:
逻辑中的蕴含消除规则对应函数调用:若有一个类型为 A→BA→B 的函数 (证明),且输入类型 AA 的值 (前提),则输出类型 BB 的值 (结论)。而对于一阶谓词,例如 ∃x.P(x)∃x.P(x),就必须提供一个 xx 的实例,这构成了基于构造的 “直觉逻辑” 的基础
操作系统的最大职责是为程序提供一个透明的运行环境。从程序的角度来看,它似乎“独占”了整个计算机的资源,并按顺序执行每一条指令。然而,实际情况并非如此:操作系统在后台持续运行,管理着硬件资源、进程调度、文件操作等复杂任务。
当程序执行系统调用(如 read
、write
、fork
等)时,程序的执行会被暂时挂起,而操作系统接管控制权,完成相应的任务(如与硬件交互或管理进程)。完成任务后,操作系统会将控制权交还给程序,使其继续执行。对于程序而言,这一过程是完全透明的,程序不会感知到时间的流逝或操作系统的干预。
一个最基本的 Unix 模型应该包含:
程序与操作系统之间的唯一通信桥梁是系统调用。系统调用是操作系统提供给程序的一组接口,允许程序请求操作系统完成特定任务(如文件读写、进程创建等)。例如,在 x86-64 架构中,程序可以通过 syscall
指令发起系统调用。
系统调用的重要性体现在以下几个方面:
操作系统提供了许多工具(如 strace
)来分析和调试程序的系统调用行为,帮助开发者理解程序的运行机制。
程序是状态机的静态描述,它描述了所有可能的程序状态,程序 (动态) 运行起来,就成了进程 (进行中的程序)。
操作系统作为进程(状态机)的管理者,一个直观的想法就如同 Windows api 一样,spawn 用来创建状态机,exit 用来销毁状态机,但 Unix 的答案是 fork 用来复制状态机,execve 用来复位状态机。
fork 的行为:
立即复制状态机,包括所有状态的完整拷贝,寄存器 & 每一个字节的内存,新创建进程返回 0,执行 fork 的进程返回子进程的进程号——“父子关系”。
Tips:
创建一棵层的进程树(A->B->C),并随机退出其中的一些进程——我们可以观察进程退出前后父子进程的关系。
----父进程(B
)终止后,子进程(C
)成为“孤儿”,操作系统内核(而非用户进程)负责回收终止进程的资源,并重置其子进程的父进程。这一机制确保所有进程最终都有有效的父进程,避免资源泄露。
int pid = fork();
if (pid == -1) { // 错误
perror(「fork」); goto fail;
} else if (pid == 0) { // 子进程
execve(...);
perror(「execve」); exit(EXIT_FAILURE);
} else { // 父进程
...
int status;
waitpid(pid, &status, 0); // testkit.C 中有
}
我们可以使用 pmap 查看某个进程的地址空间,pmap
命令通过读取 /proc/[pid]/maps
和 /proc/[pid]/smaps
文件来获取进程的内存映射信息。Linux 内核将这些信息以文本形式暴露在 /proc
文件系统中,pmap
通过解析这些文件实现功能,而无需直接调用特定的系统调用。
进程创建开始时,有一个初始状态,System V ABI 规定了部分寄存器和栈,Binary 中指定的 PT_LOAD 段,内存分为一段一段,每一段有访问权限。
区域地址范围用途内核空间0xC0000000~0xFFFFFFFF 操作系统内核代码和数据(所有进程共享)栈(Stack)向下增长存储局部变量、函数调用信息(如返回地址、参数)堆(Heap)向上增长动态内存分配(malloc
/free
)共享库映射区固定存放动态链接库(如
区域 | 地址范围 | 用途 |
---|---|---|
内核空间 | 0xC0000000~0xFFFFFFFF | 操作系统内核代码和数据(所有进程共享) |
栈(Stack) | 向下增长 | 存储局部变量、函数调用信息(如返回地址、参数) |
堆(Heap) | 向上增长 | 动态内存分配(malloc/free) |
共享库映射区 | 固定 | 存放动态链接库(如libc.so),多个进程可共享同一份代码。文件映射,匿名映射 |
数据段(Data) | 固定 | 存放全局变量、静态变量 |
代码段(Text) | 固定 | 存放可执行指令(只读) |
libc.so
),多个进程可共享同一份代码。
现代 OS 的主流方式,将地址空间划分为固定大小的页(Page,通常 4KB),物理内存划分为页帧(Page Frame)。
进程的初始状态:
名称 | 状态 |
---|---|
特殊寄存器 (浮点单元状态) | 所有错误状态标志都清零控制字设置 :RC=0: 默认的舍入方式:四舍五入PC=11: 默认使用最高的 双扩展精度M, UM, OM, ZM, DM, IM=1: 所有的浮点异常处于屏蔽 (Masked) 状态。这意味着如果发生这些错误,FPU 会自动处理 |
rFLAGS 寄存器 | DF=0: 方向标志向前(很多内存操作会向高地址增长)。CF=0: 没有进位/借位发生(就像刚重置了计算器)。PF, AF=0: 没有特定的奇偶校验位或半字节进位标志被设置(可以忽略)。ZF=0: 没有结果为0的比较(相当于还没开始比较)。SF=0: 结果是非负数(或者理解为结果的高位不是1)。OF=0: 没有发生有符号数的溢出(计算结果没有超出范围)。 |
堆栈状态 | High Addresses ... ...Auxiliary vector entries ... 0 Environment pointers ... 8+8*argc+%rsp 0 8+%rsp 参数数组 %rsp 参数个数 --- 它指向当前栈顶... ... Low Addresses |
线程状态 | 这个新线程会继承父线程当前的所有浮点单元状态在此之后,新线程的浮点状态就是其私有 (private) 的了。任何修改(例如更改舍入模式或屏蔽位)只影响该线程自身,不会影响父线程或其他线程。 |
able data-draft-node="block" data-draft-type="table" data-size="normal" data-row-style="normal">
able data-draft-node="block" data-draft-type="table" data-size="normal" data-row-style="normal">
M,
UM,
OM,
ZM,
DM,
IM=1: 所有的浮点异常处于屏蔽 (Masked)
状态。这意味着如果发生这些错误,FPU 会自动处理
名称 | 状态 |
---|---|
特殊寄存器 (浮点单元状态) | 所有错误状态标志都清零控制字设置 :RC=0: 默认的舍入方式:四舍五入PC=11: 默认使用最高的 双扩展精度M, UM, OM, ZM, DM, IM=1: 所有的浮点异常处于屏蔽 (Masked) 状态。这意味着如果发生这些错误,FPU 会自动处理 |
rFLAGS 寄存器 | DF=0: 方向标志向前(很多内存操作会向高地址增长)。CF=0: 没有进位/借位发生(就像刚重置了计算器)。PF, AF=0: 没有特定的奇偶校验位或半字节进位标志被设置(可以忽略)。ZF=0: 没有结果为0的比较(相当于还没开始比较)。SF=0: 结果是非负数(或者理解为结果的高位不是1)。OF=0: 没有发生有符号数的溢出(计算结果没有超出范围)。 |
堆栈状态 | High Addresses ... ...Auxiliary vector entries ... 0 Environment pointers ... 8+8*argc+%rsp 0 8+%rsp 参数数组 %rsp 参数个数 --- 它指向当前栈顶... ... Low Addresses |
线程状态 | 这个新线程会继承父线程当前的所有浮点单元状态在此之后,新线程的浮点状态就是其私有 (private) 的了。任何修改(例如更改舍入模式或屏蔽位)只影响该线程自身,不会影响父线程或其他线程。 |
rFLAGS 寄存器
able data-draft-node="block" data-draft-type="table" data-size="normal" data-row-style="normal">
able data-draft-node="block" data-draft-type="table" data-size="normal" data-row-style="normal">
able
data-draft-node="block" data-draft-type="table" data-size="normal" data-row-style="normal">
ZF
=0: 没有结果为 0名称状态特殊寄存器 (浮点单元状态)所有错误状态标志都清零控制字设置 :RC=0: 默认的舍入方式:四舍五入PC=11: 默认使用最高的 双扩展精度M, UM, OM, ZM, DM, IM=1: 所有的浮点异常处于屏蔽 (Masked) 状态。这意味着如果发生这些错误,FPU 会自动处理rFLAGS 寄存器DF=0: 方向标志向前(很多内存操作会向高地址增长)。CF=0: 没有进位/借位发生(就像刚重置了计算器)。PF, AF=0: 没有特定的奇偶校验位或半字节进位标志被设置(可以忽略)。ZF=0: 没有结果为0的比较(相当于还没开始比较)。SF=0: 结果是非负数(或者理解为结果的高位不是1)。OF=0: 没有发生有符号数的溢出(计算结果没有超出范围)。堆栈状态High Addresses ... ...Auxiliary vector entries ... 0 Environment pointers ... 8+8*argc+%rsp 0 8+%rsp 参数数组 %rsp 参数个数 --- 它指向当前栈顶... ... Low Addresses线程状态这个新线程会继承父线程当前的所有浮点单元状态在此之后,新线程的浮点状态就是其私有 (private) 的了。任何修改(例如更改舍入模式或屏蔽位)只影响该线程自身,不会影响父线程或其他线程。的比较(相当于还没开始比较)。SF
=0: 结果是非负数(或者理解为结果的高位不是名称状态特殊寄存器 (浮点单元状态)所有错误状态标志都清零控制字设置 :RC=0: 默认的舍入方式:四舍五入PC=11: 默认使用最高的 双扩展精度M, UM, OM, ZM, DM, IM=1: 所有的浮点异常处于屏蔽 (Masked) 状态。这意味着如果发生这些错误,FPU 会自动处理rFLAGS 寄存器DF=0: 方向标志向前(很多内存操作会向高地址增长)。CF=0: 没有进位/借位发生(就像刚重置了计算器)。PF, AF=0: 没有特定的奇偶校验位或半字节进位标志被设置(可以忽略)。ZF=0: 没有结果为0的比较(相当于还没开始比较)。SF=0: 结果是非负数(或者理解为结果的高位不是1)。OF=0: 没有发生有符号数的溢出(计算结果没有超出范围)。堆栈状态High Addresses ... ...Auxiliary vector entries ... 0 Environment pointers ... 8+8*argc+%rsp 0 8+%rsp 参数数组 %rsp 参数个数 --- 它指向当前栈顶... ... Low Addresses线程状态这个新线程会继承父线程当前的所有浮点单元状态在此之后,新线程的浮点状态就是其私有 (private) 的了。任何修改(例如更改舍入模式或屏蔽位)只影响该线程自身,不会影响父线程或其他线程。1)。able data-draft-node="block" data-draft-type="table" data-size="normal" data-row-style="normal">
le data-draft-node="block" data-draft-type="table" data-size="normal" data-row-style="normal">
名称 | 状态 |
---|---|
特殊寄存器 (浮点单元状态) | 所有错误状态标志都清零控制字设置 :RC=0: 默认的舍入方式:四舍五入PC=11: 默认使用最高的 双扩展精度M, UM, OM, ZM, DM, IM=1: 所有的浮点异常处于屏蔽 (Masked) 状态。这意味着如果发生这些错误,FPU 会自动处理 |
rFLAGS 寄存器 | DF=0: 方向标志向前(很多内存操作会向高地址增长)。CF=0: 没有进位/借位发生(就像刚重置了计算器)。PF, AF=0: 没有特定的奇偶校验位或半字节进位标志被设置(可以忽略)。ZF=0: 没有结果为0的比较(相当于还没开始比较)。SF=0: 结果是非负数(或者理解为结果的高位不是1)。OF=0: 没有发生有符号数的溢出(计算结果没有超出范围)。 |
堆栈状态 | High Addresses ... ...Auxiliary vector entries ... 0 Environment pointers ... 8+8*argc+%rsp 0 8+%rsp 参数数组 %rsp 参数个数 --- 它指向当前栈顶... ... Low Addresses |
线程状态 | 这个新线程会继承父线程当前的所有浮点单元状态在此之后,新线程的浮点状态就是其私有 (private) 的了。任何修改(例如更改舍入模式或屏蔽位)只影响该线程自身,不会影响父线程或其他线程。 |
名称 | 状态 |
---|---|
特殊寄存器 (浮点单元状态) | 所有错误状态标志都清零控制字设置 :RC=0: 默认的舍入方式:四舍五入PC=11: 默认使用最高的 双扩展精度M, UM, OM, ZM, DM, IM=1: 所有的浮点异常处于屏蔽 (Masked) 状态。这意味着如果发生这些错误,FPU 会自动处理 |
rFLAGS 寄存器 | DF=0: 方向标志向前(很多内存操作会向高地址增长)。CF=0: 没有进位/借位发生(就像刚重置了计算器)。PF, AF=0: 没有特定的奇偶校验位或半字节进位标志被设置(可以忽略)。ZF=0: 没有结果为0的比较(相当于还没开始比较)。SF=0: 结果是非负数(或者理解为结果的高位不是1)。OF=0: 没有发生有符号数的溢出(计算结果没有超出范围)。 |
堆栈状态 | High Addresses ... ...Auxiliary vector entries ... 0 Environment pointers ... 8+8*argc+%rsp 0 8+%rsp 参数数组 %rsp 参数个数 --- 它指向当前栈顶... ... Low Addresses |
线程状态 | 这个新线程会继承父线程当前的所有浮点单元状态在此之后,新线程的浮点状态就是其私有 (private) 的了。任何修改(例如更改舍入模式或屏蔽位)只影响该线程自身,不会影响父线程或其他线程。 |
名称 | 状态 |
---|---|
特殊寄存器 (浮点单元状态) | 所有错误状态标志都清零控制字设置 :RC=0: 默认的舍入方式:四舍五入PC=11: 默认使用最高的 双扩展精度M, UM, OM, ZM, DM, IM=1: 所有的浮点异常处于屏蔽 (Masked) 状态。这意味着如果发生这些错误,FPU 会自动处理 |
rFLAGS 寄存器 | DF=0: 方向标志向前(很多内存操作会向高地址增长)。CF=0: 没有进位/借位发生(就像刚重置了计算器)。PF, AF=0: 没有特定的奇偶校验位或半字节进位标志被设置(可以忽略)。ZF=0: 没有结果为0的比较(相当于还没开始比较)。SF=0: 结果是非负数(或者理解为结果的高位不是1)。OF=0: 没有发生有符号数的溢出(计算结果没有超出范围)。 |
堆栈状态 | High Addresses ... ...Auxiliary vector entries ... 0 Environment pointers ... 8+8*argc+%rsp 0 8+%rsp 参数数组 %rsp 参数个数 --- 它指向当前栈顶... ... Low Addresses |
线程状态 | 这个新线程会继承父线程当前的所有浮点单元状态在此之后,新线程的浮点状态就是其私有 (private) 的了。任何修改(例如更改舍入模式或屏蔽位)只影响该线程自身,不会影响父线程或其他线程。 |
名称 | 状态 |
---|---|
特殊寄存器 (浮点单元状态) | 所有错误状态标志都清零控制字设置 :RC=0: 默认的舍入方式:四舍五入PC=11: 默认使用最高的 双扩展精度M, UM, OM, ZM, DM, IM=1: 所有的浮点异常处于屏蔽 (Masked) 状态。这意味着如果发生这些错误,FPU 会自动处理 |
rFLAGS 寄存器 | DF=0: 方向标志向前(很多内存操作会向高地址增长)。CF=0: 没有进位/借位发生(就像刚重置了计算器)。PF, AF=0: 没有特定的奇偶校验位或半字节进位标志被设置(可以忽略)。ZF=0: 没有结果为0的比较(相当于还没开始比较)。SF=0: 结果是非负数(或者理解为结果的高位不是1)。OF=0: 没有发生有符号数的溢出(计算结果没有超出范围)。 |
堆栈状态 | High Addresses ... ...Auxiliary vector entries ... 0 Environment pointers ... 8+8*argc+%rsp 0 8+%rsp 参数数组 %rsp 参数个数 --- 它指向当前栈顶... ... Low Addresses |
线程状态 | 这个新线程会继承父线程当前的所有浮点单元状态在此之后,新线程的浮点状态就是其私有 (private) 的了。任何修改(例如更改舍入模式或屏蔽位)只影响该线程自身,不会影响父线程或其他线程。 |
名称 | 状态 |
---|---|
特殊寄存器 (浮点单元状态) | 所有错误状态标志都清零控制字设置 :RC=0: 默认的舍入方式:四舍五入PC=11: 默认使用最高的 双扩展精度M, UM, OM, ZM, DM, IM=1: 所有的浮点异常处于屏蔽 (Masked) 状态。这意味着如果发生这些错误,FPU 会自动处理 |
rFLAGS 寄存器 | DF=0: 方向标志向前(很多内存操作会向高地址增长)。CF=0: 没有进位/借位发生(就像刚重置了计算器)。PF, AF=0: 没有特定的奇偶校验位或半字节进位标志被设置(可以忽略)。ZF=0: 没有结果为0的比较(相当于还没开始比较)。SF=0: 结果是非负数(或者理解为结果的高位不是1)。OF=0: 没有发生有符号数的溢出(计算结果没有超出范围)。 |
堆栈状态 | High Addresses ... ...Auxiliary vector entries ... 0 Environment pointers ... 8+8*argc+%rsp 0 8+%rsp 参数数组 %rsp 参数个数 --- 它指向当前栈顶... ... Low Addresses |
线程状态 | 这个新线程会继承父线程当前的所有浮点单元状态在此之后,新线程的浮点状态就是其私有 (private) 的了。任何修改(例如更改舍入模式或屏蔽位)只影响该线程自身,不会影响父线程或其他线程。 |
名称 | 状态 |
---|---|
特殊寄存器 (浮点单元状态) | 所有错误状态标志都清零控制字设置 :RC=0: 默认的舍入方式:四舍五入PC=11: 默认使用最高的 双扩展精度M, UM, OM, ZM, DM, IM=1: 所有的浮点异常处于屏蔽 (Masked) 状态。这意味着如果发生这些错误,FPU 会自动处理 |
rFLAGS 寄存器 | DF=0: 方向标志向前(很多内存操作会向高地址增长)。CF=0: 没有进位/借位发生(就像刚重置了计算器)。PF, AF=0: 没有特定的奇偶校验位或半字节进位标志被设置(可以忽略)。ZF=0: 没有结果为0的比较(相当于还没开始比较)。SF=0: 结果是非负数(或者理解为结果的高位不是1)。OF=0: 没有发生有符号数的溢出(计算结果没有超出范围)。 |
堆栈状态 | High Addresses ... ...Auxiliary vector entries ... 0 Environment pointers ... 8+8*argc+%rsp 0 8+%rsp 参数数组 %rsp 参数个数 --- 它指向当前栈顶... ... Low Addresses |
线程状态 | 这个新线程会继承父线程当前的所有浮点单元状态在此之后,新线程的浮点状态就是其私有 (private) 的了。任何修改(例如更改舍入模式或屏蔽位)只影响该线程自身,不会影响父线程或其他线程。 |
名称 | 状态 |
---|---|
特殊寄存器 (浮点单元状态) | 所有错误状态标志都清零控制字设置 :RC=0: 默认的舍入方式:四舍五入PC=11: 默认使用最高的 双扩展精度M, UM, OM, ZM, DM, IM=1: 所有的浮点异常处于屏蔽 (Masked) 状态。这意味着如果发生这些错误,FPU 会自动处理 |
rFLAGS 寄存器 | DF=0: 方向标志向前(很多内存操作会向高地址增长)。CF=0: 没有进位/借位发生(就像刚重置了计算器)。PF, AF=0: 没有特定的奇偶校验位或半字节进位标志被设置(可以忽略)。ZF=0: 没有结果为0的比较(相当于还没开始比较)。SF=0: 结果是非负数(或者理解为结果的高位不是1)。OF=0: 没有发生有符号数的溢出(计算结果没有超出范围)。 |
堆栈状态 | High Addresses ... ...Auxiliary vector entries ... 0 Environment pointers ... 8+8*argc+%rsp 0 8+%rsp 参数数组 %rsp 参数个数 --- 它指向当前栈顶... ... Low Addresses |
线程状态 | 这个新线程会继承父线程当前的所有浮点单元状态在此之后,新线程的浮点状态就是其私有 (private) 的了。任何修改(例如更改舍入模式或屏蔽位)只影响该线程自身,不会影响父线程或其他线程。 |
名称 | 状态 |
---|---|
特殊寄存器 (浮点单元状态) | 所有错误状态标志都清零控制字设置 :RC=0: 默认的舍入方式:四舍五入PC=11: 默认使用最高的 双扩展精度M, UM, OM, ZM, DM, IM=1: 所有的浮点异常处于屏蔽 (Masked) 状态。这意味着如果发生这些错误,FPU 会自动处理 |
rFLAGS 寄存器 | DF=0: 方向标志向前(很多内存操作会向高地址增长)。CF=0: 没有进位/借位发生(就像刚重置了计算器)。PF, AF=0: 没有特定的奇偶校验位或半字节进位标志被设置(可以忽略)。ZF=0: 没有结果为0的比较(相当于还没开始比较)。SF=0: 结果是非负数(或者理解为结果的高位不是1)。OF=0: 没有发生有符号数的溢出(计算结果没有超出范围)。 |
堆栈状态 | High Addresses ... ...Auxiliary vector entries ... 0 Environment pointers ... 8+8*argc+%rsp 0 8+%rsp 参数数组 %rsp 参数个数 --- 它指向当前栈顶... ... Low Addresses |
线程状态 | 这个新线程会继承父线程当前的所有浮点单元状态在此之后,新线程的浮点状态就是其私有 (private) 的了。任何修改(例如更改舍入模式或屏蔽位)只影响该线程自身,不会影响父线程或其他线程。 |
辅助向量 (Auxiliary Vector
)
操作系统通过这个表向新进程传递一些关键的系统信息或运行时参数。这些信息对于程序正常启动、动态链接器 (ld-Linux
) 工作等都至关重要。
类型名称 (a_type) | 值 (Value) | 信息内容 (a_un) | 含义解释 |
---|---|---|---|
AT_NULL | 0 | ignored | 辅助向量的结束标记! 遇到这个类型,表示后面没有有效条目了,辅助向量结束。 |
AT_IGNORE | 1 | ignored | 忽略此条目。 这个条目没有意义,里面的值(a_un)也不用管。 |
AT_EXECFD | 2 | a_val (整数值) | 解释器程序可用的文件描述符。 如果程序需要一个解释器(如 ld-linux,负责动态链接),系统可能会将这个文件描述符传给解释器,解释器可以用它来直接读取真正要运行的程序文件(比如你的 ./myprogram)。 |
AT_PHDR | 3 | a_ptr (指针) | 程序在内存中的程序头表 (Program Header Table) 位置。 如果系统已经把你的程序加载到内存里了,这个指针就指向 ELF 文件结构中程序头表的起始地址。告诉解释器(如果需要的话)去哪里找内存映射信息。 |
AT_PHENT | 4 | a_val (整数值) | 每个程序头表条目的大小 (字节)。 告诉解释器上面提到的程序头表(AT_PHDR)中,每个条目的长度是多少字节(通常是 sizeof(Elf64_Phdr) = 56 字节)。 |
AT_PHNUM | 5 | a_val (整数值) | 程序头表条目的个数。 告诉解释器上面提到的程序头表(AT_PHDR)里总共有多少个条目。 |
AT_PAGESZ | 6 | a_val (整数值) | 系统内存分页大小 (字节)。 告诉程序操作系统管理内存的基本单位有多大(比如 4KB = 4096 字节)。很多内存操作需要对齐到这个大小。 |
AT_BASE | 7 | a_ptr (指针) | 解释器程序自身在内存中的加载基地址。 如果程序是通过解释器运行(如 ld-linux),这个指针指向解释器自己代码在内存里的起始地址。 |
AT_FLAGS | 8 | a_val (位掩码) | 解释器相关的标志位 (位掩码)。 给解释器(如 ld-linux)使用的特定标志位组合。未定义的位是0。应用程序一般不需要关心。 |
AT_ENTRY | 9 | a_ptr (函数指针) | 程序真正的入口地址 (Entry Point)。 指向程序可执行文件的入口点地址(在 ELF 文件中标记为 e_entry)。这是程序的代码开始执行的地方(比如 _start 符号位置)。解释器最终要把控制权交给这里。这是最重要的指针之一。 |
AT_NOTELF | 10 | a_val (整数值) | 非 ELF 文件标志。 如果值 非0,就表示当前运行的程序 不是 标准的 ELF 格式文件(可能是其他格式如 a.out, PE等)。如果是 0,就是标准的 ELF 格式。 |
AT_UID / AT_EUID / AT_GID / AT_EGID | 11 / 12 / 13 / 14 | a_val (整数值) | 用户和组标识符。AT_UID: 进程的实际用户ID (启动进程的用户)。AT_EUID: 进程的有效用户ID (通常与 AT_UID 相同,或受 setuid 程序影响)。AT_GID: 进程的实际组ID。AT_EGID: 进程的有效组ID。 |
AT_PLATFORM | 15 | a_ptr (字符串指针) | 硬件平台标识字符串。 指向一个描述底层硬件平台架构的字符串(例如 "x86_64")。 |
AT_HWCAP | 16 | a_val (位掩码) | CPU 硬件能力位掩码。 一个整数值,其中的位代表 CPU 支持的特定扩展指令集或功能(例如 MMX, SSE, SSE2, AVX 等)。这个值对应于 CPUID 指令(功能号1)返回的 EDX 寄存器中的一部分特征位。 |
AT_CLKTCK | 17 | a_val (整数值) | 系统时钟节拍频率 (clock tick)。 每秒有多少次 times() 函数会增加时钟计数(通常是 100)。 |
AT_SECURE | 23 | a_val (整数值) | 安全模式标志。 如果值是 1,表示程序是以提升权限模式启动的(例如用户执行了一个设置了 suid 或 sgid 位的程序)。值是 0 则表示没有特殊权限。安全敏感的程序需要注意这个标志。 |
AT_BASE_PLATFORM | 24 | a_ptr (字符串指针) | 基础平台标识字符串。 与 AT_PLATFORM 类似,但指向的字符串描述的是更基础、更通用的平台架构,可能与硬件平台字符串不同(例如 AT_PLATFORM 是 "i686", AT_BASE_PLATFORM 可能是 "i386")。 |
AT_RANDOM | 25 | a_ptr (指针) | 16字节安全随机数 (Securely Generated Random Bytes)。 指向操作系统提供的、专门用于增强安全性的 16 字节随机数据(通常由内核的安全随机数生成器产生)。程序可以使用这个随机数作为密钥种子或 ASLR 的加强。 |
AT_HWCAP2 | 26 | a_val (位掩码) | 扩展 CPU 硬件能力位掩码。 未来可用的额外 CPU 特性位掩码(当前 AMD64 ABI 定义中该值为 0,保留给未来扩展)。可能对应 CPUID 后续指令或不同寄存器的特征位。 |
AT_EXECFN | 31 | a_ptr (字符串指针) | 被执行的程序的文件名。 指向一个字符串,该字符串保存了用户(或 shell)传递给 exec() 系列函数的原始程序路径名(例如 "/usr/bin/ls" 或 "./hello")。与 argv[0] 不同,argv[0] 可能是修改过的(例如通过符号链接启动),而 AT_EXECFN 通常指向实际被执行的文件路径。 |
mmap 系统调用,可以在状态机状态上增加/删除/修改一段可访问的内存:
ptrace/process_vm_writev
,可以修改另一个进程的地址空间
ptrace
系统调用
通过 PTRACE_PEEKDATA
和 PTRACE_POKEDATA
请求读写目标进程内存。调试工具(如 GDB)即依赖此机制,但需附加到目标进程(可能暂停其执行)。process_vm_writev
系统调用
允许直接向目标进程写入数据,效率高于 ptrace
,且无需暂停目标进程。需要 CAP_SYS_PTRACE
能力或权限配置。适用场景:
ptrace
无需频繁暂停目标进程,延迟更低文件描述符是指向操作系统对象的 “指针”——系统调用通过这个指针 (fd) 确定进程希望访问操作系统中的哪个对象。我们有 open, close, read/write, lseek, dup 管理文件描述符。
//open
p = malloc(sizeof(FileDescriptor));
//close
delete(p);
//read/write
*(p.data++);
//lseek
p.data += offset;
//dup
q = p;
任何可以读写的东西都可以是文件,比如:
真实的设备
虚拟的设备 (文件)
优点
open()/read()/write()/close()
等少量 APIecho 「hello」 > /dev/pts/0
直接写入终端ls | grep txt | sort | uniq
program 2>&1 > log.txt
/proc/pid/maps
查看进程内存映射/sys/class/net
管理网络设备缺点
解决方案
分层 API 架构,操作系统通过「内核基础 API + 用户态封装」来解决这些矛盾:
Windows NT
┌───────────────────────┐
│ Win32 API │ ← 图形/多媒体/COM 等高级接口
├───────────────────────┤
│ POSIX 子系统层 │ ← 翻译 Unix API 调用,兼容层适配传统抽象
├───────────────────────┤
│ NT 内核基础 API │
│ (Objects, Handles) │ ← 微内核设计:进程/线程/虚拟内存等基本对象
└───────────────────────┘
WSL (Windows Subsystem for Linux)
┌───────────────────────┐
│ Linux 二进制 (ELF) │
├───────────────────────┤
│ lxss.sys (翻译层) │ ← 转换 Linux 系统调用→NT API
├───────────────────────┤
│ Windows NT 内核 │
└───────────────────────┘
甚至连我们与电脑交互的终端也都是文件,pty:
伪终端(pseudo terminal,有时也被称为 pty)是指伪终端 master 和伪终端 slave 这一对字符设备。其中的 slave 对应 /dev/pts/ 目录下的一个文件,而 master 则在内存中标识为一个文件描述符(fd)。伪终端由终端模拟器提供,终端模拟器是一个运行在用户态的应用程序。
Master 端是更接近用户显示器、键盘的一端,slave 端是在虚拟终端上运行的 CLI(Command Line Interface,命令行接口)程序。Linux 的伪终端驱动程序,会把 master 端(如键盘)写入的数据转发给 slave 端供程序输入,把程序写入 slave 端的数据转发给 master 端供(显示器驱动等)读取。请参考下面的示意图(此图来自互联网):
我们打开的终端桌面程序,比如 GNOME Terminal,其实是一种终端模拟软件。当终端模拟软件运行时,它通过打开 /dev/ptmx 文件创建了一个伪终端的 master 和 slave 对,并让 shell 运行在 slave 端。当用户在终端模拟软件中按下键盘按键时,它产生字节流并写入 master 中,shell 进程便可从 slave 中读取输入;shell 和它的子程序,将输出内容写入 slave 中,由终端模拟软件负责将字符打印到窗口中。
/dev/ptmx 是一个字符设备文件,当进程打开 /dev/ptmx 文件时,进程会同时获得一个指向 pseudoterminal master(ptm)的文件描述符和一个在 /dev/pts 目录中创建的 pseudoterminal slave(pts) 设备。通过打开 /dev/ptmx 文件获得的每个文件描述符都是一个独立的 ptm,它有自己关联的 pts,ptmx(可以认为内存中有一个 ptmx 对象)在内部会维护该文件描述符和 pts 的对应关系,对这个文件描述符的读写都会被 ptmx 转发到对应的 pts。我们可以通过 lsof 命令查看 ptmx 打开的文件描述符。
可以看到进程的 0u(标准输入)、1u(标准输出)、2u(标准错误输出)都绑定到了伪终端 /dev/pts/8 上,这样一来对这个文件描述符的读写都会被 ptmx 转发到对应的 pts。
查看具体的关联
# 查看伪终端主从设备
$ ps -ef | grep pts
user 5678 1234 0 10:00 pts/1 00:00:00 bash
# 查看文件描述符指向
$ ls -l /proc/1234/fd/12
lrwx------ 1 user user 64 Jan 01 12:34 12 -> /dev/ptmx
$ ls -l /dev/pts/1
crw--w---- 1 user tty 136, 1 Jan 01 10:00 /dev/pts/1
完整数据流转示例(以执行 ls -l
为例)
当我们对当前终端进行操作,或者运行程序时:
ls | grep txt &
,这个管道命令里的 ls
和 grep
通常就在同一个新的后台进程组里。而 shell 自己在另一个进程组。Ctrl-C
产生的 SIGINT
)通常是发给整个前台进程组的!这就是为什么 Ctrl-C
能杀掉一个正在运行的复杂命令(比如包含管道的命令)里的所有相关进程。Ctrl-Z
(发送 SIGTSTP
暂停信号)也是发给整个前台进程组。为什么我们需要 sigaction 替代 Unix 的信号机制?
// 传统 signal 的脆弱实现
void handler(int sig) {
signal(SIGINT, handler); // 必须手动重新注册
// 处理逻辑
}
signal(SIGINT, handler);
// 使用 sigaction 的可靠实现
struct sigaction sa;
sa.sa_handler = handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART; // 自动重启系统调用
sigaction(SIGINT, &sa, NULL);
为了这一章,我直接看了一本书,写的很好,相见恨晚:
加载器:
.text
段只读,.data
段可读写)。glibc
),加载器调用动态链接器(ld-Linux.so
)加载共享库并解析符号地址(运行时完成)。_start
)。连接器:
.o
/.obj
)中的函数/变量引用与其定义匹配(例如,main.C
调用 printf()
时,链接器在 libc.a
中查找其实现)。.text
)、数据段(.data
)分配内存地址,并修正代码中的相对地址引用。.a
)代码直接复制到可执行文件中,或记录动态库(如.so
)的引用信息。动态链接核心:
GOT(全局偏移表)
.got
或 .got.plt
节区),内容在运行时动态修改。ld-Linux.so
)解析符号地址并填入 GOT。GOT[0]
:.dynamic
段的地址(动态链接元数据)。GOT[1]
:link_map
结构指针(描述已加载共享库的链表)。GOT[2]
:_dl_runtime_resolve
函数地址(符号解析入口)。GOT[3..n]
:存储实际函数地址(如 printf
、scanf
)。PLT(过程链接表)
.plt
节区),内容在编译后固定不变。jmp *GOT[n]
)实现延迟绑定。PLT[0]
:公共桩代码,调用 GOT[2]
(_dl_runtime_resolve
)。PLT[1..n]
:每个外部函数对应一项,包含:jmp *GOT[n]
(首次指向 PLT 内部的 push
指令)。push 序号
(符号在重定位表中的索引)。jmp PLT[0]
(触发解析流程)。步骤 | 行为 |
---|---|
① | 程序调用 call printf@plt(跳转到 PLT[n])。 |
② | PLT[n] 执行 jmp *GOT[n],此时 GOT[n] 指向 PLT[n] 的第二条指令(push 序号)。 |
③ | 执行 push 序号(压入符号索引),再 jmp PLT[0]。 |
④ | PLT[0] 将 GOT[1](link_map)压栈,跳转至 GOT[2](_dl_runtime_resolve)。 |
⑤ | _dl_runtime_resolve 解析符号地址,写入 GOT[n],跳转到目标函数。 |
结果:符号地址被缓存至GOT,后续调用直接跳转。 | call printf@plt → jmp *GOT[n] → 直接跳转至目标函数地址(无需解析)。 |
我们可以很容易地把状态机模型扩展为共享内存上的多线程模型——只是每次选择一个状态机执行一步,通过提供 spawn 和 join 两个 API 来利用现有多处理器系统的共享内存能力。
然而,由于编译优化的 “无处不在” (处理器也是编译器),共享内存并发的行为十分复杂。与此同时,人类又恰好是物理世界 (宏观时间) 中的 “sequential creature”,编程语言的直觉 (顺序/选择/循环结构) 也是围绕顺序程序设计,因此共享内存上的并发编程是非常具有挑战性的 “底层技术”。
互斥:别的我没学会,一把大刀保平安,我用的可熟了
我们希望控制事件发生的先后顺序,比如 A->B->C,互斥锁只是确保分开 A,B,C,但做不到顺序控制,这里边得需要同步。
// 万能消费者生产者公式
// 想清楚生产/消费的条件是什么?
void fish_before(char ch) {
mutex_lock(&lk);
while (!can_print(ch)) {
cond_wait(&cv, &lk);
}
quota--;
mutex_unlock(&lk);
}
void fish_after(char ch) {
mutex_lock(&lk);
quota++;
current = next(ch);
assert(current);
cond_broadcast(&cv);
mutex_unlock(&lk);
}
如何避免死锁?
Lock ordering
1. 任意时刻系统中的锁都是有限的
2. 给所有锁编号 (Lock Ordering)
3. 严格按照从小到大的顺序获得锁
任意时刻,总有一个线程获得 “编号最大” 的锁,这个线程总是可以继续运行,因为他已经有了所有需要的小锁
# 例子
假设 5 个哲学家(线程)和 5 把筷子(锁),编号为 1~5。规则要求:
哲学家必须先拿编号小的筷子,再拿编号大的筷子(如必须先拿 3 号筷才能拿 4 号筷)。
运行过程:
哲学家 A 拿起筷子 1 和 2(锁 1、2),哲学家 B 拿起筷子 1 和 3(锁 1、3)。
此时系统中最大编号为 3(被哲学家 B 持有)。
哲学家 B 无需等待更大编号的锁(已持有所有小于 3 的锁),可吃完后释放锁 1 和 3。
释放后,其他哲学家(如等待锁 3 的哲学家 C)可继续获取锁 3 并推进任务。
Do not communicate by sharing memory; instead, share memory by communicating.
——Effective Go
我们拿优盘插到电脑中,然后就看了这个盘符,我们可以进去查看文件和拷贝文件,这里面发生了什么?
其实,这里面用到了 udev
技术,监听内核发出的设备事件(如插入、拔出、状态变化),在 Linux 系统中用户空间(Userspace)的动态设备管理 /dev
目录下的设备文件。
关键流程如下:
usb-storage
),udev 依赖驱动已成功加载,否则无法获取设备信息,驱动程序工作在内核空间(Kernel Space),直接与硬件交互,比如声卡驱动将音频数据转换为电信号驱动扬声器发声netlink
套接字发送 uevent
事件(包含设备路径、子系统类型、属性等元数据)udevd
守护进程(用户空间)监听 uevent
,解析事件内容并匹配预定义的规则udevd
读取 /etc/udev/rules.d/
和 /lib/udev/rules.d/
下的规则文件,按文件名数字优先级升序执行匹配# /etc/udev/rules.d/99-usb.rules
SUBSYSTEM==「block」, KERNEL==「sd*」, ATTRS{idVendor}==「0781」, ATTRS{idSerial}==「A1B2C3D4」, SYMLINK+=「backup_disk」, MODE=「0660」, GROUP=「storage」
匹配键(条件):
SUBSYSTEM(设备类型)、KERNEL(内核设备名)、ATTRS{...}(/sys 中的设备属性,如厂商 ID)
操作键(动作):
SYMLINK(创建软链接)、MODE(权限)、GROUP(归属组)、RUN(执行脚本)
/dev/backup_disk
)。MODE=「0666」
、GROUP=「users」
修改设备文件权限。SYMLINK+=「cdrom」
指向 /dev/sr0
)。RUN+=「/bin/mount /dev/sdb1 /mnt」
)/dev
下生成设备文件,完成设备可用性配置这样我们就可以以文件的形式使用设备了。
好滴,那么为啥我们就能以文件的形式去使用这些设备的,关键就在于驱动程序:
Everything Is a File, 通过执行操作系统对象的指针可以访问一切,open/close,read/write,lseek。
驱动程序实现了当前设备的 struct file_operations 的实现,它把系统调用翻译成与设备能听懂的数据,甚至包含如/dev/null 和 proc/stat 这种虚拟文件。
ioremap()
将 BAR 映射的物理地址转为内核虚拟地址,直接读写设备寄存器read()
/write()
或 ioctl()
发送控制命令没想到吧,连虚拟机都是文件,KVM 的所有关键操作(虚拟机生命周期管理、CPU 调度、内存映射)均通过 ioctl
实现用户态与内核态的交互:
kvm_fd = open(「/dev/kvm」, O_RDWR); // 打开 KVM 设备
vm_fd = ioctl(kvm_fd, KVM_CREATE_VM, 0); // 创建虚拟机
vcpu_fd = ioctl(vm_fd, KVM_CREATE_VCPU, 0); // 创建 vCPU
ioctl(vcpu_fd, KVM_RUN, 0); // 运行 vCPU
当调用 KVM_RUN
后,虚拟 CPU 开始执行客户机指令。若客户机触发 I/O 操作或特权指令(如访问设备寄存器),CPU 会通过硬件虚拟化技术(Intel VT/AMD-V)退出到宿主机内核模式(VM-Exit)。KVM 内核模块捕获退出原因(如 KVM_EXIT_IO
),并通过 ioctl
的返回值通知用户态 QEMU 处理 I/O 模拟
存储的本质在于通过改变物理介质的状态来记录信息,该介质需具备至少两种可稳定区分的状态。
存储经历了百年的发展历史,从磁带,磁鼓,到磁盘,软盘,到 CD 光盘,Flash memory 闪存和 U 盘,flash memory 活到了今天,配合 FTL 技术(软件定义磁盘)成为了存储界的版本答案,让我们来一一回顾,存储的前世今生。
磁带存储的本质是磁场与磁性材料的可控相互作用:电流脉冲定向磁化介质,剩磁状态固化信息,电磁感应逆向解码。其高密度、低成本的特性,使其在云备份(如 AWS Glacier)、科研归档等场景仍不可替代。随着 HAMR(热辅助磁记录)等技术的发展,磁带存储密度正迈向 100Gb/in²以上,持续拓展数字历史的承载边界。
写入原理:电信号→磁化状态
读取原理:磁化状态→电信号
擦除原理:退磁还原
工作方式
优劣势:
磁带(1928 年由 Fritz Pfleumer 发明)本质是线性一维存储介质:数据按顺序记录在磁性涂层上,读写需顺序扫描。其优势是成本低、容量大,但随机访问效率极低(如读取末端数据需遍历整盘磁带) 1932 年奥地利工程师 Gustav Tauschek 发明的磁鼓采用圆柱形旋转结构:数据分布在鼓面磁道上,通过固定磁头与高速旋转(>3000 RPM)实现毫秒级延迟访问。以 IBM 701 计算机(1953 年)为例,磁鼓作为内存使用,容量 62.5KB,远超同期磁带。
线性移动 -> 旋转扫描,带来革命: 录音 -> 实时计算
磁鼓有两个核心局限:
磁盘有一些工程优化的突破:
磁盘到软盘的演进,主要是便携性需求驱动带来的。
磁盘的固有限制
软盘的技术突破
市场催化
软盘到 CD 光盘:容量危机与多媒体需求升级
随着激光技术的发展,我们可以利用光学反射进行相位差解码:
这就带来了两个技术飞跃:
CD 盘片从下至上分层构成
层级材料与功能厚度/特性聚碳酸酯基板透明塑料,承载凹坑结构 1.2mm,折射率 1.55反射层铝或金膜,反射激光(铝=「银盘」,金=「金盘」)0.05μm,反射率>70%
层级 | 材料与功能 | 厚度/特性 |
---|---|---|
聚碳酸酯基板 | 透明塑料,承载凹坑结构 | 1.2mm,折射率1.55 |
反射层 | 铝或金膜,反射激光(铝="银盘",金="金盘") | 0.05μm,反射率>70% |
保护层 | 丙烯酸树脂,防氧化与物理损伤 | 提升盘面硬度和耐刮性 |
封面层 | 印刷标签层 | 不影响光学读取 |
保护层丙烯酸树脂,防氧化与物理损伤提升盘面硬度和耐刮性封面层印刷标签层不影响光学读取
开始聊聊当前的版本答案---闪存,从光学机械式向固态电子式跃迁,“你还能比电快啊?”。
CD 光盘的固有缺陷
闪存技术的颠覆性创新
闪存的基本存储单元是浮栅晶体管,由控制栅(Control Gate)、浮栅(Floating Gate)、源极(Source)和漏极(Drain)构成
。浮栅被二氧化硅(SiO₂)绝缘层包裹,形成电荷“陷阱”,其电荷状态决定数据值(0 或 1):
写入(Program)
擦除(Erase)
读取(Read)
闪存是有擦写限制的,SLC 闪存约 10 万次,QLC 闪存仅 100 次。若频繁写入同一物理块,会导致局部快速损坏。
传统文件系统(如 EXT4、NTFS)和块设备驱动(如 Linux 的 block_device_operations
)基于扇区(512B)或块(通常 4KB) 的覆盖写入设计。操作系统通过 BIO(Block I/O)接口提交读写请求,默认支持原地更新(in-place update)。
FTL 的本质是在闪存物理限制与传统系统之间构建的“翻译层”,(操作系统->BIO->闪存)通过三大创新解决结构性矛盾:
FTL 的映射机制
逻辑地址(LBA)→物理地址(PBA)转换:FTL 维护动态映射表,将操作系统的扇区请求转换为闪存的页操作。当数据更新时,FTL 将新数据写入空白页,并更新映射表指向新位置,原位置标记为“无效”(Invalid),通过“异地更新”(Out-of-Place Update)模拟磁盘的覆盖写行为,避免物理擦除延迟
新数据优先写入擦写次数少的“年轻块”(Low EC)
缓存多个小 BIO,合并为整页写入
删除操作是对一个块的,而块包含多个页面,操作系统如果只删除某个页面时,就会出现一个块部分页面为有效,部分为无效,所以将无法删除,因为擦除操作需作用于整个块,且仅当块内所有页均为“无效”状态时才能执行,这样那部分无效空间就完全没法利用了。所以回收机制就是,先迁移有效页到新块,再擦除原块。
闪存出厂即含坏块,使用中还会新增坏块。FTL 通过映射表屏蔽坏块,将数据重定向到备用块
FTL 利用 RAM 缓存映射表,并通过多通道并发访问闪存芯片提升吞吐量
ECC 纠错:集成 BCH/LDPC 算法修复位翻转(Bit Flip)
没有 FTL,闪存将无法被现有操作系统直接使用,其寿命和性能会因物理限制而大幅缩水。
Linux 系统中以.
开头的隐藏文件机制起源于 Unix 系统的设计哲学。在早期 Unix 系统中,ls
命令存在一个显示逻辑缺陷:默认不显示名称以.
开头的文件。这个设计最初是为了隐藏系统关键文件(如.
和..
),后来逐渐演变为隐藏用户配置文件的通用方案。
# 显示所有隐藏文件(含系统级)
ls -a # 输出示例:.bashrc .cache .git/
# 创建隐藏文件/目录
touch .secret_config
mkdir .config
# 修改现有文件为隐藏
mv config .config
隐藏文件有如下应用场景:
# 典型隐藏配置文件
.bashrc # 用户 Shell 配置
.gitconfig # Git 全局配置
# 热加载机制:修改后立即生效
source ~/.bashrc
# 隐藏 SSH 密钥
chmod 600 ~/.ssh/id_rsa
# 防止配置泄露
echo 「export SECRET_KEY=...」 >> .env
内核实现逻辑如下:
// fs/ext4/dir.C 中的关键函数
int ext4_readdir(struct file *file, struct dir_context *ctx) {
struct ext4_dir_entry_2 *de;
while ((de = ext4_next_dir_entry(file, ctx->pos)) != NULL) {
if (de->name[0] == 『.』) continue; // 跳过隐藏文件
ctx->pos = de->inode;
if (!dir_emit(ctx, de->name, de->name_len, de->inode, DT_UNKNOWN))
return 0;
}
return 0;
}
Linux 目录结构遵循文件系统层次标准(FHS)
,其核心设计理念源于 Unix 的「一切皆文件」哲学。这种树状结构通过严格的层级划分实现:
/
为起点/usr
和/opt
实现软件生态扩展目录功能定位典型内容示例访问权限特殊属性/bin
基础用户命令存放区ls
,cp
,mv
,rm
,chmod
755 静态链接二进制文件,单用户模式可用/sbin
系统管理命令存放区fdisk
,ifconfig
,reboot
,service
750 仅 root 可执行,包含硬件管理工具/boot
系统启动关键文件存储vmlinuz
,initrd.img
,grub.cfg
,System.map
755 内核镜像存放区,需保留足够空间(建议≥2GB)/dev
设备文件集散地/dev/sda1
,/dev/tty
,/dev/null
,/dev/zero
755 虚拟设备文件,按需动态创建/etc
系统配置中枢passwd
,fstab
,hosts
,ssh/sshd_config
644 关键配置文件集群,修改需谨慎/home
用户主目录容器/home/user1
,/home/developer
755 每个用户独立目录,支持配额管理/lib
32 位系统库文件存储libc.so.6
,libstdc++.so.6
755 静态库与内核模块存放区/lib64
64 位系统库文件存储libc-2.31.so
,libcrypto.so.3
755 动态链接库主要存放位置/media
可移动设备自动挂载点/media/usb
,/media/cdrom
755udev 自动挂载,支持热插拔/mnt
临时挂载点/mnt/backup
,/mnt/data
755 手动挂载专用,推荐使用systemd-mount/opt
第三方软件安装区/opt/Docker
,/opt/postgresql
755 商业软件标准安装路径,建议保持独立/proc
内核状态镜像/proc/cpuinfo
,/proc/meminfo
,/proc/net/dev
动态权限虚拟文件系统,反映实时系统状态/root
超级用户主目录/root/.bashrc
,/root/scripts
700 仅 root 可访问,存放敏感配置/run
运行时数据暂存区/run/user/1000
,/run/Docker.sock
1777tmpfs 文件系统,重启数据清空/srv
服务数据存储/srv/www
,/srv/nfs
755FHS 标准服务数据存储区/sys
内核对象树/sys/devices
,/sys/bus
,/sys/class/net
动态权限 sysfs 文件系统,展示硬件拓扑/tmp
临时文件回收站/tmp/cache
,/tmp/uploads
1777 自动清理机制,建议配合
目录 | 功能定位 | 典型内容示例 | 访问权限 | 特殊属性 |
---|---|---|---|---|
/bin | 基础用户命令存放区 | ls, cp, mv, rm, chmod | 755 | 静态链接二进制文件,单用户模式可用 |
/sbin | 系统管理命令存放区 | fdisk, ifconfig, reboot, service | 750 | 仅root可执行,包含硬件管理工具 |
/boot | 系统启动关键文件存储 | vmlinuz, initrd.img, grub.cfg, System.map | 755 | 内核镜像存放区,需保留足够空间(建议≥2GB) |
/dev | 设备文件集散地 | /dev/sda1, /dev/tty, /dev/null, /dev/zero | 755 | 虚拟设备文件,按需动态创建 |
/etc | 系统配置中枢 | passwd, fstab, hosts, ssh/sshd_config | 644 | 关键配置文件集群,修改需谨慎 |
/home | 用户主目录容器 | /home/user1, /home/developer | 755 | 每个用户独立目录,支持配额管理 |
/lib | 32位系统库文件存储 | libc.so.6, libstdc++.so.6 | 755 | 静态库与内核模块存放区 |
/lib64 | 64位系统库文件存储 | libc-2.31.so, libcrypto.so.3 | 755 | 动态链接库主要存放位置 |
/media | 可移动设备自动挂载点 | /media/usb, /media/cdrom | 755 | udev自动挂载,支持热插拔 |
/mnt | 临时挂载点 | /mnt/backup, /mnt/data | 755 | 手动挂载专用,推荐使用systemd-mount |
/opt | 第三方软件安装区 | /opt/docker, /opt/postgresql | 755 | 商业软件标准安装路径,建议保持独立 |
/proc | 内核状态镜像 | /proc/cpuinfo, /proc/meminfo, /proc/net/dev | 动态权限 | 虚拟文件系统,反映实时系统状态 |
/root | 超级用户主目录 | /root/.bashrc, /root/scripts | 700 | 仅root可访问,存放敏感配置 |
/run | 运行时数据暂存区 | /run/user/1000, /run/docker.sock | 1777 | tmpfs文件系统,重启数据清空 |
/srv | 服务数据存储 | /srv/www, /srv/nfs | 755 | FHS标准服务数据存储区 |
/sys | 内核对象树 | /sys/devices, /sys/bus, /sys/class/net | 动态权限 | sysfs文件系统,展示硬件拓扑 |
/tmp | 临时文件回收站 | /tmp/cache, /tmp/uploads | 1777 | 自动清理机制,建议配合tmpreaper使用 |
使用
目录树本质上是多叉树(N-ary Tree)的变种,每个节点包含:
// Linux 内核目录项结构体示例
struct dentry {
/* RCU lookup touched fields */
unsigned int d_flags; /* protected by d_lock */
seqcount_t d_seq; /* per dentry seqlock */
struct hlist_bl_node d_hash; /* lookup hash list */
struct dentry *d_parent; /* parent directory 父目录*/
struct qstr d_name;
struct inode *d_inode; /* Where the name belongs to - NULL is
* negative 与该目录项关联的 inode */
unsigned char d_iname[DNAME_INLINE_LEN]; /* small names 短文件名*/
/* Ref lookup also touches following */
struct lockref d_lockref; /* per-dentry lock and refcount */
const struct dentry_operations *d_op; /* 目录项操作 */
struct super_block *d_sb; /* The root of the dentry tree 这个目录项所属的文件系统的超级块(目录项树的根)*/
unsigned long d_time; /* used by d_revalidate 重新生效时间*/
void *d_fsdata; /* fs-specific data 具体文件系统的数据 */
struct list_head d_lru; /* LRU list 未使用目录以 LRU 算法链接的链表 */
/*
* d_child and d_rcu can share memory
*/
union {
struct list_head d_child; /* child of parent list 目录项通过这个加入到父目录的 d_subdirs 中*/
struct rcu_head d_rcu;
} d_u;
struct list_head d_subdirs; /* our children 本目录的所有孩子目录链表头 */
struct hlist_node d_alias; /* inode alias list 索引节点别名链表*/
};
一个有效的 dentry 结构必定有一个 inode 结构,这是因为一个目录项要么代表着一个文件,要么代表着一个目录,而目录实际上也是文件。所以,只要 dentry 结构是有效的,则其指针 d_inode 必定指向一个 inode 结构。但是 inode 却可以对应多个(硬链接)。
整个结构其实就是一棵树,如果看过我的设备模型 kobject 就能知道,目录其实就是文件(kobject、inode)再加上一层封装,这里所谓的封装主要就是增加两个指针,一个是指向父目录,一个是指向该目录所包含的所有文件(普通文件和目录)的链表头。
这样才能有我们的目录操作(比如回到上次目录,只需要一个指针步骤【..】,而进入子目录需要链表索引需要多个步骤)
功能特性:
传统文件系统的目录树本质上是带标签的多叉树,但随着现代文件系统通过引入共享机制,使目录树演变为有向无环图(DAG):
多叉树适用场景
有向无环图适用场景
节点与边的数据结构定义
typedef struct dag_inode {
uint64_t cid; // 内容寻址标识(基于 SHA-256 哈希值)
vector_t parent_dirs; // 父目录指针数组(核心:支持多父节点)
inode_meta_t meta; // 元数据(权限、时间戳等)
block_ref_t data_ref; // 数据块指针(指向实际文件内容)
} dag_inode_t;
dag_inode_t
结构体代表一个文件或目录节点。cid
:文件内容的唯一标识(通过哈希计算),相同内容的文件共享同一个 cid,实现数据去重。parent_dirs
:存储所有父目录的指针(传统文件系统仅支持单父目录,此设计实现多目录硬链接)。data_ref
:指向文件数据块的指针(若为目录则可能为空)。边(隐式存在于 dir_entry_t
)
typedef struct {
dag_inode_t* inode; // 指向目标节点的指针(定义边的方向)
char name[256]; // 目录项名称(边的标签)
} dir_entry_t;
dir_entry_t
表示。inode
字段指向目标节点(文件或子目录),形成有向边(目录 → 文件)。name
是边在父目录中的名称(例如 libc.so
)。block_ref_t
实现(图中未展开)。parent_dirs
中的指针连接(例如 /home/user
指向其父目录 /home
)。通过内容寻址建立节点间的有向关系:
data
字段相同),多个目录项指向同一 inode(引用计数管理)1. 硬链接的 DAG 特性
2. 软连接的 DAG 特性
组件传统树结构 DAG 结构优化收益元数据存储线性 inode 表 B+树索引查询速度提升 5x数据块引用直接/间接指针内容寻址(CID)冗余数据减少 70%
组件 | 传统树结构 | DAG结构 | 优化收益 |
---|---|---|---|
元数据存储 | 线性inode表 | B+树索引 | 查询速度提升5x |
数据块引用 | 直接/间接指针 | 内容寻址(CID) | 冗余数据减少70% |
链接机制 | 仅符号链接 | 硬链接+软链接+跨卷链接 | 跨设备共享实现 |
链接机制仅符号链接硬链接+软链接+跨卷链接跨设备共享实现
文件元数据(Metadata)是数字世界的「身份档案」,它以结构化形式记录着文件的全部非内容属性。在 Linux 系统中,每个文件至少包含 46 项元数据字段,构成其数字身份的核心要素
// ext4 文件系统 inode 结构体(简化版)
struct ext4_inode {
__le16 i_mode; // 文件类型与权限(0644)
__le16 i_uid; // 所有者 ID(0=root)
__le32 i_size; // 文件大小(字节)
__le32 i_atime; // 访问时间戳
__le32 i_mtime; // 修改时间戳
__le32 i_ctime; // 状态变更时间戳
__le16 i_gid; // 所属组 ID
__le16 i_links_count; // 硬链接数
__le32 i_blocks; // 占用数据块数
__le32 i_flags; // 文件特性标志
__le64 i_block[15]; // 数据块指针数组
__le32 i_extra_isize; // 扩展元数据长度
// ...(其他扩展字段)
};
以上为传统的元数据,现在又出现了一种很牛的设计叫 xattr。
扩展属性(xattr)是突破传统文件元数据限制的革命性设计,允许用户以键值对形式附加任意元数据到文件/目录上,突破 POSIX 标准定义的 12 个基础属性限制。其核心特性包括:
user
:普通用户可读写(如 user.version
)security
:安全框架专用(如 SELinux 标签)system
:系统级元数据(如文件编码)trusted
:需 root 权限访问典型场景:在 Linux ext4 中单文件 xattr 容量达 4KB,支持数万属性条目;而 macOS HFS+通过 B*树内联存储,限制单属性大小但支持无限数量
Linux 中的 mount 操作是连接外部存储设备到文件系统的核心机制。其过程涉及用户空间命令到内核的系统调用、数据结构构建和资源管理。以下是详细解析:
一、用户空间:mount 命令的解析
当执行 mount /dev/sdb1 /mnt 时:
参数解析
-t 指定文件系统类型(如 ext4、nfs)
-o 定义挂载选项(如 ro 只读、rw 读写、noexec 禁用执行)
设备路径(如 /dev/sdb1)和挂载点(如 /mnt)
系统调用触发
命令通过 glibc 库调用内核的 sys_mount() 系统调用,传递参数到内核空间
二、内核空间:挂载的核心流程
1. 系统调用入口:sys_mount()
将用户空间参数(设备路径、挂载点、文件系统类型)复制到内核空间。
2. 文件系统类型识别
调用 get_fs_type(),根据 type 参数(如 ext4)查找已注册的 file_system_type 结构体。该结构体包含文件系统操作函数指针(如 mount 回调)。
3. 创建源文件系统:vfs_kern_mount()
分配 vfsmount**:内核创建 vfsmount 结构,记录挂载元数据(如设备名、挂载标志)。
填充超级块:调用文件系统特定的 get_sb() 函数(如 ext4_fill_super()),从设备读取超级块(super_block),初始化根目录的 dentry(目录项)和 inode(索引节点)。
4. 挂载点查找与锁定:lock_mount()
检查挂载点目录(如 /mnt)是否有效,并防止并发挂载冲突。
若挂载点已被其他文件系统覆盖(如嵌套挂载),需找到最顶层的挂载点。
5. 关联源文件系统:graft_tree()
将新创建的 vfsmount 链接到挂载点,更新全局挂载哈希表 mount_hashtable,建立父子关系。
6. 命名空间集成
将新挂载的文件系统添加到当前进程的挂载命名空间(namespace),使挂载对所有共享该命名空间的进程可见。
挂载操作的核心是 将存储设备关联到文件系统树:
vfsmount
、super_block
)→ 命名空间集成。通过 mount
,Linux 将异构存储转化为统一的文件树,这正是 “一切皆文件” 哲学的基石。
pivot_root
是 Linux 系统调用,用于将进程的根文件系统切换到新路径,同时将旧根移动到新根下的子目录。其核心目标是实现强隔离的根文件系统切换,避免传统 chroot
的挂载泄漏问题。,单纯的 chroot
仅重定向路径解析,进程仍共享主机的挂载命名空间(如 /proc
)。而 pivot_root
结合 Mount Namespace 可创建完全独立的文件系统视图--------Docker 的感觉是不是出来了
OverlayFS 是一种 联合挂载文件系统,通过叠加多个目录(称为“层”)形成单一视图,实现文件系统的动态合并与隔离。其核心结构包含三个目录:
lowerdir=/dir1:/dir2
),优先级从左到右递减。关键机制
lower
层文件时,OverlayFS 自动将其复制到 upper
层再修改,保持底层只读性。lower
层文件时,在 upper
层创建特殊标记(字符设备文件),使文件在合并视图中“消失”。upper
层文件,其次是 lower
层中靠左的目录(后写入者优先)。OverlayFS 一大应用就是 Docker 镜像层,Docker 镜像由多层只读文件(镜像层)组成,容器运行时需将这些层叠加为统一视图。联合文件系统(如 OverlayFS)依赖底层块设备接口实现分层管理:
/dev/loop0
)将镜像层文件虚拟化为独立的“磁盘”,供 OverlayFS 挂载为 lowerdir
(只读层)和 upperdir
(可写层)。启用写时复制(CoW)机制
LVM(Logical Volume Manager,逻辑卷管理器)是 Linux 环境下对物理磁盘进行抽象化管理的核心机制,通过逻辑层动态分配存储资源,解决传统分区僵化问题。
LVM 在物理磁盘和文件系统之间构建逻辑层,将存储资源池化并动态分配,其核心组件如下:
/dev/sda1
)、RAID 设备或整个硬盘。pvcreate /dev/sdb1
(将分区初始化为 PV)。/dev/vg_data/lv_home
)。lvcreate -L 20G -n lv_data vg_data
。mount
挂载使用。有了 lvm,可以很轻松进行磁盘的动态管理:
// 扩容
lvextend -L +5G /dev/vg_data/lv_data # 增加 5GB 空间
resize2fs /dev/vg_data/lv_data # 调整文件系统大小(在线生效)
// 缩容
umount /data # 卸载文件系统
resize2fs /dev/vg_data/lv_data 15G # 先缩小文件系统
lvreduce -L 15G /dev/vg_data/lv_data # 再缩小 LV
// 快照(Snapshot)
创建 LV 的只读时间点副本,仅记录变更数据(CoW 机制)
备份数据库时创建快照,避免服务中断。
lvcreate -s -n lv_snap -L 2G /dev/vg_data/lv_data
典型应用场景
文件目录其实也可以当数据库使用:
# 目录结构
db/
├── users/
│ ├── .schema # id:int, name:string
│ ├── 1.txt # {「id」:1, 「name」:「Alice」}
│ └── by_role/ # 索引目录
│ └── 3/ → ../1.txt # 按角色 ID 分类
└── orders/
├── 100.txt # {「id」:100, 「user_id」:1}
└── 100_user.txt → ../users/1.txt # 关联用户
表结构映射
users/
目录即 users
表)。users/1.txt
表示 id=1
的用户)。// users/1.txt
{「id」:1, 「name」:「Alice」, 「role_id」:3}
软链接实现表关联:
一对一双向软链接互指profile/1.txt → users/1.txt
(反之亦然)一对多在“多”方目录创建指向“一”方的软链接orders/100.txt → users/1.txt
多对多建立中间目录存储关联对
一对一 | 双向软链接互指 | profile/1.txt → users/1.txt(反之亦然) |
---|---|---|
一对多 | 在“多”方目录创建指向“一”方的软链接 | orders/100.txt → users/1.txt |
多对多 | 建立中间目录存储关联对 | user_role/1_3.txt(软链接至 users/1 和 roles/3) |
user_role/1_3.txt
(软链接至users/1
和roles/3
)
查询操作的实现
grep
模拟 WHERE:
grep -l 『「role_id」:3』 users/*.txt # 查找角色 ID 为 3 的用户
find
模拟 JOIN:
find orders/ -lname 『*users/1.txt』 # 查找用户 1 的所有订单
awk
统计:awk -F: 『{sum += $2} END {print sum}』 orders/*.txt # 计算订单总金额
关系型数据库通过表结构和指针机制重构目录树逻辑:
user_id
)是跨表索引的内存地址映射SELECT orders.* FROM users
JOIN orders ON users.id = orders.user_id -- ID 的指针跳转
WHERE users.name = 『Alice』;
SQL 本质:JOIN 操作即指针配对(users.id
→ orders.user_id
)
事务与 ACID:指针操作的原子保障 ACID 确保指针跳转的可靠性:
ACID 属性指针操作意义实现机制原子性指针批量更新要么全成功要么全失败 Undo Log 回滚一致性指针始终指向有效内存地址外键约束+锁机制隔离性并发指针互不干扰 MVCC 多版本控制持久性指针变更永久有效 Redo Log
ACID属性 | 指针操作意义 | 实现机制 |
---|---|---|
原子性 | 指针批量更新要么全成功要么全失败 | Undo Log回滚 |
一致性 | 指针始终指向有效内存地址 | 外键约束+锁机制 |
隔离性 | 并发指针互不干扰 | MVCC多版本控制 |
持久性 | 指针变更永久有效 | Redo Log持久化 |
持久化
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- 指针 A
UPDATE accounts SET balance = balance + 100 WHERE id = 2; -- 指针 B
COMMIT; -- 原子提交两个指针更新
海量优化:当关系模型遇到性能瓶颈
随着数据量增长,传统关系模型需针对性优化:
SELECT id,name FROM users
)游标:精细化指针控制器 游标(Cursor)实现逐行精准控制,避免内存溢出
DECLARE user_cursor CURSOR FOR
SELECT id FROM users WHERE status = 『active』; -- 定义结果集指针
OPEN user_cursor;
FETCH NEXT FROM user_cursor; -- 指针向前移动
WHILE @@FETCH_STATUS = 0 DO
-- 处理当前行数据
FETCH NEXT FROM user_cursor;
END WHILE;
CLOSE user_cursor;
优化方向实现方式典型案例水平分片按 ID 哈希分布到不同物理节点 MySQL 分库分表读写分离写主库→读从库(异步指针同步)AWS RDS Proxy列式存储按列组织数据,提升聚合查询速度
优化方向 | 实现方式 | 典型案例 |
---|---|---|
水平分片 | 按ID哈希分布到不同物理节点 | MySQL分库分表 |
读写分离 | 写主库→读从库(异步指针同步) | AWS RDS Proxy |
列式存储 | 按列组织数据,提升聚合查询速度 | Google BigQuery |
Google BigQuery
关系型数据库的瓶颈无法满足 Web 2.0 需求
数据模型的革新需求
当数据模型突破二维结构,NoSQL 提供新思路:
类型数据结构指针逻辑适用场景键值数据库Key-ValueKey 即数据地址指针缓存(Redis)文档数据库JSON
类型 | 数据结构 | 指针逻辑 | 适用场景 |
---|---|---|---|
键值数据库 | Key-Value | Key即数据地址指针 | 缓存(Redis) |
文档数据库 | JSON文档 | 文档内嵌指针(_id) | 内容管理(MongoDB) |
列族数据库 | 列簇+行键 | 行键定位列簇指针 | 日志分析(HBase) |
图数据库 | 节点+边 | 边即关系指针 | 社交网络(Neo4j) |
文档文档内嵌指针(_id)内容管理(MongoDB)列族数据库列簇+行键行键定位列簇指针日志分析(HBase)图数据库节点+边边即关系指针社交网络(Neo4j)
BASE: Basically Available(基本可用)
Soft-state(软状态)
Eventually consistent(最终一致)
例:微信朋友圈使用 HBase 实现写扩散,异步同步到好友时间线
UID(用户标识符)
/etc/passwd
静态分配,或 LDAP 动态映射。GID(组标识符)
/etc/group
配置),进程的权限取所有所属组的并集Linux 进程实际运行时涉及四类身份标识:
标识类型缩写作用实际用户 IDruid 进程启动者的原始 UID 有效用户 IDeuid 权限检查的依据,决定文件访问能力保存用户 IDsuid 临时存储 euid,用于权限恢复文件系统用户 IDfuid
标识类型 | 缩写 | 作用 |
---|---|---|
实际用户ID | ruid | 进程启动者的原始 UID |
有效用户ID | euid | 权限检查的依据,决定文件访问能力 |
保存用户ID | suid | 临时存储 euid,用于权限恢复 |
文件系统用户ID | fuid | 文件操作时的最终权限标识(通常等于 euid) |
文件操作时的最终权限标识(通常等于 euid)
当进程尝试访问文件时,内核比较 euid 和文件属主的 UID,并检查文件模式(mode)中的权限位
文件模式(mode):权限规则的载体
文件模式是一个 12 位整数(如 0o40755
),分为两部分:
rwxr-xr-x
:分别定义属主、属组、其他用户的读/写/执行权限。/tmp
)chmod u+s /usr/bin/passwd
此时普通用户执行 passwd 时,进程 euid 临时变为 0(root),从而修改 /etc/shadow
程序权限需求 SetUID 作用passwd
修改 /etc/shadow 临时赋予 root 写权限ping
发送 ICMP 包获取 RAW Socket 权限
程序 | 权限需求 | SetUID 作用 |
---|---|---|
passwd | 修改 /etc/shadow | 临时赋予 root 写权限 |
ping | 发送 ICMP 包 | 获取 RAW Socket 权限 |
sudo | 切换用户 | 以目标用户身份执行命令 |
切换用户以目标用户身份执行命令
缓冲区溢出
高地址
┌─────────────────┐
│ 参数 │ ← 调用者压入的参数(若参数过多)
├─────────────────┤
│ 返回地址(EIP)│ ← 函数执行完后跳转的位置
├─────────────────┤
│ 旧 EBP │ ← 保存调用者的栈帧基址
├─────────────────┤
│ 局部变量 │ ← 当前函数的变量(如 buffer[64])
└─────────────────┘
低地址(栈顶) → ESP
ESP(栈指针):始终指向当前栈顶(最低地址)
EBP(基址指针):指向当前栈帧的基地址,用于定位参数和局部变量(如[EBP-4]表示第一个局部变量)
EIP(指令指针):存储下一条待执行指令的地址,被覆盖后程序将跳转到攻击者指定的地址
#include <stdio.h>
#include <string.h>
void vulnerable() {
char buffer[64]; // 局部变量,位于栈帧底部
gets(buffer); // 无边界检查的输入函数
}
int main() {
vulnerable();
return 0;
}
vulnerable()
时,栈中压入 main
的返回地址(EIP)和旧 EBP。buffer
分配在旧 EBP 下方(如地址 0xbffff2c0
)。「A」*68 + 「\xef\xbe\xad\xde」
):buffer
。0xdeadbeef
)。vulnerable()
执行 RET
指令时,从栈顶弹出覆盖后的 EIP 到指令寄存器。0xdeadbeef
执行(可能是攻击代码入口)。攻击者如何利用此漏洞
攻击载荷构造
plaintext
复制
| 填充数据(64 字节) | 覆盖的 EBP(4 字节) | 恶意代码地址(4 字节) | Shellcode(恶意指令) |
/bin/sh
),通常注入到 buffer
起始位置。buffer
起始地址(需预测地址,或通过 NOP 雪橙增加命中率)。实际攻击步骤
buffer
到 EIP 的偏移(如 72 字节)。编程层防护
fgets(buffer, sizeof(buffer), stdin)
替代 gets()
,限制输入长度。-fstack-protector
:插入金丝雀值(Canary)于 EBP 和局部变量之间,函数返回前校验其完整性。操作系统级防护
机制原理效果ASLR随机化栈/堆/库的基址,增加预测 EIP 的难度迫使攻击者需地址泄露漏洞DEP/NX标记栈内存为“不可执行”,阻止 Shellcode 运行需结合 ROP 绕过KPTI隔离用户/内核页表,阻止 Meltdown
机制 | 原理 | 效果 |
---|---|---|
ASLR | 随机化栈/堆/库的基址,增加预测EIP的难度 | 迫使攻击者需地址泄露漏洞 |
DEP/NX | 标记栈内存为“不可执行”,阻止Shellcode运行 | 需结合ROP绕过 |
KPTI | 隔离用户/内核页表,阻止Meltdown类攻击窃取内核数据 | 缓解旁路攻击 |
类攻击窃取内核数据缓解旁路攻击
硬件漏洞 meltdown
利用 CPU 乱序执行和推测执行的优化缺陷:
0xffffffffc0000000
)触发异常,但乱序执行已读取数据。array[data*4096]
的加载时间差异泄露字节内容KPTI(内核页表隔离)防御机制
之前 nemu 运行程序只有原来本机性能的 1/10,vmware 做到了什么?使虚拟机成为了商用的产品,即 1 台 pc 可以虚拟化出 100 台卖给你,ISP 厂商狂喜。
guest ring3 -> host ring3
#GP
),控制权移交 VMware Hypervisor。IN/OUT
端口操作)替换为调用 Hypervisor 模拟函数的跳转指令。x86 硬件虚拟化,8 级页表
影子页表(Shadow Page Table)的复杂化处理
mov cr3
指令),而非全量同步,减少 VM Exit 次数。硬件辅助内存虚拟化(EPT/NPT)的深度整合
invvpid
)场景下动态切换回影子页表,兼顾性能与兼容性。操作系统虚拟化---namespace+cggroups - Docker
云时代-微服务-- k8s
serverless--容器的概念也不要了-faas
oversubscribed -》 流量计费 qps
cicd
HTTPS://jyywiki.cn/OS/manuals/sysv-abi.pdf
HTTPS://www.cnblogs.com/yubo-guan/p/18804432
HTTPS://www.cnblogs.com/sparkdev/p/11605804.HTML
深入了解之链接器与加载器_加载器_邱学喆_InfoQ 写作社区
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。