第01节-从源代码到程序运行
第 1 节:从源代码到程序运行
所属课程:操作系统自学路线:面向网络安全、逆向工程与漏洞分析
所属周次:第 1 周
课程主题:计算机如何运行程序
本节目标:理解一个 C 程序从源代码变成正在运行的进程,中间经历了哪些阶段,操作系统在其中承担什么职责。
1. 本节课你要学会什么
学完这一节,你应该能回答下面几个问题:
- 什么是操作系统?
- 程序为什么不能“自己运行”?
- 源代码、可执行文件、进程之间有什么区别?
- 编译、汇编、链接、加载分别在做什么?
- 操作系统为什么要把程序和硬件隔离开?
- 为什么网络安全、逆向工程、漏洞分析都必须理解程序运行过程?
本节课不要求你立刻理解所有底层细节,但你要建立一条主线:
1 | |
这条线后面会反复出现。
2. 什么是操作系统
操作系统可以先粗略理解成:
操作系统是运行在硬件之上的基础软件,它负责管理硬件资源,并为普通程序提供统一、安全、方便的运行环境。
如果没有操作系统,一个普通程序想要完成最简单的事情,比如打印一行文字,理论上就要自己处理:
- CPU 如何执行指令
- 内存如何分配
- 屏幕或终端如何输出
- 文件如何读写
- 键盘输入如何接收
- 多个程序如何同时运行
- 程序之间如何互不干扰
- 用户权限如何控制
这显然太复杂。
所以操作系统替应用程序管理底层资源。
常见操作系统包括:
- Linux
- Windows
- macOS
- Android
- iOS
它们的内部实现不同,但核心职责类似。
3. 操作系统主要管理什么
从学习操作系统的角度,可以先抓住五类资源。
3.1 CPU
CPU 是执行指令的硬件。
操作系统要决定:
- 哪个程序先运行
- 每个程序运行多久
- 程序阻塞时切换到谁
- 多个线程如何调度
这部分对应后面的:
- 进程
- 线程
- 调度
- 上下文切换
3.2 内存
程序运行时需要内存。
操作系统要决定:
- 给每个进程分配多少内存
- 每个进程能访问哪些地址
- 不同进程之间如何隔离
- 程序访问非法地址时如何处理
这部分对应后面的:
- 虚拟内存
- 页表
- 栈
- 堆
- ASLR
- DEP / NX
- 内存漏洞
3.3 文件
程序经常要读写文件。
操作系统要提供:
- 文件创建
- 文件读取
- 文件写入
- 文件删除
- 文件权限控制
- 目录管理
这部分对应后面的:
- 文件系统
- 文件描述符
- Windows Handle
- 恶意代码文件行为分析
3.4 设备和 I/O
键盘、鼠标、硬盘、网卡、显示器都属于设备。
普通程序一般不直接控制硬件,而是通过操作系统提供的接口访问设备。
这部分对应后面的:
- 驱动程序
- I/O
- 中断
- 内核
3.5 权限和安全边界
操作系统还要控制:
- 谁能访问某个文件
- 谁能监听某个端口
- 谁能安装驱动
- 哪个进程能调试另一个进程
- 普通用户和管理员/root 的区别
这部分对应后面的:
- 用户权限
- root / Administrator
- Token
- ACL
- UAC
- setuid
- 提权漏洞
4. 程序、可执行文件、进程的区别
这三个词很容易混淆。
4.1 源代码
源代码是人写的文本,比如:
1 | |
它是给人看的,CPU 不能直接执行 C 语言源代码。
4.2 可执行文件
源代码经过编译、链接之后,会变成可执行文件。
Linux 下可能是:
1 | |
Windows 下可能是:
1 | |
可执行文件本质上是一个有特定格式的二进制文件。
Linux 常见格式是 ELF:
1 | |
Windows 常见格式是 PE:
1 | |
可执行文件里通常包含:
- 机器指令
- 字符串
- 全局变量初始值
- 动态库依赖信息
- 程序入口点
- 段和节信息
4.3 进程
进程是正在运行的程序实例。
一个可执行文件放在磁盘上时,它只是文件。
当你运行它时,操作系统会把它加载到内存,创建运行环境,这时它才成为进程。
例如:
1 | |
此时操作系统会创建一个进程。
同一个可执行文件可以运行多次,所以可以产生多个进程。
比如你打开三个记事本:
1 | |
所以:
1 | |
5. 从 C 源代码到可执行文件
以这个程序为例:
1 | |
使用 gcc 编译:
1 | |
表面上只执行了一条命令,但内部大致经历了四步。
1 | |
6. 第一步:预处理
预处理主要处理 # 开头的内容。
例如:
1 | |
它的意思不是“运行时导入 stdio.h”,而是在编译前把相关声明展开进来。
预处理会处理:
#include#define- 条件编译,比如
#ifdef - 去除注释
你可以用下面命令观察预处理结果:
1 | |
生成的 hello.i 仍然是 C 代码,但会比原文件大很多。
7. 第二步:编译
编译是把 C 代码转换成汇编代码。
命令:
1 | |
生成的 hello.s 是汇编代码。
汇编代码已经很接近 CPU 能理解的指令,但仍然是文本形式。
你可能会看到类似内容:
1 | |
这一步非常重要,因为逆向工程中经常看到的就是汇编指令。
你写的 C 代码:
1 | |
最终会变成若干条机器相关的指令。
8. 第三步:汇编
汇编是把汇编代码转换成目标文件。
命令:
1 | |
目标文件 hello.o 已经是二进制文件,但它通常还不能直接运行。
为什么?
因为它里面可能还有一些外部符号没有解决。
例如:
1 | |
printf 不是你自己写的函数,它来自 C 标准库。
此时目标文件可能只知道:
1 | |
9. 第四步:链接
链接负责把多个目标文件和库组合成最终可执行文件。
普通编译命令:
1 | |
最后一步会链接 C 标准库。
链接要解决的问题包括:
printf在哪里?- 程序入口点在哪里?
- 需要哪些动态库?
- 各个函数和变量最终放到什么位置?
链接分两种:
9.1 静态链接
把需要的库代码直接打进可执行文件。
优点:
- 依赖少
- 部署方便
缺点:
- 文件大
- 多个程序无法共享同一份库代码
9.2 动态链接
可执行文件不包含完整库代码,只记录运行时需要哪些动态库。
Linux 常见动态库:
1 | |
Windows 常见动态库:
1 | |
动态链接是逆向工程中的重点,因为导入函数能反映程序行为。
例如 Windows 程序导入了:
1 | |
你大致可以猜测它可能有:
- 文件操作
- 注册表操作
- 创建进程
- 网络连接
这就是恶意代码分析常用的入口。
10. 可执行文件里有什么
一个可执行文件不是简单的“机器指令堆在一起”。
它有格式。
Linux ELF 文件通常包含:
- ELF Header
- Program Header
- Section Header
.text.data.bss.rodata.plt.got- 动态库信息
- 符号表
Windows PE 文件通常包含:
- DOS Header
- PE Header
- Section Table
.text.rdata.data.rsrc- Import Table
- Export Table
- IAT
现在你不需要完全掌握这些结构,只要先知道:
操作系统不是随便运行一个文件,而是识别可执行文件格式,根据里面的元信息把程序加载起来。
11. 从可执行文件到进程
当你执行:
1 | |
操作系统大致做了这些事:
- 检查文件是否存在
- 检查当前用户是否有执行权限
- 识别文件格式,比如 ELF
- 读取可执行文件头部
- 创建新的进程结构
- 为进程创建虚拟地址空间
- 把程序代码和数据映射到内存
- 加载动态链接器和共享库
- 初始化栈
- 设置参数和环境变量
- 设置程序入口点
- 把 CPU 控制权交给程序入口
这时程序开始运行。
注意:
1 | |
而是:
1 | |
12. 用户态和内核态的初步概念
操作系统通常会把执行环境分成至少两类:
1 | |
普通程序运行在用户态。
用户态程序不能直接:
- 操作硬盘
- 控制网卡
- 修改页表
- 随意访问其他进程内存
- 执行特权指令
如果普通程序想读文件、写屏幕、发网络包,需要请求操作系统帮忙。
这种请求就叫:
1 | |
例如 Linux 中:
readwriteopencloseforkexecvemmapsocketconnect
Windows 中更常见的是先调用 Win32 API,例如:
CreateFileWReadFileWriteFileCreateProcessWVirtualAllocCreateThread
这些 API 底层可能继续调用更底层的 Native API 和系统调用。
13. 为什么要区分用户态和内核态
如果所有程序都能直接控制硬件,会有严重问题。
13.1 稳定性问题
一个普通程序如果写错代码,可能直接破坏整个系统。
比如:
1 | |
13.2 安全问题
如果普通程序能随便访问内存,它就可以读取其他程序的数据。
例如:
- 浏览器密码
- 聊天记录
- SSH 私钥
- 其他进程的敏感信息
所以操作系统必须隔离进程。
13.3 权限问题
普通用户程序不应该随便:
- 删除系统文件
- 安装驱动
- 监听低端口
- 修改其他用户文件
- 注入系统进程
这就是权限边界。
很多漏洞的本质,就是程序错误地跨越了权限边界。
14. printf 背后发生了什么
我们写:
1 | |
你可能以为它只是“打印文字”。
但从系统角度,它大致经历了:
1 | |
也就是说,你看到的一行输出,背后经过了库函数和系统调用。
这就是为什么我们要学:
- C 库
- 系统调用
- 用户态 / 内核态
- 文件描述符
- 终端设备
15. Linux 实验:观察 hello 程序
15.1 创建源文件
创建 hello.c:
1 | |
15.2 编译
1 | |
如果没有报错,会生成:
1 | |
15.3 运行
1 | |
输出:
1 | |
15.4 查看文件类型
1 | |
你可能看到类似输出:
1 | |
重点关注:
ELF64-bitx86-64dynamically linkedpie executable
解释:
| 字段 | 含义 |
|---|---|
| ELF | Linux 常见可执行文件格式 |
| 64-bit | 64 位程序 |
| x86-64 | 面向 x86-64 架构 |
| dynamically linked | 动态链接 |
| PIE | 地址可随机化的可执行文件 |
15.5 查看动态库依赖
1 | |
你可能看到:
1 | |
说明这个程序依赖 C 标准库。
printf 就来自 libc。
15.6 查看 ELF 头
1 | |
重点看:
- Class
- Machine
- Type
- Entry point address
例如:
1 | |
入口点不是 main。
这是一个重要结论:
程序最开始执行的位置通常不是 main,而是运行时启动代码。启动代码完成初始化后才调用 main。
15.7 观察系统调用
1 | |
你会看到很多输出。
重点找:
1 | |
这说明最终输出文字时,程序调用了 write 系统调用。
1 是标准输出文件描述符。
1 | |
16. Windows 对照:hello.exe 背后发生了什么
Windows 下你可以写类似程序:
1 | |
编译成:
1 | |
Windows 下它通常是 PE 文件。
你可以用 PE-bear 或 Detect It Easy 查看:
- PE Header
- 入口点
- Section
- Import Table
- 依赖 DLL
如果使用 Process Monitor 观察运行过程,可以看到它可能访问:
- exe 文件本身
- DLL 文件
- 控制台相关对象
- 注册表配置
如果用 x64dbg 打开,可以看到程序不是直接进入 main,而是先经过运行时启动代码。
这和 Linux 是类似的。
17. 从逆向工程角度看本节内容
逆向工程不是凭空读汇编。
你要知道一个程序运行前后经历了什么。
拿到一个二进制程序时,逆向分析常见入口包括:
- 它是什么文件格式?
- 是 ELF 还是 PE?
- 是 32 位还是 64 位?
- 入口点在哪里?
- 是否动态链接?
- 导入了哪些库和 API?
- 字符串在哪里?
- main 函数在哪里?
- 程序运行时调用了哪些系统 API?
- 它有什么文件、网络、进程行为?
本节课对应逆向里的基础能力:
- 识别文件格式
- 理解入口点
- 理解动态链接
- 理解 API 和系统调用
- 理解程序不是从源码运行,而是从二进制入口运行
18. 从漏洞分析角度看本节内容
漏洞不是孤立存在的。
一个漏洞能不能被利用,取决于很多操作系统机制。
例如:
- 程序如何加载到内存
- 栈和堆在哪里
- 地址是否随机化
- 内存是否可执行
- 程序权限是什么
- 系统调用能做什么
- 输入从哪里进入程序
本节课虽然还没有讲漏洞细节,但你已经建立了基本路线:
1 | |
后面学习缓冲区溢出、UAF、ROP、提权漏洞时,都会回到这条路线。
19. 从恶意代码分析角度看本节内容
恶意代码也是普通程序。
它同样需要:
- 被操作系统加载
- 调用 API
- 创建进程或线程
- 读写文件
- 访问网络
- 分配内存
- 使用权限
所以分析恶意代码时,经常先看:
- 它是什么格式?
- 导入了哪些 API?
- 是否动态加载函数?
- 运行时创建了哪些文件?
- 是否连接网络?
- 是否创建子进程?
- 是否申请可执行内存?
这些都建立在操作系统知识上。
20. 本节重点总结
你需要记住这些核心结论:
- 操作系统负责管理硬件资源,并为程序提供运行环境。
- 源代码不能直接运行,必须经过编译、汇编、链接。
- 可执行文件是磁盘上的二进制文件,进程是运行中的程序实例。
- Linux 常见可执行文件格式是 ELF,Windows 常见格式是 PE。
- 程序运行时,操作系统会创建进程、分配虚拟内存、加载代码和库。
- 普通程序运行在用户态,需要通过系统调用请求内核服务。
printf背后最终会走到系统调用,例如 Linux 的write。- 逆向工程、漏洞分析、恶意代码分析都离不开程序加载和运行机制。
21. 本节课后作业
作业 1:编译并观察 hello 程序
要求:
- 写
hello.c - 编译为
hello - 执行:
1 | |
提交记录:
1 | |
作业 2:拆分编译过程
执行:
1 | |
观察:
1 | |
回答:
- 哪些是文本文件?
- 哪些是二进制文件?
hello.o为什么通常不能直接运行?hello和hello.o有什么区别?
作业 3:Windows PE 对照
如果你有 Windows 环境:
- 编译一个
hello.exe - 用 Detect It Easy 查看文件类型
- 用 PE-bear 查看入口点和导入表
- 用 Process Monitor 观察运行时行为
记录:
1 | |
22. 自测题
题 1
源代码、可执行文件、进程三者有什么区别?
题 2
为什么 C 程序需要链接?
题 3
为什么 main 通常不是程序真正开始执行的第一条代码?
题 4
用户态程序为什么不能直接访问硬件?
题 5
Linux 下 printf("hello") 最终大概率会触发哪个系统调用?
题 6
为什么导入 API 对逆向分析有帮助?
题 7
为什么操作系统要区分用户态和内核态?
23. 自测题参考答案
答 1
源代码是人写的文本;可执行文件是编译链接后的二进制文件;进程是操作系统加载并正在运行的程序实例。
答 2
因为程序中可能引用外部函数或库,例如 printf。链接负责把目标文件和库组合起来,并解决符号引用、入口点、动态库依赖等问题。
答 3
因为程序运行前需要初始化运行时环境,例如设置栈、初始化 libc、处理参数和环境变量等。启动代码完成这些工作后才调用 main。
答 4
为了稳定性、安全性和权限控制。否则普通程序可以破坏系统、访问其他进程内存或绕过权限限制。
答 5
通常会触发 write 系统调用,把内容写到标准输出。
答 6
导入 API 能反映程序可能具备的行为。例如导入文件、网络、进程、注册表相关 API,说明程序可能执行对应操作。
答 7
内核态拥有高权限,可以管理硬件和系统资源;用户态权限受限。区分二者可以防止普通程序破坏系统或越权访问资源。
24. 下一节预告
下一节课会讲:
1 | |
你会开始理解:
- 地址是什么
- 指针为什么重要
- 栈、堆、全局区、代码区分别是什么
- 为什么逆向和漏洞分析离不开内存地址