五、编译与链接
5.1 编译过程详解
┌─────────────────────────────────────────────────────────────────────────────┐
│ 编译过程 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ source.c ──→ 预处理 ──→ 编译 ──→ 汇编 ──→ 链接 ──→ executable │
│ (cpp) (cc1) (as) (ld) │
│ │
│ 1. 预处理: │
│ - 展开宏定义(#define) │
│ - 包含头文件(#include) │
│ - 处理条件编译(#ifdef) │
│ │
│ 2. 编译: │
│ - 词法分析 → 语法分析 → 语义分析 → 中间代码生成 │
│ - 优化 → 汇编代码生成 │
│ │
│ 3. 汇编: │
│ - 将汇编代码转换为目标文件(.o)(机器码) │
│ │
│ 4. 链接: │
│ - 符号解析:处理外部引用 │
│ - 重定位:确定最终的内存地址 │
│ - 静态链接(.a) vs 动态链接(.so) │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
5.2 ELF文件格式
/*
* ELF (Executable and Linkable Format) 文件结构
*
* ┌─────────────────────────────────────┐
* │ ELF Header │ ← 文件类型、入口点
* ├─────────────────────────────────────┤
* │ Program Headers │ ← 段信息(用于加载)
* │ (程序头表,用于运行时加载) │
* ├─────────────────────────────────────┤
* │ │
* │ .text (代码段) │ ← 可执行指令
* │ │
* ├─────────────────────────────────────┤
* │ .data (数据段) │ ← 已初始化全局变量
* ├─────────────────────────────────────┤
* │ .bss (未初始化数据) │ ← 未初始化全局变量
* ├─────────────────────────────────────┤
* │ .rodata (只读数据) │ ← 字符串常量
* ├─────────────────────────────────────┤
* │ Section Headers │ ← 节信息(用于链接)
* │ (节头表,用于链接和调试) │
* └─────────────────────────────────────┘
*/
// 查看ELF文件信息
// readelf -h a.out // 查看头部
// readelf -S a.out // 查看节表
// objdump -d a.out // 反汇编
// size a.out // 查看各段大小
5.3 动态链接与PLT/GOT
/*
* 动态链接过程
*
* 程序调用共享库函数:
* call printf@plt
*
* PLT(Procedure Linkage Table):过程链接表
* GOT(Global Offset Table):全局偏移表
*
* 第一次调用:
* call printf@plt ──→ .plt ──→ 跳转到 GOT
* │
* ↓
* 动态链接器解析printf地址
* │
* ↓
* 更新GOT条目 ──→ 执行printf
*
* 后续调用:
* call printf@plt ──→ .plt ──→ GOT(已存地址)──→ 直接执行printf
*/
// 查看动态依赖
ldd a.out
// linux-vdso.so.1 (0x00007ffe5bdfe000)
// libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f8a2c800000)
// /lib64/ld-linux-x86-64.so.2 (0x00007f8a2ca2c000)
// 查看GOT表
objdump -R a.out
六、操作系统底层原理
6.1 进程与线程的底层实现
/*
* Linux进程描述符(task_struct)
*/
struct task_struct {
volatile long state; // 进程状态
void *stack; // 内核栈
unsigned int flags; // 进程标志
int prio, static_prio; // 优先级
struct mm_struct *mm; // 内存描述符
struct files_struct *files; // 文件描述符表
struct signal_struct *signal; // 信号处理
struct task_struct *parent; // 父进程
struct list_head children; // 子进程列表
// 调度相关
int on_rq;
struct sched_entity se; // CFS调度实体
struct sched_rt_entity rt; // 实时调度实体
// 时间相关
cputime_t utime, stime; // 用户态/内核态CPU时间
struct timespec start_time; // 启动时间
// 命名空间
struct nsproxy *nsproxy; // 命名空间代理
};
/*
* 进程创建(fork系统调用)
*
* fork() → sys_fork() → do_fork() → copy_process()
*
* copy_process()关键步骤:
* 1. 分配新的task_struct
* 2. 复制进程内存空间(写时复制,COW)
* 3. 复制文件描述符
* 4. 复制信号处理表
* 5. 设置进程状态为TASK_RUNNABLE
* 6. 返回子进程PID
*/
// 写时复制(Copy-on-Write)原理
// fork后父子进程共享物理内存页
// 只有当某个进程尝试写入时,才复制该页
6.2 上下文切换成本
/*
* 上下文切换需要保存和恢复的寄存器
*/
// 内核栈上保存的上下文(简化)
struct context {
unsigned long r15, r14, r13, r12, rbp, rbx;
unsigned long r11, r10, r9, r8;
unsigned long rax, rcx, rdx, rsi, rdi;
unsigned long rip; // 指令指针
unsigned long cs, flags; // 代码段、标志寄存器
unsigned long rsp; // 栈指针
unsigned long ss; // 栈段
};
// 上下文切换成本:
// 1. 保存/恢复寄存器:~100-200个时钟周期
// 2. TLB刷新:~几十到几百个时钟周期(取决于TLB大小)
// 3. 缓存失效:L1/L2缓存污染,L3部分保留
// 4. 切换页表:cr3寄存器写入,TLB flush(部分处理器有TLB条目tag)
// 总成本:约1-10微秒(取决于体系结构和负载)
6.3 系统调用原理
/*
* 系统调用流程(x86-64)
*
* 用户态:
* 1. 将系统调用号放入rax
* 2. 将参数放入rdi, rsi, rdx, r10, r8, r9
* 3. 执行syscall指令
*
* 内核态:
* 4. CPU切换到内核态(权限级别从3切换到0)
* 5. 根据rax中的系统调用号查找sys_call_table
* 6. 执行对应的内核函数
* 7. 返回值放入rax
* 8. 执行sysret返回用户态
*/
// 示例:write系统调用
// ssize_t write(int fd, const void *buf, size_t count);
// 汇编实现
write:
movq $1, %rax // write系统调用号=1
movq $1, %rdi // fd=1 (stdout)
leaq msg(%rip), %rsi // buf指针
movq $13, %rdx // count=13
syscall // 陷入内核
ret
msg:
.string "Hello, World\n"
// 使用strace追踪系统调用
// strace -c ./program // 统计系统调用次数和耗时
// strace -e trace=file ./program // 只追踪文件相关调用
6.4 虚拟内存与分页
/*
* 虚拟内存地址转换(x86-64 四级页表)
*
* 虚拟地址(48位有效):
* ┌───────┬───────┬───────┬───────┬────────────┐
* │ PML4 │ PDP │ PD │ PT │ Offset │
* │ 9位 │ 9位 │ 9位 │ 9位 │ 12位 │
* └───────┴───────┴───────┴───────┴────────────┘
*
* 转换过程:
* 1. 从CR3寄存器读取PML4页表基址
* 2. 使用PML4索引找到PDP页表
* 3. 使用PDP索引找到PD页表
* 4. 使用PD索引找到PT页表
* 5. 使用PT索引找到物理页帧号(PFN)
* 6. 物理地址 = PFN << 12 + Offset
*/
// 查看进程内存映射
cat /proc/<PID>/maps
// 示例输出:
// 00400000-0040b000 r-xp 00000000 08:01 12345 /usr/bin/cat
// 0060a000-0060b000 r--p 0000a000 08:01 12345 /usr/bin/cat
// 0060b000-0060c000 rw-p 0000b000 08:01 12345 /usr/bin/cat
// 7f8a2c000000-7f8a2c200000 rw-p 00000000 00:00 0 [heap]
// 7f8a2ca00000-7f8a2cc00000 r-xp 00000000 08:01 67890 /lib/x86_64-linux-gnu/libc.so.6
// 7ffe5bdfe000-7ffe5be1f000 rw-p 00000000 00:00 0 [stack]
// ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
// 内存映射区域权限:
// r = 读, w = 写, x = 执行, p = 私有, s = 共享