NJUOS-12-进程的地址空间
本文最后更新于:2 年前
延续虚拟化部分的内容,继续讲讲地址空间昂!!!
复习&状态机
注意是第一个init程序昂!后面就OS就变成中断处理程序啦!!!
C -> 汇编状态机视角
- C的视角的状态机
- 汇编语言的状态机:
什么什么代码段啊,堆栈啊,都没有。在汇编语言的眼里,本质上内存里就是一块儿平坦的地址空间。
- 函数与系统调用:
syscall开销很大诶,能不能不这么大开销。既能陷入内核,又不那么大开销。 -> 不用syscall陷入内核可以吗?
进程的地址空间
神奇指针
char *p
可以和 intptr_t
互相转换
- 可以指向 “任何地方”
- 合法的地址 (可读或可写)
- 代码 (
main
,%rip
会从此处取出待执行的指令),只读 - 数据 (
static int x
),读写 - 堆栈 (
int y
),读写 - 运行时分配的内存 (???),读写
- 动态链接库 (???)
- 代码 (
- 非法的地址
NULL
,导致 segmentation fault
- 合法的地址 (可读或可写)
它们停留在概念中,但实际呢?
查看进程的地址空间
pmap (1) - report memory of a process
- Claim: pmap 是通过访问 procfs (
/proc/
) 实现的 - 如何验证这一点?
pmap 进程号:可以查看所有的地址空间。尝试用上面这个pamp,来调试下面编译生成a.out并且跑起来的进程。
查看进程的地址空间
- minimal.S (静态链接)
- 最小的 Hello World (静态/动态链接)
- 进程的地址空间:若干连续的 “段”
- “段” 的内存可以访问
- 不在段内/违反权限的内存访问 触发 SIGSEGV
- gdb 可以 “越权访问”,但不能访问 “不存在” 的地址
静态链接简单,动态链接麻烦一点!!!
操作系统提供查看进程地址空间的机制
RTFM: /proc/[pid]/maps
(man 5 proc)
- 进程地址空间中的每一段
- 地址 (范围) 和权限 (rwxsp)
- 对应的文件: offset, dev, inode, pathname
- TFM 里有更详细的解释
- 和 readelf (-1) 里的信息互相验证
- 课后习题:定义一些代码/数据,观察变化
1 |
|
更完整的地址空间映象
1 |
|
- 是不是 bss? 给我们的代码加一个大数组试试!
在c语言里面加了大数组之后,可以看看,跑起来之后,分配的内存的类型,以及后面的标识昂。发现是anno(anonymous)
RTFM (5 proc): 我们发现的宝藏
vdso (7): Virtual system calls: 只读的系统调用也许可以不陷入内核执行。
无需陷入内核的系统调用
- 例子: time (2)
- 直接调试 vdso.c
- 时间:内核维护秒级的时间 (所有进程映射同一个页面)
1 |
|
- 例子: gettimeofday (2)
- RTFSC (非常聪明的实现)
- 更多的例子:RTFM
- 计算机系统里没有魔法!我们理解了进程地址空间的全部!
对应上面的C -> 汇编状态机视角,诶嘿,time这个系统函数的调用,不需要陷入内核,好像不用syscall???
time和gettimeofday都是,不进入操作系统内核的情况下,完成操作系统的调用!!!
- 原理:
每个进程有一段地址空间,操作系统会给每个进程都映射同一块儿的vvar,里面存的是系统当前时间。操作系统会(比如没过一秒),把这个vvar加个1。(所有进程都映射到同一个vvar昂!)
- 不用陷入内核的系统调用:
man vdso -> 上面这图就来自于vdso的手册昂!!!
小总结
- 所以这里讲了这么多,其实给我们提供了很多的方法,让我们真真正正能够看到。一个运行的进程,在我们的内存中,是如何给它进行内存的分配和管理之类的。
- 能看到,就是全部的全部昂!!!
(小知识) 系统调用的实现
“执行系统调用时,进程陷入内核态执行”——不,不是的。
系统调用就是一组接口的约定,谁说一定要 int
指令?-> 很慢!!!
- 光一条指令就要保存 ss, rsp, cs, rip, rflags (40 字节) 到内存
SYSCALL — Fast System Call -> 做的操作比上面提到的int少得多,不需要保存所有的寄存器,系统调用可以像函数调用一样执行,只保存尽可能少的一些信息。
1 |
|
(小知识) 系统调用的实现 (cont’d)
我们看到上的VDSO和SYSCALL了,有没有可能在不进入操作系统内核的时候,实现几乎所有的系统调用呢?有!还真有
能不能让其他系统调用也 trap 进入内核?
- 疯狂的事情也许真的是能实现的 (这算是魔法吗?)
把vdso做的更加激进一点!!!系统调用无非就是通信嘛!!!syscall并不是唯一的同步方式!!!
使用共享内存和内核通信!
- 内核线程在 spinning 等待系统调用的到来
- 收到系统调用请求后立即开始执行
- 进程 spin 等待系统调用完成
- 如果系统调用很多,可以打包处理
进程的地址空间管理
Execve 之后……
进程只有少量内存映射
- 静态链接:代码、数据、堆栈、堆区
- 动态链接:代码、数据、堆栈、堆区、INTERP (ld.so)
地址空间里剩下的部分是怎么创建的?
- libc.so 都没有啊…… -> 操作系统帮助我们提供系统调用,把libc.so读取进来。
- 创建了以后,我们还能修改它吗?
- 肯定是能的:动态链接库可以动态加载 (M4)
- 当然是通过系统调用了
进程的地址空间 (cont’d)
进程的地址空间 = 内存里若干连续的 “段”
- 每一段是可访问 (读/写/执行) 的内存
- 可能映射到某个文件和/或在进程间共享
管理进程地址空间的系统调用
1 |
|
RTFM
- 说人话:状态上增加/删除/修改一段可访问的内存
对于内存这个状态机,做了一些改变呢!!!
man 2 mmap
,可以查看mmap的手册昂!
1 |
|
手册里面有很多东西很全昂,比如这里对于上面的 int prot,这个参数的讲解,protection保护权限,总共就下面这几种昂!!!
甚至可以把文件的一部分,直接映射到内存里面来。把文件搬进来,而且可以做到,我想访问文件哪里,我就去搬文件的哪里进内存。 -> 非常好用!!!
我们的加载器,其实用的就是这个昂!!!!!! -> 进程在创建的时候,加载器分配内存用的都是mmap昂!!!
例子
- 使用strace查看动态编译后的文件
readelf -l a.out
告诉我们:当加载到这个程序的时候,要把什么东西,加载到哪里。文件是什么,权限是什么,偏移量是什么。
mmap很简单,就是把上面的elf文件中的内容,读取并进行处理,作为参数传递给系统调用mmap,就可以完成正确的文件 -> 内存的映射。
把文件映射到进程地址空间?
它们的确好像没有什么区别
- 文件 = 字节序列
- 内存 = 字节序列
- 操作系统允许映射好像挺合理的……
- 带来了很大的方便
- ELF loader 用 mmap 非常容易实现
- 解析出要加载哪部分到内存,直接 mmap 就完了
使用 Memory Mapping
Example 1:
- 用 mmap 申请大量内存空间(mmap-alloc.c)
- 瞬间完成
- 不妨 strace/gdb 看一下
- libc 的 malloc/free 在初始空间用完后使用 sbrk/mmap 申请空间
1 |
|
分配3GB的内存,瞬间完成,非常快!!!
- 查看strace:
只花费了非常短的时间昂!!!自己先考虑一下!why?
mmap只需要标记这块内存被分配给我了就ok,取用的时候,再实际分配给我就行昂!(lazy load)
Example 2:
- 用 mmap 映射整个磁盘(mmap-disk.py)
- 瞬间完成
1 |
|
mmap可以映射内存,也可以映射磁盘!!!
/dev/sda是一个虚拟设备,虚拟磁盘昂!映射128G的内存,把前面的512个字节打印出来。
- mmap是非常简单的系统调用,但是非常强大!!!
Memory-Mapped File: 一致性
但我们好像带来了一些问题……
- 如果把页面映射到文件
- 修改什么时候生效?
- 立即生效:那会造成巨大量的磁盘 I/O
- unmap (进程终止) 时生效:好像又太迟了……
- 若干个映射到同一个文件的进程?
- 共享一份内存?
- 各自有本地的副本?
- 修改什么时候生效?
请查阅手册,看看操作系统是如何规定这些操作的行为的
- 例如阅读
msync (2)
,man 2 msync
,修改过的内存,重新推回到文件里面昂! - 这才是操作系统真正的复杂性
地址空间的隔离
地址空间:实现进程隔离
每个 *ptr
都只能访问本进程 (状态机) 的内存
- 除非 mmap 显示指定、映射共享文件或共享内存多线程
- 实现了操作系统最重要的功能:进程之间的隔离
任何一个程序都不能因为 bug 或恶意行为侵犯其他程序执行
- “连方法都没有”
- 吗……?
很有意思的内容(外挂!!!)
电子游戏的上一个黄金时代
电子竞技的先行者:“即时战略游戏” (Real-Time Strategy)
- Command and Conquer(Westwood), Starcraft (Microsoft)…
- 如果我们想 “侵犯” 游戏的执行……呢?
前互联网时代的神器 (1): 金山游侠
在进程的内存中找到代表 “金钱”、“生命” 的重要属性并且改掉
只要有访问其他进程内存和在程序上 “悬浮显示” 的 API 即可
- 想象成是另一个进程内存的 “调试器”
- 在 Linux 中可以轻松拥有:dosbox-hack.c
1 |
|
- 找到了所有可以改的数据,不知道对应关系怎么办?
手动造一个东西(比如消耗钱),看看修改过后,哪些数据是被更改了。 -> 施加对于值的扰动+观察内存的变化,能够帮助我们快速确定,我们要找的内存昂!
- 把我们找到的对应的内存位置(也就是钱钱对应的位置),改成对应的值,诶嘿,你就是上帝了orz。
前互联网时代的神器 (2): 按键精灵
大量重复固定的任务 (例如 2 秒 17 枪)
这个简单,就是给进程发送键盘/鼠标事件
- 键盘or鼠标,做个驱动。OS把你的驱动认为是正常的设备!很多鼠标支持自定义宏,其实就是这个原理。比如你点了一下,鼠标默认连续触发100次电路这种;或者
- 利用操作系统/窗口管理器提供的 API
前互联网时代的神器 (3): 变速齿轮
调整游戏的逻辑更新速度 -> pokemon,笑死了…难绷…
- 比如某神秘公司慢到难以忍受的跑图和战斗,e.g.
1 |
|
怎么办?不是改数据了,改代码!!!把sleep的代码对应的内存给改了,hack一个跳转。hack到我们自己的sleep指令,根据我们的齿轮,我们自己的sleep的时间,可以我们自己控制的。就很方便昂!!!
本质是 “欺骗” 进程的时钟
- 源头:闹钟、睡眠、
gettimeofday
- 拦截它们需要稍稍更复杂的技术
更强大的游戏外挂?
游戏也是程序,也是状态机
- 通过 API 调用 (和系统调用) 最终取得状态、修改状态
- 外挂本质上就是,想象成是一个 “为这个游戏专门设计的 gdb”
代码注入 (Hooking)
我们可以改内存,也可以改代码!
安全补丁!!!也是这么来的!软件运行的时候,可以把数据和代码都改掉昂!!!修复安全漏洞很重要!!!
The Light Side
- “软件热补丁” dsu.c (mprotect) -> 05, 06年就出现这个想法了。
- Ksplice: Automatic rebootless Kernel updates (EuroSys’09) -> 甚至Linux内核都可以动态更新!!!
1 |
|
DSU可以热更新,运行时,把foo()从old变成new,很厉害!!!
这就是个补丁 -> 从一段代码,hack掉。原函数的执行,跳转到另外一个代码段去执行了(热补丁)。
The Dark Side
- 对于外挂,代码可以静态/动态/vtable/DLL… 注入
render(objects)
→render_hacked(objects)
游戏外挂:攻与防
控制/数据流完整性(System Security)
- 保护进程的完整性
- 独立的进程/驱动做完整性验证
- 保护隐私数据不被其他进程读写
- 拦截向本进程的
ReadProcessMemory
和WriteProcessMemory
,发现后立即拒绝执行
- 拦截向本进程的
- 例子
其他解决方法
- AI 监控/社会工程学:如果你强得不正常,当然要盯上你。(统计学习之类的,都可以综合使用!!!)
- 云/沙盒 (Enclave) 渲染:“计算不再信任操作系统”(CPU有一个运行模式,隔离出一个blackbox(黑地),没有人能够看到所有的运行相关东西,system的问题了。假设计算不再信任操作系统,拉起一个可信的执行环境!!!高可信软件,可信计算!!!)
总结
- win还是mac还是linux,都给了我们一些,修改访问和修改内存中变量的功能。 -> 调试器需要这种的机制,才能够运行!!!
- 留了这样的口子,但是一般的进程,是没有权限去做这样的事情的。(当然你是管理者,你乐意怎么搞就怎么搞就好……)
总结
本次课回答的问题
- Q: 进程的地址空间是如何创建、如何更改的?
- fork
- Execve
Take-away messages
- 进程的地址空间
- 能文件关联的、带有访问权限的连续内存段
- a.out, ld.so, libc.so, heap, stack, vdso
- 地址空间的隔离
- 能文件关联的、带有访问权限的连续内存段
- 进程地址空间的管理 API
- mmap
References
本节视频包含很多调试,对比,实际操作流程。建议看看,会对我们很多内容的理解,比笔记来的直观的多。
- ppt link: http://jyywiki.cn/OS/2022/slides/12.slides#/
- 手册很好用!!!
- file a.out -> file指令查看文件相关的信息
- vi中,:set nowrap,可以设置为没有换行,看起来可能方便昂!
- pmap, readelf -l a.out
记得tldr指令昂!!!
1 |
|
- gdb中的info inferiors -> 可以在starti之后,查看进程号
- Address Space Layout Randomization -> 地址空间随机化,每次程序都被加载到随机的位置上,但是调试器会把这个特性关掉(GDB)。导致我们每次运行程序,看到程序的地址空间都是固定的昂!
- Feature: GDB不能访问另外一个进程的vvar昂!
- man vdso -> https://zhuanlan.zhihu.com/p/436454953 -> 类似于mmap?
- pmap, readelf,很多指令昂!!!
- elf文件:https://baike.baidu.com/item/ELF/7120560