第13节-线程是什么
第 13 节:线程是什么
所属课程:操作系统自学路线:面向网络安全、逆向工程与漏洞分析
所属周次:第 7 周
课程主题:线程与并发
本节目标:理解线程的基本概念,掌握进程和线程的区别,知道一个进程内多个线程共享什么、各自私有什么,并能用 Linux pthread 和 Windows 工具观察线程行为。
1. 本节课你要学会什么
学完这一节,你应该能回答下面几个问题:
- 线程是什么?
- 进程和线程有什么区别?
- 一个进程里的多个线程共享哪些资源?
- 每个线程私有哪些资源?
- 线程为什么比进程更轻量?
- 什么是线程栈?
- Linux 下 pthread 是什么?
- Windows 下 Thread 是什么?
- 如何观察一个进程中的多个线程?
- 为什么恶意代码和系统程序经常创建线程?
本节课的主线是:
1 | |
上一周我们学习了进程,这一节开始理解进程内部的多个执行流。
2. 为什么需要线程
如果只有进程,也可以完成很多任务。
例如:
- 一个进程处理用户输入
- 一个进程处理文件读写
- 一个进程处理网络请求
但是进程之间隔离较强,创建和切换成本较高。
很多时候,我们希望一个程序内部同时做多件事。
例如浏览器:
- 一个线程负责 UI 响应
- 一个线程负责网络请求
- 一个线程负责渲染页面
- 一个线程负责 JavaScript 执行
- 一个线程负责磁盘缓存
再例如服务器:
- 主线程监听端口
- 工作线程处理客户端请求
- 后台线程写日志或清理资源
这时线程就很有用。
线程让同一个进程内部可以有多个执行流。
3. 线程是什么
线程英文是:
1 | |
可以先这样理解:
线程是进程中的一条执行流。
一个进程至少有一个线程。
这个线程通常叫:
1 | |
进程可以创建更多线程。
例如:
1 | |
这些线程运行在同一个进程中,共享进程的大部分资源。
4. 进程和线程的关系
可以把进程理解成资源容器,把线程理解成执行单位。
简化理解:
1 | |
一个进程拥有:
- 地址空间
- 打开的文件
- 权限信息
- 环境变量
- 当前工作目录
- 代码和数据
线程使用这些资源来执行代码。
如果没有线程,进程里的代码就没有执行流。
5. 进程和线程的区别
| 对比项 | 进程 | 线程 |
|---|---|---|
| 资源拥有 | 拥有独立地址空间和资源 | 共享所属进程资源 |
| 创建成本 | 较高 | 较低 |
| 切换成本 | 较高 | 较低 |
| 隔离性 | 强 | 弱 |
| 通信方式 | 需要 IPC,如管道、socket、共享内存 | 可直接共享变量 |
| 崩溃影响 | 一个进程崩溃通常不直接影响其他进程 | 一个线程崩溃通常导致整个进程崩溃 |
| 典型用途 | 程序实例、服务隔离 | 并发任务、后台工作 |
这张表很重要。
安全分析中,你经常需要判断:
1 | |
6. 多线程共享什么
同一个进程中的多个线程通常共享:
- 代码段
- 全局变量
- 堆
- 打开的文件描述符或句柄
- 当前工作目录
- 环境变量
- 进程权限
- 地址空间
- 动态库映射
例如全局变量:
1 | |
如果多个线程同时访问 counter,它们访问的是同一个变量。
这既方便,也危险。
方便在于线程间通信简单。
危险在于如果没有同步,可能产生竞态条件。
下一节会专门讲同步、竞争和死锁。
7. 每个线程私有什么
虽然线程共享进程资源,但每个线程也有自己的东西。
每个线程通常私有:
- 线程 ID
- 寄存器上下文
- 指令指针 RIP/EIP
- 栈指针 RSP/ESP
- 自己的线程栈
- 线程局部存储 TLS
- 调度状态
- errno 等部分线程局部数据
最重要的是:
1 | |
这意味着不同线程可以同时执行不同函数,拥有各自的局部变量和调用栈。
8. 线程栈是什么
上一节我们学过函数调用和栈帧。
每个线程都有自己的调用栈。
例如一个进程有三个线程:
1 | |
线程 1 可以正在执行:
1 | |
线程 2 可以正在执行:
1 | |
线程 3 可以正在执行:
1 | |
它们的调用栈互不相同。
9. 多线程为什么容易出问题
多线程强大,但复杂。
主要原因是:
1 | |
例如:
1 | |
看起来是一句代码。
但底层可能包含:
1 | |
如果两个线程同时执行,可能发生:
1 | |
结果本来应该是 2,实际却是 1。
这就是竞态条件。
下一节会深入讲。
10. 用户级线程和内核级线程
线程可以从不同层次理解。
10.1 用户级线程
用户级线程由用户态库管理。
优点:
- 创建和切换快
- 不一定每次进入内核
缺点:
- 如果内核不知道这些线程,调度和阻塞处理会受限制
10.2 内核级线程
内核级线程由操作系统内核管理和调度。
现代主流系统中的线程通常会映射到内核可调度实体。
Linux 中,线程和进程在内核实现上关系很近。
Linux 的线程创建底层常和 clone 系统调用有关。
11. Linux 中的线程
Linux 下常用 pthread 编写多线程程序。
pthread 是:
1 | |
常见函数:
1 | |
创建线程的典型模式:
1 | |
含义:
1 | |
12. 第一个 pthread 程序
创建 thread_demo.c:
1 | |
编译:
1 | |
运行:
1 | |
你会看到:
1 | |
注意:
两个线程的 pid 一样。
因为它们属于同一个进程。
13. pthread_create 参数解释
pthread_create 原型大致是:
1 | |
参数含义:
| 参数 | 含义 |
|---|---|
| thread | 用于保存新线程 ID |
| attr | 线程属性,NULL 表示默认属性 |
| start_routine | 新线程要执行的函数 |
| arg | 传给线程函数的参数 |
线程函数通常长这样:
1 | |
为什么参数和返回值都是 void*?
因为这样可以传递任意类型指针。
14. pthread_join 是什么
pthread_join 用于等待线程结束。
例如:
1 | |
意思是:
1 | |
如果不 join,主线程可能先退出。
进程一旦退出,其他线程通常也会被结束。
所以创建线程后,经常需要 pthread_join 等待它完成。
也可以创建 detached thread,但入门阶段先掌握 join 模式。
15. 给线程传参数
示例:
1 | |
这里传给线程的是 id 的地址。
注意:
线程拿到的是指针。
如果主线程中这个变量生命周期结束,或者多个线程共享同一个变量地址,可能导致问题。
例如在循环里创建多个线程时,常见错误是把同一个循环变量地址传给所有线程。
16. 创建多个线程
创建 multi_thread_demo.c:
1 | |
编译运行:
1 | |
你可能发现输出顺序不固定。
例如:
1 | |
这说明线程调度顺序不由你直接控制。
17. 线程调度顺序不确定
多线程程序中,线程执行顺序通常是不确定的。
操作系统调度器会根据:
- CPU 核心数量
- 时间片
- 优先级
- I/O 等待
- 系统负载
- 调度策略
决定哪个线程运行。
所以不要写依赖特定线程执行顺序的代码。
如果线程之间需要顺序或互斥,需要使用同步机制。
下一节会讲:
- mutex
- semaphore
- condition variable
- deadlock
- race condition
18. 用 ps 观察线程
运行一个睡眠中的多线程程序。
创建 thread_sleep_demo.c:
1 | |
编译运行:
1 | |
另开终端:
1 | |
-T 可以显示线程。
你会看到同一个进程下多个线程。
19. 用 /proc 观察线程
Linux 下可以查看:
1 | |
这里会列出该进程中的线程 ID。
每个线程在 /proc/<pid>/task/<tid> 下都有自己的状态信息。
例如:
1 | |
你可以看到:
- Name
- State
- Tgid
- Pid
- PPid
- Threads
其中:
1 | |
Linux 中线程和进程在内核实现上非常接近,这也是为什么 /proc/<pid>/task 很重要。
20. 用 strace 观察线程创建
对 pthread 程序执行:
1 | |
你可能看到:
1 | |
在 Linux 中,pthread 创建线程底层常使用 clone 系统调用。
clone 可以通过不同 flags 控制共享哪些资源。
线程创建时会共享:
- 地址空间
- 文件描述符表
- 信号处理等
所以从系统调用角度看,线程和进程创建都可能和 clone 有关,只是 flags 不同。
21. Windows 中的线程
Windows 中,进程也包含一个或多个线程。
常见 API:
1 | |
Windows 线程也有:
- Thread ID
- 寄存器上下文
- 栈
- TLS
- 优先级
- 状态
进程启动时,Windows 会创建主线程。
程序可以继续创建其他线程执行任务。
22. Windows 线程和逆向
Windows 逆向中,经常会看到:
1 | |
这些都和线程执行有关。
其中:
1 | |
尤其值得注意。
因为它可以在另一个进程中创建线程。
在恶意代码分析中,CreateRemoteThread 常和进程注入相关。
当然,合法软件也可能使用远程线程技术,但需要结合上下文分析。
23. Windows 工具观察线程
可以用 Process Explorer:
- 找到目标进程
- 双击打开属性
- 切换到 Threads 标签
- 查看线程列表
你可以看到:
- TID
- Start Address
- CPU 使用
- 状态
- 调用栈,若符号可用
x64dbg 中也能查看线程窗口。
当调试多线程程序时,你可以:
- 切换当前线程
- 暂停某个线程
- 查看不同线程的寄存器
- 查看不同线程的栈
这对分析多线程恶意代码很重要。
24. 线程和全局变量共享实验
创建 thread_global_demo.c:
1 | |
编译运行:
1 | |
你会看到多个线程都能访问同一个全局变量。
但这个程序还没有正确同步。
如果循环次数增加,就可能出现竞态问题。
25. 线程和局部变量
每个线程有自己的栈,所以线程函数中的局部变量通常在该线程自己的栈上。
例如:
1 | |
如果多个线程运行这个函数,每个线程的 local_var 地址通常不同。
实验:
1 | |
观察不同线程局部变量地址。
26. 线程和堆共享
堆属于进程地址空间。
所以多个线程共享同一个堆。
一个线程 malloc 出来的内存,另一个线程理论上也可以访问,只要它拿到了指针。
这很方便,但也很危险。
如果一个线程释放了堆内存,另一个线程还继续使用这个指针,就可能产生:
1 | |
多线程环境下,UAF 和竞态条件常常结合出现。
这在真实漏洞中很常见。
27. 线程和文件描述符共享
同一个进程内的线程共享文件描述符表。
如果一个线程打开文件:
1 | |
另一个线程如果能访问到 fd 变量,也可以写这个文件。
如果多个线程同时写同一个文件描述符,输出可能交错。
这也是为什么日志系统、多线程文件写入需要同步机制。
28. 线程和信号的复杂性
Linux 中信号和线程的关系比较复杂。
例如:
- 信号可能发送给进程
- 也可能发送给指定线程
- 每个线程有自己的信号掩码
- 某些信号会导致整个进程终止
入门阶段不需要深入信号细节。
只需要知道:
1 | |
如果某个线程发生段错误,通常整个进程都会崩溃。
29. 线程为什么比进程轻量
线程比进程轻量,主要因为它们共享进程资源。
创建新进程通常需要:
- 创建新的地址空间
- 建立独立资源结构
- 复制或设置文件描述符
- 设置更多进程级状态
创建线程通常不需要新的独立地址空间。
它只需要:
- 创建线程控制结构
- 分配线程栈
- 设置寄存器上下文
- 加入调度队列
所以线程创建和切换通常比进程更轻。
但代价是隔离性更弱,更容易互相影响。
30. 多线程和性能
多线程不一定让程序更快。
它适合:
- I/O 密集任务
- 多核 CPU 上可并行计算的任务
- UI 和后台任务分离
- 服务器并发请求处理
但多线程也有成本:
- 线程创建成本
- 上下文切换成本
- 锁竞争
- 缓存一致性开销
- 调试复杂度
- 死锁和竞态风险
所以不要为了“看起来高级”随便使用多线程。
31. 线程和逆向工程
逆向多线程程序时,要注意:
- 哪个线程创建了哪个线程
- 线程入口函数在哪里
- 每个线程负责什么任务
- 是否有后台线程
- 是否有定时线程
- 是否有反调试线程
- 是否有网络通信线程
- 是否有监控或自保护线程
有些程序主线程看起来很简单,真正逻辑在工作线程中。
所以动态调试时,要观察线程列表。
在 x64dbg 中,如果程序突然创建新线程,要关注线程入口地址。
32. 线程和漏洞分析
多线程程序容易出现并发漏洞。
常见包括:
- 竞态条件
- TOCTOU
- Use-After-Free
- Double Free
- 数据竞争
- 死锁
- 锁顺序错误
例如:
1 | |
这可能导致 UAF。
又例如:
1 | |
这可能导致 TOCTOU 问题。
下一节会重点讲同步和竞态。
33. 线程和恶意代码分析
恶意代码经常创建线程。
常见用途:
- 主线程继续运行,后台线程执行恶意逻辑
- 一个线程负责网络通信
- 一个线程负责监控分析工具
- 一个线程负责解密或解包
- 一个线程负责持久化
- 一个线程负责注入或执行 payload
- 一个线程负责延迟执行
如果只看主线程,可能漏掉关键行为。
分析时要关注:
- 线程创建 API
- 线程入口地址
- 线程开始后调用哪些 API
- 是否创建远程线程
- 是否有长时间 sleep
- 是否有监控调试器的线程
34. Linux 实验:pthread 基础
写 thread_demo.c 并运行:
1 | |
然后用:
1 | |
观察是否出现 clone。
提交时记录:
1 | |
35. Linux 实验:观察线程列表
运行 thread_sleep_demo。
另开终端:
1 | |
提交内容:
1 | |
36. Linux 实验:观察线程栈地址
运行局部变量地址实验。
提交内容:
1 | |
37. Windows 实验:Process Explorer 观察线程
如果你有 Windows 环境:
- 打开 Process Explorer
- 选择一个进程,例如 notepad 或自己写的程序
- 双击进入属性
- 打开 Threads 标签
- 查看线程列表
记录:
1 | |
38. Windows 实验:x64dbg 观察线程
用 x64dbg 打开一个程序。
练习:
- 打开 Threads 窗口
- 查看当前线程
- 设置断点
- 单步执行
- 如果程序创建新线程,观察线程列表变化
- 切换线程查看寄存器和栈
重点:
1 | |
39. 本节重点总结
你需要记住这些核心结论:
- 线程是进程中的一条执行流。
- 一个进程至少有一个线程,也可以有多个线程。
- 进程更像资源容器,线程更像 CPU 调度执行单位。
- 同一进程内线程共享地址空间、堆、全局变量、文件描述符等资源。
- 每个线程有自己的线程 ID、寄存器上下文、指令位置和线程栈。
- 多线程输出顺序和执行顺序通常不确定。
- Linux 下常用 pthread 创建线程,底层常和
clone有关。 - Windows 下常见线程 API 包括
CreateThread、CreateRemoteThread等。 - 多线程程序容易出现竞态条件、UAF、死锁等问题。
- 逆向和恶意代码分析时,要关注线程创建和线程入口函数。
40. 本节课后作业
作业 1:第一个 pthread 程序
写并运行 thread_demo.c。
提交内容:
1 | |
作业 2:多个线程输出顺序
写并运行 multi_thread_demo.c。
运行 5 次。
提交内容:
1 | |
作业 3:观察线程列表
运行 thread_sleep_demo。
执行:
1 | |
提交内容:
1 | |
作业 4:线程共享全局变量
运行 thread_global_demo.c。
提交内容:
1 | |
作业 5:线程私有栈实验
运行线程局部变量地址程序。
提交内容:
1 | |
作业 6:Windows 线程观察
如果你有 Windows 环境,用 Process Explorer 或 x64dbg 观察线程。
提交内容:
1 | |
41. 自测题
题 1
线程是什么?
题 2
进程和线程有什么区别?
题 3
同一个进程中的多个线程共享哪些资源?
题 4
每个线程通常私有哪些资源?
题 5
为什么每个线程需要自己的栈?
题 6
pthread_create 和 pthread_join 分别做什么?
题 7
为什么多线程程序执行顺序通常不确定?
题 8
Windows 中 CreateRemoteThread 为什么在安全分析中值得关注?
题 9
多线程程序中可能出现哪些安全问题?
42. 自测题参考答案
答 1
线程是进程中的一条执行流。一个进程至少有一个线程,也可以创建多个线程并发执行不同任务。
答 2
进程拥有独立地址空间和资源,隔离性较强;线程属于某个进程,多个线程共享进程资源,但每个线程有自己的执行上下文和栈。线程创建和切换通常比进程更轻量。
答 3
同一进程中的线程通常共享代码段、全局变量、堆、打开的文件描述符或句柄、当前工作目录、环境变量、权限信息、动态库映射和地址空间。
答 4
每个线程通常私有线程 ID、寄存器上下文、指令指针、栈指针、线程栈、线程局部存储和调度状态等。
答 5
因为不同线程可能同时执行不同函数调用链,需要各自保存局部变量、返回地址和调用栈信息,所以每个线程需要自己的栈。
答 6
pthread_create 用于创建新线程,并让新线程从指定函数开始执行;pthread_join 用于等待指定线程结束并回收相关资源。
答 7
因为线程由操作系统调度器根据 CPU、优先级、时间片、I/O 等因素决定运行顺序,程序不能假设线程按固定顺序执行。
答 8
因为 CreateRemoteThread 可以在其他进程中创建线程,常见于进程注入等行为。它不一定恶意,但在异常上下文中值得重点分析。
答 9
多线程程序可能出现竞态条件、TOCTOU、Use-After-Free、Double Free、数据竞争、死锁、锁顺序错误等问题。
43. 下一节预告
下一节课会讲:
1 | |
你会学习:
- 什么是竞态条件
- 为什么
counter++不是绝对安全的 - mutex、semaphore、condition variable
- 死锁四条件
- TOCTOU 漏洞基本思想
- 如何用实验观察加锁前后的差异