从一个ELF程序的加载窥探操作系统内核-(1)
操作系统加载一个ELF程序看似一个EASY的动作,其实下面隐藏了很多很多OS内核的关键实现,让我们一起来解密其中的流程
作者是一个micro kernel的开发者,在设计动态链接器的时候,在此留下一些笔记,重点参考了以下资料文献
- 《程序员的自我修养》
- 《深入理解计算机系统》
- 《现代操作系统-原理与实现》
- 《深入理解LINUX内核》
- 《设计模式/JAVA》
一个ELF程序的构成与运行
这里的ELF程序指的是可执行类型的文件,动态库和静态库也是ELF文件的一种类型。
-
从ELF的生成角度看
一个ELF程序由main程序和libs库组成,这里的库分为静态库或者动态库,经过链接器,最终生成ELF可执行文件,也就是最后我们运行的程序。
-
从ELF的运行角度看
一个ELF程序的运行其实可以看作一个进程的执行,那么我们可以引入经典的LINUX地址空间布局图,理解这个地址空间布局很重要哦!!!
左侧描述32位下的整个地址空间的布局,注意这里是虚拟地址
- 0~3G低端内存
- 用作用户空间使用,也就是用户态的代码/数据/堆栈等相关资源都要映射到这个地址范围内
- 3G~4G高端内存
- 用作内核空间使用,保存内核的代码/数据/堆栈
- 如果你愿意你可以任意修改这个范围
右侧的图描述了一个进程运行时在虚拟地址空间的布局情况,我们来逐一解析
- Reserved区域
- 注意这里预留了0x08048000空间,为什么?在哪里实现的?
- 为什么:如果我们直接从0x00000000开始,那么我们的ELF程序的起始地址就是0x00000000,这个地址和NULL地址一样了,好像不太吉利。其实你是可以从这个地址开始的,这并不影响什么
- 如何做到从这地址开始:我们在ELF的链接文件ld里,在起始地址定义即可
SECTIONS { . = 0x08048000; .text : { *(.text .text.*) } }
- Code区域
- 这里存储了ELF文件的code区域,包括code(代码)和RO(常量字符串等)
- 为什么要把代码和常量合二为一,code和常量虽然都是只读,但是code是具有可执行权限的,常量是没有的哦?
- 如果把code和ro进行分开映射会导致性能损失,在进行页表映射的时候会映射两次,而且容易造成内存碎片
- Data区域
- 保存了已初始化的静态变量和全局变量
- Bss区域
- 保存了未初始化或初始化为0的静态变量和全局变量
- BSS区域是不占用文件空间的,在ELF加载时会重新去分配内存空间并清零
- 在Mmap时,data和bss区域是合二为一进行映射的,他们都具有相同属性
- Heap区域
- 用户堆从bss结束后的地址+随机偏移进行分配
- 随机偏移是一种安全策略,在栈和mmap中也有运用
- 用户堆的内存管理是放在用户态做的,Linux下的heap是由glibc的ptmalloc来实现,当然还有性能更好的jemalloc/tcmalloc
- 用户堆的内存管理的初始化操作需要放入crt中实现
- glibc下超过128K内存直接从mmap中申请
- Mmap区域
- mmap区域主要是存放进程所依赖的动态共享库以及用户使用MMAP系统调用申请的Heap/共享内存
- mmap的方向是从高到低增长的,这样和Heap区正好形成一个对冲方向,这样能最大化使用地址空间
- mmap的随机偏移也是一种安全策略
- Stack区域
- 存放进程的局部变量/函数参数/函数返回值
- 堆栈随机偏移是为了实现栈地址随机化,是一种安全策略
- 堆栈的大小有一个最大值,LINUX下默认为8M,但是这个空间可以调整
- 堆栈的增长方向和堆是相反的,是向下增长的