【OS】Linux Process Memory的探究
前言
刷B站刷到了南大OS的课程,不得不说酒吧舞的教育水平真滴高,狠狠的看了一些关于进程地址相关的课程。
进程地址空间
1.导言
先导入两个问题:
如下的程序会输出什么?
#include<stdio.h> int main() { printf("%p\n", main); }
如果生成一个随机指针,什么情况会segmentation fault?
chat *ptr = random(); *p;
要解决上述两个问题,我们可以向进程空间中尝试了解。
那么,在Linux中,一个进程的地址空间是如何映射的?可以使用什么样的手段去查看某个进程的地址分配呢?
2.手动解决第一个问题
实践是检验真理的唯一标准,我们可以通过编译的方式来查看第一个问题的答案。
动态编译
gcc -o libc_ main.c
静态编译
gcc --static -o static_ main.c
写成一个Makefile
all: clean static libc clean: rm -rf static_ libc_ static: gcc --static -o static_ main.c libc: gcc -o libc_ main.c
运行两个程序,静态编译的结果每次都是相同的0x401745
,而动态编译出来的结果是0x55ff1b6ff149
,并且每次除了后3位不变其他都是会变的。
(之所以动态编译的main函数地址会变,是因为gcc编译的选项默认开启了PIE的保护手段,会将代码段的函数地址给随机化)
3.尝试分析结果
上述的实验结果已经看到了,那么为什么会这样呢?同时0x40开头的地址和0x55开头的地址分别是什么?
这些问题的答案我们不妨使用gdb来调试查看
针对静态编译的文件,进行starti
的操作,来到起点函数,随后执行vmmap
进行查看
我这里可以执行vmmap是因为安装了gdb的插件pwndbg
可以非常鲜明的看到当前的内存状态。vmmap
函数的底层是通过读取/proc/[pid]/maps
这个文件来进行内存的查看的,我们直接cat这个文件来看看
通过ps
来读取static_
这个文件的pid,随后进行maps的查看
我们的main函数地址就在0x401000~0x498000
这一段代码段中,具有可执行权限。
在main
函数的位置打个断点,然后continue
到main函数的开始部分,查看maps
我们发现0x4c5000
这个具有可读可写的段存放的其实是got
表,同时0x4c8000
开始的一段heap
内存在经历_start
函数之后被扩容了
随后就是0x7f
开头的地址了,那么这里的vvar、vdso
分别是什么呢?
(stack我们知道就是执行函数调用栈的一个内存数据结构)
看玩静态编译的程序后,我们再来看看动态编译的程序!
这是在_start
函数之前,maps的状态
可以看到比起静态编译,多了4段,都是与ld-linux-x86-64.so.2
有关的。
ld是用来连接libc文件的,所以会在_start
函数中将libc连接到这个进程中
我们continue
到main
函数的位置,再来看看maps的状态:
惊奇的发现,libc.so.6
被注入了进程,这就意味着我们可以调用库函数了。
但是这里的anon
又是什么东西?gpt-3.5给出的解释如下:
在Linux中,
anon
是一种匿名内存映射,它是一个没有关联文件名的内存段。它通常用于动态分配内存、堆栈和共享内存等用途。当进程需要使用一些内存时,它可以通过系统调用(如
malloc()
)向内核请求一段内存。如果这个内存区域不需要关联任何文件,那么内核会创建一个anon
内存段来为进程提供所需的内存。
anon
内存段的创建时机包括:
- 进程调用
malloc()
等动态内存分配函数来分配内存;- 进程调用
mmap()
函数映射一块匿名内存区域;- 进程使用堆栈时,堆栈也是使用
anon
内存段分配的。
anon
内存段在进程终止或释放内存时会被销毁。
看到上述的回答我自己都有点疑惑,malloc申请出来的内存不是会在[heap]中吗?
别急,gpt-3.5给出的解释如下:
你提到的
heap
是一个进程的堆空间,它用于存储动态分配的内存。当进程调用malloc()
等函数分配内存时,它们通常会在堆空间中分配内存。而
anon
内存段与堆空间是不同的概念。anon
内存段通常是使用mmap()
函数映射的一块匿名内存区域,或者是操作系统通过其他方式动态分配的一块内存。anon
内存段通常不会被标识为heap
,因为它们与堆空间的分配方式不同。当进程使用
malloc()
等函数分配内存时,通常会在堆空间中分配内存,但是在某些情况下(例如大块内存分配),malloc()
函数可能会选择使用mmap()
函数来分配内存。此时,分配的内存将被标识为heap
。但是,即使是使用mmap()
函数分配的内存也可能不会被标识为heap
,而是被标识为anon
或其他类型的内存段。因此,
anon
内存段不一定是通过malloc()
函数分配的,也不一定与堆空间有关。它们通常是使用mmap()
函数映射的一块匿名内存区域,或者是操作系统通过其他方式动态分配的一块内存。
也就是说,anon
是一种不需要关联文件的内存段。
4.拓展vvar与vdso
咱们看到啥,不懂啥,就去学啥。这里对于vvar
和vdso
的解释是来自gpt3.5的
在 Linux 中,vvar 和 vdso 都是进程虚拟地址空间中的特殊区域。
vvar(variable address)是一段特殊的内核虚拟地址空间,它存储了一些全局变量,这些变量与进程有关,例如进程 ID、线程 ID 等。它通常位于进程虚拟地址空间的高地址区域。
vdso(virtual dynamic shared object)是一种特殊的共享库,它包含了一些用户态应用程序可以使用的系统调用的实现。vdso 不同于一般的共享库,它是直接映射到用户态应用程序的地址空间中,而不需要通过动态链接器进行加载。这种技术可以避免进入内核态的开销,从而提高系统调用的性能。
总的来说,vvar 和 vdso 都是 Linux 内核提供的一种机制,用于提高进程与内核之间的通信效率和性能。
这里对vdso进行一下拓展的介绍。
咱们在C中或多或少调用过这样一个函数time()
,获取当前的时间。
但是有没有想过,如果每次调用time
这种函数陷入内核态,再回到用户态,开销是否太大了?
所以可以使用vdso的技术,将时间戳等信息共享在vdso
中,让用户调用time()
的时候,无需陷入内核态就可以获取时间
5.总结
上述使用gdb的操作,让我们了解到了进程中的内存空间是怎样的。
其实除了vmmap
这种拓展的gdb指令,使用linux自带的pmap
指令,或者直接去cat /proc/[pid]/maps
查看进程内存map也是可以的
我们在实践后,可以得到如下的结论:
- 进程的地址空间是
若干个连续的内存段
- 每个
段
有着不同的权限(rwx) - 如果触发了违反
段权限
的操作,就会sigsegv
管理进程地址空间
1.导言
在上述见识到了一个process的maps中都有什么之后,我们应该有这样的思考?
- 一个进程的地址空间如何进行管理?
- Linux中有什么系统调用与进程空间相关?
2.mmap
这里直接给出一种修改进程地址空间的系统调用
,mmap
要使用mmap,首先得#include<sys/mman.h>
,因为在这个头文件中有mmap的定义
void *mmap (void *__addr, size_t __len, int __prot, int __flags, int __fd, __off_t __offset);
int munmap (void *__addr, size_t __len);
int mprotect (void *__addr, size_t __len, int __prot);
- mmap是获取内存映射
- munmap是释放内存映射
- mprotect则是修改内存映射空间的权限
来做一个例子:
#include <unistd.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#define GiB *(1024LL * 1024 * 1024)
int main() {
volatile uint8_t *p = mmap(NULL, 8 GiB, PROT_READ | PROT_WRITE, 0x20 | MAP_PRIVATE, -1, 0);
printf("mmap: %lx\n", (uintptr_t)p);
if ((intptr_t)p == -1) {
perror("cannot map");
exit(1);
}
*(p + 2 GiB) = 1;
*(p + 4 GiB) = 2;
*(p + 7 GiB) = 3;
printf("Read get: %d\n", *(p + 4 GiB));
printf("Read get: %d\n", *(p + 6 GiB));
printf("Read get: %d\n", *(p + 7 GiB));
}
如上代码映射了一个8G的空间,可读可写,同时是一个匿名私人空间,不使用fd来记录,使用gdb调试发现开辟的空间在libc的上方,且偏移是固定的
之后对内存空间进行写入,随后读取其中的数据。
看似对8G空间的内存进行读写是非常耗时的,但是由于我们使用的是mmap
的系统调用,并且使用的是匿名空间
,所以执行的速度非常快
我们这里尝试使用mmap去映射文件到内存中
import mmap, hexdump
with open("/mnt/e/virtualMachine/win7-en.iso", "rb") as f: # 映射一个iso镜像文件到内存中
m = mmap.mmap(f.fileno(), prot=mmap.PROT_READ, length=5 << 30)
hexdump.hexdump(m[-512:])
发现读取的速度非常快,这是因为我们没有读取这个文件中的所有,而仅仅是访问了一段映射的内存。
所以我们可以使用mmap
来进行大文件的操作。不管是读取还是拷贝,都是不错的选择。
3./proc/[pid]/mem
linux已经对一个process的内存地址进行了抽象,将其抽象成了一个file(everything is a file)
所以如果我们使用fopen
的方式将这个mem
文件给打开,随后使用fseek
的方式将指针偏移到某个地址,最后fwrite
的方式将内存修改,就完成了一次对进程空间的修改。这种写文件的方式不同于ptrace的调用。
这种思想方式甚至出过某个Misc题目,印象还挺深刻,春秋杯的一个Python文件通过open("/proc/self/mem")写入内存,由于python的二进制文件没开PIE,所以可以通过修改got表的方式将某个函数hook成system的地址,然后执行execve的调用。
后话
学了Linux这么些时日,发现原来挺多细小的知识点还是没细究...