burpow
第01节-从源代码到程序运行

第01节-从源代码到程序运行

第 1 节:从源代码到程序运行

所属课程:操作系统自学路线:面向网络安全、逆向工程与漏洞分析
所属周次:第 1 周
课程主题:计算机如何运行程序
本节目标:理解一个 C 程序从源代码变成正在运行的进程,中间经历了哪些阶段,操作系统在其中承担什么职责。


1. 本节课你要学会什么

学完这一节,你应该能回答下面几个问题:

  1. 什么是操作系统?
  2. 程序为什么不能“自己运行”?
  3. 源代码、可执行文件、进程之间有什么区别?
  4. 编译、汇编、链接、加载分别在做什么?
  5. 操作系统为什么要把程序和硬件隔离开?
  6. 为什么网络安全、逆向工程、漏洞分析都必须理解程序运行过程?

本节课不要求你立刻理解所有底层细节,但你要建立一条主线:

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
2
3
4
5
6
#include <stdio.h>

int main() {
printf("hello os\n");
return 0;
}

它是给人看的,CPU 不能直接执行 C 语言源代码。


4.2 可执行文件

源代码经过编译、链接之后,会变成可执行文件。

Linux 下可能是:

1
hello

Windows 下可能是:

1
hello.exe

可执行文件本质上是一个有特定格式的二进制文件。

Linux 常见格式是 ELF:

1
Executable and Linkable Format

Windows 常见格式是 PE:

1
Portable Executable

可执行文件里通常包含:

  • 机器指令
  • 字符串
  • 全局变量初始值
  • 动态库依赖信息
  • 程序入口点
  • 段和节信息

4.3 进程

进程是正在运行的程序实例。

一个可执行文件放在磁盘上时,它只是文件。

当你运行它时,操作系统会把它加载到内存,创建运行环境,这时它才成为进程。

例如:

1
./hello

此时操作系统会创建一个进程。

同一个可执行文件可以运行多次,所以可以产生多个进程。

比如你打开三个记事本:

1
2
notepad.exe 文件只有一个
notepad.exe 进程可以有三个

所以:

1
2
3
源代码:人写的程序文本
可执行文件:编译链接后的磁盘文件
进程:操作系统创建的运行中程序实例

5. 从 C 源代码到可执行文件

以这个程序为例:

1
2
3
4
5
6
#include <stdio.h>

int main() {
printf("hello os\n");
return 0;
}

使用 gcc 编译:

1
gcc hello.c -o hello

表面上只执行了一条命令,但内部大致经历了四步。

1
预处理 -> 编译 -> 汇编 -> 链接

6. 第一步:预处理

预处理主要处理 # 开头的内容。

例如:

1
#include <stdio.h>

它的意思不是“运行时导入 stdio.h”,而是在编译前把相关声明展开进来。

预处理会处理:

  • #include
  • #define
  • 条件编译,比如 #ifdef
  • 去除注释

你可以用下面命令观察预处理结果:

1
gcc -E hello.c -o hello.i

生成的 hello.i 仍然是 C 代码,但会比原文件大很多。


7. 第二步:编译

编译是把 C 代码转换成汇编代码。

命令:

1
gcc -S hello.c -o hello.s

生成的 hello.s 是汇编代码。

汇编代码已经很接近 CPU 能理解的指令,但仍然是文本形式。

你可能会看到类似内容:

1
2
3
call    puts
mov eax, 0
ret

这一步非常重要,因为逆向工程中经常看到的就是汇编指令。

你写的 C 代码:

1
printf("hello os\n");

最终会变成若干条机器相关的指令。


8. 第三步:汇编

汇编是把汇编代码转换成目标文件。

命令:

1
gcc -c hello.c -o hello.o

目标文件 hello.o 已经是二进制文件,但它通常还不能直接运行。

为什么?

因为它里面可能还有一些外部符号没有解决。

例如:

1
printf("hello os\n");

printf 不是你自己写的函数,它来自 C 标准库。

此时目标文件可能只知道:

1
这里要调用 printf,但 printf 的最终地址还没确定

9. 第四步:链接

链接负责把多个目标文件和库组合成最终可执行文件。

普通编译命令:

1
gcc hello.c -o hello

最后一步会链接 C 标准库。

链接要解决的问题包括:

  • printf 在哪里?
  • 程序入口点在哪里?
  • 需要哪些动态库?
  • 各个函数和变量最终放到什么位置?

链接分两种:

9.1 静态链接

把需要的库代码直接打进可执行文件。

优点:

  • 依赖少
  • 部署方便

缺点:

  • 文件大
  • 多个程序无法共享同一份库代码

9.2 动态链接

可执行文件不包含完整库代码,只记录运行时需要哪些动态库。

Linux 常见动态库:

1
libc.so

Windows 常见动态库:

1
2
3
4
kernel32.dll
user32.dll
ntdll.dll
msvcrt.dll

动态链接是逆向工程中的重点,因为导入函数能反映程序行为。

例如 Windows 程序导入了:

1
2
3
4
5
CreateFileW
WriteFile
RegSetValueExW
CreateProcessW
connect

你大致可以猜测它可能有:

  • 文件操作
  • 注册表操作
  • 创建进程
  • 网络连接

这就是恶意代码分析常用的入口。


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
./hello

操作系统大致做了这些事:

  1. 检查文件是否存在
  2. 检查当前用户是否有执行权限
  3. 识别文件格式,比如 ELF
  4. 读取可执行文件头部
  5. 创建新的进程结构
  6. 为进程创建虚拟地址空间
  7. 把程序代码和数据映射到内存
  8. 加载动态链接器和共享库
  9. 初始化栈
  10. 设置参数和环境变量
  11. 设置程序入口点
  12. 把 CPU 控制权交给程序入口

这时程序开始运行。

注意:

1
程序运行不是 CPU 直接从磁盘读 hello 文件执行

而是:

1
操作系统把可执行文件映射到进程地址空间,再让 CPU 从入口点开始执行

12. 用户态和内核态的初步概念

操作系统通常会把执行环境分成至少两类:

1
2
用户态:普通程序运行的地方
内核态:操作系统内核运行的地方

普通程序运行在用户态。

用户态程序不能直接:

  • 操作硬盘
  • 控制网卡
  • 修改页表
  • 随意访问其他进程内存
  • 执行特权指令

如果普通程序想读文件、写屏幕、发网络包,需要请求操作系统帮忙。

这种请求就叫:

1
系统调用 syscall

例如 Linux 中:

  • read
  • write
  • open
  • close
  • fork
  • execve
  • mmap
  • socket
  • connect

Windows 中更常见的是先调用 Win32 API,例如:

  • CreateFileW
  • ReadFile
  • WriteFile
  • CreateProcessW
  • VirtualAlloc
  • CreateThread

这些 API 底层可能继续调用更底层的 Native API 和系统调用。


13. 为什么要区分用户态和内核态

如果所有程序都能直接控制硬件,会有严重问题。

13.1 稳定性问题

一个普通程序如果写错代码,可能直接破坏整个系统。

比如:

1
2
3
程序 A 错误修改了磁盘控制器
程序 B 的文件也被破坏
整个系统崩溃

13.2 安全问题

如果普通程序能随便访问内存,它就可以读取其他程序的数据。

例如:

  • 浏览器密码
  • 聊天记录
  • SSH 私钥
  • 其他进程的敏感信息

所以操作系统必须隔离进程。


13.3 权限问题

普通用户程序不应该随便:

  • 删除系统文件
  • 安装驱动
  • 监听低端口
  • 修改其他用户文件
  • 注入系统进程

这就是权限边界。

很多漏洞的本质,就是程序错误地跨越了权限边界。


14. printf 背后发生了什么

我们写:

1
printf("hello os\n");

你可能以为它只是“打印文字”。

但从系统角度,它大致经历了:

1
2
3
4
5
6
7
main
-> printf / puts
-> C 标准库
-> write 系统调用
-> 内核
-> 终端设备 / 控制台
-> 屏幕显示

也就是说,你看到的一行输出,背后经过了库函数和系统调用。

这就是为什么我们要学:

  • C 库
  • 系统调用
  • 用户态 / 内核态
  • 文件描述符
  • 终端设备

15. Linux 实验:观察 hello 程序

15.1 创建源文件

创建 hello.c

1
2
3
4
5
6
#include <stdio.h>

int main() {
printf("hello os\n");
return 0;
}

15.2 编译

1
gcc hello.c -o hello

如果没有报错,会生成:

1
hello

15.3 运行

1
./hello

输出:

1
hello os

15.4 查看文件类型

1
file hello

你可能看到类似输出:

1
hello: ELF 64-bit LSB pie executable, x86-64, dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, ...

重点关注:

  • ELF
  • 64-bit
  • x86-64
  • dynamically linked
  • pie executable

解释:

字段 含义
ELF Linux 常见可执行文件格式
64-bit 64 位程序
x86-64 面向 x86-64 架构
dynamically linked 动态链接
PIE 地址可随机化的可执行文件

15.5 查看动态库依赖

1
ldd hello

你可能看到:

1
2
3
linux-vdso.so.1
libc.so.6
/lib64/ld-linux-x86-64.so.2

说明这个程序依赖 C 标准库。

printf 就来自 libc。


15.6 查看 ELF 头

1
readelf -h hello

重点看:

  • Class
  • Machine
  • Type
  • Entry point address

例如:

1
2
3
4
Class:                             ELF64
Machine: Advanced Micro Devices X86-64
Type: DYN
Entry point address: 0x1050

入口点不是 main

这是一个重要结论:

程序最开始执行的位置通常不是 main,而是运行时启动代码。启动代码完成初始化后才调用 main。


15.7 观察系统调用

1
strace ./hello

你会看到很多输出。

重点找:

1
write(1, "hello os\n", 9)

这说明最终输出文字时,程序调用了 write 系统调用。

1 是标准输出文件描述符。

1
2
3
0 -> 标准输入 stdin
1 -> 标准输出 stdout
2 -> 标准错误 stderr

16. Windows 对照:hello.exe 背后发生了什么

Windows 下你可以写类似程序:

1
2
3
4
5
6
#include <stdio.h>

int main() {
printf("hello os\n");
return 0;
}

编译成:

1
hello.exe

Windows 下它通常是 PE 文件。

你可以用 PE-bear 或 Detect It Easy 查看:

  • PE Header
  • 入口点
  • Section
  • Import Table
  • 依赖 DLL

如果使用 Process Monitor 观察运行过程,可以看到它可能访问:

  • exe 文件本身
  • DLL 文件
  • 控制台相关对象
  • 注册表配置

如果用 x64dbg 打开,可以看到程序不是直接进入 main,而是先经过运行时启动代码。

这和 Linux 是类似的。


17. 从逆向工程角度看本节内容

逆向工程不是凭空读汇编。

你要知道一个程序运行前后经历了什么。

拿到一个二进制程序时,逆向分析常见入口包括:

  1. 它是什么文件格式?
  2. 是 ELF 还是 PE?
  3. 是 32 位还是 64 位?
  4. 入口点在哪里?
  5. 是否动态链接?
  6. 导入了哪些库和 API?
  7. 字符串在哪里?
  8. main 函数在哪里?
  9. 程序运行时调用了哪些系统 API?
  10. 它有什么文件、网络、进程行为?

本节课对应逆向里的基础能力:

  • 识别文件格式
  • 理解入口点
  • 理解动态链接
  • 理解 API 和系统调用
  • 理解程序不是从源码运行,而是从二进制入口运行

18. 从漏洞分析角度看本节内容

漏洞不是孤立存在的。

一个漏洞能不能被利用,取决于很多操作系统机制。

例如:

  • 程序如何加载到内存
  • 栈和堆在哪里
  • 地址是否随机化
  • 内存是否可执行
  • 程序权限是什么
  • 系统调用能做什么
  • 输入从哪里进入程序

本节课虽然还没有讲漏洞细节,但你已经建立了基本路线:

1
输入进入程序 -> 程序处理输入 -> 内存状态变化 -> 控制流可能受影响 -> 系统保护机制决定后果

后面学习缓冲区溢出、UAF、ROP、提权漏洞时,都会回到这条路线。


19. 从恶意代码分析角度看本节内容

恶意代码也是普通程序。

它同样需要:

  • 被操作系统加载
  • 调用 API
  • 创建进程或线程
  • 读写文件
  • 访问网络
  • 分配内存
  • 使用权限

所以分析恶意代码时,经常先看:

  • 它是什么格式?
  • 导入了哪些 API?
  • 是否动态加载函数?
  • 运行时创建了哪些文件?
  • 是否连接网络?
  • 是否创建子进程?
  • 是否申请可执行内存?

这些都建立在操作系统知识上。


20. 本节重点总结

你需要记住这些核心结论:

  1. 操作系统负责管理硬件资源,并为程序提供运行环境。
  2. 源代码不能直接运行,必须经过编译、汇编、链接。
  3. 可执行文件是磁盘上的二进制文件,进程是运行中的程序实例。
  4. Linux 常见可执行文件格式是 ELF,Windows 常见格式是 PE。
  5. 程序运行时,操作系统会创建进程、分配虚拟内存、加载代码和库。
  6. 普通程序运行在用户态,需要通过系统调用请求内核服务。
  7. printf 背后最终会走到系统调用,例如 Linux 的 write
  8. 逆向工程、漏洞分析、恶意代码分析都离不开程序加载和运行机制。

21. 本节课后作业

作业 1:编译并观察 hello 程序

要求:

  1. hello.c
  2. 编译为 hello
  3. 执行:
1
2
3
4
5
gcc hello.c -o hello
file hello
ldd hello
readelf -h hello
strace ./hello

提交记录:

1
2
3
4
5
6
7
1. 源代码
2. 编译命令
3. file 输出
4. ldd 输出
5. readelf -h 中的 Entry point address
6. strace 中的 write 系统调用
7. 用自己的话解释:程序从源代码到运行经历了什么

作业 2:拆分编译过程

执行:

1
2
3
4
gcc -E hello.c -o hello.i
gcc -S hello.c -o hello.s
gcc -c hello.c -o hello.o
gcc hello.o -o hello

观察:

1
2
3
4
file hello.i
file hello.s
file hello.o
file hello

回答:

  1. 哪些是文本文件?
  2. 哪些是二进制文件?
  3. hello.o 为什么通常不能直接运行?
  4. hellohello.o 有什么区别?

作业 3:Windows PE 对照

如果你有 Windows 环境:

  1. 编译一个 hello.exe
  2. 用 Detect It Easy 查看文件类型
  3. 用 PE-bear 查看入口点和导入表
  4. 用 Process Monitor 观察运行时行为

记录:

1
2
3
4
5
1. 文件格式
2. 程序入口点
3. 导入了哪些 DLL
4. 导入了哪些函数
5. 是否观察到文件或注册表访问

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
二进制、地址和内存基本概念

你会开始理解:

  • 地址是什么
  • 指针为什么重要
  • 栈、堆、全局区、代码区分别是什么
  • 为什么逆向和漏洞分析离不开内存地址
隐藏
换装
本文作者:burpow
本文链接:https://youthfulnesszxx.github.io/2026/05/28/第01节-从源代码到程序运行/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可