程序是如何一步步跑进内存的?白帽白捡一个n1
从“点下运行”到 CPU 执行,中间到底发生了什么?
我们每天都在运行程序,却很少有人真正想过一个问题:
当你双击一个程序,或者在终端敲下回车的那一刻,计算机到底做了什么?
这不是一个“操作系统课本问题”,而是理解计算机系统的关键入口。
一、程序在运行之前,只是一个“普通文件”
先明确一个非常重要的概念:
程序 ≠ 进程
1️⃣ 程序是什么?
在运行之前,程序只是磁盘上的一个文件,我们也叫做可执行文件:
• .exe
• ELF(Linux)
• Mach-O(macOS)
一段按照特定格式组织的二进制数据
二、运行程序的第一步:操作系统介入
当我们执行一个程序:
./hello
真正“开始工作”的其实不是程序,而是——
2️⃣ 操作系统做的第一件事
操作系统会先问一个问题:
这个文件能不能运行?
• 文件类型(是否是可执行格式)
• 权限位(是否有执行权限)
• 架构是否匹配(x86 / ARM)
所以程序执行的第一步就是操作系统先去检查它.
三、创建进程:程序通过什么去运行?
如果检查通过,操作系统会:
创建一个新的进程(Process)
这一步非常关键。
3️⃣ 什么是进程?
进程不是代码,而是一整套运行环境,包括:
• 独立的虚拟地址空间
• 寄存器状态
• 打开的文件
• 权限信息
• 调度信息
进程是操作系统分配资源的最小单位
四、加载程序:代码如何进入内存?
现在,操作系统要做一件核心工作:
把程序“加载”进内存
4️⃣ 加载 ≠ 全部读进内存
这是一个常见误区。
现代操作系统使用的是:
按需加载(Lazy Loading)
• 程序文件在磁盘
• 只有需要的部分才会被映射到内存
5️⃣ 内存中的典型布局
一个进程的内存空间通常长这样:
┌────────────┐
│ 栈 Stack │ ← 函数调用、局部变量
├────────────┤
│ 堆 Heap │ ← 动态内存
├────────────┤
│ 数据段 │ ← 全局变量
├────────────┤
│ 代码段 │ ← 程序指令
└────────────┘
这些区域并不是“随便放的”,
而是由操作系统和程序格式共同决定。
五、虚拟内存
这里是整个过程最重要的一点。
6️⃣ 为什么要有虚拟内存?
这时候每个进程都会觉得:
“我独占了整个内存”
• 内存是共享的
• 地址是虚拟的
• 映射由操作系统维护
虚拟内存的作用:
• 进程隔离
• 内存保护
• 简化程序设计
• 支持按需加载
六、动态链接:程序并不孤单
我们写的程序,其实很少是“单独运行”的。
7️⃣ 动态库是什么时候加载的?
• libc
• libm
在程序启动时:
• 加载器(loader)解析依赖
• 映射共享库到内存
• 修正符号地址
这也是为什么多个程序可以共享同一份库代码。
七、设置入口点:CPU 从哪里开始执行?
程序被加载进内存后,
还差最后一步。
8️⃣ 程序的“第一条指令”
每个可执行文件都有一个:
入口地址(Entry Point)
操作系统会:
• 设置 CPU 寄存器
• 把指令指针指向入口点
• 准备好栈和参数
CPU 开始执行第一条指令
从这一刻开始,程序真正“跑起来了”。
八、从 main() 之前开始的世界
很多人以为程序是从 main() 开始的。
main() 之前,已经发生了很多事情
• 运行时环境初始化
• 动态库初始化
• 全局对象构造
main() 只是你能看到的入口之一。
九、为什么理解这个过程如此重要?
因为很多“看似高级”的问题,本质都在这里:
• 程序为什么会崩溃?
• 内存为什么会越界?
• 程序为什么启动慢?
• 为什么同一程序能运行多个实例?
理解程序如何进入内存,本质是在理解操作系统如何管理世界
理解程序从磁盘到内存,从静态到运行态的这个过程,会让你对计算机底层有更深的了解,接下来我也会不断更新这个系列的文章,我是N1,欢迎阅读.


