前言
该篇为linux内核内存管理第一篇,答主文中所述主要作学习记录使用,文章中难免有逻辑错误、思虑不周之处。欢迎路过的各位牛牛们告知并督促答主改正,不吝赐教。
初见内存
在冯诺依曼体系结构中,存储器是5大部件之一。试想一下,我们编写了一个打印hello world的程序,如何才能让程序执行起来。
程序执行需要借助cpu,也就是5大部件中的控制器和运算器,而程序运行过程的本质就是“CPU取指”,然后执行。在取指中,指令从何处来?
其实,指令就存储在内存中的。程序执行的过程我们可以简单的看作:CPU将指令物理地址发送到总线上,由总线控制从内存的对应位置取出来,逐条执行。因此,想要执行的程序是加载到内存中的。
内存地址
内存分段
简单设想
现在我们编写一个helloworld程序,我们知道要让CPU可以运行这个程序,该程序文件是需要加载到内存中的。
#include <stdio.h>
void helloworld(void)
{
printf("hello world !\n");
}
int main(void)
{
helloworld();
return 0;
}
......
gcc helloworld.c -o helloworld -m32
......
objdump -h helloworld
helloworld: file format elf32-i386
Sections:
Idx Name Size VMA LMA File off Algn
0 .interp 00000013 08048134 08048134 00000134 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
......
12 .text 0000017c 08048330 08048330 00000330 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
......
23 .data 00000004 08049680 08049680 00000680 2**2
CONTENTS, ALLOC, LOAD, DATA
通过objdump等命令查看helloworld二进制文件,我们发现程序是分段的,比如上面看到的text代码段、还有data数据段等,之前我们已经想到了,ELF格式的可执行程序文件等如果想执行,是需要放到内存里面的,我们可以设想程序文件是从磁盘分段映射到内存中的。
内存地址修正
在helloworld程序中,调用了helloworld函数用来打印信息,对应到指令代码可看成call ip1。我们设想一下,如果此处的ip1对应的是内存地址的话,也就是可执行程序文件中此处的指令是call 0x8048430,这要求我们在编译可执行程序的时候就提前知道了helloworld函数映射的位置,这是不现实的,因为你无法提前确定该地址对应的段是否已经被使用,也可能已经被其他未执行的程序文件占用了。
其实,一般程序的段映射是发生在运行时的,既在运行时才动态选择了基地址为0x8048330的段来映射代码部分,所以process1内部的跳转值我们可以使用偏移值,比如call 0x100。当程序执行的时候重新进行地址计算。
比如:
新地址 = 段基址 + 偏移值
调用helloworld其实读取的是内存地址(0x8048330 + 0x100 = 0x8048430)处的指令。在这里0x8048330可称为某段内存的段基址。
分段地址计算
上面是我们设想的一个简单的内存分段并取指的模型,真实情况下,我们一般用一个segment 段+ offset偏移 的方式表示一个地址,这些地址包含在机器指令中,表示某条指令或者某个立即数,可以叫做逻辑地址或者虚拟地址,其中所描述的segment段其实是某内存段的段基址。
在所用到的寄存器中,CS是代码段寄存器,当然还有数据段寄存器DS、堆栈段寄存器SS和附加段寄存器ES\FS\GS,而IP是指令寄存器。某种意义上,我们会使用CS:IP表示某个指令地址,其中CS是代码段寄存器,表示段表的下标值(可称为段号),而IP我们可以当成相对偏移值,这就是逻辑地址的一种表示。
通过上面我们知道,在使用内存分段管理过程中,我们需要进行地址转换计算出来内存区对应的真实地址,此时我们称为线性地址,既上图中我们所描述的内存区就是一整块线性地址区间。
线性地址 = 段基址 + 偏移值,段表中存储的是段描述符集合,根据特定段号(index),我们就可以找到对应的段描述符,而得到我们所需要的段基地。
分段机制的前世今生
在8086 cpu之前,可称为实模式,其寄存器和总线都是16位的。可以直接使用地址访问物理内存的,也就不存在逻辑地址、线性地址和物理地址。
在8086 cpu时代,想把寻址内存空间扩充到1M(需要20bit地址长度),但是算数逻辑单元的带宽是16位的,没办法直接计算20bit的地址,这时候就引入了内存分段管理的概念,也就有了段寄存器(cs/ss/ds/es/fs/gs)。此时的段寄存器是16位的,其中存储的地址值为段地址,在给地址总线传递地址之前,在段寄存器上将16位的地址映射到20位地址的高16位上(既段寄存器的值左移4位),同时加上逻辑地址偏移值直接得到物理地址,发送给总线。
在80286 cpu时代,出现了保护模式,总线扩展成了24位,寻址大小为16M,出现了保护模式。
在