一个躺在磁盘上的 a.out 文件,到你看到的运行的进程,中间到底发生了什么?本页用 7 个图把每一次内核/运行库的动作拆给你看。
你在 shell 里敲下 ./demo,回车。接下来发生的事被拆成 8 个阶段。点任意一个阶段看它在哪一层、由谁执行。
编译链接后的 demo 是一个 ELF(Executable and Linkable Format) 文件,躺在磁盘上。它的结构像下面这样(点任一区块看说明)。注意:它只是数据,CPU 此刻并没有在跑它。
几乎所有程序都是由另一个进程生出来的。你双击、命令行、点图标,归根结底都是某进程调用了这两个系统调用。
execve 时内核并不直接把文件拷进内存,而是先做两件关键事:① 规划虚拟地址空间(建 VMA);② 按 段(Segment) 而非 section 来映射。
内核建立 mm_struct,里面挂一串 vm_area_struct。每个 VMA 描述「[起,止] 地址 + 权限 + 背后文件偏移」。此时物理内存一页都没分配——这叫按需分页。
另外 PT_INTERP 段指向动态链接器路径,内核读到它就顺带把 ld-linux.so 也映射进来——这是下一节动态链接的起点。
现代程序几乎都依赖共享库(libc 等)。这些库的确切地址在运行时才确定(位置无关 + ASLR)。于是调用 printf 这样的外部函数时,不能写死地址,要靠 PLT(过程链接表)+ GOT(全局偏移表) 在运行时解析。下面演示第一次调用 printf 的完整跳转。
内核建好 VMA、PC 指向入口后返回用户态。CPU 去取第一条指令地址 → MMU 查页表 → 没有对应的物理页 → 触发 缺页异常(page fault) → 陷入内核。
execve 时内核在进程最高地址往下铺好初始栈,作为进入 __libc_start_main 之前的「交接清单」。点任意一行看内容。
| 维度 | 静态程序(磁盘 ELF) | 动态进程(运行中) |
|---|---|---|
| 存在形式 | 一串字节(指令+数据) | 有 PCB、地址空间、寄存器状态的活实例 |
| PID | 无 | 有(fork 时分配) |
| 地址空间 | 只有编译期假设的虚拟地址 | 内核建好的 VMA + 真实页表 |
| 物理内存 | 0(按需分页,访问才搬) | 已分配在用到的页 |
| 栈 / 堆 | 无 | 栈由内核铺、堆靠 brk 长出来 |
| 外部函数地址 | GOT 里是占位桩 | 运行时由 ld.so 解析回填 |
| 共享库 | 只是「依赖线索」 | 已 mmap 进地址空间、多进程共用 |
| CPU 在跑吗 | 否 | 是(时间片轮转) |